Skip to content

Commit

Permalink
add clojure mindset and addressable data pages
Browse files Browse the repository at this point in the history
  • Loading branch information
Mario T. Lanza committed May 28, 2024
1 parent 3c034b5 commit ac30afa
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 34 deletions.
61 changes: 27 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,45 @@ Highlights:

* well suited for web apps
* deploy the code you write — [point-free pipelines and partial application](/placeholder-partial.md), no build required
* implements much Clojure's standard library
* implements much of Clojure's standard library
* [functional core](src/core), [imperative shell](src/shell) w/ FRP
* [nil-punning](https://ericnormand.me/article/nil-punning) handles null in sensible ways

Atomic is [protocol oriented](src/core/protocols) to its very foundation. Objects are treated as [abstract types](https://en.wikipedia.org/wiki/Abstract_data_type) 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](https://en.wikipedia.org/wiki/Monkey_patch). [Protocols](https://clojure.org/reference/protocols), which are the most apt solution to this problem, have [yet to be adopted](https://github.com/tc39/proposal-first-class-protocols) into the language.
Atomic is [protocol oriented](src/core/protocols) to its very foundation. Objects are treated as [abstract types](https://en.wikipedia.org/wiki/Abstract_data_type) per their behaviors, without regard to their concrete types.

Atomic is [functional first](functional-first.md). Functions are preferred to methods. This makes sense given how protocols treat things as abstractions.
[Protocols](https://clojure.org/reference/protocols) are the centerpiece of Clojure and, by extension, Atomic. They offer a flavor of polymorphism that sets Clojure apart as a data-transforming juggernaut. Clojure would not be Clojure without them!

Atomic has no [maps](https://clojuredocs.org/clojure.core/hash-map) or [vectors](https://clojuredocs.org/clojure.core/vector) but used objects and arrays as faux [records and tuples](https://tc39.es/proposal-record-tuple/) since before these new types were even proposed. It had previously integrated them via [Immutable.js](https://immutable-js.com) but, in practice, using objects and arrays as persistents worked well enough it wasn't worth the cost of another library. Its integration [was dropped](https://github.com/mlanza/atomic/commit/8e1787f6974df5bfbb53a371a261e09b5efee8ee). This bit of history serves to demonstrate how protocols can seamlessly integrate disparate types into a consistent api. Concrete types, even third-party ones, can be regarded as abstract things, by their apis.
Protocols also provide the only safe means of dynamically [extending natives and third-party types](https://en.wikipedia.org/wiki/Monkey_patch). In short, [its first-class citizenship status](https://github.com/tc39/proposal-first-class-protocols) is long overdue. Atomic fills the gaping hole.

Atomic is [functional first](functional-first.md). Functions are preferred to methods. This makes sense given the preference for abstractions over concretions.

Atomic has no [maps](https://clojuredocs.org/clojure.core/hash-map) or [vectors](https://clojuredocs.org/clojure.core/vector). It doesn't need them. It treats objects and arrays as value types until [records and tuples](https://tc39.es/proposal-record-tuple/) arrive.

In practice, this worked so well the [Immutable.js](https://immutable-js.com) integration [was dropped](https://github.com/mlanza/atomic/commit/8e1787f6974df5bfbb53a371a261e09b5efee8ee). It wasn't worth the cost of loading the library. This bit of history is noted only to demonstrate how, through protocols, third-party types can be easily and seamlessly integrated into any standardized api.

## Premise
Atomic was born from an experiment answering:

> Why not do ClojureScript directly in JavaScript and eliminate the transpiler?
> 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, the right facilities can eliminate build steps.
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.

* [first-class protocols](https://github.com/tc39/proposal-first-class-protocols)
* [records & tuples](https://github.com/tc39/proposal-record-tuple) (including sets and typed records & tuples!)
* [records & tuples](https://github.com/tc39/proposal-record-tuple)
* [partial application](https://github.com/tc39/proposal-partial-application)
* [pipeline operator](https://github.com/tc39/proposal-pipeline-operator)
* [temporal](https://github.com/tc39/proposal-temporal)

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 only protocols provide. They're the centerpiece of Clojure and, by extension, Atomic. Clojure would not be Clojure without them!

Atomic provides the necessary facilities and showcases how even plain JavaScript can adopt the Clojure mindset!
Atomic provides facilities to showcase how any language—even JavaScript!—[can adopt the Clojure mindset](adopting-the-clojure-mindset.md).

## Purity Through Protocol

Since JavaScript lacks certain value types (i.e. [records and tuples](https://tc39.es/proposal-record-tuple/) and [temporals](https://github.com/tc39/proposal-temporal)), purity has historically been gained through discipline. Atomic makes this still easier.

It permits reference types to be optionally treated as immutable value types. It provides mutable and/or immutable protocols for interacting with natives so arrays can double as [tuples](https://tc39.es/proposal-record-tuple/) and objects as [records](https://tc39.es/proposal-record-tuple/). Yet, again, protocols reduce mountains to mole hills.
It permits reference types to be optionally treated as value types. It provides mutable and/or immutable protocols for interacting with natives so arrays can double as [tuples](https://tc39.es/proposal-record-tuple/) and objects as [records](https://tc39.es/proposal-record-tuple/). Yet, again, protocols reduce mountains to mole hills.

## Getting Started

Expand All @@ -53,25 +56,17 @@ npm run bundle

Copy the contents of `dist` to `libs` in a project then import from either `libs\atomic` or `libs\atomic_` depending on whether [placeholder partial](./placeholder-partial.md) is wanted.

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

## Modules

The `core` module is the basis for the functional core, `shell` for the imperative shell and `dom` for the UI. Elm sold FRP, so by the time CSP appeared in `core.async` that ship had already sailed. `shell` is based on reactives.

Its state container, the bucket which houses an app's big bang [world state](https://docs.racket-lang.org/teachpack/2htdpuniverse.html), 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](https://rxjs.dev/guide/subject) does. This is well suited to the developing of user interfaces. And like [xstream](https://staltz.com/why-we-built-xstream.html) it doesn't rely on many operators, providing a simple but sufficient platform for FRP.
A typical app imports the trifecta—`core`, `shell`, and `dom`—as `_`, `$` and `dom` respectively. These provide what's necessary for building a functional core, an imperative shell, and a user interface, everything an app needs. These modules hint at a 3-part architecture—a core, shell, and ui. Pragmatically, the shell will often also contain the ui, so 2 parts (a `core` and a `shell` module) will usually be good enough.

The typical UI imports the trifecta—`core`, `shell`, and `dom`—as `_`, `$` and `dom` respectively. The `_` doubles as a partial application placeholder when using [placeholder partial](./placeholder-partial.md). [To facilitate interactive development](./interactive-development.md) these can be readily imported into the console.
To facilitate [interactive development](./interactive-development.md) these modules can be readily loaded into the console. The `_` doubles as a [placeholder for partial application](./placeholder-partial.md).

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:
* [`swap`](https://clojuredocs.org/clojure.core/swap!)
* [`get`](https://clojuredocs.org/clojure.core/get)
* [`update`](https://clojuredocs.org/clojure.core/update)
* [`updateIn`](https://clojuredocs.org/clojure.core/update-in)
* [`assoc`](https://clojuredocs.org/clojure.core/assoc)
* [`assocIn`](https://clojuredocs.org/clojure.core/assoc-in)
A functional component uses a state container, the bucket which keeps its [world state](https://docs.racket-lang.org/teachpack/2htdpuniverse.html). In Atomic this is a `cell`. It's mostly equivalent to a Clojure atom. The only significant difference is it invokes its callback upon subscription the way an Rx [subject](https://rxjs.dev/guide/subject) does.

The beauty of these functions (kudos to Hickey!) is how easy they make it to surgically replace the state held in a state container without mutating it.
Since Elm had already sold FRP by the time CSP appeared in `core.async`, Atomic is based on reactives and state containers. The [world state is addressable](./addressable-data.md) and can be updated using Clojure's standard functions for surgical state updates.

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)
Expand All @@ -81,25 +76,23 @@ In the absence of threading macros and pipeline syntax several functions exist (

## Vendoring As A Safety Net

The author's philosophy is to sparingly add libraries, to keep projects lean. [Dependencies breed](https://tonsky.me/blog/disenchantment/). The ever-changing landscape of modern libraries (Vue, React, Angular, Svelte, etc.) brim with excellent ideas, yet the author continually ships working software without them.

Rather, and only when a deficit is felt, are the ideas, not the dependencies, grafted in. This prevents fast-moving vendors from dictating schedules and alleviates the pressure of falling out of step with the latest release.
The author prefers to sparingly add libraries, to [keep projects lean](https://tonsky.me/blog/disenchantment/). The gradual extension of a code base is preferred to adding a new dependency to meet every need. This conservative approach keeps faster-moving parties from dictating schedules.

Because Atomic has been used primarily by a small, internal audience, the change process hasn't been formalized to protect a wider audience. The author [vendors it](https://stackoverflow.com/questions/26217488/what-is-vendoring) into his projects to eliminate the pressure of keeping up with releases. This permits safe use.
Because Atomic has been used primarily by a small, internal audience, the change process hasn't been formalized to protect a wider audience. The author [vendors it](https://stackoverflow.com/questions/26217488/what-is-vendoring) into his projects. This practice alleviates the pressure of keeping up with releases and permits safe use.

## 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.
Start by housing a world state made up of plain objects and arrays in a state container. It'll likely have been created via an `init` function or loaded from a data store.

Then write [swappable](https://clojuredocs.org/clojure.core/swap!) 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.
Then write pure, [swappable](https://clojuredocs.org/clojure.core/swap!) functions which drive transitions based on anticipated user actions. These will be used later to actuate side effects in the imperative shell/UI layer(s).

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.
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 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](https://www.youtube.com/watch?v=f84n5oFoZBc), 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.
This entire effort begins with [forethought](https://www.youtube.com/watch?v=f84n5oFoZBc), 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 helps 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.
If an app involves animation, ponder this aspect too. How one renders elements which are animated is often different from how one renders those which aren't. Fortunately, modern CSS can now do what once required libraries.

## Progressive Enhancement

Expand Down
25 changes: 25 additions & 0 deletions addressable-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Addressable Data

Functional programming relies on addressable data. Instead of having lots of objects each containing bits of data, a program/component stores its state in a single state container. And since most programs deal in dozens of entities and disparate concepts, the state held in the container will usually be a structured, mashup of lots of things.

The developer, having devised that structure, will have a strategy for querying and updating it. In some respects, he will select and update as he would any large structure (e.g. the DOM). This is made possible through Clojure's protocols for querying, updating and overwriting data.

* [`get`](https://clojuredocs.org/clojure.core/get) — reading
* [`update`](https://clojuredocs.org/clojure.core/update) — update a property
* [`updateIn`](https://clojuredocs.org/clojure.core/update-in) — update given a path
* [`assoc`](https://clojuredocs.org/clojure.core/assoc) — overwrite a property
* [`assocIn`](https://clojuredocs.org/clojure.core/assoc-in) — overwrite given a path

With just a handful of functions, he surgically updates the data held in the container.

With Atomic functions, the names which match Clojure functions are usually the functional equivalent of Clojure's. This means one can usually use the Clojure documentation to understand what a function does.

Like Clojure, addressable data is replaced via a succession of [swap!](https://clojuredocs.org/clojure.core/swap!) operations.

```js
const $state = $.cell(/* real estate data */);
$.swap($state, _.assocIn(_, ["property-manager", "address", "street"], "101 Boardwalk Place"));
const condo = _.chain($state, _.deref, _.get(_, "condos"), _.detect(_.comp(_.gte(_, 500000), _.get(_, "price")), _));
$.swap($state, _.updateIn(_, ["condos", condo, "price"], _.mult(_, .9))); //offer 10% discount
$.swap($state, _.assoc(_, "last-modified", _.date()));
```
Loading

0 comments on commit ac30afa

Please sign in to comment.