A categorical programming language with effects
Install • Tutorial • Editor support • Development
Very work-in-progress
(.playerA .points - .playerB .points)
{ leader = (> 0) [ true = "A", false = "B"],
delta = abs show }
"Player {.leader} is winning by {.delta} points!"
The Lawvere language (and the executable bill) is named
after William
Lawvere.
Once installed, start a Lawvere REPL
(Read-Eval-Print-Loop) with bill -i:
$ bill -i
--------------
Lawvere v0.0.0
--------------
> 40 + 2
42
> "hello"
"hello"
(Or cabal run bill -- -i or
stack exec bill -- -i if bill isn't
install.)
bill can also be given a file:
bill -i example.law. By omitting the -i flag,
the main arrow is executed directly and the REPL is not
started. In the REPL, :r will reload the loaded file, and
:q will terminate the session. This README file is a
literate Lawvere script, so you can load it and try out the
examples:
$ bill -i README.md
Check OK!
> answer + 1
43
Values of basic types are written as in other programming languages,
e.g. 42 and "hello world". But in Lawvere,
everything is an arrow (Lawvere's equivalent of a function), so that
these actually denote constant arrows. For example, 42
denotes the arrow which is constantly 42:
ar answer : {} --> Int = 42
The above code defines an arrow using the ar keyword.
The arrow has source {} (which is the syntax for the unit
type) and target Int.
When the REPL accepts an input, it actually executes it (with the
Haskell evaluator) on the unit input. So for example inputting
incr (which expects an Int) will result in an
error.
Lawvere also has support for basic arithmetic and comparisons. These
are operations on arrows, for example f + g forms the
pointwise addition of arrows f and g.
The main way to build up larger programs from smaller ones is by
using composition. The syntax for this is very lightweight -
it's simply whitespace! That is, f g denotes the
composition of f and g. If you are coming from
Haskell, note that this does not correspond to .,
but to >>>, that is, f comes first,
then g.
(If you know Forth composition and literals as constant functions will feel familiar.)
To illustrate this we can use the built-in arrow incr,
which increments an integer:
ar plus3 : Int --> Int = incr incr incr
ar fourtyFive : {} --> Int = 42 plus3
will output 45.
To run this, create a file with the above contents and use
bill:
$ bill -i test.law
--------------
Lawvere v0.0.0
--------------
checking..
Check OK!
> fourtyFive
45
> fourtyFive plus3 incr
49
Note: The checker is a work-in-progress and is far from complete.
The identity arrow is called identity, but you can also
write it with nothing at all (or whitespace). So the mathematical
function [x ↦ x * x + 1] can be written
identity * identity + 1, or simply * + 1:
ar squarePlusOne : Int --> Int = identity * identity + 1
ar squarePlusOne' : Int --> Int = * + 1
$ bill -i test.law
> 2 squarePlusOne'
5
Datatypes in lawvere are called objects (since
they correspond to objects in category theory -- as arrows, as you might
have guessed, correspond to morphisms). We define a new object
Point with the keyword ob. If the object is a
product type (or 'struct', or 'record'), specify it using braces:
ob Base Point = { x: Float, y: Float }
The arrow which projects out the x component from
Point is written .x. (Think of the
foo.x notation that is usual in other programming
languages, except without anything preceding the dot.)
$ bill -i
> { user = { name = "Mina", age = 2 }, req = { format = "json" } } .user .name
"Mina"
To create arrow to a product, we specify, again using braces, arrows to each component of the product (in categorical terms, a cone) . For example,
ar somePoint : {} --> Point =
{ x = 2.3, y = 4.6 }
This works because 2.3 and 4.6 are arrows
of type {} --> Float, and the braces syntax uses arrows
which all have the same source.
In general, arrows of type
X --> { a: A, b: B, c: C, ... } can be written as
{ a = f, b = g, ... }
if f : X --> A, g : X --> B,
h : X --> C, etc.
Here's a fuller example of using products:
ob Base Point = { x: Float, y: Float }
ar linFun : Point --> Float = 2.0 * .x + 3.0 * .y
ar someNum : {} --> Float =
{ x = 2.3, y = 4.6 } linFun
someNum is then 18.4.
The empty product object (in categorical terms, the terminal object)
is written as {}, and the unique arrow to it is also
{}. If there is any ambiguity (which occurs when defining
functors) then you can use {:} and {=}.
By using parentheses instead of braces, the components are positional
rather than named. In this case the projections are .1,
.2, etc. Using a positional product for Point
the previous program would be:
ob Base PointPos = (Float, Float)
ar linFunPos : PointPos --> Float = 2.0 * .1 + 3.0 * .2
ar someNumPos : {} --> Float =
(2.3, 4.6) linFunPos
A string can contain interpolated expressions. For example,
"Name: {f}, Age: {g}" denotes an arrow
A --> String as long as both f and
g are also morphisms A --> String.
The program:
ar james : {} --> String =
{ name= "James", hobby= "playing Go" } "{.name} likes {.hobby}."
will result in "James likes playing Go."
We can define sum types too. For instance, booleans:
ob Base Bool = [ true: {}, false: {} ]
Using square brackets we define a sum type with two summands,
true and false, each with {} as
payload.
Sum types come equipped with constructors (injection). The
constructor into the component with name foo is denoted
foo., simply mirroring the notation for projections.
In order to define some simple boolean functions, we'll need to learn how to map from sums. This is like pattern matching, specifying an arrow for each summand (and thus, in categorical language, a cocone). This is similar to cones, except using square brackets instead of braces. To illustrate this let's define the negation function:
ar not : Bool --> Bool
= [ true = false.,
false = true. ]
In words, we split the arrow into two cases. In the first case (on
the true component) we use false. constructor,
on the other component we use true..
In general, to specify an arrow:
[ a: A, b: B, c: C, ... ] --> X
one uses a cocone
[ a = f, b = g, c = h, ... ]
where f : A --> X, g : B --> X,
h : C --> X, etc.
Continuing with boolean functions, let's try to define the
and function:
ar and : {x: Bool, y: Bool} --> Bool = ?
This is an arrow to a sum (Bool), so we can't
use a cocone, and from a product
({x : Bool, y: Bool }), so we can't use a cone---are we
stuck? Intuitively we want to inspect one of the two arguments
(x or y) in order to continue. For this we
will use the distributor @x. To understand what
this does, first let's re-write {x : Bool, y : Bool} by
expanding the definition of Bool at the x
summand:
{ x: [ true: {}, false: {}], y: Bool }
The type of @x is:
@x : { x: [ true: {}, false: {}], y: Bool } --> [ true: { x: {}, y: Bool}, false: { x: {}, y: Bool } ]
The morphism @x transforms the product into a sum; a sum
with the same summand names as the sum in the component it targets. So
in this case we end up with a sum with summands true and
false, and the x component contains the
unwrapped payload for the original sum at x (in this case
they are both {}).
Using this we can define and as follows:
ar and : {x : Bool, y : Bool} --> Bool =
@x [ true = .y,
false = {} false. ]
In words: "Perform a case analysis on x, if
x is true, then return y, otherwise return
false". Note the similarity with the equivalent Elm program
(Haskell doesn't have anonymous records, making the comparison less
clear), even though Lawvere has no variables or λs:
and : { x : Bool, y : Bool } -> Bool
and input =
case input.x of
True -> input.y
False -> FalseIn this example we'll sum up a list of values.
First we'll define lists of Ints (we'll learn how to
define the list functor later, which means we don't need to
define a new object for each possible object of elements):
ob Base ListI =
[ empty: {},
cons: { head: Int, tail: ListI }
]
An example list can be built up by composing morphisms together:
ar aFewPrimes : {} --> ListI =
empty.
{ head = 2, tail = } cons.
{ head = 3, tail = } cons.
{ head = 5, tail = } cons.
Note that in { head = 2, tail = }, the arrow being used
at the tail component is the identity. This could also be
written { head = 2, tail = identity}. Another thing to note
is that the 2 being used here doesn't have source
{}, indeed all integer literals actually have type
forall a. a --> Int (and similarly for other scalars).
This saves one from having to write {} 2. Like Haskell,
Lawvere has some syntactic sugar for list-building:
ar morePrimes : {} --> ListI =
#(2, 3, 5, 7, 11)
We can sum over a list using a cocone:
ar sum : ListI --> Int =
[ empty = 0,
cons = .head + .tail sum ]
In words: If the list is empty, then return
0. Otherwise take the head, and the
sum of the .tail, and + them
together.
> morePrimes sum
28
(Very WIP)
Lawvere is is a pure language but allows programming with effects using free Freyd categories, much like Haskell is pure but allows programming with effects using monads or arrows. In fact Freyd categories and arrows are very similar, e.g. see Categorical semantics for arrows.
The IO effect is built-in. Here is an example of a
morphism which performs I/O:
ar Base[IO] hello : {} --> String =
~"What is your name?" putLine
getLine
~"Hello {}" putLine
To run this, one must use the io functor:
ar InputOutput helloIO : {} --> {} =
io(hello)
This will print What is your name, wait for the user to
input their name, and then greet them.
Cones ({..}) are not permitted in effectful morphisms,
but one can still perform effects at a single component. Here is a
program which asks for 2 pieces of user input:
// Turn a question into an answer.
ar Base[IO] ask : String --> String =
putLine getLine
// Ask some questions and then print a greeting.
ar Base[IO] greet : {} --> {} =
~{name = "What is your name?", hobby = "What is your favourite hobby?"}
!name(ask)
!hobby(ask)
~"Hello {.name}, I like {.hobby} too!" putLine
ar InputOutput greetIO : {} --> {} =
io(greet)
Effectful programming will be explained more in the next section. In practice the main points are:
{..}) are not permitted.~.!label(..) syntax.InputOutput
category with io.In this example we'll define two sorts of effects: integer state and throwing a string error. See here
effect IntState over Base {
get : {} --> Int,
put : Int --> {}
}
effect Err over Base {
err : String --> []
}
This defines a theory IntState for extending the
Base category with two distinguished morphisms for state
manipulation: get and put. Similarly
Err is a theory with a distinguished morphism
err : String -> []. Note that [] is the
empty sum, i.e. the initial object of Base. Therefore
err can be used to map from String to
any object, by composing with [], the empty
cocone.
We can then define morphisms in this abstract extension of
Base. The following morphism increments the state while
returning the original value:
ar Base[IntState] next : {} --> Int =
get ~{ current = , next = incr} !next(put) ~.current
There are two new pieces of syntax:
~ denotes the canonical injection into the
effect-category. So this can be used for lifting any pure morphism. This
performs the same role as the arr
method of the Arrow
type class in Haskell.!label(..) (where label can be any
component name). Effect categories do not (necessarily) have products,
so using the cone syntax is prohibited. The sequencing of effects is
specified by using the categorical composition. The effect-category has
the same objects as the pure category it extends however, and an
effectful morphisms can be performed at one component of a product of
the base category. If f : A --> B' is an effectful
morphism and {a : A, b : B, c : C} is a product in the pure
category, then
!b(f) : {a : A, b : B, c : C} --> {a : A, b : B', c : C}
is another effectful morphism. In other words, !b(f) means
"perform effect f at component b". This
performs the same role as first
in Arrow
except for any component.So next works as follows:
get the current state,put) using the pure morphism
{current = , next = incr},put on the next component,current component.For testing the error effect, we'll make a version which throws an error if the next number is greater than 3:
ar Base[IntState, Err] nextSub3 : {} --> Int =
next ~( { sub3 = < 3, ok = } @sub3 )
[ true = ~.ok,
false = ~"Was not under 3!" err []]
Next we'll specify how to map this function over a list. We can't
reuse the list functor because that doesn't specify how to
sequence the effects: should the effect be performed first on the head
or the tail of the list?
ar Base[IntState, Err] mapNextSub3 : list({}) --> list(Int) =
[ empty = ~empty.,
cons = !head(nextSub3) !tail(mapNextSub3) ~cons. ]
We explicitly sequence the effects, using composition, on first the head and then the tail of the list.
The effect-category is still abstract, to actually use the above we must define an effect-category over base and interpret the effects:
category ErrIntState {
ob = ob Base,
ar A --> B = ar Base :
{ state: Int, value: A } -->
[ err: String, suc: { state: Int, value: B } ],
identity = suc.,
f g = f [ err = err., suc = g ],
SumOb(idx) = SumOb(idx),
sumInj(label) = { state, value = .value sumInj(label) } suc.,
sumUni(cocone) = @value sumUni(cocone)
}
This defines a new category with the same objects as
Base, but with a different composition, identity and sum.
Next we make this into an effect category over Base:
effect_category pureErrIntState ErrIntState over Base {
~f = { state, value = .value f } suc.,
side(f) =
{ runeff = { state = .state,
value = .value .eff } f,
onside = .value .pur }
@runeff
[ err = .runeff err.,
suc = { state = .runeff .state,
value = { eff = .runeff .value,
pur = .onside } } suc. ]
}
This is done by defining the canoncial injection ~ and
side, the action of the effect-category at product
components. This is done by interpreting !eff{..}, lifting
some effectful morphism A --> B into a product
{ pur: P, eff: A } --> { pur: P, eff: B }.
Then we provide interpretations for the effects we want to use:
interpret IntState in ErrIntState
{ get = { state = .state, value = .state} suc.,
put = { state = .value, value = {} } suc.
}
interpret Err in ErrIntState
{ err = .value err. }
Finally, we can execute this effect:
ar Base main : {} --> Int =
{ state = 0, // initialise the state to 0
value = #({}, {}, {}) } // we'll map over a list of size 3
pureErrIntState(mapNextSub3)
This returns the list #(0, 1, 2), but if one increases
the size of the length of the list to 4, then it will instead throw an
error.
Checkout the full example.
Lawvere has a compiler to a Categorical Abstract Machine. Again not all features are supported yet.
Compiling the sum function above (called on an empty
list) produces:
$ bill --target vmcode examples/sum.law
#main:
empty.
call #sum
#sum:
cocone empty:#sum_0 cons:#sum_1
#sum_0:
scal 0
#sum_1:
push
scal 1
push_cone
pop
push
.tail
call #sum
push_cone
pop
end_cone 1 2
prim plus
And you can execute the code on the virtual machine with:
bill --target vm examples/sum.law
--------------
Lawvere v0.0.0
--------------
Checking..
Check OK!
Running on categorical machine..
Result: 0
To compile to JavaScript, use the --target js
option:
$ bill --target js test.law
This will output a JavaScript program that logs the output. You can
pipe this directly to node:
$ bill --target js test.law | node
45
The JavaScript compiler isn't well maintained and will just error out on the anything but the most basic language features.
You can build the project with stack or nix.
First install stack
(curl -sSL https://get.haskellstack.org/ | sh ) and then
use stack build. To install the bill
executable (to ~/.local/bin) run
stack install.
On linux you may also need to install tinfo, e.g.
sudo apt-get install libtinfo-dev on Ubuntu.
Make sure you have nix and optionally direnv installed.
Optional: (but faster) install cachix
(nix-env -iA cachix -f https://cachix.org/api/v1/install)
and use the lawvere cache:
cachix use lawvere.
To build project dependencies and tooling the first time enter a nix shell either using direnv (recommended):
$ echo "use nix" > .envrc
$ direnv allow
or manually:
$ nix-shell
Once in the nix shell, to build a release and run it:
$ nix-build nix/release.nix
$ result/bin/bill <file>
For the moment there is only an emacs mode. The syntax looks much better if you use a font with programming ligatures, e.g. FiraCode or alternatives.
To update the nix derivation when project dependencies change:
$ hpack
$ cabal2nix . > nix/packages/lawvere.nix
$ direnv reload
Note: Cabal is also available in the nix shell so you can build with it as well if you like:
$ cabal build