Write ClojureScript in JavaScript without a transpiler.
Highlights:
- well suited for web apps
- deploy the modules you write, not bundles, no build required
- point-free pipelines and partial application
- use a familiar Clojure api and way of thinking
- functional core, imperative shell w/ FRP
- nil-punning handles null in sensible ways
Atomic is protocol oriented to its very foundation. Since protocols are the centerpiece of Clojure, they are, by extension, Atomic too. They provide the only safe means of dynamically extending natives and third-party types. They make cross-realm operability possible. Plus, it's better to think abstractly about apis and behaviors, giving them greater attention than types.
Atomic is functional first. This makes sense given that functions, not methods, are first class. Why choose a paradigm which limits the places you'll go.
Atomic has Clojure-like maps and vectors via atomic/immutables
(imported as imm
) courtesy of an integrated Immutable.js and its imm.map
and imm.list
. Protocols so seamlessly blend third-party types into an api, except for their instantiation, one might not even notice their use. Chalk up another one for protocols!
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. In many cases, natives perform well enough to not warrant loading the immutables library. For heavier lifts, load it and drop a persistent into your constructors. You're done!
Yet, again, protocols reduce mountains to mole hills. In short, their first-class citizenship status is long overdue.
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.
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 sokoban.js
$ touch main.js
$ touch main.css
Copy the Atomic dist
folder's contents to the libs
folder. Vendoring it permits safe use and alleviates the pressure of keeping up with change.
Copy the following contents to the respective 3 files you just created:
// ./sokoban.js - named for your domain, pure functions go here
import _ from "./libs/atomic_/core.js";
// ./main.js - everything else goes here
import _ from "./libs/atomic_/core.js";
import $ from "./libs/atomic_/shell.js";
import {reg} from "./libs/cmd.js";
import * as s from "./sokoban.js";
<!-- ./index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Sokoban</title>
<script type="module" src="./main.js"></script>
<link rel="stylesheet" href="./main.css">
</head>
<body>
</body>
</html>
This set of files hints at an architecture. Your FCIS program begins with a core (sokoban
) and shell (main
) module of its own. Pragmatically, main
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.
Your first task, in main
, 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.
// ./sokoban.js
function init(){
/* depends on what your app is about */
}
// ./main.js
const $state = $.atom(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 (atoms, signals, routers, queues, buffers, buses, etc.) and glue code goes into main
.
Keep main
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 the following line over and over:
$.swap($state, /* TODO */);
Plug $.swap
with a pure, swappable function. The functions you supply drive transitions based on anticipated user actions. They can be authored/issued via the browser console and/or the code.
For a while, you'll be writing and issuing pure functions to tell some version of a story your app tells. This is what it means to start with simulation.
This makes functional programming a pleasure. 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 domain module (the core) simulates what your program is about, the main module (the shell) actuates its effects. The domain module, playing Sokoban or managing To-dos, for example, is a library of pure functions. The main module, 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.
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>
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.
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.
See these sample programs to learn more: