8 Qualified types
In the previous lecture, we saw how ML infers types for programs that
lack type annotations. In this lecture, we see how to extend ML with a
principled form of overloading, similar to how it appears in Haskell and
Rust. In particular, we will extend type schemes to a form
,
where
is a logical formula over types that must be
satisfied to use a value having that type scheme.
8.1 Syntax
8.2 Dynamic semantics
8.3 Static semantics
As in ML the static semantics assigns prenex type schemes to let-bound values, but type schemes now have an additional component. The syntax of types is as follows.
8.3.1 Syntax of types
8.3.2 The types of constants
8.3.3 Instantiation and entailment
8.3.4 Syntax-directed typing
The typing judgment is of the form , where
gives constraints on the types in
. Even though it
appears on the left,
should be thought of as an out-parameter.
First we type , which produces a predicate context
.
Then we apply the entailment relation to reduce
to a context
that entails it,
. (This step can be omitted, but it reflects
the idea that we probably want to simplify predicate contexts before
including them in type schemes.) Then we build a type scheme
by
generalizing all the type variables in
and
that do
not appear in
, and bind that
in the environment to
type
. Note that the resulting predicate context for the
judgment is only
, the constraints required by
,
since the constraints required by
are carried by the
resulting type scheme.
Alternatively, we could split the predicates of (or
)
into those relevant to
, which we would package up in the type
scheme, and those irrelevant to
, which we would propogate
upward.
Exercise 60. Use Haskell’s type classes to implement bijections between the natural numbers and lists.
Because Haskell is whitespace-sensitive, copying code from webpages is fraught; accordingly the declarations in the code below are all in XEnum.hs
|
{-# LANGUAGE ScopedTypeVariables #-} |
import Test.QuickCheck |
import Numeric.Natural |
|
class XEnum a where |
into :: a -> Natural |
outof :: Natural -> a |
|
instance XEnum Natural where |
into n = n |
outof n = n |
|
The class declaration introduces a new predicate XEnum that supports two operations, into and outof. These are two functions that realize a bijection between the type a and the natural numbers.
The instance declaration says that the type Natural supports enumeration by giving the functions that translate from the naturals to the naturals (i.e., the identity function).
For our first substantial instance, fill in the into and outof functions to define a bijection between the natural numbers and the integers:
instance XEnum Integer where |
into x = error "not implemented" |
outof n = error "not implemented" |
prop_inout :: (Eq a, XEnum a) => a -> Bool |
prop_inout x = outof (into x) == x |
main = quickCheck (prop_inout :: Integer -> Bool) |
Once you have finished that, add the support for (disjoint) unions. To do that we need to assume we have two enumerable things and then we are going to add a bijection using the Either type:
instance (XEnum a , XEnum b) => XEnum (Either a b) where |
into (Left x) = error "not implemented" |
into (Right x) = error "not implemented" |
outof n = error "not implemented" |
instance (XEnum a , XEnum b) => XEnum (a , b) where |
into (a , b) = error "not implemented" |
outof n = error "not implemented" |
instance XEnum a => XEnum [a] where |
into l = error "not implemented" |
outof n = error "not implemented" |
Be aware that the formulas and the bijections you’ve built work only for infinite sets (i.e., the naturals, the integers, pairs of them, etc.) If you want to use these bijections on sets that are finite, you need to add a size operation:
data ENatural = Fin Natural | Inf |
|
class XEnum a where |
into :: a -> Natural |
outof :: Natural -> a |
size :: ENatural |
8.4 Type inference algorithm
The above type system provides a satisfactory account of which terms type and which do not, but it does not give us an algorithm that we can actually run. In this section, we extend ML’s Algorithm W for qualified types.
Algorithm W for qualified types takes a type environment and a term, and
returns a substitution, a type, and a predicate context:
.
8.5 Evidence translation
Exercise 61. What is the most general type scheme of the term
?
How would you implement such a function—in particular, how does it figure out the equality for a generic/unknown type parameter? Well, our operational semantics cheated by relying on Racket’s underlying polymorphic equal? function. Racket’s equal? relies on Racket’s object representations, which include tags that distinguish number from Booleans from pairs, etc. But what about in a typed language that does not use tags and thus cannot support polymorphic equality?
One solution is called evidence passing, wherein using a qualified type requires passing evidence that it is inhabited, where this evidence specifies some information about how to perform the associated operations. In our type classes example, the evidence is the equality or less-than function specialized to the required type. (In a real evidence-passing implementation such as how Haskell is traditionally implemented, the evidence is a dictionary of methods.)
We can use the evidence environment to summon or construct evidence if
it’s available. In particular, the judgment
uses evidence environment
to construct
, which is
evidence of predicate
. In particular, if
is
then
should be an equality function of type
; if
is
then
should be a less-than function of type
.
Four rules of the typing judgment are unremarkable, simply passing the evidence environment through and translating homomorphically:
Exercise 62. Rust uses monomorphization to implement generics and traits. It does this by duplicating polymorphic code, specializing it at each required type. Write a relation that formalizes monomorphization for describes λ-qual.