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.compare, Kit automatically dispatches to your implementations
when comparing, sorting, or displaying values of your type.
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) (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 (returns -1, 0, or 1) |
TypeName.to-str |
a -> String |
String representation (used by println) |
TypeName.encode |
a -> JsonValue |
JSON serialization |
TypeName.decode |
JsonValue -> a |
JSON deserialization |
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) =>
Person { name: n1, age: a1 } = a
Person { name: n2, age: a2 } = b
n1 == n2 && a1 == a2
# Comparison: sort by age, then by name
Person.compare = fn(a, b) =>
Person { name: n1, age: a1 } = a
Person { name: n2, age: a2 } = b
match Int.compare a1 a2
| Equal -> String.compare n1 n2
| result -> result
# Less-than: can be derived from compare
Person.lt? = fn(a, b) =>
Person.compare a b == -1
# String representation
Person.to-str = fn(p) =>
Person { name: n, age: a } = p
"${n} (age ${a})"
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 sort
The sort function looks for TypeName.compare or
TypeName.lt? to order elements:
people = [
Person { name: "Charlie", age: 35 },
Person { name: "Alice", age: 30 },
Person { name: "Bob", age: 30 }
]
sorted = sort people # Uses Person.compare
# => [Alice (age 30), Bob (age 30), Charlie (age 35)]
Printing with println
When printing a value, Kit uses TypeName.to-str for the
string representation:
println alice # => Alice (age 30)
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. Import the Traits module
to access them:
import Traits
# 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) # => 1 (Greater)
println (String.compare "a" "b") # => -1 (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 compare protocol function can return either an Int
(-1, 0, 1) or an Ordering value. Use pattern matching to handle
comparison results:
import Traits
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
Point.lt? = fn(a, b) => Point.compare a b == -1
Point.to-str = fn(p) =>
Point(x, y) = p
"(${x}, ${y})"
# Points are automatically sortable
points = [
Point(3, 4),
Point(1, 5),
Point(1, 2),
Point(2, 1)
]
sorted = sort points
println sorted # => [(1, 2), (1, 5), (2, 1), (3, 4)]
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) =>
User { id: id1 } = a
User { id: id2 } = b
id1 == id2
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
import JSON
type Config = Config { host: String, port: Int }
Config.encode = fn(c) =>
Config { host: h, port: p } = c
{ host: h, port: p }
Config.decode = fn(json) =>
Config { host: json.host, port: json.port }
config = Config { host: "localhost", port: 8080 }
println (JSON.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
==,<, orprintlnto 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