Traits
A trait describes behaviour that many types can share. It is Keel's mechanism
for ad-hoc polymorphism: one name (show, compare, …) that resolves to a
different implementation depending on the type it is used with. Traits replace the
old catch-all comparable type variable with something precise and extensible.
Why traits exist
Before traits, a function that needed to compare or order its argument relied on a
magic comparable type variable. That worked for the built-in primitives but you
could never extend it: there was no way to say "my type can be ordered too", and
the compiler could not tell you when a type genuinely lacked an ordering. Traits fix
both problems — ordering and equality are now ordinary, nameable capabilities, and
the compiler reports a clear error when a required implementation is missing.
Declaring a trait
A trait declaration names the capability, a type parameter, and the method signatures that any implementation must provide:
trait Show a
show : a -> String
This says: a type a implements Show when it provides a show function of type
a -> String.
Implementing a trait
trait … for … gives an implementation of a trait for a concrete type. Each method
is written as an ordinary fn (the signature is taken from the trait definition, so
none is repeated here):
trait Show a
show : a -> String
trait Show for Int
fn show x = "an int"
trait Show for Bool
fn show x = "a bool"
(show 42, show True)
Try itshow 42 dispatches to the Show for Int implementation and show True to the
Show for Bool one — the compiler picks the right one from the argument's type.
Only one implementation of a trait may exist per type. A second
trait Show for Int block is a compile error (DuplicateTraitImpl), and an
implementation of a trait that was never declared is a TraitNotFound error.
Constrained functions
A function can require that its type parameter implement a trait by writing a
constraint before its signature with =>:
trait Show a
show : a -> String
trait Show for Int
fn show x = "n"
fn describe : Show a => a -> String
fn describe x = show x
describe 7
Try itfn describe : Show a => a -> String reads "for any a that implements Show,
describe takes an a and returns a String." Inside the body, show x resolves
through the constraint. Calling describe with a type that has no Show
implementation is a MissingTraitImpl error, reported at the call site.
Multiple constraints are written in parentheses:
fn pair : (Show a, Show b) => a -> b -> String
Superconstraints
A trait can require another trait with needs. Ord (ordering) needs Eq
(equality), because anything you can order you can also compare for equality:
trait Ord a needs Eq a
compare : a -> a -> Int
When you implement Ord for a type, the compiler also checks that Eq is
implemented for it — a missing superconstraint surfaces as MissingTraitImpl.
Built-in traits: Eq, Ord, and Hash
Keel ships built-in Eq and Ord implementations for every primitive type:
Int, Float, Decimal, String, Bool, and Char. This is what makes ==
and ordering work out of the box, and it is what List.sort relies on:
import List
[3, 1, 2] |> List.sort
Try itList.sort has the signature Ord a => [a] -> [a], so sorting a list of a type
without an Ord implementation is a compile error rather than a silent runtime
surprise. List.minimum, List.maximum, and Math.min / max / clamp are
constrained by Ord in the same way; List.unique is constrained by Eq.
Because Ord needs Eq, a value with an Ord implementation can always be
compared with == as well.
Hash
Hash a requires Eq a and provides:
hash : Hash a => a -> Int
All primitive types (Int, Float, Decimal, String, Bool, Char,
Symbol) have built-in Hash implementations when deriving (Hash) appears
anywhere in the program. The function uses FNV-1a (64-bit) and returns a signed
Int. Equal values always produce the same hash:
enum Color = Red | Green | Blue deriving (Hash)
hash Color::Red -- some deterministic Int
hash Color::Red == hash Color::Red -- True
hash Color::Red == hash Color::Blue -- False (in practice)
Hash is the foundation for future dictionary and set types where the key type
must be hashable.
Built-in numeric traits
Keel's numeric type hierarchy is expressed as a trait tower. Each level is a superconstraint of the one below:
Numeric a — Int, Float, Decimal (+, -, *, /)
└── Integral a — Int only (div, mod, …)
└── Fractional a — Float, Decimal (/, decimal division)
└── Real a — Float, Decimal (transcendental functions)
The Real constraint is required by the six transcendental Math functions:
| Function | Signature |
|---|---|
Math.sqrt | Real a => a -> Maybe a |
Math.pow | Real a => a -> a -> Maybe a |
Math.exp | Real a => a -> Maybe a |
Math.log | Real a => a -> Maybe a |
Math.log10 | Real a => a -> Maybe a |
Math.log2 | Real a => a -> Maybe a |
These accept both Float and Decimal arguments. Passing Int, String, or
Bool is a compile-time MissingTraitImpl error.
Real also provides two conversion methods:
Real.toRational : a -> Decimal— converts aFloatorDecimaltoDecimal. ForFloatinputs the conversion is exact for values representable withinDecimal's 28-digit precision; larger or non-representable values round.Real.fromInt : Int -> a— produces aFloatorDecimalfrom an integer literal. The return type must be determinable from context; a bareReal.fromInt 5without an annotation is anAmbiguousTypeerror.
import Math
import Maybe
-- Float dispatch
Math.sqrt 4.0 -- Just 2.0 : Maybe Float
-- Decimal dispatch (full decimal arithmetic, no float round-trip)
Math.sqrt 4.0d -- Just 2.0d : Maybe Decimal
-- fromInt with annotation
let x : Decimal = Real.fromInt 5 -- 5.0d
deriving — automatic implementations
For common structural traits, Keel can generate an implementation automatically.
Write deriving (TraitName) at the end of an enum definition:
enum Direction = North | South | East | West deriving (Eq, Ord, Show, Hash)
Supported derivable traits:
| Trait | Effect |
|---|---|
Eq | eq tests constructor identity (and field equality for tuple variants) |
Ord | compare orders constructors by declaration order, then fields |
Show | show renders to a string like "North" or "Pair 1 2" |
Hash | hash computes a deterministic integer via FNV-1a and polynomial field mixing |
Deriving Ord automatically includes Eq. Deriving Hash automatically includes
Eq. Attempting to derive an unknown trait name is a compile error
(DerivingUnknownTrait).
Row constraints: Lacks
A row constraint is a condition on the open row variable of a DataFrame type,
separate from the trait system. The only row constraint today is Lacks.
r Lacks "col" asserts that the row variable r does not already contain a column
named col. It is written in the same constraint prefix as trait constraints:
fn addTimestamp : r Lacks "ts" => DataFrame { | r } -> DataFrame { ts: DateTime | r }
fn addTimestamp df = ...
When the function is called, the compiler checks that the concrete row bound to r
does not include ts. If it does, a LacksConstraintViolated compile error is
emitted:
-- ERROR: r Lacks "ts" violated — input already has column "ts"
addTimestamp alreadyHasTsDf
Multiple Lacks constraints use the parenthesised form:
fn addAuditCols
: (r Lacks "created_at", r Lacks "updated_at")
=> DataFrame { | r }
-> DataFrame { created_at: DateTime, updated_at: DateTime | r }
fn addAuditCols df = ...
Lacks and trait constraints may appear together in the same prefix:
fn process : (Show a, r Lacks "result") => a -> DataFrame { | r } -> DataFrame { result: String | r }
When the open row variable r is not yet bound (the DataFrame is still abstract),
the constraint is deferred — no error fires until a concrete row is substituted.
When r is bound to RowTail::Unknown (a DataFrame whose schema is not statically
known, such as the result of fromRecords), the constraint is vacuously satisfied:
keel's gradual typing accepts unknown schemas where concrete ones would be checked.
Lacks constraints enable safe composition of schema-extending pipelines:
fn withId : r Lacks "id" => DataFrame { | r } -> DataFrame { id: Int | r }
fn withTimestamp : r Lacks "ts" => DataFrame { | r } -> DataFrame { ts: DateTime | r }
-- OK: withId adds "id", then withTimestamp adds "ts" — no overlap
let result = df |> withId |> withTimestamp
Without Lacks, calling withTimestamp on a DataFrame that already has a ts
column would silently produce a broken schema. With Lacks, it is a compile error.
What traits do not support yet
Keel's traits are deliberately a small, well-understood core. The following are not supported today:
- Associated types — a trait has one type parameter and method signatures only.
- Multi-parameter traits — a trait constrains exactly one type variable.
These are natural future extensions, but leaving them out keeps dispatch simple and every trait error precise.