Traits
The Traits module provides type-based polymorphism for Kit. Traits define shared behavior that types can implement, enabling generic programming and consistent interfaces across different types.
# Use trait methods on built-in types
println (Int.eq? 5 5) # => true
println (Int.compare 3 7) # => Less
println (Int.show 42) # => "42"
Overview
Kit's trait system consists of two parts:
- Trait definitions — declare the methods a type must implement
- Trait implementations — provide the actual methods for specific types
Built-in types like Int, Float, String, and Bool
come with pre-implemented trait methods that you can use directly via the
TypeName.method pattern.
Defining Traits
Define a trait using the trait keyword with a name and type parameter:
# Trait with one required method
trait Eq a
eq?: (a, a) -> Bool
Traits declare required methods with a type signature (e.g., eq?: (a, a) -> Bool).
Types implementing the trait must provide implementations for all required methods.
Supertraits
A trait can require other traits using the requires keyword:
# Ord requires Eq - any type implementing Ord must also implement Eq
trait Ord a requires Eq
compare: (a, a) -> Ordering
Implementing Traits
Implement a trait for a type using extend with with:
# Implement Eq for Int
extend Int with Eq
eq? = fn(a, b) => a == b
# Implement Ord for Int (requires Eq already implemented)
extend Int with Ord
compare = fn(a, b) =>
if a < b then Less
else if a > b then Greater
else Equal
Implementations may also use braces, for example
extend Int with Eq { eq? = fn(a, b) => a == b }. Kit rejects a second
implementation of the same trait for the same type because its methods would overlap.
Method Dispatch
You can call a trait method with a type-qualified name, or call it bare when the first argument has
a statically known implementing type. For example, Dog.describe dog and
describe dog select the same implementation. A real local binding with the same name
takes precedence over bare trait dispatch.
Conditional Implementations
For parameterized types, use where to require trait bounds on type parameters:
# A list is Eq if its elements are Eq
extend List a with Eq where Eq a
eq? = fn(xs, ys) =>
match (xs, ys)
| ([], []) -> true
| ([x | xt], [y | yt]) -> eq? x y && eq? xt yt
| _ -> false
Standard Traits
The Traits module defines these core traits:
| Method | Type | Description |
|---|---|---|
eq? | (a, a) -> Bool | Required: equality test |
ne? | (a, a) -> Bool | Default: inequality test (not (eq? a b)) |
Ordering type is
Less | Equal | Greater.
| Method | Type | Description |
|---|---|---|
compare | (a, a) -> Ordering | Required: three-way comparison |
lt? | (a, a) -> Bool | Default: less than |
gt? | (a, a) -> Bool | Default: greater than |
le? | (a, a) -> Bool | Default: less than or equal |
ge? | (a, a) -> Bool | Default: greater than or equal |
For boolean comparisons, use pattern matching on the result:
match Int.compare a b | Less -> true | _ -> false
| Method | Type | Description |
|---|---|---|
show | a -> String | Required: convert to string |
| Method | Type | Description |
|---|---|---|
default | a | Required: the default value |
# Default values for built-in types
# Int.default = 0
# Float.default = 0.0
# String.default = ""
# Bool.default = false
| Method | Type | Description |
|---|---|---|
add | (a, a) -> a | Addition |
sub | (a, a) -> a | Subtraction |
mul | (a, a) -> a | Multiplication |
neg | a -> a | Negation |
from-int | Int -> a | Convert from Int |
| Method | Type | Description |
|---|---|---|
min-bound | a | Minimum value of the type |
max-bound | a | Maximum value of the type |
| Method | Type | Description |
|---|---|---|
clone | a -> a | Create a copy of the value |
# For primitive types, clone is identity
# For compound types, clone creates a deep copy
original = [1, 2, 3]
copy = clone original
eq?(a, b) then hash(a) == hash(b).
| Method | Type | Description |
|---|---|---|
hash | a -> Int | Compute hash value |
# Implement Hash for a custom type
extend MyType with Hash
hash = fn(x) => hash(x.id)
# Then use in Map/Set
my-map = Map.empty : Map MyType String
| Method | Type | Description |
|---|---|---|
debug | a -> String | Debug string representation |
x = 42
println (show x) # => "42"
println (debug x) # => "Int(42)"
name = "Alice"
println (show name) # => "Alice"
println (debug name) # => "String(\"Alice\")"
| Method | Type | Description |
|---|---|---|
message | a -> String | Required: error message |
kind | a -> Keyword | Error category (default: :error) |
code | a -> Option Int | Optional error code |
type DbError =
| ConnectionFailed String
| QueryFailed String Int
| Timeout
extend DbError with Error
message = fn(e) => match e
| ConnectionFailed msg -> msg
| QueryFailed msg _ -> msg
| Timeout -> "Database operation timed out"
kind = fn(e) => match e
| ConnectionFailed _ -> :connection-failed
| QueryFailed _ _ -> :query-failed
| Timeout -> :timeout
force is the identity function.
For lazy values, force evaluates the thunk.
| Method | Type | Description |
|---|---|---|
force | a -> a | Force evaluation of a value |
# Works with both eager and lazy values
compute = fn(x) => Int.force(x) * 2
compute(5) # => 10
compute(lazy(fn => 5)) # => 10 (after forcing)
Serialization Traits
These traits provide standard interfaces for encoding and decoding values to various formats. Each format package defines its own marker type.
| Method | Type | Description |
|---|---|---|
encode | a -> String | Encode value to string |
# In kit-json package
type Json = Json
extend User with StringEncode Json
encode = fn(user) =>
"{\"name\": \"${user.name}\", \"age\": ${user.age}}"
# Usage
Json.encode(user) # => {"name": "Alice", "age": 30}
| Method | Type | Description |
|---|---|---|
decode | String -> Result a error | Decode string to value |
extend User with StringDecode Json JsonError
decode = fn(s) =>
# parsing logic...
Ok {name: "...", age: 0}
# Usage
Json.decode(str) : Result User JsonError
| Method | Type | Description |
|---|---|---|
encode-bytes | a -> List Int | Encode value to bytes |
| Method | Type | Description |
|---|---|---|
decode-bytes | List Int -> Result a error | Decode bytes to value |
Standard Types
The Traits module also defines several useful algebraic data types.
Ord trait's
compare method.
match Int.compare a b
| Less -> println "a < b"
| Equal -> println "a == b"
| Greater -> println "a > b"
Ordering Helper Functions
| Function | Type | Description |
|---|---|---|
Ordering.is-less? | Ordering -> Bool | Returns true if Less |
Ordering.is-equal? | Ordering -> Bool | Returns true if Equal |
Ordering.is-greater? | Ordering -> Bool | Returns true if Greater |
Ordering.then | (Ordering, Ordering) -> Ordering | Chains orderings (if first is Equal, use second) |
# Ordering predicates
println (Ordering.is-less? Less) # => true
println (Ordering.is-equal? Equal) # => true
# Chain comparisons (compare by x first, then by y)
compare-points = fn(p1, p2) =>
Ordering.then
(Int.compare p1.x p2.x)
(Int.compare p1.y p2.y)
Left typically
represents the "first" or "error" case, while Right typically
represents the "second" or "success" case.
# Either can represent alternative types
parse-id : String -> Either String Int
parse-id = fn(s) =>
match Int.parse s
| Some n -> Right n
| None -> Left s # Keep as string if not a number
match parse-id "123"
| Left s -> println "String ID: ${s}"
| Right n -> println "Numeric ID: ${n}"
Either implements Eq, Show, Debug, and Clone when its type parameters do.
Unchanged), "set to value" (Assign),
and "set to null" (Clear).
| Constructor | Meaning |
|---|---|
Unchanged | Field not provided — preserve existing value |
Assign a | Explicitly set to a new value |
Clear | Explicitly set to null/empty |
Useful for REST API PATCH semantics, configuration merging, and any scenario where you need to distinguish "not provided" from "set to null".
Assign instead of Set to avoid
conflict with Kit's Set collection type.
Patch Helper Functions
| Function | Type | Description |
|---|---|---|
patch-unchanged? | Patch a -> Bool | Returns true if Unchanged |
patch-has-value? | Patch a -> Bool | Returns true if Assign _ |
patch-clear? | Patch a -> Bool | Returns true if Clear |
patch-unwrap-or | a -> Patch a -> a | Returns value if Assign, otherwise the default |
option-to-patch | Option a -> Patch a | Converts None to Unchanged, Some x to Assign x |
apply-patch | Option a -> Patch a -> Option a | Apply patch to current value |
Patch Example
# Define a patch type for updating users
type UserPatch = {
name: Patch String,
email: Patch String,
age: Patch Int
}
# Apply patch to an existing user
apply-user-patch = fn(user, patch) =>
name = apply-patch (Some user.name) patch.name |>> Option.unwrap-or user.name
email = apply-patch (Some user.email) patch.email |>> Option.unwrap-or user.email
age = apply-patch (Some user.age) patch.age |>> Option.unwrap-or user.age
{name: name, email: email, age: age}
# Example: Update only the email
user = {name: "Alice", email: "old@example.com", age: 30}
patch = {name: Unchanged, email: Assign "new@example.com", age: Unchanged}
updated = apply-user-patch user patch
# => {name: "Alice", email: "new@example.com", age: 30}
Patch implements Eq, Ord, Show, Debug, Clone, Default, and Hash when its type parameter does.
Utility Functions
The Traits module provides these utility functions that work with the Ord trait:
| Function | Type | Description |
|---|---|---|
min | a -> a -> a (where Ord a) | Returns the smaller of two values |
max | a -> a -> a (where Ord a) | Returns the larger of two values |
clamp | a -> a -> a -> a (where Ord a) | Restricts a value to a range |
# min and max
println (min 3 7) # => 3
println (max 3 7) # => 7
# clamp restricts a value to [low, high]
println (clamp 5 1 10) # => 5 (within range)
println (clamp -5 1 10) # => 1 (below range)
println (clamp 15 1 10) # => 10 (above range)
Built-in Type Methods
Kit's built-in types have pre-implemented trait methods available via
the TypeName.method? pattern. Boolean-returning methods
use the ? suffix convention.
| Type | Available Methods |
|---|---|
Int |
eq?, ne?, lt?, gt?, le?, ge?, compare, show |
Float |
eq?, ne?, lt?, gt?, le?, ge?, compare, show |
String |
eq?, ne?, lt?, gt?, le?, ge?, compare |
Bool |
eq?, ne?, show |
BigInt |
eq?, ne?, lt?, gt?, le?, ge?, compare |
Examples
# Equality
println (Int.eq? 5 5) # => true
println (String.eq? "a" "b") # => false
# Ordering
println (Int.lt? 3 7) # => true
println (Float.compare 3.14 2.71) # => Greater
# Use compare result in pattern matching
match Int.compare 10 20
| Less -> println "10 < 20"
| Equal -> println "10 == 20"
| Greater -> println "10 > 20"
# => 10 < 20
# String conversion
println (Int.show 42) # => "42"
println (Bool.show true) # => "true"
Traits vs Protocols
Kit offers two ways to add polymorphic behavior to types: Traits and Protocols. Understanding when to use each helps you write cleaner code.
| Feature | Traits | Protocols |
|---|---|---|
| Syntax | trait Eq a + extend Int with Eq |
Point.eq? = fn(...) |
| Supertraits | Yes (requires) |
No |
| Conditional impl | Yes (where clauses) |
No |
| Default methods | Yes | No |
| Formal interface | Yes (compiler-checked) | No (convention-based) |
| Setup effort | More (define trait first) | Less (just define functions) |
When to Use Traits
- Reusable abstractions — When multiple types share a common interface (e.g., all comparable types implement
Ord) - Supertraits — When one capability depends on another (e.g.,
Ord requires Eq) - Default implementations — When you can derive methods from others (e.g.,
ne?fromeq?) - Conditional implementations — When a container's behavior depends on its element type (e.g.,
List aisEqifaisEq) - Library design — When defining interfaces that others will implement
When to Use Protocols
- Quick customization — When you just need to add
eq?orto-strto a single type - Operator overloading — When you want
==or<to work with your type - Printing and display — When you need custom output with
println - Simple cases — When you don't need supertraits or conditional implementations
Protocol Example
type Point = Point(Int, Int)
# Define protocol functions for Point
Point.eq? = fn(p1, p2) =>
Point(x1, y1) = p1
Point(x2, y2) = p2
x1 == x2 && y1 == y2
Point.to-str = fn(p) =>
Point(x, y) = p
"Point(${x}, ${y})"
# Kit uses your protocol functions automatically
p1 = Point(1, 2)
p2 = Point(1, 2)
println (p1 == p2) # => true (uses Point.eq?)
println p1 # => Point(1, 2) (uses Point.to-str)
See Protocols in the Language Guide for more details.
Int.compare a b when
inference cannot determine the receiver type.