divs1210 / atomic

Write Clojuresque functional core, imperative shell programs in JavaScript.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Atomic

A wholemeal, Clojure-inspired approach for programming in JavaScript which by default treats its native data types as immutables.

Highlights:

  • functional composition via partial application and pipelines
  • command-query separtion as a rule of thumb
  • functions over methods
  • core (much of the Clojure standard lib)
  • protocols (to the very foundation)
  • reactives (FRP)
  • transducers

Atomic provides a robust core for web development sufficient enough to meet standard use cases without additional libraries.

Atomic provides a uniform functional api. Arguments are explicitly passed to functions with functions preferring an explicit self in the first position. The use of this is all but eliminated.

To aid REPL-driven development type properties are treated as private as a matter of discipline and not actually hidden.

Atomic encourages one to think about objects as abstract types rather than concrete types. This falls out of protocol-based apis. Don't ask "What's your type?" but rather "What's your behavior? Your interface?" Take the common api of cells, subjects, and observables. When they're passed around a function needn't mind what it has but rather the role what it has plays.

Atomic protocols exist in the absence of TypeScript. This makes it possible to integrate third-party types such as the persistents found in Immutable.js while maintaining a familiar api.

At a time when pipeline operators and partial application have not yet landed in JavaScript proper, point-free programming can be used without a build step. This is what autopartial affords and this is what'd be required without it.

Forward

Atomic has been used in production since its 2015 release, originally as AMDs but now ES6 modules. There was no fanfare because there was no announcement. And no announcement because, at the time, it was still an experiment aimed at answering: how well does the Clojure mindset fare in the land of JavaScript?

ClojureScript had already answered. Splendidly! But those suffering the weight of transpiled cljs may have wondered, can't this be done without a build step and without the barf of transilation? And can't the necessary types for supporting Clojure idioms be implemented directly in JavaScript?

Atomic is usable, 1.0 complete, and production-ready. And while this has long been the case, it was kept under wraps. This was done to avoid the open source shackles of issue and enhancement requests and the concern that a change might break something someone else depends on. The door is being opened a wee bit, in in response to recent inquiry, and to help others vet the same questions.

Atomic can be freely used but, for now, fork and maintain your own version. Work your own experiment. While it is stable and production ready, the typical use-at-your-own-risk disclaimer applies.

Getting Started

Build the dist from the command line using:

npm install
npm run bundle

Include the contents of this folder under lib in a project then import from either lib\atomic or lib\atomic_ depending on whether autopartial is wanted. With it one gets partial application and no build step.

Modules

The core module provides what's needed in most general situations. 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.

The atom equivalent and the type which houses an app's big bang world state is cell. From it observables/signals are derived. It's only significant difference from an atom is how, like a subject, in addition to on change, it invokes its callback on subscribe as well. This seemed to make sense in most UIs it was used to build.

Like xstream it doesn't rely on many operators. And personal experience has proven UIs work better with hot than with cold observables, so some of these notions are baked into the defaults.

The holy trinity of modules is core, reactives, dom and, if a forth, transducers. The others were situationally used. And because protocols are usually sufficient on their own, in practice, Clojure-like multimethods were rarely implemented.

Although Clojure's transients were implemented so all the familiar functions could be used with mutables too, in practice, this module was much less used. Rather, one usually fell back on treating objects and arrays, privately, as the mutables they actually are. In some situations, like the dom, where there was no fallback, it found use.

The immutables module wraps Immutable.js types. This was grafted in to provide true persistents, however, in practice, it was rarely used. First, there was the cost of having to marshal this dependency over the network. Second, when native arrays and objects are treated like immutables—as all the modules do by default!—they're going to give you what you require most of the time. Records and tuples will eventually fill this gap.

The boon of protocols is how they seamlessly wrap third-party types with familiar (and uniform!) apis. This makes it easy to experiment with new types while keeping a consistent api. And when a concrete type gets ditched, a substitution can be made and the program(s) which rely on it need never be the wiser!

Typical module assignments follow:

  • _core (it also doubles as a partial application placeholder)
  • $reactives
  • domdom
  • ttransducers
  • muttransients

These assignments can be readily imported by entering cmd() from a browser console where Atomic is loaded. Obviously, this facilitates interactive, REPL-driven development.

See the various READMEs disbursed among the source modules for a bit more help.

Guidance for Writing Apps

Start with a functional core, a persistent type or compound structure which represents the world state. For this plain objects and arrays are often good enough. Just, as a matter of discipline, don't mutate them! The UI can come later, sometimes after the functional core is nearly complete.

Birth the world state with an init function and wrap it 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 UI layer.

If the benefit justifies the extra work, add a layer of commands and events, both just plain messages (DTOs). Such messages are terrific for over-the-wire transmission (as JSON) and for keeping logs. When recorded they provide a complete and auditable history.

The very essence of "easy to reason about" comes from functional cores whose data can be readily examined in a REPL (browser console). Being able to examine the world state after each and every transition means you can quickly pinpoint a flawed function.

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

One learns a functional core can often be written before a UI is ever started. However, not all apps have data which is simple enough to visually digest from the REPL. In such cases, to improve the feedback loop, it may be necessary to start and develop the UI in parallel.

It is useful to 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 helps guide the work. And all this helps curtail snafus. Not everything needs working out, but have a sense of how things fit together and visually work before writing the first line of code. Keep notes!

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. In this ever changing landscape, the author has met customer requirements without necessitating any of the many modern libraries (Vue, React, Angular, Svelte, etc.) though they were examined for their ideas.

Sanity is won when vendors are neither driving our work nor requiring updates and upgrades on their schedules.

Atomic in Action

Atomic has been used for developing apps, business and personal, in production, reliably for years and has recently been used to create digital card and board games.

These examples are sufficient for demonstrating and digesting the concepts explained above:

All followed the above guideance. The dom events of these apps are typically handled using an $.on which is similar to jQuery's. While creating and diffing a virtual dom has been considered, experience has shown other approaches work well too. In some apps $.hist provides two frames (the present and the immediate past) of world state history which can be diffed whenever convenient. In practice, however, for simpler UIs, this extra effort isn't usually needed.

Once one of the above small apps is understood, build one. This orients a dev. Familiarity with Clojure's standard library helps.

Larger and more robust projects exist and serve private customers and, for that reason, cannot be shared. It has been used on typical websites hosted on Cloudflare as well as on SharePoint sites and in Power Apps.

Its author, perhaps obviously so by now, is a strategy board games superfan!

About

Write Clojuresque functional core, imperative shell programs in JavaScript.


Languages

Language:JavaScript 98.8%Language:CSS 1.1%Language:HTML 0.1%