Implementing and Understanding Type Classes (2014)
okmij.org
okmij.org
Hindley-Milner has problems with inferring types in the presence of polymorphic recursion, and a user provided type annotation is usually necessary. Polymorphic recursion does allow some cool things such as arbitrarily nested lists. This is a feature that users from a dynamically typed language might miss.
"Cool" as it might be, please don't inflict it on others.
For example in scala, maybe you have data about Users
case class User(id: Long, name: String, friends: List[User])
However, this structures is kinda inflexible, you don't always want to deal with an entire tree of users at a time. Maybe, you just want to have the friend userids, so you can introduce a type parameter for the recursive data case class User[Friend](id: Long, name: String, friends: List[Friend])
and then you can have a User[Long] which contains the friend ids, or User[String] which contains the friend names. However, if you want to go back and have the list be of Users themselves, you would have to know statically how deep the tree you're returning is. For example, this type would include full user information down to the grandchild level, but then terminate with the ids: User[User[User[Long]]]
This is actually very useful! Sometimes we may want exactly this deep of a structure. If we want to get an arbitrarily deep tree of users, we would need to have an infinitely nested type like User[User[User[User[User[User[...]]]]]]
which is of course impossible. So instead, people have a work-around using higher-kinded types, called `Fix`: case class Fix[F[_]](unfix: F[Fix[F]])
This signature may look scary, but if you apply it to User you get Fix[User](unfix: User[Fix[User]])
and all this means is that now we can deal with Fix[User] which means the same thing as User[User[...]]. You can construct one such Fix[User] differing levels like so: Fix(
User(
id = 1L,
name = "Josh",
friends = List(
Fix(
User(
id = 2L,
name = "John",
friends = List(
Fix(
User(
id = 3L,
name = "Stacey"
friends = List.empty
)
)
)
),
Fix(
User(
id = 4L
name = "Mary"
friends = List.empty
)
)
)
) datatype 'a tree = T of 'a * 'a tree list
Do you?Ex.
def fact(n: Int) = if (n == 0) 1 else n * fact(n - 1)
will fail to compile and tell you that you need to provide a return type.
Strictly speaking Hindley-Milner never requires type annotations. It can infer the type of any well-formed expression. You must be thinking of some extension of HM (of which there are many).
Here is an example of the type error (I used the datatype from the Wikipedia article on polymorphic recursion): https://repl.it/@CalebHelbling/PolymorphicRecursionError
OCaml and Haskell support it by requiring type annotations.
In general, polymorphic recursion is occasionally useful for some specific data structures (see Okasaki's purely functional datastructures for some examples) and when playing with GADTs.
Additionally, GHC will try to monomorphise bounded functions if it can, because it's simply much faster without the extra layer of indirection.
I could be equating a few things which aren't equal, but I'd love to hear your take (or anyone elses)
What do you mean? You can implement arbitrarily nested lists (rose trees) in Haskell.
data Tree1 a = Leaf1 a | Node1 [Tree1 a]
flatten :: Tree1 -> [a]
But this type needs it:
data Tree2 a = Leaf2 a | Node2 (Tree2 [a])
flatten2 :: Tree2 -> [a]
Tree1 can vary in depth between nodes, while Tree2 is either a, [a], [[a]], [[[a]]] etc.