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?, or to-str for 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