Jon Aquino's Mental Garden

Engineering beautiful software jon aquino labs | personal blog

Wednesday, March 19, 2014

Mustache: How to test for existence without iteration

In Mustache templates, sometimes you just want to test if an array exists, without iterating over it. However, {{#myArray}}...{{/myArray}} will iterate over myArray. What if you just want to check if myArray exists?

Solution: {{#myArray.0}}...{{/myArray.0}}

This will operate on the first (i.e., 0th) element of myArray.

Tuesday, March 04, 2014

Lisphp at Ning - Sample Code

Here is the Lisp that I have written as an experiment with Lisphp. It is probably not great Lisp code—I don't have prior experience with Lisp—so any suggestions for improvement are welcome. I encourage others to make their Lisphp code publicly available so that we can all learn. Another Lisphp example I have found is balrog.

This code is for rendering an activity feed (like a Facebook Wall). It takes a bunch of activity-item objects, processes them, then feeds them into a Mustache template. The processing step sets up variables for the Mustache template to use.

For new Lisp programmers, I recommend the use of an editor plugin that automatically indents your Lisp code. See my first Lisphp blog post for related recommendations.

render-network-feed.lisp

;;; Top-level module for rendering an activity feed for the network.
;;;
;;; @param string title  the title for the activity section
;;; @param array feed-events  the feed-event objects, parsed from JSON
;;; @param integer excerpt-length  the length at which to excerpt activity items
;;; @param string like-type  the naming scheme for likes: like, promote, or favorite
;;; @param string network-name  the name of the network
;;; @param string network-url  the URL of the network
;;; @param string network-icon-url  48x48 network icon
;;; @return string  HTML for the network feed

(import 'lib/components/activity/lib/process-feed-events.lisp')
(import 'lib/components/activity/lib/add-type-specific-properties.lisp')

;;; Filters, caches, and processes the given network feed events.
;;;
;;; @param array feed-events  the feed-event objects
;;; @param array options  options: excerpt-length
;;; @return array  the feed-event objects after processing
(define (process-feed-events feed-events options)
        ;; Remove rolled-up feed events for now, until we implement handling
        ;; for them.
        (setf! feed-events (remove-rollup-feed-events feed-events))
        (setf! feed-events (add-basic-properties feed-events options))
        (setf! feed-events (add-type-specific-properties feed-events options)))

(setf! processed-feed-events
       (process-feed-events feed-events
                            (hash 'excerpt-length'   excerpt-length
                                  'like-type'        like-type
                                  'network-name'     network-name
                                  'network-url'      network-url
                                  'network-icon-url' network-icon-url)))

;; Send the feed-events into the Mustache template.
((-> B renderMustache)
 'activity/section.mustache'
 (hash
   'title' title
   'feed-events' processed-feed-events))

process-feed-events.lisp

;;; Functions for processing activity events.

(use floor)
(use xg_elapsed_time)

;;; Removes rolled-up feed events. This is a temporary measure until we
;;; implement handling of rollups.
(define (remove-rollup-feed-events feed-events)
        (filter (lambda (feed-event)
                  (not (array-get feed-event 'rollUpType')))
                feed-events))

;;; Adds basic properties, such as humanReadableDate, to each of the feed events
(define (add-basic-properties feed-events options)
        (map (lambda (feed-event)
               (let* ([content-id (array-get feed-event 'event' 'properties' 'contentId')])
                 (arr feed-event
                      (hash (event-type feed-event) true
                            'humanReadableDate'     (human-readable-date feed-event)
                            'content'               (to-content-properties content-id (at options 'excerpt-length'))))))
             feed-events))

;;; Returns the event type suffixed with "Type", e.g., createBlogPostLikeType.
(define (event-type feed-event)
        (. (array-get feed-event 'event' 'eventType') 'Type'))

;;; Returns a friendly date for the event.
(define (human-readable-date feed-event)
        (xg_elapsed_time
          ;; Prefix timestamp with @ so that strtotime will understand it
          (. '@' (floor (/ (array-get feed-event 'event' 'createdDate') 1000)))
          nil nil false))

Lisphp at Ning: Custom functions

In my previous post, I mentioned that we are experimenting with Lisphp at Ning, and we have some custom functions that seem to be helpful:

  • import: imports a lisp file: (import 'foo/bar/baz.php')
  • php: runs a PHP function without having to import it: (php :htmlentities 'foo')
  • cons: prepends an item to an array: (cons 'strawberry' flavors)
  • hash: creates an array of key-value pairs: (hash 'key1' 'value1' 'key2' 'value2')
  • array-set: sets an item on a multidimensional array: $flavors['foo']['bar'] = 'baz' is (array-set flavors 'foo' 'bar' 'baz')
  • array-get: gets an item from a multidimensional array: $flavors['foo']['bar'] is (array-get flavors 'foo' 'bar')
  • arr: array_replace_recursive()
  • environment: this is the Lisp environment itself added as a variable, to allow you to check if a function exists: (exists-at? environment 'my-function')

Here is the code that we use to call Lisphp. Note that you have a choice of runFile() or runCode(); also note the custom functions. You may need to make some modifications to get this to run in your own environment:

<?php

/**
 * Parses Lisphp files.
 */
class XG_Lisp {

    /**
     * Executes a Lisp file using Lisphp.
     *
     * @param string $path  the path to the lisp file
     * @param array $env  names and values to add to the environment;
     *                             see Lisphp_Environment
     * @return  the result
     * @see http://jona.ca/blog/lisphp-at-ning-introduction
     */
    public function runFile($path, $env = []) {
        return $this->run(Lisphp_Program::load(NF_APP_BASE . '/' . $path), $env);
    }

    /**
     * Executes Lisp code using Lisphp.
     *
     * @param string $code  the Lisp code
     * @param array $env  names and values to add to the environment;
     *                             see Lisphp_Environment
     * @return  the result
     */
    public function runCode($code, $env = []) {
        return $this->run(new Lisphp_Program($code), $env);
    }

    /**
     * Executes a Lisp program using Lisphp.
     *
     * @param Lisphp_Program $program  a Lisp program object
     * @param array $env  names and values to add to the environment;
     *                             see Lisphp_Environment
     * @return  the result
     */
    protected function run($program, $env = []) {
        $environment = Lisphp_Environment::full();
        foreach ($env as $name => $value) {
            $environment[$name] = $value;
        }
        $environment['B'] = B(); // XG_BaseService
        // Import a PHP file: (import 'lib/components/activity/lib/foo.lisp')
        $environment['import'] = new Lisphp_Runtime_PHPFunction(function ($path) use ($environment) {
            $program = Lisphp_Program::load(NF_APP_BASE . '/' . $path);
            return $program->execute($environment);
        });
        // Prepends an element to a list
        $environment['cons'] = new Lisphp_Runtime_PHPFunction([$this, 'cons']);
        $environment['->$'] = new XG_Lisp_GetField;
        $environment['array-get'] = new Lisphp_Runtime_PHPFunction([$this, 'arrayGet']);
        $environment['array-set'] = new Lisphp_Runtime_PHPFunction([$this, 'arraySet']);
        $environment['hash'] = new Lisphp_Runtime_PHPFunction([$this, 'hash']);
        $environment['var_dump'] = new Lisphp_Runtime_PHPFunction('var_dump');
        $environment['arr'] = new Lisphp_Runtime_PHPFunction('array_replace_recursive');
        $environment['environment'] = $environment;
        return $program->execute($environment);
    }

    /**
     * Prepends an element to a list.
     *
     * @param mixed $item  the item to prepend
     * @param array|Lisphp_List $list  the list to prepend the item to
     * @return array|Lisphp_List  a new list
     */
    public function cons($item, $list) {
        if (is_array($list)) {
            return array_merge([$item], $list);
        }
        return new Lisphp_List(array_merge([$item], $list->getArrayCopy()));
    }

    /**
     * Retrieves an item from a multidimensional array
     *
     * @param array $array  the array to read from
     * @param string $key1  the first key
     * @param string $key2  the second key, etc.
     * @return mixed  the value at the given keys
     */
    public function arrayGet() {
        $args = func_get_args();
        $result = array_shift($args);
        foreach ($args as $key) {
            if (!is_array($result)) {
                return null;
            }
            if (!array_key_exists($key, $result)) {
                return null;
            }
            $result = $result[$key];
        }
        return $result;
    }

    /**
     * Sets an item on a multidimensional array
     *
     * @param array $array  the array to read from
     * @param string $key1  the first key
     * @param string $key2  the second key, etc.
     * @param string $value  the value to set
     * @return mixed  the new multidimensional array
     */
    public function arraySet() {
        $args = func_get_args();
        $array = array_shift($args);
        $subtree = &$array;
        $value = array_pop($args);
        $lastKey = array_pop($args);
        foreach ($args as $key) {
            if (!array_key_exists($key, $subtree)) {
                $subtree[$key] = [];
            }
            $subtree = &$subtree[$key];
        }
        $subtree[$lastKey] = $value;
        return $array;
    }

    /**
     * Converts a list into an array of key-value pairs.
     *
     * @param string $key1  the first key
     * @param string $value1  the first value
     * @param string $key2  the second key
     * @param string $value2  the second value, etc.
     * @return array  the key-value pairs
     */
    public function hash() {
        $args = func_get_args();
        $hash = [];
        for ($i = 0; $i < count($args); $i += 2) {
            $hash[$args[$i]] = $args[$i+1];
        }
        return $hash;
    }

}

Lisphp at Ning - Introduction

At Ning, we are experimenting with Lisphp, which allows us to call Lisp from PHP and vice versa. It's like an oasis of functional programming in the midst of PHP.

There isn't much on the web on Lisphp, so I am writing some blog posts (in my Lisphp category) about my experiences with it.

First, some helpful resources:

You'll notice that Lisphp does not come with any documentation other than what is on the Github page. A list of all the functions is in the Environment.php file. Here are brief descriptions of what some of the functions do:

  • define - defines a function or global variable
  • let - sets local variables
  • let* - sets local variables - the definitions can refer to each other
  • setf! - sets a local variable in an "imperative" style: (setf! foo 5). For setting local variables, prefer let first, followed by let*, followed by setf!
  • lambda - creates an anonymous function
  • apply - applies a function to an array of arguments
  • list - creates a Lisphp list: (list) or (list 'a' 'b' 'c')
  • array - creates a PHP array: (array) or (array 'a' 'b' 'c')
  • do - executes code several times in a loop
  • car - returns the first item in a list: (car flavors)
  • cdr - returns the remaining items in the list (i.e., not the first one): (cdr flavors)
  • at - returns the value at the given key: (at flavors 'key')
  • set-at! - sets the value at the given key: (set-at! flavors 'key' 'value')
  • unset-at! - unsets the value at the given key: (unset-at! flavors 'key' 'value')
  • exists-at? - does isset() on the value at the given key: (exists-at? flavors 'key')
  • count - returns the number of items in the list: (count flavors)
  • map - applies a function to every item in a list
  • filter - filters out items from a list
  • fold - (aka "reduce") goes through a list to create a single value
  • if - if statement
  • cond - switch statement
  • = - ==
  • == - ===
  • !=, !==, <, >, <=, >=, +, -, /, *, %, not, and, or, nil, true, false
  • . - concatenates strings
  • isa? - returns whether the object is an instance of the given class: (isa? foo <ArrayObject>)
  • string - strval()
  • substring - substr()
  • string-upcase - strtoupper()
  • string-downcase - strtolower()

I'm not sure about the following - if you know, let me know:

  • eval
  • quote
  • symbol
  • macro
  • dict - I'm not exactly sure how this works. I made a replacement called "hash"—see below.

I also added the following custom functions - I'll give the code in my next blog post:

  • import: imports a lisp file: (import 'foo/bar/baz.php')
  • php: runs a PHP function without having to import it: (php :htmlentities 'foo')
  • cons: prepends an item to an array: (cons 'strawberry' flavors)
  • hash: creates an array of key-value pairs: (hash 'key1' 'value1' 'key2' 'value2')
  • array-set: sets an item on a multidimensional array: $flavors['foo']['bar'] = 'baz' is (array-set flavors 'foo' 'bar' 'baz')
  • array-get: gets an item from a multidimensional array: $flavors['foo']['bar'] is (array-get flavors 'foo' 'bar')
  • arr: array_replace_recursive()
  • environment: this is the Lisp environment itself added as a variable, to allow you to check if a function exists: (exists-at? environment 'my-function')

Finally, some tips:

  • See if your editor has a plugin that will automatically indent your Lisp code. For example, Sublime Text has a lispindent plugin that will indent your code whenever you press Enter; you can also press Command+I to re-indent the selected code.
  • Sometimes you may need to dive into the Lisphp code to fix things. This is a good opportunity to learn how Lisphp works, and to contribute back by submitting a pull requests. I submitted two pull requests and they were accepted immediately.
  • To import a constant: (use +XG_Model::PLAINTEXT+). Now you can reference +XG_Model::PLAINTEXT+.