diff --git a/README.md b/README.md index 5863f0c2..e2086852 100644 --- a/README.md +++ b/README.md @@ -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! @@ -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. @@ -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. @@ -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 diff --git a/docs/abstraction-thinking.md b/docs/abstraction-thinking.md index e43868ee..533522df 100644 --- a/docs/abstraction-thinking.md +++ b/docs/abstraction-thinking.md @@ -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. @@ -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){ diff --git a/docs/adopting-the-clojure-mindset.md b/docs/adopting-the-clojure-mindset.md index 59553366..762992a6 100644 --- a/docs/adopting-the-clojure-mindset.md +++ b/docs/adopting-the-clojure-mindset.md @@ -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. diff --git a/docs/command-query-protocols.md b/docs/command-query-protocols.md deleted file mode 100644 index 86079f4b..00000000 --- a/docs/command-query-protocols.md +++ /dev/null @@ -1,24 +0,0 @@ -# Command vs. query protocols - -In JavaScript arrays and objects are mutable reference types. - -The `core` (exported as `_`) provides a protocol for operating against them as if they were immutable, value types (i.e. persistent types). This means its operations, rather than mutate subjects, return modified copies. Persistent types (even faux ones) are the basis of simulations, which is effectively what functional programming is good at. While persistent protocols and functions emulate commands, they're actually queries. - -The `shell` (exported as `$`) is about actually doing things, modifying things. Its protcols operate against reference types treating them as they actually are. Thus, its operations are actually commands and do, indeed, produce the side effects required to get work done. - -```js -const stooges = ["Moe", "Larry", "Shemp"]; -const troupe = _.conj(stooges, "Corey"); //core op, a query -``` -```js -const stooges = ["Moe", "Larry", "Shemp"]; -$.conj(stooges, "Corey"); //shell op, a command -``` - -The first example is simulation. The second, side-effecting reality. See how, in the first, `conj` returns a result because it's a query and how, in the second, it doesn't because it's a command. - -This demonstrates how reference types, like arrays, can, if desired, be handled as persistent types. The choice is in the protocol, the functions–pure or impure–one uses to operate against a thing. There are obvious performance implications surrounding the choice, but for the use cases common to many apps, not enough to be perceptible. - -## Naming - -Clojure likes to make the distinction between the pure and the impure by sometimes adding a bang to impure names. Thus, `conj` is of the former, pure variety and `conj!` the impure variety. JavaScript doesn't allow names ending with a bang. The pure/impure distinction can generally be made by the module (`core`/`shell`) from which an operation exports. diff --git a/docs/command-query-separation.md b/docs/command-query-separation.md index 0d2dbdfd..582e995d 100644 --- a/docs/command-query-separation.md +++ b/docs/command-query-separation.md @@ -12,11 +12,7 @@ The benefit of CQS is that it becomes easy to read code and discern where side e > 💡**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. +See [simulating and actuating](simulating-actuating.md) to learn how any why some commands are rewritten as simulated commands or queries. [^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/functions-first.md b/docs/functions-first.md index 7a5e1d93..f23dbcbb 100644 --- a/docs/functions-first.md +++ b/docs/functions-first.md @@ -5,7 +5,7 @@ Prefers function over methods because they: * compose * are first class (e.g., they go everywhere) * minimize the need to bind or reference `this` -* may actually be multimethods or polymorphic protocols +* may actually be multimethods or protocols 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. @@ -15,26 +15,26 @@ 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) +* can be overloaded (via `overload`, see `journal` and `dow` below) +* are the preferred means to instantiating objects (thus hiding `new` from the type consumer) ## Instantiating objects -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. +The strategy for creating instances of a type are: -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. +* The module providing a type calls `new` in the factory function(s) it exports +* The module consuming a type calls the provided factory function(s) +* A constructor function does nothing more than assign arguments to properties +* A factory function performs whatever work is necessary to instantiate an object of a type +* All properties are declared as public but treated as private -This is unfortunate because classes introduce truly private properties, a prerequisite to proper encapsulation. But before this possibility came into being, the standard had already been to declare all properties publicly and, as a rule, treat them as private. So there's no need to underscore prefix a property (e.g. `_fname`) since even `fname` should be considered private. +While consideration was given to using the class syntax added to the language well after Atomic was created to gain truly private properties, the idea was abandoned as ill suited to the functional paradigm. -That's still the rule. Properties, with the exception of those on plain objects, which are themselves treated as DTOs, are accessed/updated via functions. +Classes must define methods to access private properties. Since Atomic prefers functions, all methods must be unbound as functions. `unbind` conveniently does this. Calling these unbound functions, however, suffers a performance hit that calling the functions deliberately written for use with a type do not. Thus, working from classes adds overhead. -The strategy for birthing objects remains: +Rather privacy remains a matter of discipline. All properties are public but treated as private. There's no need to prefix property names with underscores. Use `fname` instead of `_fname`. Except with plain objects, properties are not to be accessed directly. Use functions to encapsuate access. -* Don't call a constructor function directly -* Call factory functions instead to birth objects -* Make properties public but treat them as private -* Define an object's api using functions +See how the discussed principles are demonstrated in the code snippets to follow. -## Implementing abstract behaviors ```javascript //constructor function function Journal(pos, max, history, state){ @@ -56,33 +56,11 @@ function journal1(state){ const journal = overload(null, journal1, journal2); ``` -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. - -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... -const behave = - does( - implement(IDeref, {deref}), - implement(IFunctor, {fmap}), - implement(IRevertible, {undo, redo, ...})); - -//...and applying it to a constructor. -behave(Journal); -``` -Having this behavior readily applicable as a function is useful for [cross-realm patching](./cross-realm-operatility.md). If not for this potential need, the behavior might've been directly applied to the constructor. +## Implementing concrete behaviors -```javascript -//direct application -doto(Journal, - implement(IDeref, {deref}), - implement(IFunctor, {fmap}), - implement(IRevertible, {undo, redo, ...})); -``` +Concrete functions are implemented to interact with a single known type. -## Implementing concrete behaviors -Some types have concrete functions. A concrete function, the perfect candidate for an actual method, is something which applies to a single known type. +Here the day of the week function exists only for dates: ```javascript function dow1(self){ @@ -101,5 +79,34 @@ const now = _.date(); 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. +## Implementing abstract behaviors + +[Abstract thinking](./abstraction-thinking.md) closely relates to protocols which provide the foundation by which an indefinite number of types can abide some behavior. If a type can have a behavior, there must be a way of imbuing it. + +Here `Journal` behavior associated with the `IDeref`, `IFunctor` and `IRevertible` protocols is composed or packaged as a `behave` function: + +```javascript +//package the facets of a behavior... +const behave = + does( + implement(IDeref, {deref}), + implement(IFunctor, {fmap}), + implement(IRevertible, {undo, redo, /*...*/})); + +//...and applying it to a constructor. +behave(Journal); +``` +By first packaging the behavior, it can be readily applied to choice constructors. This is useful for [cross-realm patching](./cross-realm-operatility.md) and for composing behaviors and for applying them to still other types. + +If none of that is needed, what was done in 2 steps could be done in 1: + +```javascript +doto(Journal, //apply behavior directly + implement(IDeref, {deref}), + implement(IFunctor, {fmap}), + implement(IRevertible, {undo, redo, /*...*/})); +``` + +When creating new types, one ordinarily considers and implements both its concrete functions and its abstract functions. These are, respectively, the behaviors which are and which are not exclusive to the type. +When creating new behaviors they will ordinarily begin as concrete functions for a single known type. When it is discovered the behavior applies to other types, they can be promoted into abstract functions, the kind that define protocols. The generality is things begin in the concrete and, only when needed, move toward abstraction. diff --git a/docs/mutables-for-immutables.md b/docs/mutables-for-immutables.md new file mode 100644 index 00000000..eba14385 --- /dev/null +++ b/docs/mutables-for-immutables.md @@ -0,0 +1,51 @@ +# Mutables for immutables + +Most languages have reference types, value types, and persistent types. These broadly fall into mutables (reference types) and immutables (value types, persistent types). JavaScript is no different. + +Value types: +* `Boolean` +* `String` +* `Number` +* `Float` + +Reference types, some having a persistent types counterpart: +* `Object` → `Record`[^1] +* `Array` → `Tuple`[^1] +* `Date` → `Temporal`[^1] +* `Error` +* `Map` +* `WeakMap` + +Atomic has a few persistent types of its own: +* `HashMap` +* `HashSet` +* `SerialMap` +* `SerialSet` +* `PartMap` +* `PartSet` +* etc. + +Although functional programming does better when a robust set of persistent types is present, it's not seriously hindered when they're not. One can optionally treat reference types as persistent types by writing and using *pure* faux commands and atoms. This means you can use objects and arrays as much or as little as you like. + +Notice in the following examples the subject is a plain object, not a special persistent type. + +This is how `assoc` would be used in an imperative program: +```js +const person = {rank: "Lieutenant"}; +$.assoc(person, "lname", "Columbo"); //commands actuate +``` + +However, since Atomic programs normally [start with a simuation](./start-with-simulation.md) (e.g., use an atom), this demonstrates more typical use: + +```js +const $person = $.atom({rank: "Lieutenant"}); +$.swap($person, _.assoc(_, "lname", "Columbo")); //faux commands simulate +``` + +Since `assoc` exists as part of a protocol, it knows how to `assoc` given an object or another implemented type in either [simulating or actuating](./simulating-actuating.md) contexts (`_.assoc` or `$.assoc`). Both are offered as a matter of completeness and to illustrate how a concept—in this case, associating—can be employed in either. + +The example further demonstrates how objects and/or arrays can be freely used to model the domains held in atoms. They are, after all, the cheapest and easiest mutables to create. One need only reach for true persistent types when performance becomes a concern. + +The eventual plan appears to be to introduce immutable counterparts for object and arrays—that is, records and tuples. But this example demonstrates that, even while these structures are lacking from the language, functionally appropriate code (e.g., faux commands) can be written to treat objects and arrays as if they were immutables. The benefit, of course, in using true persistents like records and tuples, will presumably be their better performance. + +[^1]: A T39 proposal and not yet fully adopted into the language. See [records and tuples](https://github.com/tc39/proposal-record-tuple) and [temporals](https://github.com/tc39/proposal-temporal). diff --git a/docs/placeholder-partial.md b/docs/placeholder-partial.md index 1b4ff535..4c9f7f49 100644 --- a/docs/placeholder-partial.md +++ b/docs/placeholder-partial.md @@ -54,4 +54,16 @@ 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. +Placeholder partial (see the exported functions `impart`, `plug` and `partly`) is preferred to currying (see `curry`) due to the library's Clojure-like preference for varadic functions. Currying works well with fixed-arity functions, not variadics. + +See how placeholder partial makes arity selection explicit: + +```js +const triple = _.mult(3, _); //unary function +const fullName3 = _.str(_, " ", _, " ", _); //ternary function, initial included +const fullName2 = _.str(_, " ", _); //binary function, initial excluded +const fullName = _.overload(null, null, fullName2, fullName3); //variadic +const doe = fullName(_, "Doe"); //explicity select the binary overload and return a unary +const john = doe("John"), + jane = doe("Jane"); +``` diff --git a/docs/protocols-for-dynamic-extension.md b/docs/protocols-for-dynamic-extension.md index 9e0dfb02..e0738aab 100644 --- a/docs/protocols-for-dynamic-extension.md +++ b/docs/protocols-for-dynamic-extension.md @@ -90,6 +90,6 @@ $.conj(actors, "Gabriel Macht"); //actuating `conj` $.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. +And although `_.conj` and `$.conj` are closely related, and perhaps deserve to share a name, they're fundamentally different. [One simulates, the other actuates](simulating-actuating.md). Atomic has a slew of same-named protocols sitting on either side of the purity divide. 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/docs/simulating-actuating.md b/docs/simulating-actuating.md new file mode 100644 index 00000000..e63467c8 --- /dev/null +++ b/docs/simulating-actuating.md @@ -0,0 +1,100 @@ +# Simulating and actuating + +The following table 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. + +However, because naturally impure, mutable operations like `$.assoc` can cause harm, they can't. Rather safe, simulated version of the command (thus, `_.assoc`) must be written. This is almost always desirable because of the principle of simulating before actuating. That is, the core of every program [begins as a simulation](./start-with-simulation.md). + +```js +const obj = {title: "Lt.", lname: "Columbo"}; +const shows = ["Columbo", "The Good Doctor"]; +``` + +|Action|Pure World|Impure World| +|-|-|-| +|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)` | + +Consider that associating (`assoc`) is about adding a property/value pair to some entity/object. It can be either actuated or simulated. Thus, `$.assoc` is impure and has side effects while `_.assoc` is pure and does not. + +The actuating/simulating command divide is made visible by the import. `shell` imports as `$`. This is primarily where impure functions are kept. `core` imports as `_`. This is primarily where pure functions are kept. + +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 only simulates them. Thus, although sharing a common protocol name and functions, the one dispenses *commands*, the other *queries*. + +Recall how [command-query separation](./command-query-separation.md) expects commands to 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 side-effecting operation or command. The impure one actually changes its subject and the pure one provides an updated copy of it. This command division applies not only to legitimate value types and persistent types but [also to reference types like objects and arrays](./mutables-for-immutables.md). + +Understanding the reason and purpose in using a simulated command instead of an actual command is vital. The divide between simulating and actuating revolves around the atom. Some data structure is held in it, so its contents can be swapped, one image for another. In this way, the inside of the atom is a pure, functional core. The atom itself and the environment in which it operates is an impure, imperative shell. + +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 pure world exists inside atoms: +```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" +``` + +The `$.assoc` function is a command. It actuates. + +The `_.assoc` function is a query. It simulates. It is a *simulated* or *faux command* in that it is actually a query meant to simulate change. These simulated commands are frequently used with atoms. + +Thus, `assoc` command was ported from the impure realm into the pure and, thus, spans both. The same with `conj` and countless other commands. + +Here effects are actuated directly: +```js +import $ from "./libs/atomic_/shell.js"; +const stooges = ["Moe", "Larry", "Shemp"]; +$.conj(stooges, "Corey"); //commands actuate +``` + +Here effects are [simulated first](./start-with-simulation.md): + +```js +import _ from "./libs/atomic_/core.js"; +const $stooges = $.atom(["Moe", "Larry", "Shemp"]); +$.swap($stooges, _.conj(_, "Corey")); //queries simulate +``` + +To actuate the effects, the program would also have to subscribe to and react to any updates made in the atom. The most convenient way to provide feedback is sending the updates to the log. + +```js +$.sub($stooges, $.log); +``` + +This is the equivalent of: + +```js +$.sub($stooges, console.log.bind(console)); +``` + +A more robust app would ordinarily subscribe to updates in order to render a graphical representation in the DOM. + +One might be falsely led to believe that if one uses `_.assoc` in the simulating part of the program one would probably use `$.assoc` in the actuating part. That is not the case. + +What's being managed inside an atom, the simuation, revolves around the domain model and what the user is doing to it. What's being managed outside the atom in the imperative shell revolves around giving the user a GUI and reacting to his inputs. Because the responsibilies are wholly different, there's no reason for a simulated `_.assoc` to correlate to an actuated `$.assoc`. It's feasible, but not obligatory. The parallel examples were offered to demonstrate how actual commands can be converted to faux commands and vice versa, nothing more. + +## Naming + +Clojure likes to make the distinction between the pure and the impure by sometimes adding a bang to impure names. Thus, `conj` is of the former, pure variety and `conj!` the impure variety. JavaScript doesn't allow names ending with a bang. The distinction is made by the module, so `_.conj` and `$.conj` for `conj` and `conj!`. diff --git a/docs/start-with-simulation.md b/docs/start-with-simulation.md index b769ff79..20140e38 100644 --- a/docs/start-with-simulation.md +++ b/docs/start-with-simulation.md @@ -195,7 +195,7 @@ When anything goes wrong, when the resulting snapshot of the state doesn't add u ### It's easier to identify and fix faulty logic When a bad snapshot emerges, there's a precise operation/step to account for it. "This frame looks fine, but the next one doesn't." -Furthermore, when a complete record of snapshots have been captured, a developer can take the last good snapshot and interactively debug/reapply the simulated command which brought it to the bad state. +Furthermore, when a complete record of snapshots have been captured, a developer can take the last good snapshot and interactively debug/reapply the faux command which brought it to the bad state. ```js const frames = []; // capture snapshots?