diff --git a/README.md b/README.md index 16810f78..ad973156 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Atomic is [protocol oriented](src/core/protocols) to its very foundation. Since Atomic is [functional first](docs/functional-first.md). This makes sense given that functions, not methods, are first class. Why choose a paradigm which limits [the places you'll go](https://en.wikipedia.org/wiki/Oh%2C_the_Places_You'll_Go!). -Atomic has Clojure-like [maps](https://clojuredocs.org/clojure.core/hash-map) and [vectors](https://clojuredocs.org/clojure.core/vector) via `atomic/immutables` (imported as `imm`) courtesy of an integrated [Immutable.js](https://immutable-js.com) 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! +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. Protocols make data type substitution less of a feat. So when a more suitable type is found or created it can be dropped in with little to no refactoring. 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)), 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](./docs/command-query-protocols.md). 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! @@ -44,6 +44,7 @@ Build it from the command line: npm install npm run bundle ``` +> 💡**Recommendation**: Build the classic [Sokoban](https://en.wikipedia.org/wiki/Sokoban) game. See [mine](https://github.com/mlanza/sokoban). It's not overly challenging and it's certainly more fun than a counter or a to-do app. Don't copy mine. Follow the path being demonstrated, but choose your own graphics and implement in a way which makes sense to you. Set up your project: @@ -56,10 +57,9 @@ $ touch sokoban.js $ touch main.js $ touch main.css ``` - Copy the Atomic `dist` folder's contents to the `libs` folder. [Vendoring it](https://stackoverflow.com/questions/26217488/what-is-vendoring) permits safe use and alleviates the pressure of keeping up with change. -Copy the following contents to the respective 3 files you just created: +Copy the following contents respectively to the 3 files you created: ```javascript // ./sokoban.js - named for your domain, pure functions go here @@ -70,8 +70,9 @@ 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 dom from "./libs/atomic_/dom.js"; import * as s from "./sokoban.js"; +import {reg} from "./libs/cmd.js"; ``` ```html @@ -89,56 +90,107 @@ import * as s from "./sokoban.js"; ``` -This set of files hints at an architecture. Your [FCIS program](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) 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. +This set of files hints at an architecture. Your [FCIS program](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) begins with 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 some amalgam of objects and arrays but, depending on the app, 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 (of objects and arrays), but it could be anything. + +The data structure I chose looks roughly like the following, but model yours in whatever way makes sense to you. ```javascript // ./sokoban.js function init(){ - /* depends on what your app is about */ + const _ = "void", + b = "building", + g = "ground", + w = "water" + x = "dest"; + return { + worker: [6,4], + crates: [[3,3],[3,4],[4,4],[3,6]], + fixtures: [ + [_, b, b, b, b, _, _, _], + [_, b, g, g, b, b, b, b], + [_, b, g, g, b, g, g, b], + [_, b, g, x, g, g, g, b], + [b, b, g, x, x, g, g, b], + [b, g, g, w, x, b, b, b], + [b, g, g, g, g, g, b, _], + [b, g, g, b, g, g, b, _], + [b, b, b, b, g, g, b, _], + [_, _, _, b, b, b, b, _] + ] + } } ``` ```javascript // ./main.js const $state = $.atom(s.init()); -reg({$state}); //register container to aid in interactive development +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. Then, to begin interacting with it, you'll want to serve it: +Keep `main` trivially simple, at first. For a time it'll provide little more than the harness necessary to run the simulation. + +The `reg` line in `main` is important. Take a detour and understand [how it faciliates interactive development](./docs/interactive-development.md) before continuing. + +To begin interacting with the app, you'll need to serve it: ```sh $ static . # server of choice ``` -Bring it up in the browser: +Launch the app in the browser, remembering the `monitor` query parameter: [http://127.0.0.1:8080/?monitor=*](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: +Finally, to bootstrap the command line, expose the browser's Developer Tools and from its console enter: ```sh cmd() ``` -This loads the globals needed to facilitate [interactive development](./docs/interactive-development.md). You'll be operating from your text editor and browser console for the unforeseeable future. +Anticipate operating from your text editor and browser console for the unforeseeable future. This'll involve writing commands (pure functions), adding them to and exporting them from the core module (e.g., `sokoban`), and routinely issuing swaps against your atom. + +Plug `$.swap` with a pure, [swappable](https://clojuredocs.org/clojure.core/swap!) function, some command for driving state transitions based on anticipated user actions. These commands can be issued via the browser console and/or the `main` module. Waffle between both. Use whichever you prefer. The `main` module is useful for recording command sequences. + +Don't worry about what goes into `main` in the early stage. It's temporary at best. Fleshing out the core module is the initial focus. -This'll mean writing the following line over and over: +The functions you write must at minimum receive the app state as an argument, but they'll oft be accompanied by other arguments to permit configurability. The choice for whether or not a command is configurable is yours. In many instances it's unavoidable. ```javascript -$.swap($state, /* TODO */); -``` +// ./sokoban.js -Plug `$.swap` with a pure, [swappable](https://clojuredocs.org/clojure.core/swap!) 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. +//configurable +export function move(direction){ + return function(state){ + //algorithm for moving based on direction + } +} -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](docs/start-with-simulation.md). +//nonconfigurable +export function up(state){ + //algorithm for moving up +} +export function down(state){ + //algorithm for moving down +} + +``` +```javascript +// ./main.js +$.swap($state, s.move("up")); //configured +$.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). 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](https://github.com/mlanza/sokoban) or managing [To-dos](https://doesideas.com/programming/todo/), 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, 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. @@ -148,15 +200,14 @@ It's only when the core is somewhat complete, the shell is finally connected to 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: +The first sides of the data flow is handling outputs or rendering. It involves subscribing to the simulation and rendering and/or patching to the DOM: ```javascript $.sub($state, function(state){ - /* render the UI and replace or patch the DOM */ + /* render and/or patch the DOM */ }); ``` - -Subscribe to the DOM and feed the simulation: +The second side of the data flow is handling inputs or responding. It involves subscribing to the DOM and feeding the simulation: ```javascript const el = dom.sel1("#sokoban"); //your root element @@ -171,136 +222,131 @@ $.on(document, "keydown", function(e){ }); ``` +You can implement them one at a time. -Define intermediary signals if you like: -```javascript -function which(key){ - return _.filter(_.pipe(_.get(_, "key"), _.eq(_, key))); -} +#### Handling outputs or rendering/patching -const $keys = $.chan(document, "keydown"); +Start with the rendering logic and keep to issuing commands against the atom. Start by writing a function which receive the app state and use it to paint the app onto the DOM. Initially, it can be a single function although you will probably want to decompose it into parts eventually. -//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")); +```javascript +$.sub($state, function(state){ + dom.html(el, /* element or fragment */); +}); +``` +More likely you'd have one or more view functions each returning either an element, potentially nested with other elements, or a document fragment. -//...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)); +```javascript +function board(state){ + return /* element or fragment */; +} -//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)); +$.sub($state, function(state){ + dom.html(el, board(state)); +}); +``` +You could alternately use an intermediary transducer and compose the subscription: + +```javascript +$.sub($state, _.map(board), dom.html(el, _)); ``` +In reality the practice of receiving and reacting directly to the latest state is too simplistic an approach. That's because a live app is a flip book with lots of frames reeling through. It would typically not make sense to render a view to the DOM and then replace the whole thing on the next frame. That could perform well enough for trivial GUIs like the one found in a counter app, but not something more substantial. + +More typically you'd create (via `hist`) and utilize a history signal. It takes your atom and, on every update, returns the current and prior frames. Opon first subscribing it invokes the callback immediately. Since there's no prior history at this point, `prior` is null. -While creating a [virtual dom](https://reactjs.org/docs/faq-internals.html) had been considered for inclusion in the library, state diffing is not always needed. When needed, compare snapshots instead. +This permits you to write rendering logic and patching logic. That is, fully render the DOM or update only the parts which correspond to things in the model which have changed. ```javascript const $hist = $.hist($state); $.sub($hist, function([curr, prior]){ - /* diff your snapshots */ + if (prior == null) { + //render everything + } else { + //patch changes by diffing curr and prior + } }); - ``` -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. +Implementing the rendering everything path is usually straightforward. What's less obvious is how to handle the patching logic. -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. +That involves comparing snapshots. Based on how the data is structured, one can readily check that entire sections of the app are unchanged since any objects which have not changed will be shared by both snapshots. 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. -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. +Now even this model is simplistic because it uses a single history signal. And what can sometimes be done is to divide the GUI into sections. This would, in turn, entail creating one or more signals which focus on parts of the data model (e.g. app state). Then, transforming those signals into history signals. -Alternately, one can abstract this further. +There's no "right way." The choice to subdivide an atom into signals or signals into still further signals is yours. Initially, you can probably get away with using the simplest possible data flow, an atom and a single history signal. -```javascript -// 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. +During this stage, continue issuing commands directly against the atom. Write your rendering logic followed by your patching logic. When you can issue any command and things are correctly displayed, you can move to the next stage. -There's no templating language. Everything is programmatic composition. In this example, `ul` and `li` and `favorites` are all partially-applied functions: +#### Handling inputs or responding -```javascript -const ul = dom.tag("ul"), - li = dom.tag("li"); +The app can now be made to respond to actions the user takes against the DOM. This may involve listening to events in the DOM and/or defining input signals. I usually start with the first because it's simpler. -const favorites = - ul({id: "favorites", class: "fancy"}, - _.map(li, _)); //composed +Whichever approach you use, everything gets wired up once. The app may create and destroy tons of elements in its lifetime, but no new receivers get connected. This is because of event delegation. -const target = dom.sel("#favs", el); +Here inputs are received as DOM events: -$.sub($favs, function(favs){ - dom.html(target, favorites(favs)); +```javascript +const el = dom.sel1("#sokoban"); //root app element + +$.on(el, "keydown", function(e){ + e.preventDefault(); + switch(e.key) { + case "ArrowUp": + $.swap($state, s.up); + break; + case "ArrowDown": + $.swap($state, s.down); + break; + case "ArrowLeft": + $.swap($state, s.left); + break; + case "ArrowRight" + $.swap($state, s.right); + break; + } }); -//a tacit, transduced alternative: -$.sub($favs, _.map(favorites), dom.html(target, _)); +//assumes the app has an up button in the GUI +$.on(el, "click", "#up-button", function(e){ + e.preventDefault(); + $.swap($state, s.up); +}); ``` +> ℹī¸ **Info**: The `on` function implementation is similar [to jQuery's](https://api.jquery.com/on/) which also uses event delegation. One difference, though the code doesn't demonstrate it, is calling it returns a callback for unsubscribing from the events. I rarely used it in practice. -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. - +Here inputs are received instead as signals: ```javascript -function favorites(favs){ - debugger - return ul(_.map(li, favs)); +function which(key){ + return _.filter(_.pipe(_.get(_, "key"), _.eq(_, key))); } -``` -```html -
- - -
-``` +const $keys = $.chan(document, "keydown"); -Compose views which read structured data: +//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")); -```javascript -const suit = { - fname: "Harvey", - lname: "Specter", - salary: 725000, - dob: new Date(1972, 0, 22), - address: ["333 Bay Street", "New York, NY 10001"] -} +//...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)); -const {address, div} = dom.tags(["address", "div"]); +//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)); +``` -const mailingLabel = - address( - div( - _.comp(_.upperCase, _.get(_, "fname")), " ", - _.comp(_.upperCase, _.get(_, "lname"))), - _.map(div, _.get(_, "address"))); +The first example responds to events. The second reifies those events into signals. As the underpinnings of both are similar, it's just preference. The second approach is composable, while the first is not. -dom.append(envelop, - stamp(), - returnLabel(), - mailingLabel(suit)); -``` -```html -
-
HARVEY SPECTER
-
333 Bay Street
-
New York, NY 10001
-
-``` +During this stage, you wire up the app to react to user interactions. Once you're done you no longer have to issue commands against the atom, although you still could if you wanted! -### Progressively enhance +### Iterative refinement -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. +While an app has a humble beginning as little more than a reactive core, one gradually grafts layers onto it. So it can be kept simple or evolved toward increasing sophistication. For example, add [journal](./src/core/types/journal) to facilitate undo/redo and stepping forward and backward along a timeline. @@ -312,7 +358,9 @@ It's as much as you want, or as little. The entire effort is preceded and interleaved with [thought](https://www.youtube.com/watch?v=f84n5oFoZBc) 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. +It may be useful to rough out the UI early on. Thinking through things and clarifying the big picture for how they work and fit together will minimize potential downstream snafus. + +I find walks do wonders for development. They allow me to chew on what I'd been tackling in and learning from the app. I've arrived at many superior approaches and alternatives, no keyboard in sight, after a rigorous mental spelunking. ## Atomic in Action diff --git a/docs/interactive-development.md b/docs/interactive-development.md index c215f255..babab4d0 100644 --- a/docs/interactive-development.md +++ b/docs/interactive-development.md @@ -1,16 +1,43 @@ # Interactive Development +Interactive development makes programming a pleasure. Atomic thrives on interactivity. -Interactive development makes programming a pleasure. Atomic wants to aid interactivity. +In a browser having loaded an Atomic app where the `cmd` module is loaded, go to its console and enter `cmd()` to upgrade it to a more proper command line interface (CLI). -In any browser where the Atomic `cmd` module is loaded, enter `cmd()` into the console, to access your commands and components. Having imported `reg` from the `cmd` module into your modules, one may have used it to expose various commands and components/signals, beyond the standard library Atomic provides itself (`_`, `$`, and `dom`). Entering `cmd()` exposes into the browser console what modules usually keep private. +## Activating the command line interface +An HTML page can be transformed into a CLI with a little help from the Atomic registry. -To expose changes happening against signals, append `monitor` to the query string of your web app. +A CLI-friendly HTML page loads a `main.js` module like the following. It imports the `_`, `$`, and `dom` standard libs. It imports its core logic (e.g., `t`) for the app. It imports `reg` (short for register) from the command (`cmd`) module. It seeds an atom with data. + +```js +// ./main.js +import _ from "./libs/atomic_/core.js"; +import $ from "./libs/atomic_/shell.js"; +import dom from "./libs/atomic_/dom.js"; +import * as t from "./todo.js"; +import {reg} from "./libs/cmd.js"; + +const $state = $.atom(t.init()); +reg({$state, t}); +``` +Calling `reg` registers objects by name in the registry. Here `$state` and `t` are registered, the first an atom, the second a namespace. This does nothing noticeable until `cmd()` is entered at the console. + +First, it subscribes to any atoms/signals it was given so changes can be reported to the log. This depends on the page being given a query string: * `?monitor=*` to monitor everything * `?monitor=$state,$data` to monitor select signals * `?nomonitor=$state,$data` to monitor everything but certain signals -Components are headless, stateful objects which provide input and/or output ports. These ports permit issuing commands and subscribing to events as well as wiring components together. A developer can gradually build up an app while directly and regularly interacting with it before all the right UI affordances are in place. Alternately, he can use this facility to monitor, debug, and/or extend an app. +Second, it exposes the registered names globally. For example, the above `reg` line copies `$state` and `t` vars to its registry. Invoking `cmd()` copies those vars from the registry to the `window` object: +```js +window['$state'] = registry.$state; +window['t'] = registry.t; +``` +This is so, from an activated console, one can interact with the app to read and/or manipulate its state. As `cmd` registers `_`, `$`, and `dom` by default these commonly-used namespaces will always be available. + +This makes an app into a proper CLI. Become accustomed to issuing `cmd()` being the first thing you do. + +## Start small, iteratively refine +Components are headless, stateful objects with input and/or output ports. These ports permit issuing commands and subscribing to events as well as wiring components together. A developer can gradually build up an app while directly and regularly interacting with it all before the right UI affordances are in place. Or he can monitor, debug, and extend an existing app. This suits the Atomic philosophy.