Skip to content
/ atomic Public

Write ClojureScript in JavaScript without a transpiler.

License

Notifications You must be signed in to change notification settings

mlanza/atomic

Repository files navigation

Atomic

Write ClojureScript in JavaScript without a transpiler.

Highlights:

Atomic is protocol oriented to its very foundation. It's more freeing to think in terms of apis and behaviors than in types.

Protocols are the centerpiece of Clojure and, by extension, Atomic. They provide the only safe means of dynamically extending natives and third-party types. They make cross-realm operability possible.

Atomic is functional first. This makes sense given that function, not methods, are first class. Why choose a paradigm which limits the places you'll go.

Atomic has no maps or vectors though it once integrated them via Immutable.js. It turns out it didn't need them. Treating objects and arrays as value types worked so well the integration was dropped. It wasn't worth the cost of loading the library. This bit of history is noted to chalk up another one for protocols. They so seamlessly blend third-party types into a desired api, they all but disappear.

Since JavaScript lacks a complete set of value types (e.g. records, tuples and temporals), purity becomes a matter of discipline, or protocol. Atomic permits even reference types, like objects and arrays, to be optionally, as a matter of protocol selection, treated as value types.

Yet, again, protocols reduce mountains to mole hills. In short, their first-class citizenship status is long overdue.

Premise

Atomic was born out of the question:

Why not do in JavaScript what ClojureScript does, but without the transpiler?

The ephiphany: since languages are just facilities plus syntax, if one can set aside syntax, having the right facilities eliminates build steps.

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

Atomic showcases the Clojure way in build-free JavaScript.

Getting Started

Build it from the command line:

npm install
npm run bundle

Set up your project:

$ mkdir sokoban # for instance
$ cd sokoban
$ mkdir libs
$ touch index.html
$ touch ./libs/sokoban.js
$ touch ./libs/app.js

Copy the Atomic dist folder's contents to the libs folder. Vendoring it permits safe use and alleviate the pressure of keeping up with change.

Copy the following contents to the respective 3 files you just created:

// ./libs/sokoban.js - named for your domain, pure functions go here
import _ from "./atomic_/core.js";
// ./libs/app.js - everything else goes here
import _ from "./atomic_/core.js";
import $ from "./atomic_/shell.js";
import {reg} from "./cmd.js";
import * as s from "./sokoban.js";
<!-- ./index.html  -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Sokoban</title>
    <link rel="stylesheet" href="style.css">
    <script type="module" src="./libs/app.js"></script>
  </head>
  <body>
  </body>
</html>

This set of files hints at an architecture. Your FCIS program begins with a core (sokoban) and shell (app) module of its own. Pragmatically, app may eventually contain the UI logic (and import dom), but it could also be implemented as a headless component to permit a separate ui module. Right now, the UI concern is a long way off.

Stand up the simulation

Your first task, in app, is to create a state container for your world state and define its init state in your pure module. It'll likely be some amalgam of objects and arrays but, depending on the app, it could be anything.

// ./libs/sokoban.js
function init(){
  /* depends on what your app is about */
}
// ./libs/app.js
const $state = $.cell(s.init());

reg({$state}); //register container to aid in interactive development

Then begin fleshing out your core with domain logic, nothing but pure functions and using them to tell your app's story. Everything else including the program machinery (cells, signals, routers, queues, buffers, buses, etc.) and glue code goes into app.

Keep app trivially simple, at first. For a time it'll provide little more than the harness necessary to run the simulation. Then, to begin interacting with it, you'll want to serve it:

$ static . # server of choice

Bring it up in the browser:

http://127.0.0.1:8080/?monitor=*

Remember to add the monitor query param to aid monitoring from the console. Expose your browser's developer tools. From its console enter:

cmd()

This loads the globals needed to facilitate interactive development. You'll be operating from your text editor and browser console for the unforeseeable future.

This'll mean writing some version of the following line:

$.swap($state, /* TBD */); //TODO write a pure function

The TBD part is filled with a pure, swappable function. These are used to drive transitions based on anticipated user actions in the app. This can be done from the code and/or from the browser console.

For a while, you'll be adding different variations of the above line, one after the other, to tell some version of a story your app tells. This is what it means to start with simulation.

This initial work is the sweet spot of functional programming. The essence of "easy to reason about" falls out of the focus on purity. It's hard to beat a model which reduces a program to a flip book, halts time, and permits any page and its subsequent to be readily examined or compared. There's immeasurable good in learning to tease the pure out of the impure, of embracing the boundary between simulation and messy reality.

The core simulates what your program is about and the shell actuates its effects. The core is the domain, playing sokoban or managing to-dos, for example, a library of pure functions. The shell, having little to do the domain, provides the plumbing necessary to make things happen. It transforms effect into simulation and vice versa. Commands flow in. Events flow out. The core directs, the shell orchestrates.

The first objective is to flesh out the core by writing the functions needed to express what the story is about, what the program does. A state container, all by itself, provides sufficient machinery to get you there.

It's only when the core is somewhat complete, the shell is finally connected to a UI.

Stand up the user interface

The guts of most programs can be fully realized from what is effectively the browser command line. The UI, although it comes much later, will eventually be needed. And hooking up both sides of the one-way data flow is how one graduates from simulation to reality.

Subscribe to the simulation and project to the DOM:

$.sub($state, function(state){
  /* render the UI and replace or patch the DOM */
});

Subscribe to the DOM and feed the simulation:

const el = dom.sel1("#sokoban"); //your root element

//prefer event delegation to subscribing to elements directly
$.on(el, "click", "button.up", (e) => $.swap($state, s.up));

$.on(document, "keydown", function(e){
  if (e.key === "ArrowUp") {
    e.preventDefault();
    $.swap($state, s.up);
  }
});

Define intermediary signals if you like:

function which(key){
  return _.filter(_.pipe(_.get(_, "key"), _.eq(_, key)));
}

const $keys = $.chan(document, "keydown");

//create desired signals...
const $up = $.pipe($keys, which("ArrowUp"));
const $down = $.pipe($keys, which("ArrowDown"));
const $left = $.pipe($keys, which("ArrowLeft"));
const $right = $.pipe($keys, which("ArrowRight"));

//...and subscribe to them.
$.sub($up, (e) => $.swap($state, s.up));
$.sub($down, (e) => $.swap($state, s.down));
$.sub($left, (e) => $.swap($state, s.left));
$.sub($right, (e) => $.swap($state, s.right));

//alternately, more concisely, do both at once:
$.sub($keys, which("ArrowUp"), (e) => $.swap($state, s.up));
$.sub($keys, which("ArrowDown"), (e) => $.swap($state, s.down));
$.sub($keys, which("ArrowLeft"), (e) => $.swap($state, s.left));
$.sub($keys, which("ArrowRight"), (e) => $.swap($state, s.right));

While creating a virtual dom had been considered for inclusion in the library, state diffing is not always needed. When needed, compare snapshots instead.

const $hist = $.hist($state);

$.sub($hist, function([curr, prior]){
  /* diff your snapshots */
});

Having access to two frames makes identifying what changed fairly simple. Based on how the data is structured, one can readily check that entire sections of the app are unchanged since data representations are persistent.

That basically means, as a rule, the parts of the data model which haven't changed can be compared cheaply by identity in the current and prior frames. That's because the original objects, if unchanged, will have been reused in the newer snapshot.

The prior snapshot will be null in the very first rendering. That's useful for knowing when to render the full UI or, most of the time, patch it.

Alternately, one can abstract this further.

// pull some list of favorites into its own signal
const $favs = $.map(_.get(_, "favorites"), $state);

As desired, split your app into separate signals. Since these signals automatically perform the identity comparison, they won't fire unless there's been a change.

There's no templating language. Everything is programmatic composition. In this example, ul and li and favorites are all partially-applied functions:

const ul = dom.tag("ul"),
      li = dom.tag("li");

const favorites =
  ul({id: "favorites", class: "fancy"},
    _.map(li, _)); //composed

const target = dom.sel("#favs", el);

$.sub($favs, function(favs){
  dom.html(target, favorites(favs));
});

//a tacit, transduced alternative:
$.sub($favs, _.map(favorites), dom.html(target, _));

But as composing functions can be hard to grasp and harder to debug, when you're not used to it, you can always fall back on functions.

function favorites(favs){
  debugger
  return ul(_.map(li, favs));
}
<div id="favs">
  <!-- `dom.html` overwrites everything or patching not always required -->
  <ul>
    <li>Columbo</li>
    <li>Prison Break</li>
    <li>Suits</li>
    <li>The Good Doctor</li>
    <li>The Gilded Age</li>
  </ul>
</div>

Compose views which read structured data:

const suit = {
  fname: "Harvey",
  lname: "Specter",
  salary: 725000,
  dob: new Date(1972, 0, 22),
  address: ["333 Bay Street", "New York, NY  10001"]
}

const {address, div} = dom.tags(["address", "div"]);

const mailingLabel =
  address(
    div(
      _.comp(_.upperCase, _.get(_, "fname")), " ",
      _.comp(_.upperCase, _.get(_, "lname"))),
    _.map(div, _.get(_, "address")));

dom.append(envelop,
  stamp(),
  returnLabel(),
  mailingLabel(suit));
<address>
  <div>HARVEY SPECTER</div>
  <div>333 Bay Street</div>
  <div>New York, NY  10001</div>
</address>

Progressively enhance

While imperative shell of an app has humble beginnings, one can gradually grafts layers of sophistication onto its reactive core. Keep 'em simple or evolve 'em.

For example, add journal to facilitate undo/redo and stepping forward and backward along a timeline.

Initially, commands are just pure functions and events just native DOM events, but these can be reified into JSON-serializable objects to faciliate being sent over the wire, or recorded in auditable histories. The core can then be wrapped with a command bus api and facilitate a host of middleware features.

It's as much as you want, or as little.

Be ever minding your big picture

The entire effort is preceded and interleaved with thought and/or note-taking. This depends largely on starting with a good data model, anticipating how the UI (and potentially its animations) will look and behave, and having some idea of the evolutionary steps planned for the app.

It may be useful to rough out the UI early on. Thinking through things — ideally, during lunchtime walks! — and clarifying the big picture for how they work and fit together will minimize potential downstream snafus.

Atomic in Action

See these sample programs to learn more: