I\'ve been doing dev in F# for a while and I like it. However one buzzword I know doesn\'t exist in F# is higher-kinded types. I\'ve read material on higher-kinded types, and
Consider the Functor
type class in Haskell, where f
is a higher-kinded type variable:
class Functor f where
fmap :: (a -> b) -> f a -> f b
What this type signature says is that fmap changes the type parameter of an f
from a
to b
, but leaves f
as it was. So if you use fmap
over a list you get a list, if you use it over a parser you get a parser, and so on. And these are static, compile-time guarantees.
I don't know F#, but let's consider what happens if we try to express the Functor
abstraction in a language like Java or C#, with inheritance and generics, but no higher-kinded generics. First try:
interface Functor {
Functor map(Function f);
}
The problem with this first try is that an implementation of the interface is allowed to return any class that implements Functor
. Somebody could write a FunnyList implements Functor
whose map
method returns a different kind of collection, or even something else that's not a collection at all but is still a Functor
. Also, when you use the map
method you can't invoke any subtype-specific methods on the result unless you downcast it to the type that you're actually expecting. So we have two problems:
map
method always returns the same Functor
subclass as the receiver.Functor
method on the result of map
.There are other, more complicated ways you can try, but none of them really works. For example, you could try augment the first try by defining subtypes of Functor
that restrict the result type:
interface Collection extends Functor {
Collection map(Function f);
}
interface List extends Collection {
List map(Function f);
}
interface Set extends Collection {
Set map(Function f);
}
interface Parser extends Functor {
Parser map(Function f);
}
// …
This helps to forbid implementers of those narrower interfaces from returning the wrong type of Functor
from the map
method, but since there is no limit to how many Functor
implementations you can have, there is no limit to how many narrower interfaces you'll need.
(EDIT: And note that this only works because Functor
appears as the result type, and so the child interfaces can narrow it. So AFAIK we can't narrow both uses of Monad
in the following interface:
interface Monad {
Monad flatMap(Function super A, ? extends Monad extends B>> f);
}
In Haskell, with higher-rank type variables, this is (>>=) :: Monad m => m a -> (a -> m b) -> m b
.)
Yet another try is to use recursive generics to try and have the interface restrict the result type of the subtype to the subtype itself. Toy example:
/**
* A semigroup is a type with a binary associative operation. Law:
*
* > x.append(y).append(z) = x.append(y.append(z))
*/
interface Semigroup> {
T append(T arg);
}
class Foo implements Semigroup {
// Since this implements Semigroup, now this method must accept
// a Foo argument and return a Foo result.
Foo append(Foo arg);
}
class Bar implements Semigroup {
// Any of these is a compilation error:
Semigroup append(Semigroup arg);
Semigroup append(Bar arg);
Semigroup append(Bar arg);
Foo append(Bar arg);
}
But this sort of technique (which is rather arcane to your run-of-the-mill OOP developer, heck to your run-of-the-mill functional developer as well) still can't express the desired Functor
constraint either:
interface Functor, A> {
, B> FB map(Function f);
}
The problem here is this doesn't restrict FB
to have the same F
as FA
—so that when you declare a type List implements Functor
, the , A>
map
method can still return a NotAList implements Functor
.
Final try, in Java, using raw types (unparametrized containers):
interface FunctorStrategy {
F map(Function f, F arg);
}
Here F
will get instantiated to unparametrized types like just List
or Map
. This guarantees that a FunctorStrategy
can only return a List
—but you've abandoned the use of type variables to track the element types of the lists.
The heart of the problem here is that languages like Java and C# don't allow type parameters to have parameters. In Java, if T
is a type variable, you can write T
and List
, but not T
. Higher-kinded types remove this restriction, so that you could have something like this (not fully thought out):
interface Functor {
F map(Function f);
}
class List implements Functor {
// Since F := List, F := List
List map(Function f) {
// ...
}
}
And addressing this bit in particular:
(I think) I get that instead of
myList |> List.map f
ormyList |> Seq.map f |> Seq.toList
higher kinded types allow you to simply writemyList |> map f
and it'll return aList
. That's great (assuming it's correct), but seems kind of petty? (And couldn't it be done simply by allowing function overloading?) I usually convert toSeq
anyway and then I can convert to whatever I want afterwards.
There are many languages that generalize the idea of the map
function this way, by modeling it as if, at heart, mapping is about sequences. This remark of yours is in that spirit: if you have a type that supports conversion to and from Seq
, you get the map operation "for free" by reusing Seq.map
.
In Haskell, however, the Functor
class is more general than that; it isn't tied to the notion of sequences. You can implement fmap
for types that have no good mapping to sequences, like IO
actions, parser combinators, functions, etc.:
instance Functor IO where
fmap f action =
do x <- action
return (f x)
-- This declaration is just to make things easier to read for non-Haskellers
newtype Function a b = Function (a -> b)
instance Functor (Function a) where
fmap f (Function g) = Function (f . g) -- `.` is function composition
The concept of "mapping" really isn't tied to sequences. It's best to understand the functor laws:
(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs
Very informally:
This is why you want fmap
to preserve the type—because as soon as you get map
operations that produce a different result type, it becomes much, much harder to make guarantees like this.