A set of specifications of signatures on algebraic modules and an implementation of a standard library built to match.
Algebraic is a set of Javascript objects which are known as algebraic modules or modules when it's clear that I'm not referring to CommonJS, AMD, or ES6 import/export modules. A module contains a set of related behaviors operating over a set of types which the module pertains to.
For instance, there is a module Array
which contains operations over the
standard JS array type.
> var A = require("algebraic");
> A.Array.of(1)
[ 1 ]
> A.Array.plus(A.Array.of(1), A.Array.of(2))
[ 1, 2 ]
Seasoned OO-ers might find this awkward and backward—we should be doing
something like [1].concat([2])
. Algebraic intentionally eschews this design
with its modular design. (If the noisy imports bother you then consider ES6
destructuring)
import { Array } from "algebraic";
const { plus, of } = Array;
const x = plus(of(1), of(2));
The fastest way to describe modular design in OO-terms is to say that a module is a class with only static methods. They, however, can also be much, much more.
A driving principle of Algebraic's design is behavior before state. What this means is that libraries and programs should be first designed against a specification of behavior and oblivious to the state required to pull the behavior off. The advantage of this adage is increased modularity driven by a clearer line between interface and implementation.
Unfortunately, standard OO practice conflates behavior and state heavily---indeed some take the definition of an object to be the conflation of behavior and state.
In most cases module objects themselves are completely state free. Indeed, in any case where they are not the observation of that state ought to be severely restricted and the intended behavior favored. Instead all of the state of Algebraic arises from the arguments passed to and received from module behaviors.
Immutability vastly reduces the state space of a program improving the ability
for the author to correctly reason about it's behavior. It's also assumed in
most descriptions of algebraic structures. For these reasons, Algebraic
eschews behaviors which mutate values opting instead to return new copies as
often as possible. This also provides standard sharing tricks like pre-emptive
O(1)
equality checks (which Algebraic uses by default) useful for
comparison-driven programs like React-style UIs.
Note: Algebraic-style immutability is dangerous when mixed with mutable JS code. Algebraic does not attempt to freeze or seal values and also maximizes sharing, so mutation of an object involved in Algebraic behaviors can have long range effects.
Algebraic takes its design inspiration heavily from two other languages and their associated communities: ML and Haskell. Modular design of programs is extraordinarily well done in ML languages like OCaml and SML where the static type system helps ensure that your interfaces are well-defined and properly used. Algebraic specifications of operations is extraordinarily well done in Haskell where laziness, purity, and the Haskell "typeclass" mechanism drove the use of algebraic laws over immutable values out of both necessity and splendor.
- Why not OO? Other FP-in-JS libraries (Fantasy Land, Ramda, Sanctuary, etc)
provide FP-like functionality by attaching methods to objects so that a value
x
is, e.g., a Monoid whenx.plus
is a partially-applied monoid operation.algebraic
eschews this by placing all functionality in an object external tox
called a module. - Immutable by default. (Obvious cache invalidation)
- Names, operations, and laws from algebra. Programming is an exercise in naming things. Algebra provides a large set of useful, consistent names along with well-defined expectations for what they mean.
- Why ES6 and not something like Typescript? Three reasons.
- This is a packaging of some code originally written for a real project using ES6, so using Typescript was not an option initially.
- I'm not confident that I like the design decisions of Typescript's type system sufficiently to want to endorse it. An untyped system is arguably worse, but it gives me the freedom to write exactly the code intended.
- I am writing some functions which could be well-typed but certainly would not be in most standard type systems. Lacking a real type checker I can introduce semi-formal notation for thee ideas and work with them.
-
Naive
: Decidable
-
String
: Decidable, Hashable, Decidable Monoid, Least, Total Order -
Boolean
-
Number
-
Integer
-
Natural
-
Array
: Decidable Monoid, Covariant, Foldable, Traversable, Monad, Reflects Decidability -
Set.OfDecidable
-
Set.OfHashable
-
Set.OfTotalOrder
-
Dict
-
Trie
-
Validation
-
Either
-
Contract
-
Decidable {
X
}- definitions
X : Type
eq : (X, X) -> Bool
- notation
- When unambiguous,
x = y
is written to meaneq(x, y)
.
- When unambiguous,
- laws
- reflexivity: for all
(a : X)
a = a
- symmetry: for all
(a b : X)
a = b
impliesb = a
. - transitivity: for all
(a b c : X)
a = b
andb = c
impliesa = c
.
- reflexivity: for all
- definitions
-
Hashable {
X
}- definitions
- include Decidable {
X
} - definitions
hash : X -> Int
- include Decidable {
- laws
- identity compatibility: for all
(a b : X)
ifa = b
thenhash(a) = hash(b)
.
- identity compatibility: for all
- definitions
-
Preorder {
X
}- definitions
X : Type
leq : (X, X) -> Bool
indistinguishable : (X, X) -> Bool
λ(a, b) . leq(a, b) && leq(b, a)
incomparable : (X, X) -> Bool
λ(a, b) . not(leq(a, b) || leq(b, a))
- notation
- Where unambiguous,
x <= y
is written to meanleq(x, y)
- Where unambiguous,
- laws
- reflexivity: for all
(a : X)
a <= a
- transitivity: for all
(a b c : X)
a <= b
andb <= c
impliesa <= c
- reflexivity: for all
- definitions
-
Partial Order {
X
} or Poset {X
}- include Preorder {
X
} - implies unique Decidable {
X = X
}- define
eq(a, b) = indistinguishable(a, b)
- define
- laws
- antisymmetry:
indistinguishable
is an equivalence relation
- antisymmetry:
- include Preorder {
-
Weak Order {
X
}- include Poset {
X
} - laws
incomparable
is transitive
- include Poset {
-
Total Order {
X
}- include Poset {
X
} - implies unique Weak Order {
X = X
}- proof: totality implies that
incomparable
is the empty relation
- proof: totality implies that
- laws
- totality: for all
(a b : X)
eithera <= b
orb <= a
- totality: for all
- include Poset {
-
Least {
X
}- definitions
X : Type
bottom : () -> X
- definitions
-
Greatest {
X
}- definitions
X : Type
top : () -> X
- definitions
-
Bounded {
X
}- include Least {
X
} and Greatest {X
}
- include Least {
-
Join {
X
} / Meet {X
}- include Decidable {
X
} - definitions (one of, called
op
)join : (X, X) -> X
meet : (X, X) -> X
- notation
- Where unambiguous,
join
is written|
- Where unambiguous,
meet
is written&
- Where unambiguous,
- laws
- associativity of
op
- commutativity of
op
- idempotence of
op
: for all(a : X)
op(a, a) = a
- associativity of
- implies unique
- Semigroup {
X
} with- define
plus(a, b) = op(a, b)
- define
- Poset {
X
} with- define
leq(a, b) = eq(a | b, b)
or - define
leq(a, b) = eq(a, a & b)
or
- define
- Semigroup {
- include Decidable {
-
Join★ {
X
} / Meet★ {X
}- include Join {
X
} / Meet {X
} asop
- definitions
joinN : [X] -> X
ormeetN : [X] -> X
- laws
op
is a Monoid morphism from arrays toX
- operation compatibility: for all
(a b : X)
op(a, b) = opN([a, b])
- implies unique Least {
X
} / Greatest {X
}- define
bottom() = joinN([])
or - define
top() = meetN([])
- define
- implies unique Monoid {
X
}- define
zero() = opN([]])
- define
- include Join {
-
Lattice
- include Decidable {
X
} - definitions
join : (X, X) -> X
andmeet : (X, X) -> X
- notation
- Where unambiguous,
join
is written|
- Where unambiguous,
meet
is written&
- Where unambiguous,
- laws
join
implies unique Join {X
}meet
implies unique Meet {X
}- absorption: for all
(a b : X)
ifq = a | (a & b)
andp = a & (a | b)
theneq(q, p)
andq = a
andp = a
- Poset {
X
} via Join {X
} is compatible with Poset {X
} via Meet {X
}
- implies unique Poset {
X
}- defined via either Join {
X
} or Meet {X
}
- defined via either Join {
- projects
- Semigroup {
X
} via Join {X
} - Semigroup {
X
} via Meet {X
}
- Semigroup {
- include Decidable {
-
Lattice★
- include Decidable {
X
} - definitions
join : (X, X) -> X
andmeet : (X, X) -> X
joinN : (X, X) -> X
andmeetN : (X, X) -> X
- notation
- Where unambiguous,
join
is written|
- Where unambiguous,
meet
is written&
- Where unambiguous,
- implies Bounded {
X
}- via implication of Least {
X
} and Greatest {X
} from Join★ {X
} and Meet★ {X
} respectively
- via implication of Least {
- laws
joinN
implies unique Join★ {X
}meetN
implies unique Meet★ {X
}- absorption: for all
(a b : X)
ifq = a | (a & b)
andp = a & (a | b)
theneq(q, p)
andq = a
andp = a
- Poset {
X
} via Join {X
} is compatible with Poset {X
} via Meet {X
}
- projects
- Monoid {
X
} via Join★ {X
} - Monoid {
X
} via Meet★ {X
}
- Monoid {
- include Decidable {
-
Distributive {
X
}- include Lattice {
X
} - laws
- distributivity
- for all
(a b c : X)
letp = a & (b | c))
andq = (a & b) | (a & c)
theneq(p, q)
or - dually, for all
(a b c : X)
letp = a | (b & c)
andq = (a | b) & (a | c)
thenp = q
- for all
- distributivity
- include Lattice {
-
Magma {
X
}- definitions
X : Type
plus : (X, X) -> X
- notation
- Where unambiguous,
plus(x, y)
is writtenx + y
- Where unambiguous,
- definitions
-
Semigroup {
X
}- include Magma {
X
} and Decidable {X = X
} - laws
- associativity of
plus
: for all(a b c : X)
eq(plus(a, plus(b, c)), plus(plus(a, b), c))
- associativity of
- include Magma {
-
Monoid {
X
}- include Semigroup {
X
} - definitions
zero : () -> X
plusN : [X] -> X
λ(xs) . xs.reduce(plus, zero())
- notation
- Where unambiguous,
zero()
is written0
- Where unambiguous,
- laws
zero
is a zero: for all(a : X)
a + 0 = a
and0 + a = a
- include Semigroup {
-
Decidable Monoid {
X
}- include Monoid {
X
} - definitions
isZero : X -> Bool
- laws
zero
isZero
:isZero(zero())
- include Monoid {
-
Group {
X
}- include Monoid {
X
} - definitions
negate : X -> X
minus : (X, X) -> X
λ(a, b) . plus(a, negate(b))
- notation
- Where unambiguous,
negate(x)
is written-x
- Where unambiguous,
minus(x, y)
is writtenx - y
- Where unambiguous,
- laws
negate
undoesplus
: for all(a : X)
x - x = 0
and-x + x = 0
- include Monoid {
The following 3 are very close to one another such that Ring {X
} subsumes both Semiring {X
} and Rng {X
} but those two are incomparable.
-
Semiring {
X
}- include Decidable {
X
} - definitions
zero : () -> X
one : () -> X
plus : (X, X) -> X
times : (X, X) -> X
- projects
zero
andplus
form the zero-plus Monoid {X
}one
andtimes
form the one-times Monoid {X
}
- notation
- Where unambiguous,
zero()
is written0
- Where unambiguous,
one()
is written1
- Where unambiguous,
plus(x, y)
is writtenx + y
- Where unambiguous,
times(x, y)
is writtenx * y
- Where unambiguous,
- laws
- the zero-plus Monoid {
X
} is commutative times
-plus
distributivity: for all(a b c : X)
- left distributivity:
a * (b + c) = (a * b) + (a * c)
- right distributivity:
(a + b) * c = (a * c) + (b * c)
- left distributivity:
zero
annihilation: for all(a : X)
0 * a = 0
anda * 0 = 0
- the zero-plus Monoid {
- include Decidable {
-
Rng
- include Decidable {
X
} - definitions
zero : () -> X
negate : X -> X
plus : (X, X) -> X
times : (X, X) -> X
minus : (X, X) -> X
λ(a, b) . plus(a, negate(b))
- notation
- Where unambiguous,
zero()
is written0
- Where unambiguous,
plus(x, y)
is writtenx + y
- Where unambiguous,
times(x, y)
is writtenx * y
- Where unambiguous,
negate(x)
is written-x
- Where unambiguous,
minus(x, y)
is written asx - y
- Where unambiguous,
- projects
zero
,plus
, andnegate
form the zero-plus Group {X
}times
forms the times Semigroup {X
}
- laws
- the zero-plus Group {
X
} is commutative times
-plus
distributivity: for all(a b c : X)
- left distributivity:
a * (b + c) = (a * b) + (a * c)
- right distributivity:
(a + b) * c = (a * c) + (b * c)
- left distributivity:
- the zero-plus Group {
- include Decidable {
-
Ring
- include (all compatible)
- Decidable {
X
} - Semiring {
X
} - Rng {
X
}
- Decidable {
- definitions
zero : () -> X
one : () -> X
negate : X -> X
plus : (X, X) -> X
times : (X, X) -> X
minus : (X, X) -> X
λ(a, b) . plus(a, negate(b))
- notation
- Where unambiguous,
zero()
is written0
- Where unambiguous,
one()
is written1
- Where unambiguous,
plus(x, y)
is writtenx + y
- Where unambiguous,
times(x, y)
is writtenx * y
- Where unambiguous,
negate(x)
is written-x
- Where unambiguous,
minus(x, y)
is written asx - y
- Where unambiguous,
- projects
zero
,plus
, andnegate
form the zero-plus Group {X
}one
-times
form the one-times Monoid {X
}
- laws
- the zero-plus Group {
X
} is commutative times
-plus
distributivity: for all(a b c : X)
- left distributivity:
a * (b + c) = (a * b) + (a * c)
- right distributivity:
(a + b) * c = (a * c) + (b * c)
- left distributivity:
- the zero-plus Group {
- include (all compatible)
Three notational shorthands are used in this section: id = x => x
, f . g = x => f(g(x))
, and . = f => g => x => f(g(x))
.
-
Parametric {
F
}- definitions
F : Type -> Type
- definitions
-
Reflects decidability {
F
}- include Parametric {
F
} - implications
- Decidable {
X
} implies Decidable {F X
}
- Decidable {
- include Parametric {
-
Covariant {
F
}- include Parametric {
F
} - definitions
map : ∀ a b . (a -> b) -> (F a -> F b)
- notation
- Where unambiguous
map(f)
is written[f]
- Where unambiguous
- laws (category morphism)
- for all
(X : Type)
, Decidable {F X
},(fa : F X)
,fa = [id](fa)
- for all
(X Y Z : Type)
, Decidable {F X
},(fx : F X)
,(g : X -> Y)
,(f : Y -> Z)
[f]([g](fx)) = [f . g](fx)
- for all
- include Parametric {
-
Contravariant {
F
}- include Parametric {
F
} - definitions
comap : ∀ a b . (b -> a) -> (F a -> F b)
- laws (category morphism)
- include Parametric {
-
Apply {
F
}- include Covariant {
F
} - notation
- When unambiguous,
f * x
is written to meanap(f, x)
.
- When unambiguous,
- definitions
ap : ∀ a b . F (a -> b) -> (F a -> F b)
smash : ∀ a b . (F a, F b) -> F {fst: a, snd: b}
smashWith((fst, snd) => {fst, snd})
smashWith : ∀ a b c . ((a, b) -> c) -> (F a, F b) -> F c
λ(f) . λ(fa, fb) . map(x => f(x.fst, x.snd))(smash(fa, fb))
- laws
- composition reassociates: for all
(X Y Z : Type)
, Decidable {F Z
},(f : F (Y -> Z))
,(g : F (X -> Y))
,(a : F x)
, we let([.](f) * g) * a = f * (g * a)
.
- composition reassociates: for all
- notes
- can be used to
traverse
non-empty structures
- can be used to
- include Covariant {
-
Applicative {
F
}- include Apply {
F
} - defintions
of : ∀ a . a -> F a
- traversals of "container-like" structures
traverseArray : ((a_i, i) -> F b_i) -> ([a_i] -> F [b_i])
traverseDict : ((a_i, k_i) -> F b_i) -> (Obj_i (k_i : a_i) -> F (Obj_i (k_i : b_i)))
- notation
- Where unambiguous,
of(x)
is written{x}
- Where unambiguous,
- laws
- identity reflection:
{id} * v = v
- application reflection:
{f} * {x} = {f(x)}
- interchange:
u * {y} = {f => f(y)} * u
- identity reflection:
- include Apply {
-
Bind {
F
}- include Apply {
F
} - definitions
bind : ∀ a b . (a -> F b) -> (F a -> F b)
seq : ∀ a b . (F a, F b) -> F b
λ(fa, fb) . bind(_ => fb)(fa)
kseq : ∀ a b . (a -> F b, b -> F c) -> (a F c)
λ(afb, bfc) . λa . bind(bfc, afb(a))
collapse : ∀ a . F (F a) -> F a
bind(id)
- notation
- Where unambiguous
bind(k, m)
is writtenm >>= k
- Where unambiguous
seq(m1, m2)
is writtenm1 >> m2
- Where unambiguous
kseq(f1, f2)
is writtenf1 >=> f2
- Where unambiguous
- projects
- Apply {
F
} via- define
ap(mf, mx) = mf >>= (f => [f](mx))
- define
- Apply {
- laws
- associativity:
f >=> (g >=> h) = (f >=> g) >=> h
- associativity:
- include Apply {
-
Monad {
F
}- include (compatibly)
- Bind {
F
} - Applicative {
F
}
- Bind {
- laws
- left identity:
of >=> f = f
- right identity:
f >=> of = f
- left identity:
- include (compatibly)
-
Foldable {
F
}- include Covariant {
F
} - definitions
foldMap : ∀ a x . (Monoid {x}, a -> x) -> (F a -> x)
toArray : ∀ x . F x -> [x]
- defined using the universal Monoid
{[X]}
available for allX
- defined using the universal Monoid
forEach : ∀ x . (F x, x -> ()) -> ()
- defined by passing through arrays
reduceLeft
- defined by passing through arrays
reduceRight
- defined by passing through arrays
- laws
- "seek and find": for all
a : Type
, all functionsF a -> a?
, and all valuesx : F a
, iff(x)
exists thenf(x)
is in the arraytoArray(x)
- "seek and find": for all
- include Covariant {
-
Traversable {
F
}- include Foldable {
F
} - definitions
traverse : ∀ a b k . (Applicative {k}, a -> k b) -> (F a -> t (F b))
- laws
- include Foldable {
- Cons {
F
}- include Foldable {
F
} - definitions
cons : ∀ a . (a, F a) -> F a
- laws
- "place and find": for all
a : Type
withDecidable {a}
, for allc : F a
andx : a
, we havecontains(x, toArray(cons(x, c))
- "place and find": for all
- include Foldable {
-
tel/ocaml-cats: An attempt to bring statically typed algebraic interfaces to OCaml.
-
fantasyland/fantasy-land: A specification of objects which provide suitable algebraic operations. It's another, somewhat popular point in the design space of algebraic FP in Javascript.
-
Ramda.js: An implementation of some algebraic and some higher-order functional techniques via object-oriented Javascript.
-
plaid/sanctuary: An implementation of more advanced algebraic data types and associated functionality written For compatibility with Ramda.
Copyright 2015, Reify Health (<joseph@reifyhealth.com>)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.