In the last blog post we covered extensible records and how we infer their types in Giml.
In this blog post we'll take a closer look at another interesting feature and the dual of extensible records - polymorphic variants.
There are several approaches for typing polymorphic variants and they have different tradeoffs as well. Giml's system is very similar to OCaml's polymorphic variants system and also shares its limitations. If you are looking for different approaches you might want to look at this/this or this.
I'm also not sure polymorphic variants is the best we can do to have extensible sum types, as having recursive polymorphic variants is quite problematic. But there are still plenty of usecases where they could be used so it's worth including them in a language.
[Word of warning, this is a bit more complicated than previous posts and it might be best to look at this section as a cookbook. If you see something weird or not explained well, it might be a testement to that I'm a little fuzzy on the details myself]
Polymorphic Variants
Polymorphic variants can be seen as the dual of extensible records.
Records are product types, they let us hold multiple values together in one structure at the same time, and by giving a label for each value we can refer to each part.
Variants are sum types, they let us define multiple values as alternatives to one another, and we label each alternative as well.
There are two operations we can do with variants, one is creating a tagged value,
for example #Ok 1
, the other is pattern matching on variants. For example:
withDefault def x =
case x of
| #Ok v -> v
| #Err _ -> def
end
The types for polymorphic variants are a bit more complicated. There are actually three different kinds of polymorphic variants: closed, lower-bounded and upper-bounded.
In a closed variant we list the different labels and the type of their payload,
for example: [ Err : String, Ok : Int ]
is a type that have two possible alternative
, #Err <some-string>
and #Ok <some-integer>
.
This type is something that can be generated by applying a polymorphic variant
to a function that takes polymorphic variants as input.
An upper-bounded polymorphic variant is open, it describes the maximum set of variants that could be
represented by this type, and is represented syntactically with a type variable followed by a little <
right after the opening bracket (for example: [ t < Err : String, Ok : Int ]
).
Why does this exist? This type can be used when we want to use a variant in a pattern matching
expression, and we have a set of variants that we can handle.
The pattern matching example above is one like that. In that pattern matching expression
we could match any subset of [ Err : a, Ok : b ]
. So we use an upper-bounded polymorphic
variant to express that in types.
Lower-bounded polymorphic variant is open as well,
it describes the minimum set of variants that could be represented by this type,
and is represented with a type variable followed by a little >
right after the opening bracket
(for example: [ t > Err : String, Ok : Int ]
).
It can unify with any superset of the specified alternatives.
Why does this exist? There are a couple of cases:
One, this type is used when we want to pass a variant to a function that does
pattern matching on that variant, and has the ability to match on unspecified amount of
variants, for example with a wildcard (_
) or variable pattern. For example:
withDefault def x =
case x of
| #Ok 0 -> def
| #Ok v -> v
| _ -> def
end
In this case, even if we passed #Foo 182
this pattern matching should work,
it will just match the wildcard pattern and return def
.
But note that expressions cannot have multiple types (at least not in Giml),
therefore we must specify in our case that the #Ok
must have a specific type (Int
)
and we cannot pass #Ok "hello"
instead. In the pattern matching above we could match
on [ Ok : Int ]
or any other type, and we write this like this: [ t > Ok : Int ]
.
Another case is the type of variant "literals" themselves! This could be considered
the dual of record selection: when we specify a tagged value (say, #Ok 1
),
what we really want for it is to fit anywhere that expect at least [ Ok : Int ]
.
Implementation
Type definition
So we add 3 new constructors to represent the three variant types:
TypeVariant [(Label, Type)]
for closed variantTypePolyVariantLB [(Constr, Type)] TypeVar
for lower-bounded polymorphic variantTypePolyVariantUB TypeVar [(Constr, Type)]
for upper-bounded polymorphic variant
Here, just like with records, we use the row type variable to encoded hidden knowledge about our types.
For lower-bounded polymorphic variants (from now on, LB), we use the row type variable to represent other variants (similar to how with records the row type variable was used to represent other fields).
For upper-bound polymorphic variants (from now on, UB) the row type variable has a different role. We use the row type variable to keep track of which UB we want to merge together (those with the same row type variable), how to convert a UB to LB (constrain the UB row type variable with the merge of the UB and LB fields + the LB row type variable), and when to treat two UBs as normal variants (those with a different row type variable).
We'll explore each scenario soon.
Elaboration and constraint generation
Creating variants
When we create a variant, we:
- Elaborate the type of the payload
- Generate a type variable
- Annotate the type of the AST node as
TypePolyVariantLB [(<label>, <payload-type>)] <typevar>
We will see how this unifies with other variants.
Pattern matching
For matching an expression with one or more patterns of polymorphic variants,
we elaborate the expression and constrain the type we got with a newly generated
type variable which is going to be our hidden row type variable and representative
of the type of the expression (and let's call it tv
for now).
For each variant pattern matching we run into, we add the constraint:
Equality (TypeVar tv) (TypePolyVariantUB tv [(<label>, <payload-type>)])
By making all variant patterns equal to tv
, we will eventually need
to unify all the types that arise from the patterns, and by keeping the
hidden row type variable tv
the same for all of them, we annotate that
these TypePolyVariantUB
s should unify by merging the unique fields
on each side and unifying the shared fields.
Another special case we have is a regular type variable pattern (capture pattern) and wildcard. In our system, a capture pattern should capture any type, including any variant. This case produces the opposite constraint:
Equality (TypeVar tv) (TypePolyVariantLB [] tv2)
Note that this special type, TypePolyVariantLB [] tv2
, unifies with non-variant types
as if it is a simple type variable, but with variant types it behave like other TypePolyVariantLB
s.
Constraint solving
We have 3 new types, TypeVariant
, TypePolyVariantLB
and TypePolyVariantUB
.
And in total we have 7 unique new cases to handle:
Equality TypeVariant TypeVariant
Equality TypeVariant TypePolyVariantLB
Equality TypeVariant TypePolyVariantUB
Equality TypePolyVariantLB TypePolyVariantLB
Equality TypePolyVariantLB TypePolyVariantUB
Equality TypePolyVariantUB TypePolyVariantUB
(where the row type variables match)Equality TypePolyVariantUB TypePolyVariantUB
(where the row type variables don't match)
The cases are symmetrical, so we'll look at one side.
Equality TypeVariant TypeVariant
For two TypeVariant
s, like with two TypeRec
s, we unify all the fields.
If there are any that are only on one side, we fail.
Equality TypeVariant TypePolyVariantLB
This case is similar to the Equality TypeRec TypeRecExt
case,
all Fields in the LB side must appear and unify in the TypeVariant
side,
and the fields that are unique to the TypeVariant
should unify with the
row type variable of LB.
We don't want the LB to have any extra fields that are not found in the regular variant side!
Equality TypeVariant TypePolyVariantUB
When an upper-bounded polymorphic variant meets a regular variant, we treat the UB as a regular variant as well. This is something that can make seemingly well typed programs to be rejected by the typechecker (in a manner that is similar to not having let-polymorphism) but we do not represent the constraint of a subset/subtype in Giml, only equality, and the two types just aren't equal.
Equality TypePolyVariantLB TypePolyVariantLB
Two lower-bounded polymorphic variant should unify all the shared fields, and their row type variants should unify with a new LB that contains the non-shared fields (and a new row type variable).
Equality TypePolyVariantLB TypePolyVariantUB
LB means any superset of the mentioned fields, and UB means any subset of the mentioned fields.
Meaning, we don't want to have a field in the LB said that doesn't exist in the UB side.
This is basically the same case as Equality TypeVariant TypePolyVariantLB
.
Equality TypePolyVariantUB TypePolyVariantUB
(where the row type variables match)
When the two row type variables match, this means that the two types represent alternative pattern matches, so we make sure their fields unify and merge them: we generate a constraint that unifies the merged UB (with the same row type variable) with the row type variable. This is so we can establish the most up-to-date type of the row type variable in the substitution.
Equality (TypePolyVariantUB tv <merged-fields>) (TypeVar tv)
This way we add to the list of alternative variants, add keep track of the row type variable is the substitution and subsequent constraints.
Equality TypePolyVariantUB TypePolyVariantUB
(where the row type variables don't match)
We treat these two as unrelated variants that represent the same subest of fields,
so the fields should match just like regular variants (Equality TypeVariant TypeVariant
).
Instantiation
Instantiation is straightforward, just like all other cases: we instantiate the row type variables here as well.
Substitution
For substitution we need to handle a few cases:
- For UB, if the row type variable maps to another type variable, we just switch the type variable. Otherwise we merge the variant fields we have with the type.
- For LB with no variant fields (
TypePolyVariantLB [] tv
) we return the value mapped in the substitution (remember that this case is also used for wildcards and captures in pattern matching and not just variants). - For LB with variant fields, we try to merge the variant fields with the type.
Merging variants happens the same as with records, but we don't have to worry about duplicate variants as the constraint solving algorithm makes sure they have the same type.
Note that this is where a UB
can flip and become LB
(if it's merged with an LB
).
Example
As always, let's finish of with an example and infer the types of the following program:
withDefault def x =
case x of
| #Some v -> v
| #Nil _ -> def
end
one = withDefault 0 (#Some 1)
First group
withDefault def x =
case x of
| #Some v -> v
| #Nil _ -> def
end
Elaboration and constraints generation
+---- targ3 -> t4 -> t5
|
| +---- targ3
| |
| | +----- t4
| | |
withDefault def x =
+------------ t4
|
case x of +---- t7
|
| #Some v -> v
| #Nil _ -> def
|
|
+---- targ3
end
-- Constraints:
[ Equality t4 t6
, Equality t5 t7
, Equality t5 targ3
, Equality t6 [ t6 < Nil : t9 ]
, Equality t6 [ t6 < Some : t7 ]
, Equality t7 t8
, Equality t9 t10
, Equality tfun2 (targ3 -> t4 -> t5)
, Equality top0 (targ3 -> t4 -> t5)
]
Constraint solving
-- Constraints:
[ Equality t4 t6
, Equality t5 t7
, Equality t5 targ3
, Equality t6 [ t6 < Nil : t9 ]
, Equality t6 [ t6 < Some : t7 ]
, Equality t7 t8
, Equality t9 t10
, Equality tfun2 (targ3 -> t4 -> t5)
, Equality top0 (targ3 -> t4 -> t5)
]
-- Substitution:
[]
1. Equality t4 t6
-- => t4 := t6
-- Constraints:
[ Equality t5 t7
, Equality t5 targ3
, Equality t6 [ t6 < Nil : t9 ]
, Equality t6 [ t6 < Some : t7 ]
, Equality t7 t8
, Equality t9 t10
, Equality tfun2 (targ3 -> t6 -> t5)
, Equality top0 (targ3 -> t6 -> t5)
]
-- Substitution:
[ t4 := t6
]
2. Equality t5 t7
-- => t5 := t7
-- Constraints:
[ Equality t7 t7
, Equality t7 targ3
, Equality t6 [ t6 < Nil : t9 ]
, Equality t6 [ t6 < Some : t7 ]
, Equality t7 t8
, Equality t9 t10
, Equality tfun2 (targ3 -> t6 -> t7)
, Equality top0 (targ3 -> t6 -> t7)
]
-- Substitution:
[ t4 := t6
, t5 := t7
]
3. Equality t7 t7
-- => Nothing to do
-- Constraints:
[ Equality t7 targ3
, Equality t6 [ t6 < Nil : t9 ]
, Equality t6 [ t6 < Some : t7 ]
, Equality t7 t8
, Equality t9 t10
, Equality tfun2 (targ3 -> t6 -> t7)
, Equality top0 (targ3 -> t6 -> t7)
]
-- Substitution:
unchanged
4. Equality t7 targ3
-- => t7 := targ3
-- Constraints:
[ Equality t6 [ t6 < Nil : t9 ]
, Equality t6 [ t6 < Some : targ3 ]
, Equality targ3 t8
, Equality t9 t10
, Equality tfun2 (targ3 -> t6 -> targ3)
, Equality top0 (targ3 -> t6 -> targ3)
]
-- Substitution:
[ t4 := t6
, t5 := targ3
, t7 := targ3
]
5. Equality t6 [ t6 < Nil : t9 ]
-- => t6 := [ t6 < Nil : t9 ] -- note that we don't do occurs check here
-- Constraints:
[ Equality [ t6 < Nil : t9 ] [ t6 < Nil : t9 ]
, Equality [ t6 < Nil : t9 ] [ t6 < Nil : t9 | Some : targ3 ]
, Equality targ3 t8
, Equality t9 t10
, Equality tfun2 (targ3 -> [ t6 < Nil : t9 ] -> targ3)
, Equality top0 (targ3 -> [ t6 < Nil : t9 ] -> targ3)
]
-- Substitution:
[ t4 := [ t6 < Nil : t9 ]
, t5 := targ3
, t7 := targ3
, t6 := [ t6 < Nil : t9 ]
]
6. Equality [ t6 < Nil : t9 ] [ t6 < Nil : t9 ]
-- => The two sides are the same, nothing to do.
-- Constraints:
[ Equality [ t6 < Nil : t9 ] [ t6 < Nil : t9 | Some : targ3 ]
, Equality targ3 t8
, Equality t9 t10
, Equality tfun2 (targ3 -> [ t6 < Nil : t9 ] -> targ3)
, Equality top0 (targ3 -> [ t6 < Nil : t9 ] -> targ3)
]
-- Substitution:
unchanged
7. Equality [ t6 < Nil : t9 ] [ t6 < Nil : t9 | Some : targ3 ]
-- => [ Equality t6 [ t6 < Nil : t9 | Some : targ3 ], Equality t9 t9 ]
-- Constraints:
[ Equality t6 [ t6 < Nil : t9 | Some : targ3 ]
, Equality t9 t9
[ Equality [ t6 < Nil : t9 ] [< Nil : t9 | Some : targ3 ]
, Equality targ3 t8
, Equality t9 t10
, Equality tfun2 (targ3 -> [ t6 < Nil : t9 ] -> targ3)
, Equality top0 (targ3 -> [ t6 < Nil : t9 ] -> targ3)
]
-- Substitution:
unchanged
8. Equality [ t6 < Nil : t9 ] [ t6 < Nil : t9 | Some : targ3 ]
-- => t6 := [ t6 < Nil : t9 | Some : targ3 ]
-- Constraints:
[ Equality t9 t9
, Equality targ3 t8
, Equality t9 t10
, Equality tfun2 (targ3 -> [ t6 < Nil : t9 | Some : targ3 ] -> targ3)
, Equality top0 (targ3 -> [ t6 < Nil : t9 | Some : targ3 ] -> targ3)
]
-- Substitution:
[ t4 := [ t6 < Nil : t9 | Some : targ3 ]
, t5 := targ3
, t7 := targ3
, t6 := [ t6 < Nil : t9 | Some : targ3 ]
]
9. Equality t9 t9
-- => Nothing to do
-- Constraints:
[ Equality targ3 t8
, Equality t9 t10
, Equality tfun2 (targ3 -> [ t6 < Nil : t9 | Some : targ3 ] -> targ3)
, Equality top0 (targ3 -> [ t6 < Nil : t9 | Some : targ3 ] -> targ3)
]
-- Substitution:
unchanged
10. Equality targ3 t8
-- => targ3 := t8
-- Constraints:
[ Equality t9 t10
, Equality tfun2 (t8 -> [ t6 < Nil : t9 | Some : t8 ] -> t8)
, Equality top0 (t8 -> [ t6 < Nil : t9 | Some : t8 ] -> t8)
]
-- Substitution:
[ t4 := [ t6 < Nil : t9 | Some : t8 ]
, t5 := t8
, t7 := t8
, t6 := [ t6 < Nil : t9 | Some : t8 ]
, targ3 := t8
]
11. Equality t9 t10
-- => t9 := t10
-- Constraints:
[ Equality tfun2 (t8 -> [ t6 < Nil : t10 | Some : t8 ] -> t8)
, Equality top0 (t8 -> [ t6 < Nil : t10 | Some : t8 ] -> t8)
]
-- Substitution:
[ t4 := [ t6 < Nil : t10 | Some : t8 ]
, t5 := t8
, t7 := t8
, t6 := [ t6 < Nil : t10 | Some : t8 ]
, targ3 := t8
, t9 := t10
]
11. Equality tfun2 (t8 -> [ t6 < Nil : t10 | Some : t8 ] -> t8)
-- => tfun2 := (t8 -> [ t6 < Nil : t10 | Some : t8 ] -> t8)
-- Constraints:
[ Equality top0 (t8 -> [ t6 < Nil : t10 | Some : t8 ] -> t8)
]
-- Substitution:
[ t4 := [ t6 < Nil : t10 | Some : t8 ]
, t5 := t8
, t7 := t8
, t6 := [ t6 < Nil : t10 | Some : t8 ]
, targ3 := t8
, t9 := t10
, tfun2 := t8 -> [ t6 < Nil : t10 | Some : t8 ] -> t8
]
12. Equality top0 (t8 -> [ t6 < Nil : t10 | Some : t8 ] -> t8)
-- => top0 := t8 -> [ t6 < Nil : t10 | Some : t8 ] -> t8
-- Constraints: []
-- Substitution:
[ t4 := [ t6 < Nil : t10 | Some : t8 ]
, t5 := t8
, t7 := t8
, t6 := [ t6 < Nil : t10 | Some : t8 ]
, targ3 := t8
, t9 := t10
, tfun2 := t8 -> [ t6 < Nil : t10 | Some : t8 ] -> t8
, top0 := t8 -> [ t6 < Nil : t10 | Some : t8 ] -> t8
]
Substitute
Let's substitute the type variables in our program and hide the row type variables:
+---- t8 -> [ t6 < Nil : t10 | Some : t8 ] -> t8
|
| +---- t8
| |
| | +----- [ t6 < Nil : t10 | Some : t8 ]
| | |
withDefault def x =
+------------ [ t6 < Nil : t10 | Some : t8 ]
|
case x of +---- t8
|
| #Some v -> v
| #Nil _ -> def
|
|
+---- t8
end
Generalize
+---- forall a b c. c -> [ b < Nil : a | Some : c ] -> c
|
| +---- c
| |
| | +----- [ b < Nil : a | Some : c ]
| | |
withDefault def x =
+------------ [ b < Nil : a | Some : c ]
|
case x of +---- c
|
| #Some v -> v
| #Nil _ -> def
|
|
+---- c
end
Second group
one = withDefault 0 (#Some 1)
Elaboration and constraints generation
+-- t6
| +--- t1
| |
| | +---- Int
| | |
| | | +---- t4 -> [ t3 > Some : t4 ]
| | | |
| | | | +--- Int
| | | | |
one = withDefault 0 (#Some 1)
------------- -------
| |
| +--- t5
|
+------ t2
-- Constraints:
[ Equality t1 (Int -> t2)
, Equality t1 (forall a b c. c -> [b < Nil : a | Some : c] -> c)
, Equality t2 (t5 -> t6)
, Equality (t4 -> [ t3 > Some : t4 ]) (Int -> t5)
, Equality top0 t6
]
Constraint solving
-- Constraints:
[ Equality t1 (Int -> t2)
, Equality t1 (forall a b c. c -> [b < Nil : a | Some : c] -> c)
, Equality t2 (t5 -> t6)
, Equality (t4 -> [ t3 > Some : t4 ]) (Int -> t5)
, Equality top0 t6
]
1. Equality t1 (Int -> t2)
-- => t1 := Int -> t2
-- Constraints:
[ Equality (Int -> t2) (forall a b c. c -> [b < Nil : a | Some : c] -> c)
, Equality t2 (t5 -> t6)
, Equality (t4 -> [ t3 > Some : t4 ]) (Int -> t5)
, Equality top0 t6
]
-- Substitution:
[ t1 := Int -> t2
]
2. Equality (Int -> t2) (forall a b c. c -> [b < Nil : a | Some : c] -> c)
-- => [ Equality (Int -> t2) (ti9 -> [ ti8 < Nil : ti7 | Some : ti9 ] -> ti9) ]
-- Constraints:
[ Equality (Int -> t2) (ti9 -> [ ti8 < Nil : ti7 | Some : ti9 ] -> ti9)
, Equality t2 (t5 -> t6)
, Equality (t4 -> [ t3 > Some : t4 ]) (Int -> t5)
, Equality top0 t6
]
-- Substitution: unchanged
3. Equality (Int -> t2) (ti9 -> [ ti8 < Nil : ti7 | Some : ti9 ] -> ti9)
-- => [ Equality Int ti9, Equality t2 ([ ti8 < Nil : ti7 | Some : ti9 ] -> ti9) ]
-- Constraints:
[ Equality Int ti9
, Equality t2 ([ ti8 < Nil : ti7 | Some : ti9 ] -> ti9)
, Equality t2 (t5 -> t6)
, Equality (t4 -> [ t3 > Some : t4 ]) (Int -> t5)
, Equality top0 t6
]
-- Substitution: unchanged
4. Equality Int ti9
-- => ti9 := Int
-- Constraints:
[ Equality t2 ([ ti8 < Nil : ti7 | Some : Int ] -> Int)
, Equality t2 (t5 -> t6)
, Equality (t4 -> [ t3 > Some : t4 ]) (Int -> t5)
, Equality top0 t6
]
-- Substitution:
[ t1 := Int -> t2
, ti9 := Int
]
5. Equality t2 ([ ti8 < Nil : ti7 | Some : Int ] -> Int)
-- => t2 := [ ti8 < Nil : ti7 | Some : Int ] -> Int
-- Constraints:
[ Equality ([ ti8 < Nil : ti7 | Some : Int ] -> Int) (t5 -> t6)
, Equality (t4 -> [ t3 > Some : t4 ]) (Int -> t5)
, Equality top0 t6
]
-- Substitution:
[ t1 := Int -> [ ti8 < Nil : ti7 | Some : Int ] -> Int
, ti9 := Int
, t2 := [ ti8 < Nil : ti7 | Some : Int ] -> Int
]
6. Equality ([ ti8 < Nil : ti7 | Some : Int ] -> Int) (t5 -> t6)
-- => [ Equality [ ti8 < Nil : ti7 | Some : Int ] t5, Equality Int t6 ]
-- Constraints:
[ Equality [ ti8 < Nil : ti7 | Some : Int ] t5
, Equality Int t6
, Equality (t4 -> [ t3 > Some : t4 ]) (Int -> t5)
, Equality top0 t6
]
-- Substitution: unchanged
7. Equality [ ti8 < Nil : ti7 | Some : Int ] t5
-- => t5 := [ ti8 < Nil : ti7 | Some : Int ]
-- Constraints:
[ Equality Int t6
, Equality (t4 -> [ t3 > Some : t4 ]) (Int -> [ ti8 < Nil : ti7 | Some : Int ])
, Equality top0 t6
]
-- Substitution:
[ t1 := Int -> [ ti8 < Nil : ti7 | Some : Int ] -> Int
, ti9 := Int
, t2 := [ ti8 < Nil : ti7 | Some : Int ] -> Int
, t5 := [ ti8 < Nil : ti7 | Some : Int ]
]
7. Equality Int t6
-- => t6 := Int
-- Constraints:
[ Equality (t4 -> [ t3 > Some : t4 ]) (Int -> [ ti8 < Nil : ti7 | Some : Int ])
, Equality top0 Int
]
-- Substitution:
[ t1 := Int -> [ ti8 < Nil : ti7 | Some : Int ] -> Int
, ti9 := Int
, t2 := [ ti8 < Nil : ti7 | Some : Int ] -> Int
, t5 := [ ti8 < Nil : ti7 | Some : Int ]
, t6 := Int
]
8. Equality (t4 -> [ t3 > Some : t4 ]) (Int -> [ ti8 < Nil : ti7 | Some : Int ])
-- => [ Equality t4 Int, Equality [ t3 > Some : t4 ] [ ti8 < Nil : ti7 | Some : Int ] ]
-- Constraints:
[ Equality t4 Int
, Equality [ t3 > Some : t4 ] [ ti8 < Nil : ti7 | Some : Int ]
, Equality top0 Int
]
-- Substitution: unchanged
9. Equality t4 Int
-- => t4 := Int
-- Constraints:
[ Equality [ t3 > Some : Int ] [ ti8 < Nil : ti7 | Some : Int ]
, Equality top0 Int
]
-- Substitution:
[ t1 := Int -> [ ti8 < Nil : ti7 | Some : Int ] -> Int
, ti9 := Int
, t2 := [ ti8 < Nil : ti7 | Some : Int ] -> Int
, t5 := [ ti8 < Nil : ti7 | Some : Int ]
, t6 := Int
, t4 := Int
]
10. Equality [ t3 > Some : Int ] [ ti8 < Nil : ti7 | Some : Int ]
-- => [ Equality [ t3 > Some : Int ] [ Nil : ti7 | Some : Int ], Equality ti8 [] ]
-- LB ~ UB are solved like LB and normal variant
-- the UB type variable should be equals to [] (the empty variant)
-- Constraints:
[ Equality [ t3 > Some : Int ] [ Nil : ti7 | Some : Int ]
, Equality ti8 []
, Equality top0 Int
]
-- Substitution: unchanged
11. Equality [ t3 > Some : Int ] [ Nil : ti7 | Some : Int ]
-- => [ Equality Int Int, Equality t3 [ Nil : ti7 ] ]
-- Constraints:
[ Equality Int Int
, Equality t3 [ Nil : ti7 ]
, Equality ti8 []
, Equality top0 Int
]
-- Substitution: unchanged
12. Equality Int Int
-- => []
-- Constraints:
[ Equality t3 [ Nil : ti7 ]
, Equality ti8 []
, Equality top0 Int
]
-- Substitution: unchanged
13. Equality t3 [ Nil : ti7 ]
-- => t3 := [ Nil : ti7 ]
-- Constraints:
[ Equality ti8 []
, Equality top0 Int
]
-- Substitution:
[ t1 := Int -> [ ti8 < Nil : ti7 | Some : Int ] -> Int
, ti9 := Int
, t2 := [ ti8 < Nil : ti7 | Some : Int ] -> Int
, t5 := [ ti8 < Nil : ti7 | Some : Int ]
, t6 := Int
, t4 := Int
, t3 := [ Nil : ti7 ]
]
15. Equality ti8 []
-- => ti8 := []
-- Constraints:
[ Equality top0 Int
]
-- Substitution:
[ t1 := Int -> [ Nil : ti7 | Some : Int ] -> Int
, ti9 := Int
, t2 := [ Nil : ti7 | Some : Int ] -> Int
, t5 := [ Nil : ti7 | Some : Int ]
, t6 := Int
, t4 := Int
, t3 := [ Nil : ti7 ]
, ti8 := Int
]
14. Equality top0 Int
-- => top0 := Int
-- Constraints:
[]
-- Substitution:
[ t1 := Int -> [ Nil : ti7 | Some : Int ] -> Int
, ti9 := Int
, t2 := [ Nil : ti7 | Some : Int ] -> Int
, t5 := [ Nil : ti7 | Some : Int ]
, t6 := Int
, t4 := Int
, t3 := [ Nil : ti7 ]
, ti8 := []
, top0 := Int
]
... and done.
Substitute
Let's substitute the type variables in our program and hide the row type variables:
+-- Int
| +--- Int -> [ Nil : ti7 | Some : Int ] -> Int
| |
| | +---- Int
| | |
| | | +---- Int -> [ Some : Int ]
| | | |
| | | | +--- Int
| | | | |
one = withDefault 0 (#Some 1)
------------- -------
| |
| +--- [ Nil : ti7 | Some : Int ]
|
+------ [ Nil : ti7 | Some : Int ]
Result
+---- forall a b c. c -> [ b < Nil : a | Some : c ] -> c
|
| +---- c
| |
| | +----- [ b < Nil : a | Some : c ]
| | |
withDefault def x =
+------------ [ b < Nil : a | Some : c ]
|
case x of +---- c
|
| #Some v -> v
| #Nil _ -> def
|
|
+---- c
end
+-- Int
| +--- Int -> [ Nil : ti7 | Some : Int ] -> Int
| |
| | +---- Int
| | |
| | | +---- Int -> [ Some : Int ]
| | | |
| | | | +--- Int
| | | | |
one = withDefault 0 (#Some 1)
------------- -------
| |
| +--- [ Nil : ti7 | Some : Int ]
|
+------ [ Nil : ti7 | Some : Int ]
Phew, looks correct!
Alternative approach: simulation in userland
Another cool thing I'd like to note is that polymorphic variants could potentially be simulated in userland if you have extensible records in the language (and this is what purescript-variant) does for example).
One approach I tweeted about that also helped me reach the polymorphic variant implementation for Giml is to:
- Make variant values functions - they take their payload and a record of functions as input, and select the label in the record that corresponds to their constructor, and pass it the payload
- Make patterns fields in a record - the relevant label takes the expected payload and does something with it
- Make pattern matching just applying variants with patterns. That way the variant selector will select the right pattern match from the record and apply the payload
Though in this approach we can't add a "default handler" label.
Summary
Polymorphic variants are a bit complicated and implementing them is a bit tricky, and they also have several limitations. But I believe that for some use cases they can really make a difference and it's worth having them in the language.
If you're interesting to see a video of me live coding polymorphic variants in Giml, check out Part 17 of my live streaming Giml development series.