diff --git a/README.md b/README.md index 73ae0815..f85ca7f5 100644 --- a/README.md +++ b/README.md @@ -5,42 +5,45 @@ Highlights: * well suited for web apps * deploy the code you write — [point-free pipelines and partial application](/placeholder-partial.md), no build required -* implements much Clojure's standard library +* implements much of Clojure's standard library * [functional core](src/core), [imperative shell](src/shell) w/ FRP * [nil-punning](https://ericnormand.me/article/nil-punning) handles null in sensible ways -Atomic is [protocol oriented](src/core/protocols) to its very foundation. Objects are treated as [abstract types](https://en.wikipedia.org/wiki/Abstract_data_type) per their behaviors irrespective of their concrete types. This flavor of polymorphism is what makes Clojure so good at transforming data. -JavaScript has no means of safely [extending natives and third-party types](https://en.wikipedia.org/wiki/Monkey_patch). [Protocols](https://clojure.org/reference/protocols), which are the most apt solution to this problem, have [yet to be adopted](https://github.com/tc39/proposal-first-class-protocols) into the language. +Atomic is [protocol oriented](src/core/protocols) to its very foundation. Objects are treated as [abstract types](https://en.wikipedia.org/wiki/Abstract_data_type) per their behaviors, without regard to their concrete types. -Atomic is [functional first](functional-first.md). Functions are preferred to methods. This makes sense given how protocols treat things as abstractions. +[Protocols](https://clojure.org/reference/protocols) are the centerpiece of Clojure and, by extension, Atomic. They offer a flavor of polymorphism that sets Clojure apart as a data-transforming juggernaut. Clojure would not be Clojure without them! -Atomic has no [maps](https://clojuredocs.org/clojure.core/hash-map) or [vectors](https://clojuredocs.org/clojure.core/vector) but used objects and arrays as faux [records and tuples](https://tc39.es/proposal-record-tuple/) since before these new types were even proposed. It had previously integrated them via [Immutable.js](https://immutable-js.com) but, in practice, using objects and arrays as persistents worked well enough it wasn't worth the cost of another library. Its integration [was dropped](https://github.com/mlanza/atomic/commit/8e1787f6974df5bfbb53a371a261e09b5efee8ee). This bit of history serves to demonstrate how protocols can seamlessly integrate disparate types into a consistent api. Concrete types, even third-party ones, can be regarded as abstract things, by their apis. +Protocols also provide the only safe means of dynamically [extending natives and third-party types](https://en.wikipedia.org/wiki/Monkey_patch). In short, [its first-class citizenship status](https://github.com/tc39/proposal-first-class-protocols) is long overdue. Atomic fills the gaping hole. + +Atomic is [functional first](functional-first.md). Functions are preferred to methods. This makes sense given the preference for abstractions over concretions. + +Atomic has no [maps](https://clojuredocs.org/clojure.core/hash-map) or [vectors](https://clojuredocs.org/clojure.core/vector). It doesn't need them. It treats objects and arrays as value types until [records and tuples](https://tc39.es/proposal-record-tuple/) arrive. + +In practice, this worked so well the [Immutable.js](https://immutable-js.com) integration [was dropped](https://github.com/mlanza/atomic/commit/8e1787f6974df5bfbb53a371a261e09b5efee8ee). It wasn't worth the cost of loading the library. This bit of history is noted only to demonstrate how, through protocols, third-party types can be easily and seamlessly integrated into any standardized api. ## Premise Atomic was born from an experiment answering: -> Why not do ClojureScript directly in JavaScript and eliminate the transpiler? +> Why not do in JavaScript what ClojureScript does, but without the transpiler? -The ephiphany: since languages are just facilities plus syntax, if one can set aside syntax, the right facilities can eliminate build steps. +The ephiphany: since languages are just facilities plus syntax, if one can set aside syntax, having the right facilities eliminates build steps. JavaScript does functional programming pretty dang well and continues to add proper facilities. * [first-class protocols](https://github.com/tc39/proposal-first-class-protocols) -* [records & tuples](https://github.com/tc39/proposal-record-tuple) (including sets and typed records & tuples!) +* [records & tuples](https://github.com/tc39/proposal-record-tuple) * [partial application](https://github.com/tc39/proposal-partial-application) * [pipeline operator](https://github.com/tc39/proposal-pipeline-operator) * [temporal](https://github.com/tc39/proposal-temporal) -Of all of the above, first-class protocols is the most critical one which, for some odd reason, has failed to gain community support. Developers it seems are failing to experience and realize the tremendous value add only protocols provide. They're the centerpiece of Clojure and, by extension, Atomic. Clojure would not be Clojure without them! - -Atomic provides the necessary facilities and showcases how even plain JavaScript can adopt the Clojure mindset! +Atomic provides facilities to showcase how any language—even JavaScript!—[can adopt the Clojure mindset](adopting-the-clojure-mindset.md). ## Purity Through Protocol Since JavaScript lacks certain value types (i.e. [records and tuples](https://tc39.es/proposal-record-tuple/) and [temporals](https://github.com/tc39/proposal-temporal)), purity has historically been gained through discipline. Atomic makes this still easier. -It permits reference types to be optionally treated as immutable value types. It provides mutable and/or immutable protocols for interacting with natives so arrays can double as [tuples](https://tc39.es/proposal-record-tuple/) and objects as [records](https://tc39.es/proposal-record-tuple/). Yet, again, protocols reduce mountains to mole hills. +It permits reference types to be optionally treated as value types. It provides mutable and/or immutable protocols for interacting with natives so arrays can double as [tuples](https://tc39.es/proposal-record-tuple/) and objects as [records](https://tc39.es/proposal-record-tuple/). Yet, again, protocols reduce mountains to mole hills. ## Getting Started @@ -53,25 +56,17 @@ npm run bundle Copy the contents of `dist` to `libs` in a project then import from either `libs\atomic` or `libs\atomic_` depending on whether [placeholder partial](./placeholder-partial.md) is wanted. -Implementing a small app is a good first step for someone unfamiliar with the herein described approach. +Implementing a small app is a good first step for someone unfamiliar with how Atomic does programs. ## Modules -The `core` module is the basis for the functional core, `shell` for the imperative shell and `dom` for the UI. Elm sold FRP, so by the time CSP appeared in `core.async` that ship had already sailed. `shell` is based on reactives. - -Its state container, the bucket which houses an app's big bang [world state](https://docs.racket-lang.org/teachpack/2htdpuniverse.html), is the `cell`. It's mostly equivalent to a Clojure atom. The main exception is it invokes the callback upon subscription the way an Rx [subject](https://rxjs.dev/guide/subject) does. This is well suited to the developing of user interfaces. And like [xstream](https://staltz.com/why-we-built-xstream.html) it doesn't rely on many operators, providing a simple but sufficient platform for FRP. +A typical app imports the trifecta—`core`, `shell`, and `dom`—as `_`, `$` and `dom` respectively. These provide what's necessary for building a functional core, an imperative shell, and a user interface, everything an app needs. These modules hint at a 3-part architecture—a core, shell, and ui. Pragmatically, the shell will often also contain the ui, so 2 parts (a `core` and a `shell` module) will usually be good enough. -The typical UI imports the trifecta—`core`, `shell`, and `dom`—as `_`, `$` and `dom` respectively. The `_` doubles as a partial application placeholder when using [placeholder partial](./placeholder-partial.md). [To facilitate interactive development](./interactive-development.md) these can be readily imported into the console. +To facilitate [interactive development](./interactive-development.md) these modules can be readily loaded into the console. The `_` doubles as a [placeholder for partial application](./placeholder-partial.md). -Since many of its core functions are taken directly from Clojure one can often use its documentation. Here are a handful of its bread and butter functions: -* [`swap`](https://clojuredocs.org/clojure.core/swap!) -* [`get`](https://clojuredocs.org/clojure.core/get) -* [`update`](https://clojuredocs.org/clojure.core/update) -* [`updateIn`](https://clojuredocs.org/clojure.core/update-in) -* [`assoc`](https://clojuredocs.org/clojure.core/assoc) -* [`assocIn`](https://clojuredocs.org/clojure.core/assoc-in) +A functional component uses a state container, the bucket which keeps its [world state](https://docs.racket-lang.org/teachpack/2htdpuniverse.html). In Atomic this is a `cell`. It's mostly equivalent to a Clojure atom. The only significant difference is it invokes its callback upon subscription the way an Rx [subject](https://rxjs.dev/guide/subject) does. -The beauty of these functions (kudos to Hickey!) is how easy they make it to surgically replace the state held in a state container without mutating it. +Since Elm had already sold FRP by the time CSP appeared in `core.async`, Atomic is based on reactives and state containers. The [world state is addressable](./addressable-data.md) and can be updated using Clojure's standard functions for surgical state updates. In the absence of threading macros and pipeline syntax several functions exist (see these demonstrated in the example programs) to facilitate pipelines and composition: * `chain` (a normal pipeline) @@ -81,25 +76,23 @@ In the absence of threading macros and pipeline syntax several functions exist ( ## Vendoring As A Safety Net -The author's philosophy is to sparingly add libraries, to keep projects lean. [Dependencies breed](https://tonsky.me/blog/disenchantment/). The ever-changing landscape of modern libraries (Vue, React, Angular, Svelte, etc.) brim with excellent ideas, yet the author continually ships working software without them. - -Rather, and only when a deficit is felt, are the ideas, not the dependencies, grafted in. This prevents fast-moving vendors from dictating schedules and alleviates the pressure of falling out of step with the latest release. +The author prefers to sparingly add libraries, to [keep projects lean](https://tonsky.me/blog/disenchantment/). The gradual extension of a code base is preferred to adding a new dependency to meet every need. This conservative approach keeps faster-moving parties from dictating schedules. -Because Atomic has been used primarily by a small, internal audience, the change process hasn't been formalized to protect a wider audience. The author [vendors it](https://stackoverflow.com/questions/26217488/what-is-vendoring) into his projects to eliminate the pressure of keeping up with releases. This permits safe use. +Because Atomic has been used primarily by a small, internal audience, the change process hasn't been formalized to protect a wider audience. The author [vendors it](https://stackoverflow.com/questions/26217488/what-is-vendoring) into his projects. This practice alleviates the pressure of keeping up with releases and permits safe use. ## Guidance for Writing Apps -Start with a functional core whose data structure representing the world state, though it is made up of objects and arrays, is held as immutable and not mutated. That state will have been birthed from an `init` function and wrapped in an atom. +Start by housing a world state made up of plain objects and arrays in a state container. It'll likely have been created via an `init` function or loaded from a data store. -Then write [swappable](https://clojuredocs.org/clojure.core/swap!) functions which drive state transitions based on anticipated user actions. These will be pure. The impure ones will be implemented later in the imperative shell or UI layer. +Then write pure, [swappable](https://clojuredocs.org/clojure.core/swap!) functions which drive transitions based on anticipated user actions. These will be used later to actuate side effects in the imperative shell/UI layer(s). -The essence of "easy to reason about" falls out of purity. When the world state can be readily examined in the browser console after each and every transition identifying broken functions becomes a much less onerous task. +The essence of "easy to reason about" falls out of purity. When the world state can be readily examined in the browser console after each and every transition identifying broken functions becomes a less onerous task. Next, begin the imperative shell. This is everything else including the UI. Often this happens once the core is complete. Not all apps have data, however, which is simple enough to visually digest from the browser console. In such situations one may be unable to get by without the visuals a UI provides and the shell may need to be created earlier and develop in parallel. -This entire effort begins with [forethought](https://www.youtube.com/watch?v=f84n5oFoZBc), preliminary work, and perhaps a bit of notetaking. Think first about the shape of the data, then the functions (and, potentially, commands/events) which transform it, and lastly how the UI looks and how it utilizes this. For more complex apps, roughing out the UI in HTML/CSS will help guide the work. Not everything needs working out, but having a sense of how things fit together and how the UI works before writing the first line of code will help avoid snafus. +This entire effort begins with [forethought](https://www.youtube.com/watch?v=f84n5oFoZBc), preliminary work, and perhaps a bit of notetaking. Think first about the shape of the data, then the functions (and, potentially, commands/events) which transform it, and lastly how the UI looks and how it utilizes this. For more complex apps, roughing out the UI in HTML/CSS will help guide the work. Not everything needs working out, but having a sense of how things fit together and how the UI works before writing the first line of code helps avoid snafus. -If an app involves animation, as a turn-based board game would, ponder this aspect too. How one renders elements which are animated is often different from how one renders those which aren't. Fortunately, CSS is now capable of driving most animations without the help of additional libraries. +If an app involves animation, ponder this aspect too. How one renders elements which are animated is often different from how one renders those which aren't. Fortunately, modern CSS can now do what once required libraries. ## Progressive Enhancement diff --git a/addressable-data.md b/addressable-data.md new file mode 100644 index 00000000..cb6f032c --- /dev/null +++ b/addressable-data.md @@ -0,0 +1,25 @@ +# Addressable Data + +Functional programming relies on addressable data. Instead of having lots of objects each containing bits of data, a program/component stores its state in a single state container. And since most programs deal in dozens of entities and disparate concepts, the state held in the container will usually be a structured, mashup of lots of things. + +The developer, having devised that structure, will have a strategy for querying and updating it. In some respects, he will select and update as he would any large structure (e.g. the DOM). This is made possible through Clojure's protocols for querying, updating and overwriting data. + +* [`get`](https://clojuredocs.org/clojure.core/get) — reading +* [`update`](https://clojuredocs.org/clojure.core/update) — update a property +* [`updateIn`](https://clojuredocs.org/clojure.core/update-in) — update given a path +* [`assoc`](https://clojuredocs.org/clojure.core/assoc) — overwrite a property +* [`assocIn`](https://clojuredocs.org/clojure.core/assoc-in) — overwrite given a path + +With just a handful of functions, he surgically updates the data held in the container. + +With Atomic functions, the names which match Clojure functions are usually the functional equivalent of Clojure's. This means one can usually use the Clojure documentation to understand what a function does. + +Like Clojure, addressable data is replaced via a succession of [swap!](https://clojuredocs.org/clojure.core/swap!) operations. + +```js +const $state = $.cell(/* real estate data */); +$.swap($state, _.assocIn(_, ["property-manager", "address", "street"], "101 Boardwalk Place")); +const condo = _.chain($state, _.deref, _.get(_, "condos"), _.detect(_.comp(_.gte(_, 500000), _.get(_, "price")), _)); +$.swap($state, _.updateIn(_, ["condos", condo, "price"], _.mult(_, .9))); //offer 10% discount +$.swap($state, _.assoc(_, "last-modified", _.date())); +``` diff --git a/adopting-the-clojure-mindset.md b/adopting-the-clojure-mindset.md new file mode 100644 index 00000000..6f480112 --- /dev/null +++ b/adopting-the-clojure-mindset.md @@ -0,0 +1,74 @@ +# 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)). + +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. + +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. + +```js +const obj = {title: "Lt.", lname: "Columbo"}; +const shows = ["Columbo", "The Good Doctor"]; +``` + +|Action|Pure World (`core`) |Impure World (`shell`)| +|-|-|-| +|Read property|`_.get(obj, "lname")`|N/A| +|Write property|`_.assoc(obj, "lname", "Specter")` | `$.assoc(obj, "name", "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. + +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. + +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. + +```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 = $.cell({lname: "Specter"}); +$.swap($harvey, _.assoc(_, "fname", "Harvey")); +const fname = _.chain($harvey, _.deref, _.get(_, "fname")); // "Harvey" +``` + +These are called simulated commands 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. + +An ordinary command would be impure and actually change the subject. In accordance with command-query separation, it would have no return value. + +```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" +``` + +Immutable `assoc` is a query, mutable `assoc` a command. The one emulates change. The other actuates it. Thus, `_`s signal emulation, `$`s actuation. + +Immutable `assoc` is tailor-made for truly persistent types, like records. But even without them, it can be implemented against reference type objects of the usual variety. + +The same applies to `conj` or any other command one imagines. It's possible to simulate change against any type. All simulation requires is an atom or, in Atomic, a cell. The cell contains the state and [`swap`](https://clojuredocs.org/clojure.core/swap!)s updates against it using simulated commands. + +What this effectively means is the above table can, as desired, be fully realized so that any mutable operation can also be simulated, 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). + +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. + +## To What End? + +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. + +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. + +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. +