diff --git a/README.md b/README.md index 4bd3b617..5863f0c2 100644 --- a/README.md +++ b/README.md @@ -6,28 +6,28 @@ Highlights: * well suited for web apps * deploy the modules you write, not bundles, no build required * [point-free pipelines and partial application](docs/placeholder-partial.md) -* use a familiar Clojure api and [way of thinking](docs/adopting-the-clojure-mindset.md) +* use a familiar Clojure api (transducers included!) and [way of thinking](docs/adopting-the-clojure-mindset.md) * functional core, imperative 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. Since [protocols](https://clojure.org/reference/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](./docs/protocols-for-dynamic-extension.md). They make [cross-realm operability](./docs/cross-realm-operability.md) possible. Plus, it's better to think [abstractly about apis and behaviors](https://en.wikipedia.org/wiki/Abstract_data_type), giving them greater attention than types. +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 [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 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. -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)), 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 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! +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! -Yet, again, protocols reduce mountains to mole hills. In short, [their first-class citizenship status](https://github.com/tc39/proposal-first-class-protocols) is long overdue. +As you can see, the [first-class status of protocols](https://github.com/tc39/proposal-first-class-protocols) in the language is long overdue! ## Premise 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. +The ephiphany: since languages are just facilities plus syntax, if one sets aside syntax, having the right facilities can eliminate the build step. -JavaScript does functional programming pretty dang well and continues to add proper facilities. +JavaScript does functional programming but, with just a few more facilities, could be amazing: * [first-class protocols](https://github.com/tc39/proposal-first-class-protocols) * [records & tuples](https://github.com/tc39/proposal-record-tuple) @@ -35,7 +35,7 @@ JavaScript does functional programming pretty dang well and continues to add pro * [pipeline operator](https://github.com/tc39/proposal-pipeline-operator) * [temporal](https://github.com/tc39/proposal-temporal) -Atomic showcases [the Clojure way](docs/adopting-the-clojure-mindset.md) in build-free JavaScript. +Atomic bridges the gap to showcase [the Clojure way](docs/adopting-the-clojure-mindset.md)! ## Getting Started Build it from the command line: @@ -44,7 +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. +> 💡**Recommendation**: Build the classic [Sokoban](https://en.wikipedia.org/wiki/Sokoban) game. It's not overly challenging and it's more fun than writing a counter or a to-do app. Don't copy [mine](https://github.com/mlanza/sokoban). Follow the path being demonstrated, but choose your own graphics and make your own way. Set up your project: @@ -90,35 +90,36 @@ import {reg} from "./libs/cmd.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 GUI logic and utilize the `dom` import, but right now that's a long way off. +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 (of 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 looks roughly like the following, but model yours in whatever way makes sense to you. +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. ```javascript // ./sokoban.js -function init(){ - const _ = "void", - b = "building", - g = "ground", - w = "water" - x = "dest"; +export const v = "void", + b = "building", + g = "ground", + w = "water", + x = "dest"; + +export function init(){ 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], + [v, b, b, b, b, v, v, v], + [v, b, g, g, b, b, b, b], + [v, b, g, g, b, g, g, b], + [v, 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, _] + [b, g, g, g, g, g, b, v], + [b, g, g, b, g, g, b, v], + [b, b, b, b, g, g, b, v], + [v, v, v, b, b, b, b, v] ] } } @@ -151,13 +152,13 @@ Finally, to bootstrap the command line in the browser, expose its Developer Tool cmd() ``` -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. +Anticipate operating from your text editor and browser console. 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. -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. +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. It's often unavoidable. ```javascript // ./sokoban.js @@ -188,13 +189,13 @@ $.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. +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. 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. -It's only when the core is somewhat complete, the shell is finally connected to a UI. +It's only when the core is somewhat complete, the shell is connected to a UI. ### Stand up the user interface @@ -207,11 +208,12 @@ $.sub($state, function(state){ }); ``` -The second side of the data flow is handling inputs or responding. It involves subscribing to the DOM and feeding 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 +const el = dom.sel1("#sokoban"); //root element -//prefer event delegation to subscribing to elements directly +//event delegation $.on(el, "click", "button.up", (e) => $.swap($state, s.up)); $.on(document, "keydown", function(e){ @@ -222,11 +224,12 @@ $.on(document, "keydown", function(e){ }); ``` -You can implement them one at a time. + +Use event delegation so that no matter how many elements get created or destroyed over the life of the app, the handlers need only be wired up once. Or subscribe to events on elements which never get destroyed. #### Handling outputs or rendering/patching -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. +Start with the rendering logic and keep to issuing commands against the atom. Write a function which receives the app state and paints and interface onto the DOM. It may initially be a single function. It will likely eventually be decomposed into parts. ```javascript $.sub($state, function(state){ @@ -244,19 +247,19 @@ $.sub($state, function(state){ dom.html(el, board(state)); }); ``` -You could alternately use an intermediary transducer and compose the subscription: +I prefer point-free style and so would `_.map` transduce the subscribe: ```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. +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 not typically make sense to render a view to the DOM and then replace the whole thing on the next frame. That could perform well in 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. +More typically you'd create a history signal which takes an atom and, on every update, returns the current and prior frames. It's callback is called immediately, before any change takes place and on every change to follow. Since there's no prior history when it's first called, `prior` will initially be null. 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); +const $hist = $.hist($state); //history signal $.sub($hist, function([curr, prior]){ if (prior == null) { @@ -266,24 +269,21 @@ $.sub($hist, function([curr, prior]){ } }); ``` -Implementing the rendering everything path is usually straightforward. What's less obvious is how to handle the patching logic. +Implementing the render-everything path is usually straightforward. What's less obvious is how to handle patching. -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. +This 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. -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. +In one approach you can model your app using a single history signal and gradually build up the conditional logic to handle all feasible scenarios. You can also sometimes divide a GUI into sections and, correspondingly, split the atom into one or more signals each focused on just a part of the GUI. -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. +There's no right way. The choice for using a history signal and/or subdividing an atom into signals is yours. -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. +During this stage, continue issuing commands directly against the atom. Write your rendering and/or patching logic. When you can issue any command and things are correctly displayed, you can move to the next stage. #### Handling inputs or responding -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. - -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. - -Here inputs are received as DOM events: +The app can now be made to respond to actions the user takes against the DOM. This may involve responding to DOM events directly or defining input signals. Remember, whichever approach you use, everything gets wired up once. +Here input is received from DOM events: ```javascript const el = dom.sel1("#sokoban"); //root app element @@ -311,9 +311,9 @@ $.on(el, "click", "#up-button", function(e){ $.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. +> ℹ️ **Info**: The `on` function implementation is similar [to jQuery's](https://api.jquery.com/on/). One difference, though the code doesn't demonstrate it because in practice I've rarely needed it, is the call returns a callback for unsubscribing. -Here inputs are received instead as signals: +Here input is received, instead, through signals: ```javascript function which(key){ return _.filter(_.pipe(_.get(_, "key"), _.eq(_, key))); @@ -340,17 +340,17 @@ $.sub($keys, which("ArrowLeft"), (e) => $.swap($state, s.left)); $.sub($keys, which("ArrowRight"), (e) => $.swap($state, s.right)); ``` -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. +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 simpler. -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! +During this stage, you wire up the app to react to user interactions. Once completed, issuing commands against the atom becomes superfluous. ### Iterative refinement -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. +An app begins as a tiny reactive core and one grafts layers onto it. 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. +The initial commands are pure functions triggered by DOM events and swapped against an atom. These commands, however, can be reified into [POJOs](https://en.wikipedia.org/wiki/Plain_old_Java_object), sent to a command bus, over the wire, and/or logged to auditable histories. Middleware can be introduced. -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. +Add a [journal](./src/core/types/journal) to facilitate undo/redo and step backwards and forwards in time. It's as much as you want, or as little. diff --git a/docs/NOTES.md b/docs/NOTES.md deleted file mode 100644 index 2a54df60..00000000 --- a/docs/NOTES.md +++ /dev/null @@ -1,28 +0,0 @@ -# PRINCIPLES -* Treat data as immutable. Write pure functions. -* Command-query separation is good pratice for writing functions. Prefer return-nothing to return-something commands. -* The modules as exported in `dist` is the api. The internal structure may be freely reorganized. -* Prefer constructing objects using factory functions over the use of `new` since constructors should be nothing more than propery assignments. The factory functions, and there can be many per type, perform any necessary work. -* To aid REPL-driven development type properties are treated as private as a matter of discipline and not actually hidden. -* Prefer a diff/patch strategy to virtual doms (React). -* Avoid recursion since it can result in stack overflows. -* Prefer the public api to the direct use of protocols (e.g. `_.reduce` over `IReduce.reduce`) as these are not necessarily equivalent. -* Prefer functions (with `self` as the first arg) over methods. -* Prefer abstractions and behavioral, protocol-centered thinking to caring about concrete types. -* Break programs down into a functional core and an imperative shell, 2 modules at minimum. -* Clojure's functional api is more practical than Ramda's. Ramda has too many slight permutations (e.g. `map`, `mapAccum`, `mapAccumRight`, `max`, `maxBy`). It's better to provide a smaller api and use typical compositions when the need arises than to export all the permutations of those compositions. -* The use of protocols adds a layer of indirection and imposes a small cost over calling concrete type-specific functions, but gaining the generality and simplified api makes it worth it. -* In some cases, when dealing with collections (Seqs), we cannot know what concrete types it will contain. HTMLDivElement and HTMLSpanElement are predictable HTML elements; however, developers can define their own custom elements via Web Components. Unless the behavior/protocols for those components are defined, the api will break when it encounters these custom elements. Protocol resolution, for performance, looks directly to the constructor and not the full inheritance chain because protocols are internally implemented using WeakMaps. Traversing the inheritance chain on every protocol lookup would be too expensive. That is why the dom traversal api assumes all items within the seq are elements. -* The Law of Abstractions: When a invoking a function against an object that returns a different representation of it, the type may vary (e.g. an Array becoming an IndexedSeq). The new representation should abide the same protocols to maintain the integrity of the abstract type. Apart from this, one must think in concrete types. -* A protocol is a contract that involves its api (commands and queries) and also its operands. That is, the `IQueryable.query` protocol should not receive a selector string to query, in one context, the dom and a T-SQL statement, in another context, to query a database. The contract is a dialect which is the sum of the api and its operands. -* ADTs and protocols are means of polymorphism. The former gains compile time type checking, the latter extensibility. -* The first argument of a protocol is the subject whose type determines the behavior. -* Types are defined in terms of behaviors which are exported and can be applied to one or more types. -* Consider using binary accum functions (e.g. `both`, `either`) from which to create reducing versions (`and`, `or`) of unlimited arity. -* The api documentation should offer practical examples showcasing the usefulness of a function. I found that some popular libraries seem to include arbitrary examples that were of no help. -* Prefer JavaScripts native arrays and objects, treated as immutables, to vectors/maps at the cost of importing another library. -* Faux commands are actually queries which return replacement objects rather than mutating the passed-in objects directly. They can be used (along with a state container) to simulate change before actualizing it. The `ICloneable.clone` provides a means to writing these functions. -* Prefer writing protocols to multimethods. -* Treat all operations as functions be they functions, protocols, or multimethods. Each is just a flavor of the function interface with different implementation details. -* `IDispatch` and `IPublish` are similar in that both relay messages to receivers. The difference, however, is that dispatch is for commands and always targets a single handler while publish is for events and targets an indefinite number of listeners. Commands can be canceled, but events cannot. -* Prefer explicit partial application (`e.g. const double = mult(2, ?)`) to self currying. The former is more flexible and permits variadic, overloaded functions. It is usually better to permit the manner of partial application to be specified at the call site than to bake it directly into the function. diff --git a/docs/abstraction-thinking.md b/docs/abstraction-thinking.md new file mode 100644 index 00000000..e43868ee --- /dev/null +++ b/docs/abstraction-thinking.md @@ -0,0 +1,88 @@ +# Abstraction Thinking + +F# supports polymorphism via discriminated unions. With this approach, you define the subtypes which fall under an abstraction. Then polymorphic functions can be defined to handle all known subtypes. + +Here `Shape` is the abstraction and `Rectangle`, `Circle` and `Prism` its concrete types: + +```fs +type Shape = + | Rectangle of width : float * length : float + | Circle of radius : float + | Prism of width : float * float * height : float + +let getShapeWidth shape = + match shape with + | Rectangle(width = w) -> w + | Circle(radius = r) -> 2. * r + | Prism(width = w) -> w +``` + +The benefit of the discriminated unions found in F# is the programs are statically checked at compile time. The subtypes and respective behaviors (functions) are defined up front by the developer. + +They can also be used to represent different states of some entity. Think "states" as in the different states, or stages, of the state machine representing that entity. + +This was plucked [from here](https://fsharpforfunandprofit.com/posts/designing-with-types-representing-states/): +```fs +type ActiveCartData = { UnpaidItems: string list } +type PaidCartData = { PaidItems: string list; Payment: float } + +type ShoppingCart = + | EmptyCart // no data + | ActiveCart of ActiveCartData + | 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. + +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. + +Let's handle the more complicated state machine scenario by defining its concrete types: + +```js +function EmptyCart(){ +} + +function ActiveCart(unpaidItems){ + this.unpaidItems = unpaidItems; +} + +function PaidCart(paidItems, payment){ + this.paidItems = paidItems; + this.payment = payment; +} + +const emptyCart = new EmptyCart(); +``` + +Seed the atom with an initial state: +```js +const $cart = $.atom(emptyCart); +``` + +Provide a [persistent command](./command-query-protocols.md) for transitioning the state: + +```js +function paid(payment){ + return function(activeCart){ + const paidItems = getUnpaidItems(activeCart); + return new PaidCart(paidItems, payment); + } +} +``` + +Then, at an opportune time, presuming right after the customer initiates checkout and the `amount` computed: + +```js +try { + const payment = await requestCredit(digits, expDate, secCode, zipCode, amount); + $.swap($cart, paid(payment)); +} catch (ex) { + //handle failed request +} +``` + +The abstract `ShoppingCart` can be any of an `EmptyCart`, an `ActiveCart` or a `PaidCart` depending on where the customer is in the checkout process. Certain protocols may need to be implemented against all 3 concrete types to provide a polymorphic, universal api. And certain actions may only make sense against a given type. For example, `paid` exists only for an `ActiveCart`. + +The crux of abstraction thinking is `$cart` is a `ShoppingCart` is an abstract data type. You are, therefore, relegated to think about its behaviors via a contracted api, not what its present concrete type happens to be or what it may become. You bear in mind the [functions](./functions-first.md) which are available for acting against it and for assessing what's logically possible, preferrably without ever checking its concrete type. + +Notice how swapping `$cart` with `paid` transitions an `ActiveCart` into a `PaidCart`. Certain actions may not only change the state of an entity's data, but, where state machines are concerned, potentially also transition its type. Thus, protocols are no less apt, with a bit of design forethought, for [making illegal states unrepresentable](https://enterprisecraftsmanship.com/posts/c-and-f-approaches-to-illegal-state/). diff --git a/docs/adopting-the-clojure-mindset.md b/docs/adopting-the-clojure-mindset.md index a89168e3..59553366 100644 --- a/docs/adopting-the-clojure-mindset.md +++ b/docs/adopting-the-clojure-mindset.md @@ -2,9 +2,9 @@ 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)). -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. +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. -Briefly, recall that command-query separation wants query functions to return a value but not command functions. The stark absence of a return value calls it out as a command. +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. ```js const obj = {title: "Lt.", lname: "Columbo"}; @@ -13,35 +13,27 @@ const shows = ["Columbo", "The Good Doctor"]; |Action|Pure World (`core`) |Impure World (`shell`)| |-|-|-| -|Read property|`_.get(obj, "lname")`|N/A| +|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)` | The above demonstrates a couple important ideas. -Some operations are natively queries. Queries remain queries whether they're used in the pure or impure part of a program. So `get` is always a read operation, or a query. There is no mutable counterpart. It's simply not needed. +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. -Furthermore, associativity (e.g. `assoc`) is a concept which involves adding/changing a property on some target. Since any command (e.g. side effect) can be simulated, `assoc` can be implemented as either an impure/mutable operation or as a pure/immutable operation. The `assoc` protocol exists in both the pure (`_`) and impure (`$`) worlds. To be clear, there's immutable `assoc`, and mutable `assoc`, two distinctly different protocols sharing a common name. +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 `_`. -Commands can be simulated by writing a function which returns a replacement for the subject. That is, a simulated `assoc` takes a subject and the key and value it wants to associate to it but, without touching the actual subject, returns a new object which is the aggregate of the original and the association(s) applied against it. +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. -```js -//basis for immutable `assoc` protocol -function assoc(self, key, value){ //query/simulated - const replacement = {...self}; - replacement[key] = value; - return replacement; //return value -} +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. -const $harvey = $.atom({lname: "Specter"}); -$.swap($harvey, _.assoc(_, "fname", "Harvey")); -const fname = _.chain($harvey, _.deref, _.get(_, "fname")); // "Harvey" -``` +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. -These are simulated or faux commands, because they are pure and don't acutally mutate anything. The `assoc` is pure, the `swap` impure. This approach allows immutability and mutability to be teased apart. It affords a specific strategy for controlling state change. +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. -An ordinary command is impure actually changes the subject. In accordance with command-query separation, it has no return value. +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 @@ -54,21 +46,36 @@ $.assoc(harvey, "fname", "Harvey"); const fname = harvey.fname; // "Harvey" ``` -Immutable `assoc` is a query, mutable `assoc` a command. The one emulates change. The other actuates it. Thus, `_`s signal emulation, `$`s actuation. +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. -Immutable `assoc` is tailor-made for truly persistent types, like records. But even without them, it can be implemented against plain objects. +The `$.assoc` function is a command. It actuates. -The same applies to `conj` or any other command one imagines. Change against any type can be simulated. All simulation requires is an atom. The atom contains the state and [`swap`](https://clojuredocs.org/clojure.core/swap!)s updates against it using simulated commands. +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. -What this effectively means is the above table can, as desired, be fully realized so that any mutable operation can be simulated, which is to say written as a reductive operation. What this reaveals is all programs are, at their very centers, [reductions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce). +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. -That's the cornerstone of how Clojure models state change. And where Clojure actually has a robust set of persistent types, JavaScript doesn't. So Atomic uses reference types and pure protocols/functions to emulate persistent types. In practice, this proves performant enough to be of little concern. +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. -## To What End? +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. -The point of discussing the two worlds, the pure and the impure, is to delinate the difference and to clearly demonstrate how side effects can be simulated before actuated. +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. -The value of handling state in this manner is hard to understand in the small. But there's an immense value proposition in learning to tease apart the pure and impure parts only to reconnect them. +## To What End? -While the end result, simulated change becoming actual change achieves the same result as before, it would be short sighted to assert the extra layer adds complexity. This separation makes a program significantly easier to understand, develop, test and maintain than when the parts were intertwined. It provides a useful lens for seeing what a program logically is and what it does. +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. diff --git a/docs/command-query-separation.md b/docs/command-query-separation.md new file mode 100644 index 00000000..0d2dbdfd --- /dev/null +++ b/docs/command-query-separation.md @@ -0,0 +1,22 @@ +# Command-Query Separation + +Atomic is [functional first}(./functional-first.md) and so the rule is write operations as functions. Very closely related is that functions must be either commands or queries. + +A **command**[^1] is a function which results in a side effect, a change somewhere to the state of the system. It actuates intent. Although exceptions can be sparingly made, ordinarily a command does not return a value. + +A **query** is a function which has no side effects. It takes arguments and responds with a return value. Where queries are pure, commands are impure. + +In the functional core, imperative shell way of thinking, the pure code (queries) are segregated in a core module and the impure code (commands) in a main module. This separation is useful and make the domain logic, usually primarily constrained to the core, much easier to reason about and test. It essentially why using the approach is such a pleasure. + +The benefit of CQS is that it becomes easy to read code and discern where side effects are or are not happening. And since all the complexity and perils revolve around this, the value of making it easy to spot where this is happening cannot be overstated. + +> 💡**Principle**: Command-query separation is good pratice for writing functions. Prefer return-nothing to return-something commands. + +## Simulating commands + +Commands can be simulated. That is, you can take an ordinarily side-effecting function and write an alternative version which is query. + +In the alternate version, the function accepts a subject potentially along with operands and returns a new subject. That subject can is a near-clone of the original except with the change applied. A function which is actually a query but which is used to simulate a command is called a **faux command**. It is commonly used against an atom to simulate change before actualizing it. It is discussed further under [command-query protocols](./command-query-protocols.md). The `ICloneable` protocol may prove helpful for writing these. + +[^1]: Not to be confused with a command POJO (e.g., an object/message) whose corollary is an event POJO. Commands and events, in that context, are messages where in this context they're operations. + diff --git a/docs/functional-first.md b/docs/functions-first.md similarity index 67% rename from docs/functional-first.md rename to docs/functions-first.md index d49a2da8..7a5e1d93 100644 --- a/docs/functional-first.md +++ b/docs/functions-first.md @@ -1,16 +1,25 @@ -# Choosing functions over methods +# Functions over methods -Atomic unilaterally prefers functions over methods because: +Prefers function over methods because they: -* Functions compose -* Functions are first class (e.g. they go everywhere) -* Functions minimize the need to bind or reference `this` -* Functions are potentially polymorphic +* compose +* are first class (e.g., they go everywhere) +* minimize the need to bind or reference `this` +* may actually be multimethods or polymorphic protocols -Furthermore, Atomic is fundamentally protocol oriented. Since protocols are themselves just functions, they do everything functions do. *For all intents and purposes, the two concepts are interchangable.* +That last deserves some explanation. The true nature of a function may be unknown. It may be a function, or a multimethod, or one operation for some protocol. Take "function," wherever it appears, to potentially be any of these. This makes all of them first class and interchangeable. + +## Guidance for writing functions + +Some additional considerations for functions are they: + +* can take `self` as a parameter (usually the first) as an alternative to `this` +* should use recursion sparingly due to the potential for a stack overflow +* can be overloaded (via `overload`) +* are the preferred means to instantiating objects (thus `new` will usually be hidden from view) ## Instantiating objects -Atomic continues to prefer constructor functions over class syntax. While thought was given to rewriting types using class syntax, once the feature entered the language, a problem soon became apparent. +Atomic prefers constructor functions over class syntax. While thought was given to rewriting types using class syntax once the feature entered the language, a problem soon became apparent. A type implemented as a class would more sensibly implement its behavior using methods. And to permit those types to operate within a primarily functional paradigm, those methods (not being first class) would have to also be bound to functions. This would introduce a layer of indirection, add overhead, and degrade performance. @@ -47,9 +56,9 @@ function journal1(state){ const journal = overload(null, journal1, journal2); ``` -Due to these rules, a module consumer won't generally use the `new` keyword. +Due to these rules, a module consumer won't generally use the `new` keyword. Furthermore, providing overloaded or even alternative factory functions, there can be numerous abstract ways for creating instances of a type. -Also, note another rule. Never do work in the constructor function. When setup work must be done, save it for the factory function. +Never do work in a constructor function. Save it for the factory function. See how `Journal` (above) does nothing but assign its arguments to its properties. ```javascript //implementing protocols to define a behavior... @@ -94,4 +103,3 @@ const day = dow(now); The distinction between concrete and abstract functions is this. A concrete function has a single known type. An abstract function, also known as a protocol, has an indefinite number of known types. -While multimethods are implemented in Atomic, protocols are the strongly preferred form of polymorphism. diff --git a/docs/placeholder-partial.md b/docs/placeholder-partial.md index f3bcc368..1b4ff535 100644 --- a/docs/placeholder-partial.md +++ b/docs/placeholder-partial.md @@ -53,3 +53,5 @@ import * as _ from "./libs/atomic/core.js"; import * as $ from "./libs/atomic/shell.js"; import * as dom from "./libs/atomic/dom.js"; ``` + +Placeholder partial is preferred to currying due to the library's Clojure-like preference for varadic functions. Currying works best with fixed-arity, not variadic functions. Placeholder partial (`e.g. const double = _.mult(2, _)`) clearly identifies which overloaded arity is targeted by the function call. diff --git a/docs/protocols-for-dynamic-extension.md b/docs/protocols-for-dynamic-extension.md index 4e8d4de4..9e0dfb02 100644 --- a/docs/protocols-for-dynamic-extension.md +++ b/docs/protocols-for-dynamic-extension.md @@ -91,3 +91,5 @@ $.conj(actors, "Patrick J. Adams"); $.log(actors); //=> ["Gabriel Macht", "Patrick J. Adams"] ``` And although `_.conj` and `$.conj` are closely related, and perhaps deserve to share a name, they're fundamentally different. [One simulates, the other actuates](./command-query-protocols.md). Atomic has a slew of same-named protocols sitting on either side of the purity dividing line. + +The use of protocols adds a layer of indirection and imposes a small cost over calling concrete type-specific functions, but gaining generality and a simplified api makes it worth it. diff --git a/src/shell/protocols/idispatch/README.md b/src/shell/protocols/idispatch/README.md new file mode 100644 index 00000000..e788dfbf --- /dev/null +++ b/src/shell/protocols/idispatch/README.md @@ -0,0 +1,5 @@ +# IDispatch + +Sends a command message to a single handler such as a command bus. As a rule, a command can be cancelled, and thus prevented from happening. + +* `dispatch(self, message)` — sends a message to some handler diff --git a/src/shell/protocols/ipublish/README.md b/src/shell/protocols/ipublish/README.md index e810a2b3..139b44fe 100644 --- a/src/shell/protocols/ipublish/README.md +++ b/src/shell/protocols/ipublish/README.md @@ -1,7 +1,7 @@ # IPublish -Send a message to the system. It will be received by `n` subscribers where `n` could be 0, 1 or more. +Send an event message as a broadcast to `n` subscribers where `n` could be 0, 1 or more. As a rule, events cannot be cancelled, since they signal what actually happened. * `pub(self, message)` — publishes a message to any interested observers * `err(self, error)` — sends the terminating error -* `complete(self)` — indicates a conclusion has been reached \ No newline at end of file +* `complete(self)` — indicates a conclusion has been reached