Skip to content

mlanza/atomic

Repository files navigation

Atomic

Write ClojureScript in JavaScript without a transpiler.

Highlights:

Atomic is protocol oriented. Objects are treated as abstract types per their behaviors irrespective of their concrete types. This flavor of polymorphism is what makes Clojure so good at transforming data.

JavaScript has no means of safely extending natives and third-party types. Protocols, which are the most apt solution to this problem, have yet to be adopted into the language.

Atomic is functional first. Functions are preferred to methods. This makes sense given how protocols treat things as abstractions.

Atomic has no maps or vectors but used objects and arrays as records and tuples before the proposal formalized the idea. It had previously integrated them via Immutable.js but, in practice, since objects and arrays filled the gap well enough it wasn't worth the cost of another library, its integration was dropped.

Don't care for point programming? Autopartial delivers point-free programming without a build step up until pipeline operators and partial application reach stage maturity.

Premise

Initially, Atomic was born from an experiment answering:

Why not do ClojureScript directly in JavaScript and eliminate the transpiler?

Here's the ephiphany: since languages are just facilities plus syntax, if one can set aside syntax, the right facilities can make transpilation superfluous.

JavaScript does functional programming pretty dang well and continues to add proper facilities.

Of all of the above, first-class protocols is the most critical one which, for some odd reason, has failed to gain community support. Developers it seems are failing to experience and realize the tremendous value add that only protocols provide. They're the centerpiece of Clojure and, by extension, Atomic. Clojure would not be Clojure without them!

In the meantime, the gaps in these facilities are filled by libraries like this one.

Purity Through Discipline

Historically, since JavaScript lacks value types (i.e. records and tuples](https://tc39.es/proposal-record-tuple/) and temporals) purity is gained through discipline. A function which receives mutable types must, as a rule, not mutate them. So in Atomic objects, arrays and dates are, as a rule, not mutated.

JavaScript aims to fill these gaps, but is not there yet. When it happens Atomic will restore objects, arrays and dates to their reference type status.

Getting Started

Build it from the command line:

npm install
npm run bundle

Copy the contents of dist to lib in a project then import from either lib\atomic or lib\atomic_ depending on whether autopartial is wanted.

Implementing a small app is a good first step for someone unfamiliar with the herein described approach.

Modules

The core module provides the foundation. If a UI is needed, reach for the reactives and dom modules, where the former provides FRP and the latter, tools for use in the, ahem, DOM.

Elm sold FRP. So by the time CSP appeared in core.async that ship had already sailed, so Atomic is based on reactives.

Its state container, the bucket which houses an app's big bang world state, is the cell. It's mostly equivalent to a Clojure atom. The main exception is it invokes the callback upon subscription the way an Rx subject does. This choice has well suited the developing of user interfaces.

Like xstream it doesn't rely on many operators. And implementation experience has seen how hot observables are easier to handle correctly than cold ones. These notions have, thus, been baked into the defaults.

The typical UI imports core, reactives, and dom as _, $ and dom respectively. The _ doubles as a partial application placeholder when using autopartial. To facilitate interactive development these assignments can be readily imported by entering cmd() from a browser console where Atomic is loaded.

Since many of its core functions are taken directly from Clojure one can often use its documentation. Here are a handful of its bread and butter functions:

The beauty of these functions (kudos to Hickey!) is how they allow one to surgically replace the state held in a state container without mutating it.

In the absence of threading macros and pipeline syntax several functions exist (see these demonstrated in the example programs) to facilitate pipelines and composition:

  • chain (a normal pipeline)
  • maybe (a null-handling pipeline)
  • comp
  • pipe

Changes

Because Atomic has been used primarily by a small, internal audience, the change process has not been formalized to protect a wider audience. However, it is easy to safely try. Vendor a copy into your project. This eliminates the pressure of keeping up with releases.

Guidance for Writing Apps

Start with a functional core whose data structure representing the world state, though it is made up of objects and arrays, is held as immutable and not mutated. That state will have been birthed from an init function and wrapped in an atom.

Then write swappable functions which drive state transitions based on anticipated user actions. These will be pure. The impure ones will be implemented later in the imperative shell or UI layer.

The essence of "easy to reason about" falls out of purity. When the world state can be readily examined in the browser console after each and every transition identifying broken functions becomes a much less onerous task.

Next, begin the imperative shell. This is everything else including the UI. Often this happens once the core is complete. Not all apps have data, however, which is simple enough to visually digest from the browser console. In such situations one may be unable to get by without the visuals a UI provides and the shell may need to be created earlier and develop in parallel.

This entire effort begins with forethought, preliminary work, and perhaps a bit of notetaking. Think first about the shape of the data, then the functions (and, potentially, commands/events) which transform it, and lastly how the UI looks and how it utilizes this. For more complex apps, roughing out the UI in HTML/CSS will help guide the work. Not everything needs working out, but having a sense of how things fit together and how the UI works before writing the first line of code will help avoid snafus.

If an app involves animation, as a turn-based board game would, ponder this aspect too. How one renders elements which are animated is often different from how one renders those which aren't. Fortunately, CSS is now capable of driving most animations without the help of additional libraries.

Sparingly add libraries. Keep projects lean. Dependencies breed. The ever-changing landscape of modern libraries (Vue, React, Angular, Svelte, etc.) brims with excellent ideas, yet the author has continually met customer requirements without necessitating any of them.

Rather, and only when the deficit is truly felt, graft in the idea, not the dependency. It permits the local team and not the vendor team to dictate the schedule. It also alleviates the pressure of falling out of step with the latest release.

Progressive Enhancement

One begins routinely with a simple core and shell and potentially layers in other useful concepts.

The journal type can be added to provide undo/redo and permit stepping forward and backward along a timeline.

Add a layer to process change via commands and events, in its simplest form, both represented as plain old JavaScript objects. Commands (yin) belong in the impure world (imperative shell), and events (yang) the pure world (functional core).

Events are folded into the world state via a master reduction. And both events and commands can be readily sent over the wire or captured in logs. When captured, they provide a complete and auditable history, one which can be readily examined from the browser console.

A Tale of Two Worlds

Rather than one, the developer writes two programs, edits two files, straddles two worlds. To get why this is done, first understand what the pure world actually is: simulation.

The functional core is where the domain logic goes and the imperative shell where the glue code or program architecture (routers, queues, buffers, buses, etc.) goes. The core simulates what your program is actually about (managing to-dos) and the shell provides the machinery (all the types and operations which, from a user's perspective, have nothing to do with managing to-dos) necessary to transform these simulations into realities.

The shell transforms effect into simulation and vice versa. Commands flow in. Events flow out. The core provides the direction, the shell the orchestration.

The benefit of starting with simulations is they're free of messy unpredictability, are easy to write tests against, and can be fully controlled. Putting it another way, they're like flipbooks where time can be stopped and any page and its subsequent examined. This model is easier to reason about, develop, troubleshoot, and fix than the imperative model alone.

Atomic in Action

Atomic has been used for developing and deploying (to typical web hosts, Deno, SharePoint, Cloudflare, and Power Apps) a variety of production apps for years and has most recently been used to create digital card and board games.

These examples model how one might write a program in Atomic:

DOM events are oft handled using an $.on which is similar to jQuery's.

While creating a virtual dom had been considered for inclusion in the library, state diffing is not always needed. When needed, however, $.hist provides two frames (the present and the immediate past) of world state history for reconciling the UI.

Contingent Improvements

Applying the Clojure mindset directly in JavaScript is possible with the right facilities. Atomic exists to showcase the fact.

As mentioned, some of its gaps are being filled by various T39 proposals. When they land in the caniuse baseline, portions of Atomic will be rewritten.