Ryan Moore
Engineer at MX
Volunteer Teaching Refugees Java
Musician
@panicwhenever
Haskell Programming From First Principles
From Nand
To Lambda
Foundational Computing
Through
Functional Principles
Nand To Tetris:
Build A Modern Computer From First Principles
Stuff We're Talking About Today:
-
Combinatorial Boolean Circuitry
-
Exciting function compositions
-
Monoidal Arithmetic Circuitry
-
Monadic Sequential Circuitry
Introducing A Language
-
Based entirely on function input and output
-
Relies on function composition for portability
-
Declarative, not imperative in nature
-
Encodes side effects in a type
HARDWARE
DESCRIPTION
LANGUAGE
CHIP MyChip {
IN a, b; // can be many ins
OUT out; // can be many outs
PARTS:
//Here we connect inputs and outputs using other chips!
//Wire the inputs a & b to BuiltInChip inputs,
//assign its output to a variable
BuiltInChip(aInput=a, bInput=b, out=myVar)
//Wire in the variable myVar (output of other chip) to BuiltInChips input,
//wire its output to the output of MyChip
BuiltInChip(a=myVar, b=myVar, out=out)
}
What's Up With NAND?
| Input A | Input B | Output |
|---|---|---|
| High | High | Low |
| High | Low | High |
| Low | High | High |
| Low | Low | High |
NAND in Haskell!
Boolean Logic
/**
* Not gate: out = not in
*/
CHIP Not {
IN in;
OUT out;
PARTS:
//Input of not is wired to both a & b of Nand
//Output of Nand is wired out
//Truth Table of Nand:
// T T -> F
// T F -> T
// F T -> T
// F F -> T
// The first and last options are our not!
Nand(a=in, b=in, out=out);
}
Not In HDL!
Not In Haskell!
CHIP And {
IN a, b;
OUT out;
PARTS:
//Wire inputs a & b to Nand
//Wire Nand output to variable x
Nand(a=a, b=b, out=x);
//Wire variable x to Not, wire x to And output
Not(in=x, out=out);
}
And In HDL!
And In Haskell!
// This file is part of www.nand2tetris.org
// and the book "The Elements of Computing Systems"
CHIP And16 {
IN a[16], b[16];
OUT out[16];
PARTS:
And(a=a[0], b=b[0], out=out[0]);
And(a=a[1], b=b[1], out=out[1]);
And(a=a[2], b=b[2], out=out[2]);
And(a=a[3], b=b[3], out=out[3]);
And(a=a[4], b=b[4], out=out[4]);
And(a=a[5], b=b[5], out=out[5]);
And(a=a[6], b=b[6], out=out[6]);
And(a=a[7], b=b[7], out=out[7]);
And(a=a[8], b=b[8], out=out[8]);
And(a=a[9], b=b[9], out=out[9]);
And(a=a[10], b=b[10], out=out[10]);
And(a=a[11], b=b[11], out=out[11]);
And(a=a[12], b=b[12], out=out[12]);
And(a=a[13], b=b[13], out=out[13]);
And(a=a[14], b=b[14], out=out[14]);
And(a=a[15], b=b[15], out=out[15]);
}And16 In HDL!
A Fanciful Sidetrack Through
Function Composition
(.) :: (b -> c) -> (a -> b) -> a -> c
(f . g) x = f (g x)
-- imagine we want to convert our Voltage to a Boolean Type
voltageToBoolean :: Voltage -> Boolean
voltageToBoolean High = True
voltageToBoolean Low = False
-- contrived example to give an opposite boolean of a voltage
lie :: Voltage -> Boolean
lie x = voltageToBoolean (not' x)
-- can be composed!
lie :: Voltage -> Boolean
lie x = (voltageToBoolean . not') x
-- can be reduced to point free!
lie :: Voltage -> Boolean
lie = voltageToBoolean . not'
(.) Is Composition
//In some UI File
Utils.getRGBWithAlpha("Red");
Is It Useful?
//In Utils File
const ColorUtils = {
getRGBWithAlpha: color => {
const hex = StringToHexLib.convert(color)
const rgb = ColorLib.toRGB(hex);
//args in wrong order, also no auto curry...
const withAlpha = OtherLib.addAlpha(rgb, 0.5);
return withAlpha
}
}
//all those functions that do nothing then call other functions...
//sure would be nice to just say
getRGBWithAlpha = convert . toRGB . (addAlpha 0.5)If
f (g x) = f . g
Can
f (g x y) = f ... g
canItCompose f g x y = f (g x y)
-- 1. convert to lambda
\f g x y -> f (g x y)
-- 2. use $ to remove parentheses
\f g x y -> f $ g x y
-- 3. Trickiest part for me, follow the types of (.) and ($)
\f g x y -> f . g x $ y
-- 4. You can eliminate an argument that is only passed to other functions
\f g x -> f . g x
-- 5. Separate f and g with parentheses to make evaluation order explicit
\f g x -> (f .) (g x)
-- 6. Use $ to remove parentheses around g
\f g x -> (f .) $ g x
-- 7. Use our ole ($) -> (.) trick\f g x -> (f .) . g $ x
-- 8. Dear me, what abomination is this
\f g -> (f .) . g
-- 9. Live your best Haskell life, assign it to a cute infix
(...) f g = (f .) . gnot (nand x y)
Can Indeed
not ... nand
What's with Data.Aviary and blackbird?
- Combinatory Logic was largely developed by Haskell Curry
- Haskell Curry was an avid bird watcher
- Raymond Smallman wrote a celebration of combinatory logic puzzles called "To Mock a Mockingbird"
- The blackbird combinator is introduced in that book
- Combinatory Logic is functionally equivalent to lambda calculus
- Haskell is lambda calculus

Back to The (More) Down To Earth Biz
Binary Arithmetic
| Operand | Operand | Sum | Carry |
|---|---|---|---|
| 1 | 1 | 0 | 1 |
| 1 | 0 | 1 | 0 |
| 0 | 1 | 1 | 0 |
| 0 | 0 | 0 | 0 |
Half Adder
Sum
| Operand | Operand | Sum |
|---|---|---|
| 1 | 1 | 0 |
| 1 | 0 | 1 |
| 0 | 1 | 1 |
| 0 | 0 | 0 |
| Operand | Operand | Sum |
|---|---|---|
| True | True | False |
| True | False | True |
| False | True | True |
| False | False | False |
Xor
Boolean To Binary
Carry
| Operand | Operand | Carry |
|---|---|---|
| 1 | 1 | 1 |
| 1 | 0 | 0 |
| 0 | 1 | 0 |
| 0 | 0 | 0 |
| Operand | Operand | Sum |
|---|---|---|
| True | True | True |
| True | False | False |
| False | True | False |
| False | False | False |
And
Boolean To Binary
/**
* Computes the sum of two bits.
*/
CHIP HalfAdder {
IN a, b; // 1-bit inputs
OUT sum, // Right bit of a + b
carry; // Left bit of a + b
PARTS:
Xor(a=a, b=b, out=sum);
And(a=a, b=b, out=carry);
}
HalfAdder In HDL!
What's A Monoid, Precious?
- Algebraic Structure
- Associative Binary Operation
- Identity Element
- Monoid a
- mappend :: a -> a -> a
- mempty :: a
Monoid And
&
Monoid Xor
HalfAdder in Haskell!
| Operand | Operand | Operand | Sum | Carry |
|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 1 |
| 1 | 1 | 0 | 0 | 1 |
| 1 | 0 | 0 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 |
Full Adder
/**
* Computes the sum of two bits.
*/
CHIP FullAdder {
IN a, b c; // 1-bit inputs
OUT sum, // Right bit of a + b
carry; // Left bit of a + b
PARTS:
HalfAdder(a=a, b=b, sum=sum1, carry=carry1);
HalfAdder(a=sum1, b=c sum=sum, carry=carry2);
Or(a=carry1, b=carry2, out=carry);
}
FullAdder In HDL!
Full Adder in Haskell!
What About A Monoidal Nand?
Nand does not have an identity value!
What About A Semigroup Nand?
Nand is not associative!
Sequencing Circuits
Digital Flip Flop
Bistable Multivibrator
Nand 1
Nand 2
Out ?
Out ?
IN ?
IN ?
IN ?
IN ?
Nand 1
Nand 2
Out ?
Out ?
IN ?
1
1
IN ?
Nand 1
Nand 2
0
1
0
1
0
1
Nand 1
Nand 2
0
1
0
1
1
1
Nand 1
Nand 2
1
0
1
0
1
0
Nand 1
Nand 2
1
0
1
1
1
0

/**
* 1-bit register.
* If load[t]=1 then out[t+1] = in[t]
* else out does not change (out[t+1]=out[t])
*/
CHIP Bit {
IN in, load;
OUT out;
PARTS:
//Multiplexor. If sel==1 then out=in else out=ffout.
Mux(a=ffout, b=in, sel=load, out=muxout);
//DFF send output to both out and input of Mux
//DFF gets input[t] and returns output[t - 1]
DFF(in=muxout, out=ffout, out=out);
}1 Bit Register In HDL!
/**
* 16-bit register
* If load[t]=1 then out[t+1] = in[t]
* else out does not change
*/
CHIP Register {
IN in[16], load;
OUT out[16];
PARTS:
Mux(a=dff0out, b=in[0], sel=load, out=dff0in);
DFF(in=dff0in, out=dff0out, out=out[0]);
Mux(a=dff1out, b=in[1], sel=load, out=dff1in);
DFF(in=dff1in, out=dff1out, out=out[1]);
Mux(a=dff2out, b=in[2], sel=load, out=dff2in);
DFF(in=dff2in, out=dff2out, out=out[2]);
Mux(a=dff3out, b=in[3], sel=load, out=dff3in);
DFF(in=dff3in, out=dff3out, out=out[3]);
Mux(a=dff4out, b=in[4], sel=load, out=dff4in);
DFF(in=dff4in, out=dff4out, out=out[4]);
Mux(a=dff5out, b=in[5], sel=load, out=dff5in);
DFF(in=dff5in, out=dff5out, out=out[5]);
Mux(a=dff6out, b=in[6], sel=load, out=dff6in);
DFF(in=dff6in, out=dff6out, out=out[6]);
Mux(a=dff7out, b=in[7], sel=load, out=dff7in);
DFF(in=dff7in, out=dff7out, out=out[7]);
Mux(a=dff8out, b=in[8], sel=load, out=dff8in);
DFF(in=dff8in, out=dff8out, out=out[8]);
Mux(a=dff9out, b=in[9], sel=load, out=dff9in);
DFF(in=dff9in, out=dff9out, out=out[9]);
Mux(a=dff10out, b=in[10], sel=load, out=dff10in);
DFF(in=dff10in, out=dff10out, out=out[10]);
Mux(a=dff11out, b=in[11], sel=load, out=dff11in);
DFF(in=dff11in, out=dff11out, out=out[11]);
Mux(a=dff12out, b=in[12], sel=load, out=dff12in);
DFF(in=dff12in, out=dff12out, out=out[12]);
Mux(a=dff13out, b=in[13], sel=load, out=dff13in);
DFF(in=dff13in, out=dff13out, out=out[13]);
Mux(a=dff14out, b=in[14], sel=load, out=dff14in);
DFF(in=dff14in, out=dff14out, out=out[14]);
Mux(a=dff15out, b=in[15], sel=load, out=dff15in);
DFF(in=dff15in, out=dff15out, out=out[15]);
}/**
* Memory of 8 registers, each 16 bit-wide. Out holds the value
* stored at the memory location specified by address. If load=1, then
* the in value is loaded into the memory location specified by address
* (the loaded value will be emitted to out after the next time step.)
*/
CHIP RAM8 {
IN in[16], load, address[3];
OUT out[16];
PARTS:
DMux8Way(in=true, sel=address, a=ls0, b=ls1, c=ls2, d=ls3, e=ls4, f=ls5, g=ls6, h=ls7);
And(a=load, b=ls0, out=load0);
Register(in=in, load=load0, out=rout0);
And(a=load, b=ls1, out=load1);
Register(in=in, load=load1, out=rout1);
And(a=load, b=ls2, out=load2);
Register(in=in, load=load2, out=rout2);
And(a=load, b=ls3, out=load3);
Register(in=in, load=load3, out=rout3);
And(a=load, b=ls4, out=load4);
Register(in=in, load=load4, out=rout4);
And(a=load, b=ls5, out=load5);
Register(in=in, load=load5, out=rout5);
And(a=load, b=ls6, out=load6);
Register(in=in, load=load6, out=rout6);
And(a=load, b=ls7, out=load7);
Register(in=in, load=load7, out=rout7);
Mux8Way16(a=rout0, b=rout1, c=rout2, d=rout3,
e=rout4, f=rout5, g=rout6, h=rout7,
sel=address, out=out);
}/**
* Memory of 64 registers, each 16 bit-wide. Out hold the value
* stored at the memory location specified by address. If load=1, then
* the in value is loaded into the memory location specified by address
* (the loaded value will be emitted to out after the next time step.)
*/
CHIP RAM64 {
IN in[16], load, address[6];
OUT out[16];
PARTS:
DMux8Way(in=true, sel=address[3..5], a=ls0, b=ls1, c=ls2, d=ls3, e=ls4, f=ls5, g=ls6, h=ls7);
And(a=load, b=ls0, out=load0);
RAM8(in=in, load=load0, address=address[0..2], out=rout0);
And(a=load, b=ls1, out=load1);
RAM8(in=in, load=load1, address=address[0..2], out=rout1);
And(a=load, b=ls2, out=load2);
RAM8(in=in, load=load2, address=address[0..2], out=rout2);
And(a=load, b=ls3, out=load3);
RAM8(in=in, load=load3, address=address[0..2], out=rout3);
And(a=load, b=ls4, out=load4);
RAM8(in=in, load=load4, address=address[0..2], out=rout4);
And(a=load, b=ls5, out=load5);
RAM8(in=in, load=load5, address=address[0..2], out=rout5);
And(a=load, b=ls6, out=load6);
RAM8(in=in, load=load6, address=address[0..2], out=rout6);
And(a=load, b=ls7, out=load7);
RAM8(in=in, load=load7, address=address[0..2], out=rout7);
Mux8Way16(a=rout0, b=rout1, c=rout2, d=rout3,
e=rout4, f=rout5, g=rout6, h=rout7,
sel=address[3..5], out=out);
}Notice Anything Similar Between The DFF And Effectful Monads in Haskell?
Resources
- Nand 2 Tetris Website: http://nand2tetris.org/
- Nand 2 Tetris Course: https://www.coursera.org/learn/build-a-computer
- Haskell Book: http://haskellbook.com/
- To Mock a Mockingbird by Raymond Smullyan: https://en.wikipedia.org/wiki/To_Mock_a_Mockingbird
- Point Free or Die (amazing ETA reduction by Amar Shah): https://www.youtube.com/watch?v=seVSlKazsNk
- Digital Flip Flops by Robot Brigade: https://www.youtube.com/watch?v=NjO68wdbWyU
- Introduction To Monoids by Julie Moronuki: https://www.youtube.com/watch?v=-mnA8_DWfik
- Parsing with Applicative from Stephen Diehl: http://dev.stephendiehl.com/fun/002_parsers.html
- My humble example of applicative parsing for assembler: https://github.com/paniclater/haskell_assembler
From NAND to Lambda
By Ryan Moore
From NAND to Lambda
A log of my journey through learning physical computing and functional programming side by side from the course Nand2Tetris: Build A Computer from First Principles and the book Haskell Programming from Foundational Principles.
- 871