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.

import Traits

# Use trait methods on built-in types
println (Int.eq? 5 5)        # => true
println (Int.compare 3 7)    # => -1 (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

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:

Eq
Equality comparison
Types that can be compared for equality.
MethodTypeDescription
eq?(a, a) -> BoolRequired: equality test
Ord
Ordering comparison (requires Eq)
Types that have a total ordering. The Ordering type is Less | Equal | Greater.
MethodTypeDescription
compare(a, a) -> OrderingRequired: three-way comparison

For boolean comparisons, use pattern matching on the result:
match Int.compare a b | Less -> true | _ -> false

Show
String representation
Types that can be converted to a string representation.
MethodTypeDescription
showa -> StringRequired: convert to string
Default
Default values
Types that have a sensible default value.
MethodTypeDescription
defaultaRequired: the default value
# Default values for built-in types
# Int.default    = 0
# Float.default  = 0.0
# String.default = ""
# Bool.default   = false
Num
Numeric operations (requires Eq)
Types that support numeric operations.
MethodTypeDescription
add(a, a) -> aAddition
sub(a, a) -> aSubtraction
mul(a, a) -> aMultiplication
nega -> aNegation
from-intInt -> aConvert from Int
Bounded
Types with minimum and maximum values
Types that have well-defined minimum and maximum values. Useful for fixed-width integers, floating point types, and bounded numeric types.
MethodTypeDescription
min-boundaMinimum value of the type
max-boundaMaximum value of the type
Clone
Explicit copying of values
Provides explicit copying semantics. While Kit values are generally immutable and copying is implicit, Clone is useful when you want to explicitly indicate that a copy is being made.
MethodTypeDescription
clonea -> aCreate 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
Hash
Hashable types (requires Eq)
Types that can compute hash values. Types implementing Hash can be used as keys in Maps and elements in Sets. The hash function must satisfy: if eq?(a, b) then hash(a) == hash(b).
MethodTypeDescription
hasha -> IntCompute 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
Debug
Debug representation with type information
Similar to Show but includes type information for debugging purposes. While Show provides human-readable output, Debug provides output useful for development and debugging.
MethodTypeDescription
debuga -> StringDebug string representation
x = 42
println (show x)   # => "42"
println (debug x)  # => "Int(42)"

name = "Alice"
println (show name)   # => "Alice"
println (debug name)  # => "String(\"Alice\")"
Error
Error handling interface (requires Show)
Provides a common interface for error types. Enables consistent error handling across packages while allowing domain-specific error types with pattern matching.
MethodTypeDescription
messagea -> StringRequired: error message
kinda -> KeywordError category (default: :error)
codea -> Option IntOptional 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
Forceable
Types that can be forced to a value
Provides a uniform interface for working with both eager and lazy values. For eager values, force is the identity function. For lazy values, force evaluates the thunk.
MethodTypeDescription
forcea -> aForce 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.

StringEncode
Text-based serialization
Encodes values to string representations. Use for text-based formats like JSON, YAML, TOML, CSV, etc.
MethodTypeDescription
encodea -> StringEncode 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}
StringDecode
Text-based deserialization
Decodes values from string representations. Returns a Result to handle parsing errors.
MethodTypeDescription
decodeString -> Result a errorDecode 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
BinaryEncode
Binary serialization
Encodes values to binary representations (byte arrays). Use for binary formats like MessagePack, Protocol Buffers, CBOR, BSON, Avro, etc.
MethodTypeDescription
encode-bytesa -> List IntEncode value to bytes
BinaryDecode
Binary deserialization
Decodes values from binary representations. Returns a Result to handle parsing errors.
MethodTypeDescription
decode-bytesList Int -> Result a errorDecode bytes to value

Standard Types

The Traits module also defines several useful algebraic data types.

Ordering
type Ordering = Less | Equal | Greater
The result type for three-way comparisons. Used by the 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

FunctionTypeDescription
Ordering.is-less?Ordering -> BoolReturns true if Less
Ordering.is-equal?Ordering -> BoolReturns true if Equal
Ordering.is-greater?Ordering -> BoolReturns true if Greater
Ordering.reverseOrdering -> OrderingReverses the ordering (LessGreater)
Ordering.thenOrdering -> Ordering -> OrderingChains orderings (if first is Equal, use second)
# Ordering predicates
println (Ordering.is-less? Less)      # => true
println (Ordering.is-equal? Equal)    # => true

# Reverse for descending sort
println (Ordering.reverse Less)       # => Greater

# 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)
Either
type Either a b = Left a | Right b
Represents a value of one of two possible types. 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.

Patch
type Patch a = Unchanged | Assign a | Clear
A three-state type for representing partial updates. Distinguishes between "not provided" (Unchanged), "set to value" (Assign), and "set to null" (Clear).
ConstructorMeaning
UnchangedField not provided — preserve existing value
Assign aExplicitly set to a new value
ClearExplicitly 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".

Note: We use Assign instead of Set to avoid conflict with Kit's Set collection type.

Patch Helper Functions

FunctionTypeDescription
Patch.unchanged?Patch a -> BoolReturns true if Unchanged
Patch.has-value?Patch a -> BoolReturns true if Assign _
Patch.clear?Patch a -> BoolReturns true if Clear
Patch.unwrap-ora -> Patch a -> aReturns value if Assign, otherwise the default
Patch.from-optionOption a -> Patch aConverts None to Unchanged, Some x to Assign x
Patch.applyOption a -> Patch a -> Option aApply patch to current value

Patch Example

import Traits

# 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 = Patch.apply (Some user.name) patch.name |> Option.unwrap-or user.name
  email = Patch.apply (Some user.email) patch.email |> Option.unwrap-or user.email
  age = Patch.apply (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: Unchangedupdated = 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:

FunctionTypeDescription
mina -> a -> a (where Ord a)Returns the smaller of two values
maxa -> a -> a (where Ord a)Returns the larger of two values
clampa -> a -> a -> a (where Ord a)Restricts a value to a range
import Traits

# 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

import Traits

# 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)  # => 1 (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? from eq?)
  • Conditional implementations — When a container's behavior depends on its element type (e.g., List a is Eq if a is Eq)
  • Library design — When defining interfaces that others will implement

When to Use Protocols

  • Quick customization — When you just need to add eq? or to-str to 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.

Note: Automatic trait dispatch for generic functions is not yet implemented. For now, use type-qualified methods directly (e.g., Int.compare a b) rather than a generic compare a b.