简体   繁体   中英

Why are two polymorphic higher order functions with different type vars equivalent regarding their types?

Coming from Javascript I understand that Haskell's list type enforces homogeneous lists. Now it has surprised me that the following different function types meet this requirement:

f :: (a -> a) -> a -> a 
f g x = g x

g :: (a -> b) -> a -> b 
g h x = h x

let xs = [f, g] -- type checks

even though g is more widely applicable than f :

f(\x -> [x]) "foo" -- type error
g(\x -> [x]) "foo" -- type checks

Shouldn't (a -> a) be treated diffenrently than (a -> b) . It seems to me as if the latter is a subtype of the former. But there are no subtype relationships in Haskell, right? So why does this work?

Haskell is statically typed, but that doesn't mean it's Fortran. Every type must get fixed at compile time , but not necessarily within a single definition. The types of both f and g are polymorphic . One way to interpret this is that f is not just a single function, but a whole family of overloaded functions. Like (in C++)

int f (function<int(int)> g, int x) { return g(x); }
char f (function<char(char)> g, char x) { return g(x); }
double f (function<double(double)> g, double x) { return g(x); }
...

Of course it wouldn't be practical to actually generate all of these functions , so in C++ you'd instead write this as a template

template <typename T>
T f (function<T(T)> g, T x) { return g(x); }

...meaning, whenever the compiler finds f if your project's code, it'll figure out what T is in the specific case, then create a concrete template instantiation (a monomorphic function fixed to that concrete type, like the example ones I wrote above) and only use that concrete instantiation at runtime.

These concrete instantiations of two template-functions may then have the same type, even if the templates looked a bit different.

Now, Haskell's parametric polymorphism is resolved a little bit different from C++ templates, but at least in your example they amount to the same: g is a whole family of functions, including the instantiation g :: (Int -> Char) -> Int -> Char (which is not compatible with the type of f ) but also g :: (Int -> Int) -> Int -> Int , which is. When you put f and g in a single list, the compiler automatically realises that only the subfamily of g whose type is compatible with f is relevant here.

Yes, this is indeed a form of subtyping. When we say “Haskell doesn't have subtyping” we mean that any concrete (Rank-0) type is disjoint from all other Rank-0 types, but polymorphic types may overlap.

@leftroundabout's answer is solid; here's a more technical supplementary answer.

There is a sort of subtyping relationship at work in Haskell: the System F “generic instance” relation. This is what the compiler uses when checking the inferred type of a function against its signature. Basically, the inferred type of a function must be at least as polymorphic as its signature:

f :: (a -> a) -> a -> a
f g x = g x

Here, the inferred type of f is forall a b. (a -> b) -> a -> b forall a b. (a -> b) -> a -> b , the same as the definition of g you gave. But the signature is more restrictive: it adds the constraint a ~ b ( a is equal to b ).

Haskell checks this by first replacing the type variables in the signature with Skolem type variables —these are fresh unique type constants that only unify with themselves (or type variables). I'll use the notation $a to represent a Skolem constant.

forall a. (a -> a) -> a -> a
($a -> $a) -> $a -> $a

You may see references to “rigid, Skolem” type variables when you accidentally have a type variable that “escapes its scope”: it's used outside the forall quantifier that introduced it.

Next, the compiler does a subsumption check . This is essentially the same as normal unification of types, where a -> b ~ Int -> Char gives a ~ Int and b ~ Char ; but because it's a subtyping relationship, it also accounts for covariance and contravariance of function types. If (a -> b) is a subtype of (c -> d) , then b must be a subtype of d (covariant), but a must be a supertype of c (contravariant).

{-1-}(a -> b) -> {-2-}(a -> b)  <:  {-3-}($a -> $a) -> {-4-}($a -> $a)

{-3-}($a -> $a) <: {-1-}(a -> b)  -- contravariant (argument)
{-2-}(a -> b) <: {-4-}($a -> $a)  -- covariant (result)

The compiler generates the following constraints:

$a <: a  -- contravariant
b <: $a  -- covariant
a <: $a  -- contravariant
$a <: b  -- covariant

And solves them by unification:

a ~ $a
b ~ $a
a ~ $a
b ~ $a

a ~ b

So the inferred type (a -> b) -> a -> b is at least as polymorphic as the signature (a -> a) -> a -> a .


When you write xs = [f, g] , normal unification kicks in: you have the two signatures:

forall a.   (a -> a) -> a -> a
forall a b. (a -> b) -> a -> b

These are instantiated with fresh type variables:

(a1 -> a1) -> a1 -> a1
(a2 -> b2) -> a2 -> b2

Then unified:

(a1 -> a1) -> a1 -> a1  ~  (a2 -> b2) -> a2 -> b2
a1 -> a1  ~  a2 -> b2
a1 -> a1  ~  a2 -> b2
a1 ~ a2
a1 ~ b2

And finally solved & generalised:

forall a1. (a1 -> a1) -> a1 -> a1

So the type of g has been made less general because it's been constrained to have the same type as f . The inferred type of xs will therefore be [(a -> a) -> a -> a] , so you'll get the same type error writing [f (\\x -> [x]) "foo" | f <- xs] [f (\\x -> [x]) "foo" | f <- xs] as you did writing f (\\x -> [x]) "foo" ; even though g is more general, you've hidden away some of that generality.


Now you might be wondering why you would ever give a more restrictive signature for a function than necessary. The answer is—to guide type inference and produce better error messages.

For example, the type of ($) is (a -> b) -> a -> b ; but in fact this is a more restrictive version of id :: c -> c ! Just set c ~ a -> b . So in fact you can write foo `id` (bar `id` baz quux) instead of foo $ bar $ baz quux , but having this specialised identity function makes it clear to the compiler that you expect to use it to apply functions to arguments, so it can bail out earlier and give you a more descriptive error message if you make a mistake.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM