1

I am building an extended run-time type system for Javascript and have some trouble implementing parametric polymorphism.

I am already able to type polymorphic first order functions:

const append = Fun("append :: [a] -> [a] -> [a]", xs => ys => xs.concat(ys));

append([1,2]) ([3,4]); // [1,2,3,4]
append([1,2]) (["a","b"]); // fails

There is state shared between the sequential function calls that enforces all as in the type annotation to be bound to the same concrete type.

However, things get much harder as soon as I try to type higher order functions:

const apply = Fun("apply :: (a -> b) -> a -> b", f => x => f(x));
const id = Fun("id :: a -> a", x => x);

apply(id) (2); // 2

The problem is that a/b of the function argument are not the same as those of the higher order function, because each function (a proxy with an apply trap behind the scene) has its on scope to bind type variables to concrete types. As a result the following unwanted implementation of apply with an explicit type cast doesn't fail:

const apply = Fun("apply :: (a -> b) -> a -> b", f => x => f(x) + "");
const inc = Fun("inc :: Number -> Number", x => x + 1);

apply(inc) (2); // "3"

Intuitively I tried to maintain state between all involved functions but this gets messy quickly.

Another approach is to adapt the type annotations of higher order functions according to those of their function arguments. Function arguments are frequently monomorphic like inc but sometime polymorphic like id as well. To stick with my example here are examples for both scenarios:

apply :: (a -> b) -> a -> b 
apply f x = f x

inc :: Int -> Int
inc x = x + 1

:t apply inc -- Int -> Int (a becomes Int, b becomes Int)

id :: a -> a 
id x = x

:t apply id -- b -> b  (a becomes b, b remains b)

It seems as if all I need were two simple substitution rules for my type annotations - no additional state necessary. I know that my reasoning is rather simple. The question is if it is too naive in order to obtain reliable parametric polymorphism. Am I oversimplifying things?

  • I do not know how this question can be answered in a specific way. Are there specifics (i.e. code) you can share from your type checker? – erisco Aug 29 '17 at 22:11
  • @erisco Unfortunately not, it would take too much code. You can get a link to the repository via my profile page, though. I am sorry if this question is too vague. I am hoping for some kind of theoretical answer that my reasoning is adequate/insufficient. Perhaps a counterexample, that simple exchanging of type annotations isn't enough. –  Aug 29 '17 at 22:37
  • 4
    The problem of polymorphism for parameteric polymorphism is type unification. When you observe that "the `a`/`b` of the function argument are not the same as those of the higher order function" it means that you need to unify the types together. This problem is called "higher order unification". Here's a stackoverflow question about it: https://stackoverflow.com/questions/1936432/higher-order-unification @jozefg has written a Haskell example of Huet's alogorithm: https://github.com/jozefg/higher-order-unification – Cirdec Aug 29 '17 at 22:48
  • 5
    That's not higher order unification. It's just normal unification. – Carter Tazio Schonwald Aug 30 '17 at 02:10
  • From what I can tell, the issue is that your type checker incorrectly unifies the most general type of `f => x => f(x) + ""` (which could be something like `Show b => (a -> b) -> a -> b` or `(a -> String) -> a -> String` depending on the type of `(+ "")`) with the specified type `(a -> b) -> a -> b`. However you've included no details of how you do typechecking, so it's impossible to say why your typechecker has this bug. Note this bug occurs before the application `apply(inc)`. – user2407038 Aug 30 '17 at 03:18
  • @Cirdec Now I have a term that I can google for. It is a start. I really need some more theoretical background on the subject. Thanks. –  Aug 30 '17 at 06:57
  • 1
    Rough idea: given the polytypes of `f :: T` and `x :: V`, you need to compute the most general unifier `sigma` to make `T = V -> c`, where `c` is a fresh type variable. If unification fails, that's a runtime type error. Otherwise the resulting polytype is `sigma(c)`. You can google for the known algorithms to compute the MGU. You'll probably want to use a more tree-like/AST notation for types instead of strings. You can pretty print them as strings for user convenience, and keep them as ASTs for type checking. – chi Aug 30 '17 at 11:53

0 Answers0