DocsLanguage

Type classes.

Write operations that work across types, and provide an implementation for each type.

A type class describes an operation that several different types can implement. When a function has a constraint on a type, the caller has to provide the matching implementation for it.

Start with a constraint.

same : Eq a => a -> a -> Bool
same left right = left == right

main() =
    print (same 3 3)
    print (same "a" "b")

This prints True and False. a is a type variable, and Eq a says that values of that type can be compared for equality. The function isn’t limited to integers or strings: it works for any type that supports ==.

Define a class and instance.

class Label a
    label : a -> String

type Item = Item String

instance Label Item
    label (Item text) = text

describe : Label a => a -> String
describe value = "item: #{label value}"

main() = print (describe (Item "book"))

This prints item: book. A class has one type parameter and an indented list of method signatures. An instance implements those methods for a particular type, and each required method must be supplied exactly once.

Constraints.

A function often needs more than one operation. List the requirements together, before the arrows:

clampLow : (Num a, Ord a) => a -> a
clampLow number = if number < 0 then 0 else number

main() =
    print (clampLow (-3))
    print (clampLow 2.5)
0
2.5

Ord a is what allows the comparison, and Num a is what allows the literal 0. Neither one picks a specific type, so the two calls use the same definition with an integer and a float.

When you leave out an annotation, inference keeps track of the constraints a function needs. When you write one, include all of them: the checker reports MISSING CONSTRAINT if the body uses an operation the signature can’t support.

Containers work the same way. An instance can require an implementation for its elements, so a Show a => Show (Box a) instance can call show on the value inside a box. You don’t need a separate implementation for every element type.

Default methods.

A default saves every instance from repeating the same relationship between methods. Put a body after the method’s signature:

class Describe a
    name : a -> String
    describe : a -> String
    describe item = "a #{name item}"

type Tool = Hammer

instance Describe Tool
    name Hammer = "hammer"

main() = print (describe Hammer)
a hammer

The Tool instance supplies name and inherits describe. If the general wording doesn’t suit a type, its instance can override the default. Defaults can call other methods of the same class, and they’re checked in the module that defines the class.

Standard classes.

Class Purpose
Num Integer-literal conversion and arithmetic
Eq Equality
Ord Ordering comparisons
Show A value’s quoted or structural representation
Display Text intended for output

print and string interpolation both use Display. A string displays as its contents, while show adds quotes. Every Show type gets Display through a blanket fallback, and a type can define its own Display to replace it. When you annotate a polymorphic function that prints or interpolates its argument, write Display a.

The classes are independent of one another, so Ord doesn’t imply Eq. To get standard instances for your own data without writing them, see deriving.

Write your own display.

import Text.Builder as Builder

type Celsius = Celsius Int

instance Display Celsius
    displayTo built (Celsius degrees) = built |> Builder.appendInt degrees |> Builder.append "°C"

main() =
    today = Celsius 21
    print today
    print "Today: #{today}"

This prints 21°C and Today: 21°C. Instead of returning a string, displayTo receives a Text.Builder holding the text written so far, appends its own part, and returns the result. The append functions take the builder last, so they chain with |>. Builders are immutable values: appending gives you a new builder and leaves the old one unchanged. Because the parts of a composite value and the holes of an interpolated string append to one builder, they don’t each produce an intermediate string. display comes for free: it renders into an empty builder and returns the text.

Show works the same way through showTo. You’ll usually derive it, but a handwritten instance implements showTo and gets show from it. An instance that defines only show or display reports MISSING METHOD, because nested values and interpolation always call the builder methods. A list displays its elements through Show, so print [today] still needs a Show Celsius instance.

Instance heads and blanket instances.

The head of an instance is the type it implements. If the head is a bare type variable, the instance becomes a blanket fallback that covers everything else:

class Label a
    label : a -> String

instance Show a => Label a
    label item = show item

type Secret = Secret String

instance Label Secret
    label _ = "<hidden>"

main() =
    print (label 42)
    print (label (Secret "password"))
42
<hidden>

The generic instance relies on Show, while the specific Secret instance avoids revealing its contents. In a polymorphic function that calls label, keep the Label a constraint, since that’s how its callers supply the operation.

Constructed heads follow a stricter overlap rule. Once you’ve declared Show a => Show (Box a), you can’t add a specialized Show (Box Int) instance for the same constructor and class. If you need distinct behavior, use a new wrapper type.

Instance visibility.

Imports make instances available transitively, even when an exposing list doesn’t mention their methods. For instances declared in the current module, source position still matters. Polymorphic functions use the evidence their callers supply.

Instance selection.

At a concrete use, the checker first prefers the most specific visible head, and then checks that head’s requirements. If a requirement is missing, it doesn’t fall back to a less specific head.

Among applicable candidates with the same head, a context that strictly contains another context wins. Equal or incomparable contexts use the latest declaration within one module, and unresolved candidates from different modules are ambiguous. These rules make conditional instances possible, but code that depends on overlaps is harder to follow, so prefer one clear implementation for each intended use.

Resolution termination.

An instance context can require other instances. If the requirements form a cycle, or grow without end, resolution fails with INSTANCE RESOLUTION. Declaring instances that depend on each other in a circle doesn’t make the cycle usable.

Defaulting.

An integer literal starts out polymorphic, so 3 can take part in an Int or a Float calculation. When nothing else fixes its type, the standard numeric constraints can default it to Int. Decimal literals always select Float.

An annotation makes your intended choice clear at a boundary:

count : Int
count = 3

ratio : Float
ratio = 3

main() =
    print (count + 1)
    print (ratio / 2)
4
1.5

Defaulting covers the standard numeric requirements such as Num, Eq, Ord, Show, and Display. It doesn’t search every available instance for a plausible type, so add an annotation when a custom class leaves the choice ambiguous.

A few limits to be aware of: a class currently has one type parameter, and superclasses, higher-kinded parameters, and extra method-local type variables aren’t supported. Method signatures can use closed effect rows, but not open effect-row variables.