JUNK: a programming notation
JUNK is an algebraic programming notation which uses quantales as a primary organising principle, both for data and for code. Quantale modules also play a role, as well as monads; dictionaries are treated in such a way that they behave both very much like other values and very much like other functions.
We start with brief overview of the mathematical preliminaries, and proceed through a tour of the data values, both eager and lazy; the functions, and their application to values; and finish with a sampling of some advanced features made possible by the core theory, followed by some algebraic highlights of the theory.
0. Mathematical Preliminaries
A unital quantale is a monoid in the category Sup of complete join-semilattices. On this page we use ASCII: we spell multiplication as comma `,` with identity `Nil`; spell the join as pipe `|` with bottom `Null`, top `Nuts`; and natural order as `~` (`x ~ y` equivales `x|y == y`). Multiplications distribute over joins, and here they are noncommutative. We will refer to multiplication-prime elements (the alphabet of the monoid) as "items", and refer to join-prime elements as "atoms".
For a quantale Q, a left Q-module is a sup-lattice M together with an action `(Q,M)→M`, satisfying the usual axioms; a right Q-module has action `(M,Q)→M`.
1. Data
There are three sorts of data values:
- simple data - eg ints or strings (with their common orderings, but we won't need that here).
- compound data - other elements of the noncommutative unital quantale over the simple data. In particular, a simple datum is both atom (indivisible under `|`) and item (indivisible under `,`).
- complex data - data containing the packaging wrappers: `[` and `]` around any element denotes a distinct item packaging that element; `{` and `}` around any element of the quantale denotes a distinct atom packaging that element. (neither `[]` nor `{}` are idempotent on data: they may be nested) The quantale join is associative, but `{}` around a `|`-sum forms commutative but non-associative sets; the quantale monoid is associative, but `[]` around a `,`-multiple forms non-commutative and non-associative lists.
User-defined algebraic datatypes also count as complex, and we can view `[]` and `{}` as being similar to other constructors, except with special mixfix syntactic representation.
The join of two items is an item, and `|` distributes across `[]`, so `[0|1]` = `[0]|[1]` which is an item but not an atom. Note that `|` does not distribute across `{}`; `{0|1}` is distinct from `{0}|{1}`.
2. Strictness
There is another simple datum: `NotYet` which is part of an ordering `:~` in which it lies below all other values, all of which are indiscretely related by `:~`. As the name implies, `NotYet` serves as a lazy placeholder for a datum which will be better approximated, later, by a stricter value.
JUNK’s order is lexicographic (:~,~), ie strictness first, joins second, but in implementation it collapses to a flat-lattice.
3. Functions
Similarly to data, there are three types of function:
- simple fns - primitives, lambdas
- compound fns - other elements of the quantale over the simple fns with lattice join `|` having top `FNuts` and bottom `FNull`; and monoid operation `,` having identity `FNil`.
(f|g)(x) == f(x)|g(x)
(f,g)(x) == g(f(x))
- complex fns - [f](x) is strict f: application evaluates x until it is proper (recursively does not contain NotYet) and evaluates f(x) until its result it also proper. {f}(x) is lazy f: NotYet propagates. (NB unlike the case for data, [] and {} are idempotent when packaging functions.)
4. Principles of Application
Application is join-preserving: `f(x|y) == f(x)|f(y)`. Furthermore, application of a function to a value outside its domain results in `Nuts`. Application is monotonic in `:~`, the ordering containing `NotYet`. Functions are naturally ordered, `f~g` is equivalent to `(f|g) == g`.
5. Advanced Features
- Two primitive higher-order functions: `Opt(f)(x)` = `f(x)` if x in the domain of f, `x` otherwise; `Rep(f)(x)` = the fixpoint of f applied to x (...f(f(...f(x)...))...) when it exists, otherwise `NotYet`.
- Another syntactic constructor allows us to form `key: value` pairs. Joins of these pairs inside {} form dictionaries, which behave similarly to functions: {1:2,3:4}(1) == 2. Dictionaries can be joined with `|` and composed with `,` like functions. In order to get a compatible ordering with functions, although pairs have the usual lattice ordering within the curly brackets, from outside the curly brackets the keys sort in the opposite fashion: dictionaries are compared by (-size, revlex of key, lex of vals). This also means composition of dictionaries is monotonic.
- Local definitions use the `$$` operator. Instead of `let y=f(x) in let z=g(y,x) in h(z,x)`, JUNK writes `h(z,x) $$ z:=g(y,x) // y:=f(x)`; this local definition mechanism will be extended to provide modules in the program-organizing sense.
- Lambdas, instead of being expressed as `(\formals. expression)`, are expressed via `expression @ pattern` and application does a pattern match on the actuals to bind the formals. If the pattern does not match, the entire operation evaluates to `Nuts`. Dual to `$$` there is a substitution mechanism for patterns `$?` which goes in the opposite direction: `(decl1 // decl0 $? pat)` equals `decl1 $? (decl0 $? pat)`.
- Local definitions containing declarations of the form `z<-g(y,x)` act as quantale-monoid-comprehensions, producing an item of output for each item of `g(y,x)` (unless `z` is a pattern which fails to match, in which case it filters) and they can be freely mixed with scalar declarations.
Note that because `$$` and `$?` are useful for analysing complex expressions and patterns into simpler subexpressions and subpatterns, they induce a straightforward translation into ANF.
6. Algebraic Highlights
- Data forms a module over functions with application as scalar multiplication.
- The quantale homs (albeit not functions in general) form a module over data with application as scalar multiplication.
- Because `h(z,x) $$ z:=g(y,x) // y:=f(x)` is equivalent to `(h(z,x) $$ z:=g(y,x)) $$ y:=f(x)` and is bilinear in the join, expressions form a module over declarations with substitution as the scalar multiplication.
- Because `(decl1 // decl0 $? pat)` is equivalent to `decl1 $? (decl0 $? pat)` and is bilinear in the join, patterns form a module of the opposite-handedness to expressions. `$$` and `$?` do not commute; `@` is the tensor mediating them.
- Declarations acting on both pattern and expression modules respect the evaluation strategies of complex functions.
- `[]` is adjoint to `{}` and hence `[f]~g == f~{g}`, as well as giving rise to a lazy monad `T(f) = {[f|}` (isomorphic to Haskell's Maybe) and a strict comonad `S(f) = [{f}]`
Influences
Shout outs to Backus, Burstall, Conway, Dijkstra, Hehner, Iverson, Kleene, McCarthy, Scott, and Wadler.