Skip to content

Commit

Permalink
edit copy
Browse files Browse the repository at this point in the history
  • Loading branch information
Mario T. Lanza committed Dec 27, 2024
1 parent 428ce6c commit 2330ccc
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 157 deletions.
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ Highlights:
* functional core, imperative shell w/ FRP
* [nil-punning](https://ericnormand.me/article/nil-punning) handles null in sensible ways

Atomic is [functions first](docs/functions-first.md). Methods only limit [the places you'll go](https://en.wikipedia.org/wiki/Oh%2C_the_Places_You'll_Go!).
Atomic is [functions first](docs/functions-first.md) as methods only limit [the places you'll go](https://en.wikipedia.org/wiki/Oh%2C_the_Places_You'll_Go!).

Atomic is built around [protocols](src/core/protocols). Since [they're the centerpiece](https://clojure.org/reference/protocols) of Clojure, they are, by extension, Atomic too. They provide the only safe means of [dynamically extending natives and third-party types](./docs/protocols-for-dynamic-extension.md). They make [cross-realm operability](./docs/cross-realm-operability.md) possible. They also, for the good of the functional paradigm, shift focus to thinking [abstractly about apis and behaviors](./docs/abstraction-thinking.md) over types.

Since JavaScript lacks a complete set of value types (e.g. [records, tuples](https://tc39.es/proposal-record-tuple/) and [temporals](https://github.com/tc39/proposal-temporal)), reference types can be used instead. Maintaining purity, thus, becomes a matter of discipline. In Atomic reference types like objects and arrays can be optionally, as a matter of protocol selection, [treated as value types](./docs/command-query-protocols.md).
Since JavaScript currently lacks compound value types such as [records, tuples](https://tc39.es/proposal-record-tuple/) and [temporals](https://github.com/tc39/proposal-temporal), reference types can be used instead. Purity is instead maintained as a matter of discipline. In Atomic reference types like objects and arrays can be optionally, as a matter of protocol selection, [treated as value types](docs/mutables-for-immutables.md).

Atomic has structures comparable to Clojure's [maps](https://clojuredocs.org/clojure.core/hash-map) and [vectors](https://clojuredocs.org/clojure.core/vector) as well as seamless [Immutable.js](https://immutable-js.com) integration. Since objects and arrays are cheap and usually perform well enough, they're to be preferred. Plus, due to protocols, if the need arises, one can always drop in a replacement type later with almost no refactoring. Mountains are reduced to mole hills!

Expand Down Expand Up @@ -93,7 +93,7 @@ import {reg} from "./libs/cmd.js";
These initial files hint at a [FCIS](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) architecture. It has a core (`sokoban`) and shell (`main`) module of its own. Pragmatically, `main` may eventually contain the GUI logic and utilize the `dom` import, but right now that's a long way off.

### Stand up the simulation
Your first task, in `main`, is to create a state container for your [world state](https://docs.racket-lang.org/teachpack/2htdpuniverse.html) and define its `init` state in your pure module. It'll likely be a compound data structure (i.e., objects and arrays), but it could be anything.
Your first task in `main` is to create a state container for your [world state](https://docs.racket-lang.org/teachpack/2htdpuniverse.html) and define its `init` state in your pure module. It'll likely be a compound data structure (i.e., objects and arrays), but it could be anything.

The data structure I chose is roughly as follows, but model yours as you see fit. There's not one right way of doing this.

Expand Down Expand Up @@ -132,7 +132,7 @@ reg({$state, s}); //registry aids interactivity
```
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.
Keep `main` trivially simple at first. For a time this lone atom provides all the harness necessary to run the simulation.

The `reg` line in `main` is important. Ensure you grasp how the registry is used to facilitate [interactive development](./docs/interactive-development.md) as that's central to everything.

Expand Down Expand Up @@ -187,15 +187,13 @@ $.swap($state, s.up); //nonconfigured counterpart/alternative
$.swap($state, s.move("down"));
$.swap($state, s.down);
```
The above separation of files illustrate well the pendulum of initial activity. You write functions in `sokoban` only to execute them in `main` and/or from the console. This facilitates your telling some version of a story your app tells. This is what it means to [start with simulation](docs/start-with-simulation.md).
The above separation of files illustrate well the pendulum of initial activity. You write functions in `sokoban` only to execute them in `main` and/or from the console. This enables your app to tell its story. This is what it means to [start with simulation](docs/start-with-simulation.md).

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 and compared. There's immeasurable good in learning to tease the pure out of the impure, of embracing the boundary between simulation and messy reality.
Telling a story from the confines of an atom is what makes this approach a pleasure. The value in learning to tease the pure from the impure is the resulting simulation is far easier to reason about, use, and maintain than the messy reality of imperative code. 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 and compared.

The domain module (the core) simulates what your program is about, the main module (the shell) actuates its effects. The domain module, playing [Sokoban](https://github.com/mlanza/sokoban/blob/main/sokoban.js) or managing [To-dos](https://github.com/mlanza/todo/blob/main/todo.js), 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 domain module (the core) simulates what your program is about, the main module (the shell) actuates its effects. The domain module, playing [Sokoban](https://github.com/mlanza/sokoban/blob/main/sokoban.js) or managing [To-dos](https://github.com/mlanza/todo/blob/main/todo.js), for example, is a library of pure functions. The main module 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 connected to a UI.
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. That and an atom is all you initially need. It's only when that's mostly done, the shell gets its UI.

### Stand up the user interface

Expand Down
4 changes: 2 additions & 2 deletions docs/abstraction-thinking.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type ShoppingCart =
| PaidCart of PaidCartData
```

The 3 concrete types here fall under the abstraction `Cart`. Where the former example offers an umbrella category, Shape, and the 3 disparate types the program is aware of, the latter example offers a single concept, Cart, in 3 different forms.
The 3 concrete types here fall under the abstraction `ShoppingCart`. Where the former example offers an umbrella category, `Shape`, and the 3 disparate types the program is aware of, the latter example offers a single concept, `ShoppingCart`, in 3 different forms.

Protocols, being just another approach to polymorphism, can well handle both scenarios. Furthermore, because JavaScript is a dynamic language, they can be adapted at runtime by anyone who wants to extend the abstraction. This may be the original developer or a third-party developer.

Expand All @@ -59,7 +59,7 @@ Seed the atom with an initial state:
const $cart = $.atom(emptyCart);
```

Provide a [persistent command](./command-query-protocols.md) for transitioning the state:
Provide a [faux command](simulating-actuating.md) for transitioning the state:

```js
function paid(payment){
Expand Down
79 changes: 5 additions & 74 deletions docs/adopting-the-clojure-mindset.md
Original file line number Diff line number Diff line change
@@ -1,81 +1,12 @@
# Adopting The Clojure Mindset

Most languages have reference types and value types, mutables and immutables. JavaScript is no different, but has gaps in its value types (e.g. [records and tuples](https://github.com/tc39/proposal-record-tuple) and [temporals](https://github.com/tc39/proposal-temporal)).
There is a distinction between [simulating and actuating](./simulating-actuating.md) whose understanding is foundational to the functional paradigm. In Atomic one keeps state in an atom and [`swaps`](https://clojuredocs.org/clojure.core/swap!) updates against it using simulated commands to models effects.

And while functional programming does better when a robust set of value types are present, it's not seriously hindered when they're not. It can treat reference types as value types. That said, although Atomic provides several types of maps, sets, etc., it will usually suffice to use plain old objects and arrays and to consider alternatives only when performance becomes a concern.
When one realizes how any mutable operation can be [first simulated](./start-with-simulation.md) he discovers he can fit any domain inside an atom. To do so he must model a data structure to represent the domain information and write the reductive operations as simulated commands to manipulate it. This, in turn, further reveals how all programs are, at their very centers, [reductions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce).

Briefly, recall how [command-query separation](./command-query-separation.md) expects queries to return a value, but not commands. The stark absence of a return value from a function identifies it as a command.
That is, every user story is told by an atom seeded with an initial value and reduced one operation after another, by a potentially indefinite series of simulated commands swapped against it. The result of simulating (in the atom) before actuating (reflecting change in the environment, data stores and/or interface) is a program doing essentially the same things but with another layer.

```js
const obj = {title: "Lt.", lname: "Columbo"};
const shows = ["Columbo", "The Good Doctor"];
```
Still, it would be short sighted to conclude the extra layer only serves to add complexity. It almost always pays for itself. It provides a useful boundary between where effects are actuated and its more important logical domain, making it far easier to understand, test and maintain the latter. It's the compelling motivation for learning to tease the pure out of the impure.

|Action|Pure World (`core`) |Impure World (`shell`)|
|-|-|-|
|Read property|`_.get(obj, "lname")`|`_.get(obj, "lname")`|
|Write property|`_.assoc(obj, "lname", "Specter")` | `$.assoc(obj, "lname", "Specter")`|
|Add element|`_.conj(shows, "Suits")` | `$.conj(shows, "Suits)` |
This cornerstone for how Clojure models state change is what Atomic adopted. Although Atomic offers some persistents which compare to Clojure's, plain objects and arrays are usually preferred. They're cheap, convenient, and sufficiently performant for most situations.

The above demonstrates a couple important ideas.

Some operations, like `get`, are naturally queries, and can be used in either the pure or impure part of a program without causing harm. Because queries are safe they move freely to both spaces. But because naturally impure, mutable operations, like `$.assoc` can cause harm, they can't. Rather one must write a safe, simulated version of the command (`_.assoc`) or, rather, reduce it to a query for this to happen.

Consider what `$.assoc` is about. It is an operation which adds a property/value pair to some entity/object by mutating it. The `_.assoc` version simulates that effect. Thus, `$.assoc` has a side effect while `_.assoc` does not. The actuating/simulating command divide is visibly demonstrated in the module from which it's imported—`shell` as `$` and `core` as `_`.

In each module there is an identically named `IAssociative` protocol presenting an `assoc` operation. The module of its origin, not the name, defines its identity and purpose. The one module actuates effects, the other simulates them.

Recall per [command-query separation](./command-query-separation.md) commands ordinarily return nothing. This is useful. Because in one instance you write an operation which takes a subject and its operands, actuates some effect against the subject and returns nothing. In the other you write an operation which takes a subject and its operands and returns a replacement subject, the subject as it would exist had the side effects been applied directly to it. A command's natural lack of a return value makes this possible.

In both instances `assoc` has the veneer of a command—that is, an operation which changes an object in some way. The one actually does and the other provides an updated copy of the original so as to maintain purity. This distinction is everything.

Its divide revolves around atoms. Some data structure is held in an atom, so that its contents can be swapped. The divide made possible by simulated commands and atoms allows a program to separate the pure from the impure. It relegates the mutation away from the object snapshot held in an atom and to the atom itself.

The atom's contents are cleanly replaced so the object(s) it holds is never actually mutated. Only the atom's bucket is mutated. Its contents are swapped, one image for another.

The impure, messy world has no atom and applies effects directly against subjects:
```js
//basis for mutable `assoc` protocol
function assoc(self, key, value){ //command/actuated
self[key] = value;
//no return value;
}

const harvey = {lname: "Specter"};
$.assoc(harvey, "fname", "Harvey");
const fname = harvey.fname; // "Harvey"
```

The *purer* world relies on an atom to dramatically constrain the how and where of mutation:
```js
//basis for immutable `assoc` protocol
function assoc(self, key, value){ //query/simulated
const replacement = {...self};
replacement[key] = value;
return replacement; //return value
}

const $harvey = $.atom({lname: "Specter"});
$.swap($harvey, _.assoc(_, "fname", "Harvey"));
const fname = _.chain($harvey, _.deref, _.get(_, "fname")); // "Harvey"
```

I say "purer" because although the mutation has not been eliminated it has been neatly managed. Purity has been introduced and confined to the atom.

The `$.assoc` function is a command. It actuates.

The `_.assoc` function is a query. It simulates. It is a special kind of query, what I call a simulated command, a faux command, or a persistent command. The *persistent* correlates to persistent types which are types designed around and optimized for simulated effect.

Thus, `assoc` is a command which was ported from the impure realm to the pure and, thus, spans both. The same with `conj` and countless other commands.

All simulation requires is an atom and a protocol which models effects with simulated commands. The atom keeps the state and uses them to [`swap`](https://clojuredocs.org/clojure.core/swap!) updates against it.

This reveals how any mutable operation can be simulated, which is to say written as a reductive operation. It further reveals how all programs are, at their very centers, [reductions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce)—that is, some initial value (held by an atom) and a potentially indefinite series of operations (simulated commands swapped against the atom) for advancing the user story.

That cornerstone for how Clojure models state change is what Atomic adopted. JavaScript, unlike Clojure, does not have a robust set of persistent types. So Atomic uses reference types and simulated commands to the same end. In practice, this proves performant enough to be of little concern.

## To What End?

There's an immense value proposition in learning to tease the pure out of the impure. While the result of simulating (in the atom) before actuating (reflecting change in the environment, data stores and/or interface) is a program doing essentially the same things but with another layer, it would be short sighted to conclude it only adds complexity.

The added layer almost always pays for itself. It provides a useful boundary between where effects are actuated and the more important logical domain, making it far easier to understand, test and maintain the latter.
24 changes: 0 additions & 24 deletions docs/command-query-protocols.md

This file was deleted.

Loading

0 comments on commit 2330ccc

Please sign in to comment.