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?, or to-str for one type
  • You want ==, <, or println 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