Zach Tellman edited this page Apr 14, 2013 · 10 revisions

first, get everything set up

These examples are meant to be tried out in the REPL. To follow along, first pull down the latest Lamina:

git clone git://
cd lamina
git checkout perf

then install graphviz:

Linux install graphviz using your package manager
OS X download the installer
Windows download the installer

Finally, open a repl using lein repl, or run lein swank and M-x slime-connect in Emacs.

an introduction

An event is a signal from outside the normal flow of computation. There are two ways we can handle this; we can synchronously wait for the event to occur before executing some code, or define code that will run asynchronously when the event occurs. This second approach is sometimes also called event-driven programming.

Neither of these approaches is necessarily better than the other, but each can be useful in different situations. The differences and trade-offs are discussed in detail in this talk, but it will suffice here to point out that while we can implement a synchronous mechanism using events and callbacks:

(let [p (promise)]
  (subscribe event-publisher #(deliver p %))

the reverse is not true. Therefore, if we wish to take advantage of both approaches where appropriate, we need a library that provides support for the asynchronous approach. Lamina is one such library, providing a rich set of operators for creating, transforming, aggregating, and responding to events.


The task macro is similar to Clojure’s future macro: it executes the body on a separate thread, and returns something representing the eventual outcome.

> (use 'lamina.executor)
> (future (+ 1 1))
#<core$future_call$reify__5684@52c8c6d9: :pending>
> (task (+ 1 1))
<< ... >>

these are both unrealized results: they represent the value that will be returned from the other thread, but they also represent any errors that may occur while the value is being computed. Both can be dereferenced, synchronously halting the thread until they are realized:

> @(future (+ 1 1))
> @(task (+ 1 1))

If there was an error while computing the sum, the dereferencing will throw an exception.

However, the async-promise returned by task has an ability that future doesn’t:

(use 'lamina.core 'lamina.executor)

(on-realized (task (+ 1 1))
  #(println "value:" %)
  #(println "error:" %))

This gives us the ability to define callbacks for when the async-promise is realized, separately handling the cases where we get a value and an error.

This is one of the fundamental building blocks of Lamina, but it is a fairly tedious way to deal with asynchronous programming. Lamina has a rich set of abstractions built upon this, to the point that it should be very rare for a developer to ever have to use on-realized.

While the above example uses an async-result representing computation on a separate thread, the same applies to any unrealized value. An async-result can also represent data from another server, input from the user, or a state-machine transition, and in all these cases Lamina can be used to interact with these values in a straightforward, idiomatic way.

At this point, there are two separate lines of functionality to explore:

Pipelines, which are a simpler and more expressive alternative to on-realized


Channels, which can define and manipulate streams of events

Both are fundamental abstractions that are necessary to understand before Lamina can be used effectively.