Creating a File
LightTable’s behavioral innards are written almost exclusively in ClojureScript. So if we want to start poking at them, the first thing we need to do is create a
.cljs file. I’m calling mine “understanding light table.cljs” (you can find the gist here), but feel free to name yours according to your own customs and beliefs.
Namespace and Requires
This is Clojure, and that means namespaces. We’ll need Light Table’s
object library. And because macros are weird in ClojureScript, we’ll need to require those separately as well. Like so:
Feel free to alias the
required namespaces as you please. But because the purpose of this post is understanding how Light Table works, I’ll avoid that bit of indirection in my examples.
Go ahead and evaluate that namespace expression (
⌘↩︎, by default). Light Table will automatically try to connect to a client REPL. If this file were part of a leiningen project, it would use that. But seeing as it’s not, it asks you to manually connect to a client:
No client available.
We don’t know what kind of client you want for this one. Try starting a client by choosing one of the connection types in the connect panel.
Luckily, Light Table provides a client specifically for evaluating expressions in the context of itself. It’s called “Light Table UI”. Select it from the list, and try evaluating the
ns expression again.
Objects Defined (And Instantiated)
Classic OOP thinking might define an “object” as “state coupled with the behavior that mucks with it”. Light Table decouples state and behavior. In Light Table, objects are responsible for state only.
Similar to the standard OOP practice of defining a class that gets instantiated into a new object, the first step of creating an object in Light Table is to define an object template:
This creates a template with a type of
:reverb.lighttable/my-template-name, a tag of
:my-tag-name, and then whatever state-holding properties we choose to give it.
I should probably say something about tags here. Tags are just a way of grouping objects that’s not coupled to said objects’ type or IDs. This will make more sense once we get to associating behavior with objects, which is customarily done through tags. But for now, think of a tag as a potential mount-point for future mixins.
So, in a common theme you’ll notice all throughout Light Table’s object model, object templates are just data structures. After
evaling the above, you can inspect it with the following:
As you can see, the template is just a simple
Once you have a template, you instantiate it with
You can check it out with:
So objects in Light Table are very much like the templates they’re instantiated from.
create just adds a unique
:li.object/id key, some properties to handle behaviors (more on behaviors next!), and stuffs the whole thing into an
Atom. No magic. Just data structures.
If objects just hold state, how do we get them to do anyhting? Why, with behaviors, of course! And for creating behaviors,
lt.macros/behavior is our go-to macro. It takes a name, some triggers (we’ll get to those), and a
:reaction, which is essentially what you want the behavior to do:
Inspecting behaviors is also straight forward and is also just data:
Of course, a behavior doesn’t do you a lot of good unless you add it to an object. For the sake of having something illustrative to
eval we’ll use
lt.object/add-behavior!. But be aware; this is generally not the best practice.
If you re-eval your object at this point, you’ll notice it now has extra data in its
Okay. So far, so good. But seriously. How do we freaking make an object do something?
Remember that trigger you gave your behavior? You activate the behavior of an object by
raiseing one of the triggers of the associated behavior:
Note that the first argument passed to the behavior’s function is the object itself. But you can pass in others in the
But Don’t Do Things Like That
Here’s the problem. Behaviors added with
add-behavior! can get clobbered whenever Light Table reloads its own behaviors. So the proper way to associate behaviors with objects is the way Light Table does it: in a behavior file like “user.behaviors” or the “.behaviors” file included with plugins.
When you reload Light Table’s behaviors, you’ll find this behavior automatically gets attached to any objects with the given tag.
You can even specify arguments in the “.behaviors” file:
Which get appended to the behaviors arguments like so:
To sum up, you create an object template that specifies some state and
:tags. Then you create behavior with some functionality and
:triggers. You associate those behaviors to any instantiated objects with the given tag in one or more .behaviors files. And finally, behaviors attached to objects have their functionallity invoked by calling raise with the appropriate trigger (or, more likely, having Light Table call raise with a given trigger in response to user input).
That seems like a crazy amount of abstraction to wrap your head around for what could be a simple
(do-thing my-stuff), right? But the benefits along the axes of customization and flexibility are pretty extreme. If you think about it, any part of the chain from tag→object→trigger→behavior→state can be redefined without disturbing its neighbors. Not only that, it could be redefined and reevaluated at run time to modify the behavior of Light Table while you’re using it.
That’s a pretty cool trick that makes developing for Light Table just as much fun as the Clojure it’s built on famously claims to be. But it also gives the open source community a lot of freedom to extend the editor in ways the original developers never anticipated.
And that, for me, is the most valuable attribute a text editor can possess.