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 .) . g

not (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