Which is a polymorphic type: a type or a set of types?
Asked Answered
N

3

15

Programming in Haskell by Hutton says:

A type that contains one or more type variables is called polymorphic.

Which is a polymorphic type: a type or a set of types?

Is a polymorphic type with a concrete type substituting its type variable a type?

Is a polymorphic type with different concrete types substituting its type variable considered the same or different types?

Norvell answered 18/7, 2019 at 0:49 Comment(1)
"Which is a polymorphic type: a type or a set of types?" Can this question be given any rigorous interpretation? Types are labels given to terms and other language constructs. Polymorphic types are types, by definition (we're calling then types after all). Are they also sets of concrete types? Maybe; you dig up a definition of a set from your favourite set theory and check the axioms. Is this ever important? We're not working in a set theory.Bresnahan
M
26

Is a polymorphic type with a concrete type substituting its type variable a type?

That's the point, yes. However, you need to be careful. Consider:

id :: a -> a

That's polymorphic. You can substitute a := Int and get Int -> Int, and a := Float -> Float and get (Float -> Float) -> Float -> Float. However, you cannot say a := Maybe and get id :: Maybe -> Maybe. That just doesn't make sense. Instead, we have to require that you can only substitute concrete types like Int and Maybe Float for a, not abstract ones like Maybe. This is handled with the kind system. This is not too important for your question, so I'll just summarize. Int and Float and Maybe Float are all concrete types (that is, they have values), so we say that they have type Type (the type of a type is often called its kind). Maybe is a function that takes a concrete type as an argument and returns a new concrete type, so we say Maybe :: Type -> Type. In the type a -> a, we say the type variable a must have type Type, so now the substitutions a := Int, a := String, etc. are allowed, while stuff like a := Maybe isn't.

Is a polymorphic type with different concrete types substituting its type variable considered the same or different types?

No. Back to a -> a: a := Int gives Int -> Int, but a := Float gives Float -> Float. Not the same.

Which is a polymorphic type: a type or a set of types?

Now that's a loaded question. You can skip to the TL;DR at the end, but the question of "what is a polymorphic type" is actually really confusing in Haskell, so here's a wall of text.

There are two ways to see it. Haskell started with one, then moved to the other, and now we have a ton of old literature referring to the old way, so the syntax of the modern system tries to maintain compatibility. It's a bit of a hot mess. Consider

id x = x

What is the type of id? One point of view is that id :: Int -> Int, and also id :: Float -> Float, and also id :: (Int -> Int) -> Int -> Int, ad infinitum, all simultaneously. This infinite family of types can be summed up with one polymorphic type, id :: a -> a. This point of view gives you the Hindley-Milner type system. This is not how modern GHC Haskell works, but this system is what Haskell was based on at its creation.

In Hindley-Milner, there is a hard line between polymorphic types and monomorphic types, and the union of these two groups gives you "types" in general. It's not really fair to say that, in HM, polymorphic types (in HM jargon, "polytypes") are types. You can't take polytypes as arguments, or return them from functions, or place them in a list. Instead, polytypes are only templates for monotypes. If you squint, in HM, a polymorphic type can be seen as a set of those monotypes that fit the schema.

Modern Haskell is built on System F (plus extensions). In System F,

id = \x -> x -- rewriting the example

is not a complete definition. Therefore we can't even think about giving it a type. Every lambda-bound variable needs a type annotation, but x has no annotation. Worse, we can't even decide on one: \(x :: Int) -> x is just as good as \(x :: Float) -> x. In System F, what we do is we write

id = /\(a :: Type) -> \(x :: a) -> x

using /\ to represent Λ (upper-case lambda) much as we use \ to represent λ.

id is a function taking two arguments. The first argument is a Type, named a. The second argument is an a. The result is also an a. The type signature is:

id :: forall (a :: Type). a -> a

forall is a new kind of function arrow, basically. Note that it provides a binder for a. In HM, when we said id :: a -> a, we didn't really define what a was. It was a fresh, global variable. By convention, more than anything else, that variable is not used anywhere else (otherwise the Generalization rule doesn't apply and everything breaks down). If I had written e.g. inject :: a -> Maybe a, afterwards, the textual occurrences of a would be referring to a new global entity, different from the one in id. In System F, the a in forall a. a -> a actually has scope. It's a "local variable" available only for use underneath that forall. The a in inject :: forall a. a -> Maybe a may or may not be the "same" a; it doesn't matter, because we have actual scoping rules that keep everything from falling apart.

Because System F has hygienic scoping rules for type variables, polymorphic types are allowed to do everything other types can do. You can take them as arguments

runCont :: forall (a :: Type). (forall (r :: Type). (a -> r) -> r) -> a
runCons a f = f a (id a) -- omitting type signatures; you can fill them in

You put them in data constructors

newtype Yoneda f a = Yoneda (forall b. (a -> b) -> f b)

You can place them in polymorphic containers:

type Bool = forall a. a -> a -> a
true, false :: Bool
true a t f = t
false a t f = f

thueMorse :: [Bool]
thueMorse = false : true : true : false : _etc

There's an important difference from HM. In HM, if something has polymorphic type, it also has, simultaneously, an infinity of monomorphic types. In System F, a thing can only have one type. id = /\a -> \(x :: a) -> x has type forall a. a -> a, not Int -> Int or Float -> Float. In order to get an Int -> Int out of id, you have to actually give it an argument: id Int :: Int -> Int, and id Float :: Float -> Float.

Haskell is not System F, however. System F is closer to what GHC calls Core, which is an internal language that GHC compiles Haskell to—basically Haskell without any syntax sugar. Haskell is a Hindley-Milner flavored veneer on top of a System F core. In Haskell, nominally a polymorphic type is a type. They do not act like sets of types. However, polymorphic types are still second class. Haskell doesn't let you actually type forall without -XExplicitForalls. It emulates Hindley-Milner's wonky implicit global variable creation by inserting foralls in certain places. The places where it does so are changed by -XScopedTypeVariables. You can't take polymorphic arguments or have polymorphic fields unless you enable -XRankNTypes. You cannot say things like [forall a. a -> a -> a], nor can you say id (forall a. a -> a -> a) :: (forall a. a -> a -> a) -> (forall a. a -> a -> a)—you must define e.g. newtype Bool = Bool { ifThenElse :: forall a. a -> a -> a } to wrap the polymorphism under something monomorphic. You cannot explicitly give type arguments unless you enable -XTypeApplications, and then you can write id @Int :: Int -> Int. You cannot write type lambdas (/\), period; instead, they are inserted implicitly whenever possible. If you define id :: forall a. a -> a, then you cannot even write id in Haskell. It will always be implicitly expanded to an application, id @_.

TL;DR: In Haskell, a polymorphic type is a type. It's not treated as a set of types, or a rule/schema for types, or whatever. However, due to historical reasons, they are treated as second class citizens. By default, it looks like they are treated as mere sets of types, if you squint a bit. Most restrictions on them can be lifted with suitable language extensions, at which point they look more like "just types". The one remaining big restriction (no impredicative instantiations allowed) is rather fundamental and cannot be erased, but that's fine because there's a workaround.

Maure answered 18/7, 2019 at 3:19 Comment(4)
Minor nitpick: the notation for type equality is usually a ~ Int, not a := Int.Donnelly
I know, but I'm not representing equality. I'm representing substitution, which is a syntactic thing, not logical. Further, the first time I saw ~, I was thoroughly confused as to its meaning. I think "Substitute a := Int" is much clearer and understandable.Maure
Great answer. I'd add the remark that in dependently-typed languages, the “ is a kind of function” aspect is much more important, e.g. in Coq forall x : P, Q is equivalent to P -> Q if x doesn't appear in Q. (If it does appear in Q, then it's a dependent type which can't be expressed in Haskell.)Betatron
This is such an amazing answer. Do you have some references, that can help dig further, or contextualize further what you wrote ?Overwhelming
F
3

There is some nuance in the word "type" here. Values have concrete types, which cannot be polymorphic. Expressions, on the other hand, have general types, which can be polymorphic. If you're thinking of types for values, then a polymorphic type can be thought of loosely as defining sets of possible concrete types. (At least first-order polymorphic types! Higher-order polymorphism breaks this intuition.) But that's not always a particularly useful way of thinking, and it's not a sufficient definition. It doesn't capture which sets of types can be described in this way (and related notions like parametricity.)

It's a good observation, though, that the same word, "type", is used in these two related, but different, ways.

Forum answered 18/7, 2019 at 1:49 Comment(3)
Values don't have types; terms have types, and bindings have types... What are you trying to say?Trenatrenail
I wanted to give the exact same kind of answer but quicky realised it doesn't hold water. Is (\x->x) a value? Why or why not? What is a value? The Haskell report doesn't ever define "value" but it does say a value has a type, which can be polymorphic.Bresnahan
I'm not sure how to be any clearer about what I meant. It's a fair point that the Haskell Report uses "value" differently, though. And also that higher rank polymorphism requires a notion of values having polymorphic types.Forum
D
2

EDIT: The answer below turns out not to answer the question. The difference is a subtle mistake in terminology: types like Maybe and [] are higher-kinded, whereas types like forall a. a -> a and forall a. Maybe a are polymorphic. The answer below relates to higher-kinded types, but the question was asked about polymorphic types. I’m still leaving this answer up in case it helps anyone else, but I realise now it’s not really an answer to the question.

I would argue that a polymorphic higher-kinded type is closer to a set of types. For instance, you could see Maybe as the set {Maybe Int, Maybe Bool, …}.

However, strictly speaking, this is a bit misleading. To address this in more detail, we need to learn about kinds. Similarly to how types describe values, we say that kinds describe types. The idea is:

  • A concrete type (that is, one which has values) has a kind of *. Examples include Bool, Char, Int and Maybe String, which all have type *. This is denoted e.g. Bool :: *. Note that functions such as Int -> String also have kind *, as these are concrete types which can contain values such as show!
  • A type with a type parameter has a kind containing arrows. For instance, in the same way that id :: a -> a, we can say that Maybe :: * -> *, since Maybe takes a concrete type as an argument (such as Int), and produces a concrete type as a result (such as Maybe Int). Something like a -> a also has kind * -> *, since it has one type parameter (a) and produces a concrete result (a -> a). You can get more complex kinds as well: for instance, data Foo f x = FooConstr (f x x) has kind Foo :: (* -> * -> *) -> * -> *. (Can you see why?)

(If the above explanation doesn’t make sense, the Learn You a Haskell book has a great section on kinds as well.)

So now we can answer your questions properly:

Which is a polymorphic higher-kinded type: a type or a set of types?

Neither: a polymorphic higher-kinded type is a type-level function, as indicated by the arrows in its kind. For instance, Maybe :: * -> * is a type-level function which converts e.g. IntMaybe Int, BoolMaybe Bool etc.

Is a polymorphic higher-kinded type with a concrete type substituting its type variable a type?

Yes, when your polymorphic higher-kinded type has a kind * -> * (i.e. it has one type parameter, which accepts a concrete type). When you apply a concrete type Conc :: * to a type Poly :: * -> *, it’s just function application, as detailed above, with the result being Poly Conc :: * i.e. a concrete type.

Is a polymorphic higher-kinded type with different concrete types substituting its type variable considered the same or different types?

This question is a bit out of place, as it doesn’t have anything to do with kinds. The answer is definitely no: two types like Maybe Int and Maybe Bool are not the same. Nothing may be a member of both types, but only the former contains a value Just 4, and only the latter contains a value Just False.

On the other hand, it is possible to have two different substitutions where the resulting types are isomorphic. (An isomorphism is where two types are different, but equivalent in some way. For instance, (a, b) and (b, a) are isomorphic, despite being the same type. The formal condition is that two types p,q are isomorphic when you can write two inverse functions p -> q and q -> p.)

One example of this is Const:

data Const a b = Const { getConst :: a }

This type just ignores its second type parameter; as a result, two types like Const Int Char and Const Int Bool are isomorphic. However, they are not the same type: if you make a value of type Const Int Char, but then use it as something of type Const Int Bool, this will result in a type error. This sort of functionality is incredibly useful, as it means you can ‘tag’ a type a using Const a tag, then use the tag as a marker of information on the type level.

Donnelly answered 18/7, 2019 at 1:29 Comment(7)
I believe this is about polymorphism as in id :: a -> a, not polymorphism as in data Maybe a = Nothing | Just a.Maure
@Maure Possibly, but the question was ambiguous in this regard. @Norvell referenced a text saying “A type that contains one or more type variables is called polymorphic”, and a type such as Maybe definitely contains a type variable and so is polymorphic. Besides, as far as I can tell, @Tim’s third question (about different substitutions yielding the same type) only makes sense in the context of data types.Donnelly
I think @Maure 's concern (please correct me if I'm wrong) has to do with Maybe :: * -> * versus forall a. Maybe a :: *, which is something I can picture being slippery ground for the OP, or more generally for someone still getting used to polymorphism.Encephalon
@Encephalon Oh, that's not what I had in mind. Actually, I really hope it's not, because I've just written the mother of all answers to finally put to rest the "what, exactly, is id :: forall a. a -> a" question, and if your interpretation is what's meant I'll have to find somewhere else to place it. (slight /s)Maure
@Maure Fear not: I agree the question is primarily about things like id :: forall a. a -> a, and so, as far as I can tell, your answer is safe :)Encephalon
Maybe is not the same as Maybe a. Maybe is a type constructor, and doesn't contain any type variables. You can stretch and think of Maybe as a collection of types, but it itself is not a type, polymorphic or otherwise.Dishwater
Excellent point @Dishwater — I actually can’t believe I missed that! Of course a polymorphic type like forall a. a -> a is different to a higher-kinded type like Maybe! (Presumably the polymorphic version of the latter is forall a. Maybe a.) That does invalidate my answer… I’ll edit it now to avoid any more confusion.Donnelly

© 2022 - 2024 — McMap. All rights reserved.