Skip to content
/ delexical Public

Call-time lexical closures for Clojure

License

Notifications You must be signed in to change notification settings

czan/delexical

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

delexical

A Clojure library that provides closures whose lexical context can be bound at call-time rather than construction-time.

Yo where my possee at ?
In the range of my voice

Smoked Out Productions – Bok Bok

Usage

[delexical "0.1.0-SNAPSHOT"]
(ns my-ns
  (:require [delexical.core :refer [defdelexical]]))

Current status

The library as of now is bugged because the way it tracks locals variables to deduce free symbols is not 100% functional. Some additional work needs to be done in dance to fix this. Another approach would be to expose the field tracking locals at the compiler level, namely closes. To do this I prepared (but have yet to make use of) a lib that allows one to copy a whole class tree into another package, renaming symbols along the way. One can then monkey-patch this class methods and load them in Clojure's classloader in a fully controlable way, leaving the original Clojure compiler and the rest of the code unchanged.

How ?

(let [d 1000]
  (defdelexical f [a b]
    (+ a b c d)))

Here we created a delexical pretty much like we would have defined a normal function. The only difference so far is that the code did not raise an "Unable to resolve symbol: c" CompilerException. Indeed the whole point of a delexical is to allow symbols that are bound to nothing at the time the closure is created to be bound at call-time instead.

To do so requires that these free symbols are bound in the lexical context of the call site.

(let [c 100]
  (f 1 10)) ;; => 1111

When calling a delexical, you cannot rebind symbols that were bound when the delexical was defined.

(let [c 100 d 0]
  (f 1 10)) ;; => 1111 (rather than 111)

But you can bind symbols that were free at construction-time by explictly passing them at call-time as additional arguments, in the order they appear in code (pre-order).

(let [c 100]
  (f 1 10 0)) ;; => 1011

Caveats

  • Delexicals cannot accept variadic arguments.
  • Although delexicals look like functions, under the hood they are macros. This implies you will have to wrap them in a function if you want to use them as higher-order functions. E.g: (map #(f 1 %) [1 2 3]).

Why ?

Let's suppose you have been experimenting with something and ended up with a several hundred lines long function (hereafter called the Big Fucking Function) mostly consisting in one monstruous let. This let is used to store the result of various computations that are then fed to various closure via the shared lexical context of that let in such a way that these closures have some of these variables in common.

Something akin to:

(defn bff [object]
  (let [a (compute-a object)
        b (compute-b object)
        ... ...
        z (compute-z object)

        func1 (fn [leaf]  (+ leaf a b c))
        func2 (fn [leaf]  (+ leaf a c d))
        ... ...
        funcn (fn [leaf]  (+ leaf a b z))]

    (if (bottom? object)
      [(-> object func1 func2 ... funcn)]
      (mapcat bff (subobjects object)))))

So far, your code works OK with most of the obvious cases, but you need to straighten it out, either because you would like to test these func1, func2 ... funcn closures, wish to reuse them elsewhere or because you find this big let ugly.

In other words, whichever the reason is, you have to move these subfunctions out of the BFF and def them in the current namespace.

Solution #1: pass as an argument what was passed via the lexical context

(defn func1 [a b c leaf]  (+ leaf a b c))
...

(defn bff [object]
  (let [a (compute-a object)
        ... ...
        z (compute-z object)]
    (if (bottom? object)
      [(->> object (func1 a b c) (func2 a c d) ... (funcn a b z))]
      (mapcat bff (subobjects object)))))

Caveats:

  • Those additionnals arguments make the extracted functions less easy to reuse.
  • Changes to the BFF are necessary.

Solution #2: reduce the number of arguments to the bare minimum

(defn func1 [parent leaf]
  (+ leaf (compute-a parent) (compute-b parent) (compute-c parent)))
...

(defn bff
  ([object]
    (bff nil object))
  ([parent object]
    (if (bottom? object)
      [(->> object (func1 parent) (func2 parent) ... (funcn parent))]
      (mapcat (partial bff object) (subobjects object)))))

Caveats:

  • Some computations are unecessarily performed multiple times.
  • Changes to the BFF are necessary.

Solution #3: reduce the number of arguments to the bare minimum and memoize

(def compute-a
  (memoize (fn [object] ...)))
...

Caveats:

  • Some computations are performed once: consider the case where the BFF, which is just a function that collects and transforms a tree's leafs, is run on a tree where certain leafs are identical => the compute-* functions will be run only once even when they contain side effects that are should be performed multiple times. This is due to the fact the memoization atom is global, whereas the variables "memoized" in the let were scoped to the tree-node (object) being processed.
  • Changes to the BFF are necessary.

Solution #4: reduce the number of arguments to the bare minimum and structure the memoization

This can be done by storing the memo in a tree-node's meta, or via dynamic bindings or via equivalently convoluted ways such as maintaining a stack of memos in a global atom or a dynamic var.

Using delexicals

Just extract those godamned funcs, define them as delexicals and you're done.

(defdelexical func1 [leaf]  (+ leaf a b c))
(defdelexical func2 [leaf]  (+ leaf a c d))
...
(defdelexical funcn [leaf]  (+ leaf a b z))

(defn bff [object]
  (let [a (compute-a object)
        b (compute-b object)
        ... ...
        z (compute-z object)]
    (if (bottom? object)
      [(-> object func1 func2 ... funcn)]
      (mapcat bff (subobjects object)))))

TODO

  • Support delexicals with multiple bodies.

License

Copyright © 2018 TristeFigure

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

About

Call-time lexical closures for Clojure

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published