Protocols
Kit provides a protocol system that enables polymorphic behavior for custom types.
By defining functions with well-known names like TypeName.eq? or
TypeName.lt?, Kit automatically dispatches to your implementations
when comparing, sorting, or interpolating values of your type into strings.
Overview
Protocols in Kit follow a simple pattern: define a function named
TypeName.method and Kit will use it when operating on values
of that type. This enables your custom types to work seamlessly with
built-in operations like equality checks, sorting, and string conversion.
# Define a custom type
type Point = Point(Int, Int)
# Define protocol functions using the TypeName.method pattern
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})"
# Now Kit automatically uses your protocol functions!
p1 = Point(1, 2)
p2 = Point(1, 2)
println (p1 == p2) # => true (uses Point.eq?)
println "${p1}" # => Point(1, 2) (interpolation uses Point.to-str)
Protocol Functions
Kit recognizes these well-known protocol function names. Note that functions
returning Bool should end with ?:
| Protocol | Type | Purpose |
|---|---|---|
TypeName.eq? |
(a, a) -> Bool |
Equality comparison (used by ==) |
TypeName.lt? |
(a, a) -> Bool |
Less-than comparison (used by < and sorting) |
TypeName.compare |
(a, a) -> Int |
Three-way comparison (must return -1, 0, or 1 to be dispatched) |
TypeName.to-str |
a -> String |
String representation (used by string interpolation) |
Note: because protocol dispatch happens at runtime, the linter may report
protocol functions with warning W002 ("defined but never used").
The functions are still called — the warning is safe to ignore for
protocol definitions.
Defining Protocols for Custom Types
To add protocol support to your custom type, define functions using the
TypeName.method naming pattern:
type Person = Person { name: String, age: Int }
# Equality: two people are equal if name and age match
Person.eq? = fn(a, b) =>
a.name == b.name && a.age == b.age
# Comparison helper: sort by age, then by name
Person.compare = fn(a, b) =>
match Int.compare a.age b.age
| Equal -> String.compare a.name b.name
| result -> result
# Less-than: derived from compare; drives < and sorting
Person.lt? = fn(a, b) =>
Person.compare a b == Less
# String representation
Person.to-str = fn(p) =>
"${p.name} (age ${p.age})"
Automatic Dispatch
Once you define protocol functions, Kit automatically uses them in the appropriate contexts:
Equality with ==
When comparing values with ==, Kit looks for a
TypeName.eq? function:
alice = Person { name: "Alice", age: 30 }
bob = Person { name: "Bob", age: 25 }
alice2 = Person { name: "Alice", age: 30 }
println (alice == alice2) # => true (uses Person.eq?)
println (alice == bob) # => false
Sorting with List.sort
The List.sort function looks for TypeName.compare or
TypeName.lt? to order elements. Note that compare is
only dispatched when it returns an Int (-1, 0, or 1) — the
Person.compare above returns an Ordering, so sorting
dispatches to Person.lt? instead:
people = [
Person { name: "Charlie", age: 35 },
Person { name: "Alice", age: 30 },
Person { name: "Bob", age: 30 }
]
sorted = List.sort people # Uses Person.lt?
# => [Alice (age 30), Bob (age 30), Charlie (age 35)]
String conversion with to-str
When a value is interpolated into a string, Kit uses
TypeName.to-str for the string representation. Printing a value
directly uses the structural representation, so interpolate to get your
custom format:
println alice # => Person(Alice, 30) (structural representation)
println "${alice}" # => Alice (age 30) (uses Person.to-str)
println "${bob}" # => Bob (age 25)
Built-in Type Methods
Kit's built-in types come with pre-defined methods that follow a similar
TypeName.method pattern. These methods are available directly:
# Equality
println (Int.eq? 5 5) # => true
println (String.eq? "a" "a") # => true
# Ordering
println (Int.lt? 3 7) # => true
println (Int.compare 10 5) # => Greater
println (String.compare "a" "b") # => Less
# String conversion
println (Int.show 42) # => 42
println (Float.show 3.14) # => 3.14
Available Methods
| Type | 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, show |
Bool |
eq?, ne?, show |
BigInt |
eq?, ne?, lt?, gt?, le?, ge?, compare |
Version |
eq?, lt?, gt?, lte?, gte?, compare |
Ordering & Comparisons
The Ordering type represents the result of comparing two values:
type Ordering = Less | Equal | Greater
The built-in compare functions (Int.compare,
Float.compare, String.compare) return an
Ordering value. A custom TypeName.compare, however,
is only dispatched automatically (by < and
List.sort) when it returns an Int (-1, 0, or 1) —
an Ordering-returning compare is still useful as a
helper for defining TypeName.lt?, which dispatches on its
Bool result. Use pattern matching to handle comparison results:
describe-order = fn(a, b) =>
match Int.compare a b
| Less -> "${a} is less than ${b}"
| Equal -> "${a} equals ${b}"
| Greater -> "${a} is greater than ${b}"
println (describe-order 5 10) # => 5 is less than 10
println (describe-order 10 10) # => 10 equals 10
println (describe-order 15 10) # => 15 is greater than 10
Practical Examples
Sorting with Custom Comparisons
type Point = Point(Int, Int)
# Compare points: first by x, then by y
Point.compare = fn(p1, p2) =>
Point(x1, y1) = p1
Point(x2, y2) = p2
match Int.compare x1 x2
| Equal -> Int.compare y1 y2
| other -> other
# lt? drives < and List.sort
Point.lt? = fn(a, b) => Point.compare a b == Less
Point.to-str = fn(p) =>
Point(x, y) = p
"(${x}, ${y})"
# Points are now sortable via Point.lt?
points = [
Point(3, 4),
Point(1, 5),
Point(1, 2),
Point(2, 1)
]
sorted = List.sort points
println (List.map (fn(p) => "${p}") sorted)
# => [(1, 2), (1, 5), (2, 1), (3, 4)]
For a one-off ordering that shouldn't be the type's default, pass an
explicit comparator to List.sort-with. The comparator must
return an Int (negative, zero, or positive):
# Sort by y only
by-y = fn(p1, p2) =>
Point(_, y1) = p1
Point(_, y2) = p2
y1 - y2
println (List.map (fn(p) => "${p}") (List.sort-with by-y points))
# => [(2, 1), (1, 2), (3, 4), (1, 5)]
Custom Equality for Records
type User = User { id: Int, email: String, name: String }
# Two users are equal if their IDs match (ignore other fields)
User.eq? = fn(a, b) =>
a.id == b.id
user1 = User { id: 1, email: "old@example.com", name: "Alice" }
user2 = User { id: 1, email: "new@example.com", name: "Alice Smith" }
println (user1 == user2) # => true (same ID)
JSON Serialization
TypeName.encode is a naming convention, not a dispatched
protocol — the JSON builtins do not call it automatically. Define it
to convert your type to plain data, then call it explicitly before
serializing with JSON.to-json and JSON.stringify
(available without an import):
type Config = Config { host: String, port: Int }
Config.encode = fn(c) =>
{ host: c.host, port: c.port }
config = Config { host: "localhost", port: 8080 }
println (JSON.stringify (JSON.to-json (Config.encode config)))
# => {"host":"localhost","port":8080}
Protocols vs Traits
Kit offers two ways to add polymorphic behavior: Protocols (covered here) and Traits (a more formal system). Here's when to use each:
| Feature | Protocols | Traits |
|---|---|---|
| Syntax | Point.eq? = fn(...) |
trait Eq a + extend Int with Eq |
| Setup effort | Minimal (just define functions) | More (define trait first) |
| Supertraits | No | Yes (requires) |
| Default methods | No | Yes |
| Conditional impl | No | Yes (where clauses) |
| Best for | Quick, simple customization | Reusable abstractions, library APIs |
Use Protocols When:
- You just need
eq?,lt?, orto-strfor one type - You want
==,<, or string interpolation to work with your type - You don't need formal interface definitions
Use Traits When:
- Multiple types share a common interface
- You need supertraits (e.g.,
Ord requires Eq) - You want default method implementations
- You're designing a library API
See Traits in the Standard Library reference for the full trait system.
Next Steps
Now that you understand Kit's protocol system, explore:
- Types — Learn about algebraic data types for defining custom types
- Pattern Matching — Master destructuring for working with custom types
- Collections — See how protocols enable generic collection operations
- Modules — Organize your protocol implementations
- Traits — Learn about Kit's formal trait system for advanced polymorphism