POLYGLOT PROGRAMMING

Better poly than sorry!

A simple Lens example in LiveScript

After reading Lenses in Pictures I felt rather dumb. I understood what is the problem, but couldn't understand why all the ceremony around the solution. I looked at OCaml Lens library and became enilghtened: it was because Haskell! Simple.

Anyway, the code looks like this:

z = require "lodash"            # `_` is a syntactic construct in LS
{reverse, fold1, map} = require "prelude-ls"

pass-thru = (f, v) --> (f v); v
abstract-method = -> throw new Error("Abstract method")


#
# LensProto - a prototype of all lens objects.
#
LensProto =
    # essential methods which need to be implemented on all lens instances
    get: abstract-method (obj) ->
    set: abstract-method (obj, val) ->
    update: (obj, update_func) ->
        (@get obj)
        |> update_func
        |> @set obj, _

    # convenience functions
    add: (obj, val) ->
        @update obj, (+ val)


#
# Lens constructors
#
make-lens = (name) ->
    LensProto with
        get: (.[name])
        set: (obj, val) ->
            (switch typeof! obj
                | \Object => ^^obj <<< obj
                | \Array  => obj.slice 0  )
            |> pass-thru (.[name] = val)

make-lenses = (...lenses) ->
    map make-lens, lenses


#
# Lenses composition
#
comp-lens = (L1, L2) ->
    LensProto with
        get: L2.get >> L1.get
        set: (obj, val) ->
            L2.update obj, (obj2) ->
                L1.set obj2, val

# Lensable is a base class (or a mix-in), which can be used with any object and
# which provides two methods for obtaining lenses for given names. The lens
# returned is bound to the object, which allows us to write:
#
#   obj.l("...").get()
#   obj.l("...").set new_val
#
# instead of
#
#   make-lens("...").get obj
#   make-lens("...").set obj, new_value
#
Lensable =
    # at - convenience function for creating and binding a lens from a string
    # path, with components separated by slash; for example: "a/f/z"
    at: (str) -> @l(...str.split("/"))

    l: (...names) ->
        # create lenses for the names and compose them all into a single lens
        lens = reverse names
            |> map make-lens
            |> fold1 comp-lens

        # bind the lens to *this* object
        lens with
            get:  ~> lens.get this
            set: (val) ~> lens.set this, val

to-lensable = (obj) -> Lensable with obj

Some tests for the code:

#
# Tests
#

o = Lensable with
    prop:
        bobr: "omigott!"
        dammit: 0


[prop, dammit] = make-lenses "prop", "dammit"
prop-dammit = comp-lens dammit,  prop


console.log z.is-equal (prop.get o),
    { bobr: 'omigott!', dammit: 0 }

console.log (prop-dammit.get o) == 0

console.log z.is-equal (prop-dammit.set o, 10),
    { prop: { bobr: 'omigott!', dammit: 10 } }

prop-dammit
    |> (.set o, "trite")
    |> (.l("prop", "bobr").set -10)
    |> z.is-equal { prop: { bobr: -10, dammit: 'trite' } }, _
    |> console.log


out = o
    .at("prop/bobr").set "12312"
    .at("prop/argh").set "scoobydoobydoooya"
    .at("prop/lst").set [\c \g]
    .at("prop/dammit").add -10
    .l("prop", "lst", 0).set \a
    .l("prop", "lst", 2).set \a

console.log z.is-equal out, {
    prop: {
        bobr: '12312', dammit: -10,
        argh: 'scoobydoobydoooya',
        lst: ["a", "g", "a"]}}


out = o
    .at("prop/bobr").set "12312"
    .at("prop/argh").set "scoobydoobydoooya"
    .at("prop/dammit").add -10

console.log z.is-equal out,
    { prop: { bobr: '12312', dammit: -10, argh: 'scoobydoobydoooya' } }


transform =
    (.at("prop/bobr").set "12312") >>
    (.at("prop/argh").set "scoobydoooya") >>
    (.at("prop/dammit").add -10)

console.log z.is-equal  (transform o),
    { prop: { bobr: '12312', dammit: -10, argh: 'scoobydoooya' } }

This showcases many of the ways you can use lenses in LiveScript. LS has many ways of creating functions and it has syntax for pipe'ing and composing functions, so it's a natural fit for everything FP-looking. LS does not do "curried by default" like Haskell or OCaml, but it gives you a partial application syntax and allows defining curried functions. It's much like Scala in this regard.

Anyway, this is what lenses are supposed to do - they should support functional updates over data sctructures. They should offer get, set and update methods, and also there should be a compose operator for them. And that's all - you can read the linked OCaml implementation to see that it's really that simple. Most of that implementation are convenience methods for creating lenses for various OCaml structures; the core code is really short.

Comments