-
Notifications
You must be signed in to change notification settings - Fork 57
Introduction
These examples are meant to be tried out in the REPL. To follow along, first pull down the latest Lamina:
git clone git://github.com/ztellman/lamina.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 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 %))
@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)
nil
> (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))
2
> @(task (+ 1 1))
2
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.