Why FP?
One overarching reason for FP is to get conspective views on a given subject in a couple of lines. Ideally a couple of lines but as far as possible within a page.
Why does Doug McIlroy describe McCarthy's first Lisp lecture as the The Most Significant Computer Science talk he ever attended??
What was done is (to me) less important than the last line:
One could go home and build it oneself without any instruction book
This is what I mean by conspective
Alan Kay says that studying Lisp's eval-apply pair increased his understanding of CS by a factor of hundred.
So to take Alan Kay's example:
The Lisp interpreter in Lisp is hardly a pageful
Which means that that page, presumably coverable in one lecture, could be said to cover the field of language implementation.
Now can McCarthy's one page lisp in lisp be compared with gcc's 15 million lines??
Some answers may be
- Lisp-in-lisp is a toy gcc is real
- Apples-n-oranges comparison
- Oh gcc is many languages, many machines...
I think all such answers miss the point.
Sure there's "more" happening in all-of-gcc than in a pageful-of-lisp. But if all those 15 million lines cannot be squeezed into a sumary of "all thats here" something crucial is lacking in our communication and ultimately in our understanding.
This is after all Einstein in a different guise: You do not understand something unless you can explain it to your grandmother (or a 6-year old or a barmaid)
Of course people do dig into gcc starting zero and there are intros to the field as there are intros to all fields egs
- the chapters of a compiler book
- a block diagram of main parts of toolchain
- a UML diagram
- etc
But in FP there is a a very special extra possibility: Show that conspectus as executable code
Compared to that all the above have a quality of hand-waving
Let me (for now) put down a few files from my laptop taught at various times.
Depending the nature of your appetite I can flesh out/further explain/add different.
Notes on non-standard notation
1. function-call notation
c.[1,2]
would look like c([1,2])
in conventional C-family language style.
ie
- C family :
f(x)
- Haskell family
f x
- Dijkstra functional notation:
f.x
ie f.x
is the Dijkstra form of the more usual f(x)
ie function call.
I could give justifications/explanations for the Dijkstra notation (if desired).
2. Algebraic Datatypes
Things like L
, I
are constructors of algebraic data types. The small notational wrinkle carrying a large semantic load here is tiny fact of "initial letter capital implies data constructor not normal function". For this Fortranism — identical in my notation to Haskell — my embarrassed apologies 😜.
3. Interactions
One interacts with the system by typing something at the ?
prompt.
The system responds with a result (as any interpreter does). And its type
Pedagogically this is a big deal: Types serve as a beginner cyclist's guide wheels
- The user almost never gives a type
- The system invariably responds with a type as if to say: Your answer is this, and its type according to me is this.
If you (Victor) have some (standard) FPL preference, I can try moving to that... For now the Catalan example is shown also in Python
Combinatorics
Catalan Numbers
-- Implements the 3rd (or 2nd) interpretation from here
-- https://en.wikipedia.org/wiki/Catalan_number#Applications_in_combinatorics
ctype Tree a where
I : Tree.t → Tree.t → Tree.t -- I: "Internal node"
L : t → Tree.t -- L: "Leaf node"
c: ℒ.t → ℒ.(Tree.t)
c.[x] = [L.x]
c.ls = [I.x.y | i ∈ [1...length.ls -1], (l,r) = splitAt.i.ls, x ∈ c.l, y ∈ c.r]
Example usage:
? c.[1,2]
[I.(L.1).(L.2)] : ℒ.(Tree.ℤ)
? c.[1,2,3]
[I.(L.1).(I.(L.2).(L.3)), I.(I.(L.1).(L.2)).(L.3)] : ℒ.(Tree.ℤ)
Now if you see those 2 lines defining the Catalan enumerator c
.
And now see the definition from wikipedia, you can see the closeness of the parallel
$$\begin{aligned}C_0 &= 1\\
C_{n+1} &= \sum_{i=0}^{n}C_iC_{n-i}\quad\text{for }n\ge 0\end{aligned}$$
Catalan explanation
So the verbalization of catalan enumeration would go like this:
Trivial case
c
for a singleton list is a singleton answer: just a L(eaf) node
Recursive case
For a list of greater than 1 element (called ls
)
Generate all i
s from [1... length.ls -1]
More usually the length.ls - 1
may look length(ls) - 1
For each such i
split the list ls
at i
Call the two halves l
and r
1,2 Basically generate all non-trivial ie non-empty splits of the input
Generate all Catalan enumerations for l
(the c.l
recursive call) Likewise r
Note these are LISTS of catalan enumerations internal-to-the-recursion generated
For each x
∈ l
-part and each y
∈ r
-part...
Collect the node (at the head of the list comprehension) I.x.y
(I
stands for internal node like L
stands for Leaf)
The telegraphic names like L
I
are for a reason: we make tree values with them; that value making becomes clunky with "readable" so-called constructors. ie we want to decide at which point to optimize readability. The point of interaction called interaction-with-the-REPL is by and large simply not available with a compiled language like C++/Java.
Python equivalent of Catalan
def c(ls):
if len(ls)==1:
x = ls[0]
return [('L',x)]
else:
r = range(1,len(ls))
return [('I',x,y) for i in r for x in c(ls[i:]) for y in c(ls[:i])]
def interm(ls):
r = range(1,len(ls))
return [(ls[:i], ls[i:]) for i in r]
- Just load this up into python
- Try out
interm
first to get the sense of all non trivial splits
- ie try first
interm([1])
, interm([1,2])
, interm([1,2,3])
etc
- Finally try
c([1])
, c([1,2])
, c([1,2,3])
Interpreter-n-Compiler
ctype Exp where
L :Int → Exp
Add, Mul: Exp → Exp →Exp
eval:Exp → Int
eval.(L.x) = x
eval.(Add.e1.e2) = eval.e1 + eval.e2
eval.(Mul.e1.e2) = eval.e1 * eval.e2
ctype Instr where
Opd: Int → Instr -- Opd = OPeranD; it loads its arg onto the stack
AddI, MulI: Instr
compile: Exp → ℒ.Instr
compile.(L.x) = [Opd.x]
compile.(Add.e1.e2) = compile.e1 ++ compile.e2 ++ [AddI]
compile.(Mul.e1.e2) = compile.e1 ++ compile.e2 ++ [MulI]
mac: ℒ.Instr → ℒ.ℤ → ℒ.ℤ
mac. []. stk = stk
mac. ((Opd.x) :: is).stk = mac.is.(x :: stk)
mac. (AddI :: is).(r::l::stk) = mac.is.(l+r::stk)
mac. (MulI :: is).(r::l::stk) = mac.is.(l*r::stk)
Showing the interpreter run:
? eval.(Add.(L.3).(L.4))
7 : ℤ
Then the compiler
? compile.(Add.(L.3).(L.4))
[Opd.3, Opd.4, AddI] : ℒ.Instr
? compile.(Add.(L.3).(L.4))
[Opd.3, Opd.4, AddI] : ℒ.Instr
Finally showing the output of the compiler given to the machine results in the top of the stack having same result as the interpreter
? mac.(compile.(Add.(L.3).(L.4))).[]
[7] : ℒ.ℤ
Summary
The two lines definition of c
capture the essence of catalan numbers.
The 4 lines eval + 4 lines compile + 5 lines machine capture a semester's worth of compiler construction.
One can of course keep fleshing out the details to one's taste.
But the summary is a lynchpin to understanding
(in my understanding 😇)
Added Later
Since you're making some efforts into understanding this (combinatorics part) I made another more detailed explanatory answer (hopefully!)
It strongly distinguishes between
- counting which must invariably be something from ℕ
- enumerating which actually creates the entities being counted
- with the convention that if the first is
foo
the second is fooE
What I've called c
here should (in this new convention) be cE
And that cE
returns a list whose length is the catalan number known in standard combinatorics accounts eg wikipedia
After you get that I'll uniformly change the convention and remove this "Added later"
[It'd be rude to change a text notations while someone is reading!!
For now I've changed the confusing type [a]
to ℒ.t
]