DEV Community

Jorge Zaccaro
Jorge Zaccaro

Posted on

1

Let Over Hiccup: Creating Closures with Reagent

Sometimes it's called a closure, other times a saved lexical environment. Or, as some of us like to say, let over lambda.
Let Over Lambda, Chapter 2: Closures.

λ

When I started to learn Reagent, I noticed a familiar pattern in form-2 components that use a let binding to return a lexical closure, an idiom that Doug Hoyte elegantly calls let over lambda in his book of the same title:

(defn form-2-component []
(let [content "Hello, world!"]
(fn [] [:div content])))

"Let over lambda is a nickname given to a lexical closure. [...] In a let over lambda scenario, the last form returned by a let statement is a lambda expression. It literally looks like let is sitting on top of lambda."

(let (...)
(lambda () ...))

"Sometimes the most valuable thing to have a let form return is an anonymous function which takes advantage of the lexical environment supplied by the let form. To create these functions in lisp we use lambda."


As Hoyte explains, "one way of thinking about closures is that they are functions with state". In Common Lisp, this pattern can be used to create a counter by surrounding a lambda expression with a let form:

(let ((counter 0))
(lambda ()
(incf counter)))

This expression returns a function that increments the counter variable every time it is called. In Clojure, anonymous functions can be created using the fn special form that is analogous to Common Lisp's lambda:

(let [counter (atom 0)]
(fn []
(swap! counter inc)))

In Reagent, the simplest form of a let over lambda would create a similar let binding, but using reagent.core/atom instead of clojure.core/atom so that Reagent can watch for and re-render the component on state changes:

(let [counter (reagent.core/atom 0)]
(fn []
(swap! counter inc))

However, here the inner function would be called only when rendering the component for the first time, but nothing would be able to access or increment the value of counter and thus trigger the component to re-render.

Let's modify the last snippet to return a Hiccup vector that describes an HTML button, adds an event listener that increments the counter on every click, and contains a child expression that dereferences the counter:

(let [counter (reagent.core/atom 0)]
(fn []
[:button
{:on-click #(swap! counter inc)}
(str "Clicks: " (deref counter))]))

This closure will remember the number of times that the button has been clicked. After being initialized with a value of 0, the value of counter will be incremented by 1 every time the #(swap! counter inc) function is called.

Finally, let's turn this button into an actual component with state by using the above way of creating closures with Reagent. But in order to create such closures as needed, we must define a function that returns a let over lambda:

(defun create-counter ()
(let ((counter 0))
(lambda () (incf counter))))

Coincidentally, this pattern used to create Reagent components with state is exactly what Hoyte calls "lambda over let over lambda". That is, form-2 components follow the lambda over let over lambda pattern:

(defn create-counter-button []
(let [counter (reagent.core/atom 0)]
(fn []
[:button
{:on-click #(swap! counter inc)}
(str "Clicks: " (deref counter))])))

Thus, every time create-counter-button is called, a new closure will be created to store the state of the new button in it's own lexical environment. It's amazing that this can be accomplished without objects and classes at all!

Let and lambda are fundamental; objects and classes are derivatives. As Steele says, the "object" need not be a primitive notion in programming languages. — Let Over Lambda, Chapter 2.

Conclusion

Much like "let over lambda is a nickname given to a lexical closure", we can say that Let Over Hiccup is a nickname for a closure that returns a Hiccup vector, regardless of how many layers of let over lambdas and nested components there will be in more sophisticated compositions.

I think it's very interesting and surprising that a simple example of a Reagent component uses a pattern that gives its name to an advanced book on Lisp macros. I'm really excited to figure out what other topics about macros can be applied to the seemingly unrelated field of functional components for building user interfaces on the Web.

Links

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay