I'm working on a small lambda calculus compiler that has a working Hindley-Milner type inference system and now also supports recursive let's (not in the linked code), which I understand should be enough to make it Turing complete.
The problem now is I have no idea how to make it support lists, or whether it already does support them and I just need to find a way to encode them. I'd like to be able to define them without having to add new rules to the type system.
The easiest way I can think of a list of x
is as something that is either null
(or the empty list), or a pair that contains both an x
and a list of x
. But to do this I need to be able to define pairs and or's, which I believe are the product and the sum types.
Seems that I can define pairs this way:
pair = λabf.fab
first = λp.p(λab.a)
second = λp.p(λab.b)
Since pair
would have the type a -> (b -> ((a -> (b -> x)) -> x))
, after passing, say, an int
and a string
, it'd yield something with type (int -> (string -> x)) -> x
, which would be the representation of a pair of int
and string
. What bothers me here is that if that represents a pair, why is that not logically equivalent to, nor implies the proposition int and string
?. However, is equivalent to (((int and string) -> x) -> x)
, as if I could only have product types as parameters to functions. This answer seem to address this problem, but I have no idea what the symbols he uses mean. Also, if this does not really encode a product type, is there anything I can do with product types I couldn't do with my definition of pairs above (considering I can also define n-tuples the same way)? If not, wouldn't this contradict the fact that you cannot express (AFAIK) conjunction using only implication?
Also, how about the sum type? Can I somehow encode it using only the function type? If so, would this be enough to define lists? Or else, is there any other way to define lists without having to extend my type system? And if not, what changes would I need to make if I want to keep it as simple as possible?
Please keep in mind that I'm a computer programmer but not a computer scientist nor a mathematician and pretty bad at reading math notation.
Edit: I'm not sure what's the technical name of what I have implemented so far, but all I have is basically the code I've linked above, which is a constraint generation algorithm that uses the rules for applications, abstractions and variables taken from the Hinley-Milner algorithm and then a unification algorithm that gets the principal type. For instance, the expression \a.a
will yield the type a -> a
, and the expression \a.(a a)
will throw an occurs check error. On top of this, there is not exactly a let
rule but a function that seems to have the same effect that lets you define recursive global functions like this pseudo-code:
GetTypeOfGlobalFunction(term, globalScope, nameOfFunction)
{
// Here 'globalScope' contains a list of name-value pair where every value is of class 'ClosedType',
// meaning their type will be cloned before unified in the unification algorithm so that they can be used polymorphically
tempType = new TypeVariable() // Assign a dummy type to `tempType`, say, type 'x'.
// The next line creates an scope with everything in 'globalScope' plus the 'nameOfFunction = tempType' name-value pair
tempScope = new Scope(globalScope, nameOfFunction, tempType)
type = TypeOfTerm(term, tempScope) // Calculate the type of the term
Unify(tempType, type)
return type
// After returning, the code outside will create a 'ClosedType' using the returned type and add it to the global scope.
}
The code basically gets the type of the term as usual, but before unifying, it adds the name of the function being defined with a dummy type into the type scope so that it can be used from within itself recursively.
Edit 2: I just realized that I'd also need recursive types, which I don't have, to define a list like I want.
let func = \x -> (func x)
) you get what I have. $\endgroup$