A purely-functional programming language with Hindley-Milner type inference and `callcc`.
Go to file
James T. Martin 9e0754daf6
Added support for Hindley-Milner type inference!
I also generalized substitution to *all* expressions (and types),
which itself involved a rewrite of the substitution function I already had.
(In hindsight, I wish I'd done that in a separate commit.)

The last four days have all been working up to this,
so I'm glad I've finally managed to integrate it into this project!
(I wrote the algorithm 4 days ago, but the infrastructure
just wasn't there to add it here.)
2021-03-18 00:00:43 -07:00
.github/workflows Cleanup: fix warnings, fix indentation, upgrade dependencies. 2021-03-05 19:04:06 -08:00
app Added support for Hindley-Milner type inference! 2021-03-18 00:00:43 -07:00
src Added support for Hindley-Milner type inference! 2021-03-18 00:00:43 -07:00
test Added support for Hindley-Milner type inference! 2021-03-18 00:00:43 -07:00
.editorconfig Cleanup: fix warnings, fix indentation, upgrade dependencies. 2021-03-05 19:04:06 -08:00
.gitignore Complexity was getting out of hand. Beginning a rewrite. Added tests. 2019-12-11 18:29:28 -08:00
LICENSE Initial commit. Some terms are still not evaluated correctly. 2019-08-15 10:42:24 -07:00
README.md Added support for Hindley-Milner type inference! 2021-03-18 00:00:43 -07:00
Setup.hs Initial commit. Some terms are still not evaluated correctly. 2019-08-15 10:42:24 -07:00
package.yaml Added support for Hindley-Milner type inference! 2021-03-18 00:00:43 -07:00
stack.yaml Combine expression representations using the 'trees that grow' design pattern. 2021-03-16 18:25:42 -07:00
stack.yaml.lock Combine expression representations using the 'trees that grow' design pattern. 2021-03-16 18:25:42 -07:00

README.md

Lambda Calculus

This is a simple programming language derived from lambda calculus, using the Hindley-Milner type system, plus some builtin types, fix, and callcc

Usage

Run the program using stack run (or run the tests with stack test).

Type in your expression at the prompt: >> . This will happen:

  • the type for the expression will be inferred and then printed,
  • then, regardless of whether typechecking succeeded, expression will be evaluated to normal form using the call-by-value evaluation strategy and then printed.

Exit the prompt with Ctrl-c (or equivalent).

Syntax

The parser's error messages currently are virtually useless, so be very careful with your syntax.

  • Variable names: any sequence of letters.
  • Function application: f x y
  • Lambda abstraction: \x y z. E or λx y z. E
  • Let expressions: let x = E; y = F in G
  • Parenthetical expressions: (E)
  • Constructors: (), (x, y) (or (,) x y), Left x, Right y, Z, S, [], (x :: xs) (or (:) x xs), Char n.
    • The parentheses around the cons constructor are not optional.
    • Char takes a natural number and turns it into a character.
  • Pattern matchers: case { Left a -> e ; Right y -> f }
    • Pattern matchers can be applied like functions, e.g. { Z -> x, S -> y } 10 reduces to y.
    • Patterns must use the regular form of the constructor, e.g. (x :: xs) and not ((::) x xs).
    • There are no nested patterns or default patterns.
    • Incomplete pattern matches will crash the interpreter.
  • Literals: 1234, [e, f, g, h], 'a, "abc"
    • Strings are represented as lists of characters.
  • Type annotations: there are no type annotations; types are inferred only.

Types

Types are checked/inferred using the Hindley-Milner type inference algorithm.

  • Functions: a -> b (constructed by \x. e)
  • Products: a * b (constructed by (x, y))
  • Unit: (constructed by ())
  • Sums: a + b (constructed by Left x or Right y)
  • Bottom: (currently useless because incomplete patterns are allowed)
  • The natural numbers: Nat (constructed by Z and S)
  • Lists: List a (constructed by [] and (x :: xs))
  • Characters: Char (constructed by Char, which takes a Nat)
  • Universal quantification (forall): ∀a b. t

Builtins

Builtins are variables that correspond with a built-in language feature that cannot be replicated by user-written code. They still are just variables though; they do not receive special syntactic treatment.

Continuations are printed as λ!. ... ! ..., like a lambda abstraction with an argument named ! which is used exactly once; however, continuations are not the same as lambda abstractions because they perform the side effect of modifying the current continuation, and this is not valid syntax you can input into the REPL.

Example code

Create a list by iterating f n times:

fix \iterate f x. { Z -> [] ; S n -> (x :: iterate f (f x) n) }
: ∀e. ((e -> e) -> (e -> (Nat -> [e])))

Use the iterate function to count to 10:

>> let iterate = fix \iterate f x. { Z -> [] ; S n -> (x :: iterate f (f x) n) }; countTo = iterate S 1 in countTo 10
: [Nat]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Append two lists together:

fix \append xs ys. { [] -> ys ; (x :: xs) -> (x :: append xs ys) } xs
: ∀j. ([j] -> ([j] -> [j]))

Reverse a list:

fix \reverse. { [] -> [] ; (x :: xs) -> append (reverse xs) [x] }
: ∀c1. ([c1] -> [c1])

Putting them together so we can reverse "reverse":

>> let append = fix \append xs ys. { [] -> ys ; (x :: xs) -> (x :: append xs ys) } xs; reverse = fix \reverse. { [] -> [] ; (x :: xs) -> append (reverse xs) [x] } in reverse "reverse"
: [Char]
"esrever"

Calculating 3 + 2 with the help of Church-encoded numerals:

>> let Sf = \n f x. f (n f x); plus = \x. x Sf in plus (\f x. f (f (f x))) (\f x. f (f x)) S Z
: Nat
5

This expression would loop forever, but callcc saves the day!

>> S (callcc \k. (fix \x. x) (k Z))
: Nat
1

And if it wasn't clear, this is what the Char constructor does:

>> { Char c -> Char (S c) } 'a
: Char
'b

Here are a few expressions which don't typecheck but are handy for debugging the evaluator:

>> let D = \x. x x; F = \f. f (f y) in D (F \x. x)
y y
>> let T = \f x. f (f x) in (\f x. T (T (T (T T))) f x) (\x. x) y
y
>> (\x y z. x y) y
λy' z. y y'