Examples

A tour of Kex through short, self-contained snippets — from main to type-directed make blocks. Each one is drawn from the language repo.

Basics

5 examples

Hello, world

Your first Kex program, prints 'Hello, world!' to the console.

examples/hello.kex
examples/hello.kex
main do
  IO.printLine("Hello, world!")
end
stdout
Hello, world!

Pattern matching with functions

Might be your first introduction to functional pattern matching...

examples/fact.kex
examples/fact.kex
factorial : Integer -> Integer
let factorial(1) = 1
let factorial(n) = n * factorial(n - 1)

main do
  IO.printLine(factorial(4))
end
stdout
24

Pattern matching on the first parameter

Might be your first real Kexample.

examples/head.kex
examples/head.kex
make [X] do
  head :> X?
  let head(@[]) = None
  let head(@[x | _]) = Just(x)

  rest :> This
  let rest(@[]) = []
  let rest(@[_ | xs]) = xs
end

main do
  let list = [1, 2, 3]
  IO.printLine(list.head)   # same as: head(list)
  IO.printLine(head([]))    # same as: [].head

  IO.printLine(list.rest)
end
stdout
Just(1)
None
[2, 3]
    

Currying & partial application

Use the tilde `~` for currying a function. `_` marks an open slot (only required if it's not the next one).

README.md
README.md
let add(a, b) = a + b
let multiply(a, b) = a * b

let inc = ~add(1)
let double = ~multiply(2)

main do
  let multipled = [1, 2, 3].map(~multiply(10))
  IO.printLine(multipled)

  let summed = (1..100).reduce(0, ~(+))
  IO.printLine(summed)
end
stdout
[10, 20, 30]
5050

Local mutation with `var` and `!`

`!` rebinds a `var` to the method’s updated value. Frozen `let` bindings refuse it.

examples/mutating.kex
examples/mutating.kex
main do
  var list = [1, 2, 3, 4, 5]

  list.push!(6)             # list = [1, 2, 3, 4, 5, 6]
  list.filter!(&.even?)     # list = [2, 4, 6]
  list.map! { |x| x * 10 }  # list = [20, 40, 60]

  IO.printLine(list.to(String))
end
stdout
[20, 40, 60]

Types

2 examples

Records, operators, UFCS

Define a record, attach behavior with `make`, overload `+` and `*`, and chain calls.

examples/vectors_advanced.kex
examples/vectors_advanced.kex
record Vector2D do
  x : Float
  y : Float
end

make Vector2D do
  let +(other: This) -> This do
    return Vector2D { x: @x + other.x, y: @y + other.y }
  end

  let *(factor: Float) -> This do
    return Vector2D { x: @x * factor, y: @y * factor }
  end

  let to(String) -> String do
    return "(${@x}, ${@y})"
  end
end

main do
  let position = Vector2D { x: 3.0, y: 4.0 }
  let velocity = Vector2D { x: 1.0, y: -0.5 }

  let next = position + velocity * 2.0
  IO.printLine("next position: ${next.to(String)}")
end
stdout
next position: (5.0, 3.0)

Traits with defaults

Declare a contract, give it a default, implement it for each type, override where you want to.

examples/traits.kex
examples/traits.kex
trait Shape do
  area :> Float
  perimeter :> Float

  let describe = "area=${this.area}" # default implementation
end

record Circle do
  radius: Float
end

make Circle, implement: Shape do
  let area = Math.PI * @radius * @radius
  let perimeter = 2.0 * Math.PI * @radius
end

main do
  let c = Circle { radius: 5.0 }
  IO.printLine(c.describe)
end
stdout
area=78.53981633974483

Control

1 example

Pattern matching as control flow

Branch on a tuple of remainders with guards and wildcards — readable from top to bottom.

examples/fizzbuzz_pattern_matching.kex
examples/fizzbuzz_pattern_matching.kex
let fizzBuzz(n: Integer) -> String do
  match (n.modulo(3), n.modulo(5)) do
    (0, 0) -> "FizzBuzz"
    (0, _) -> "Fizz"
    (_, 0) -> "Buzz"
    (_, _) -> n.to(String)
  end
end

main do
  (1..100).map(&.fizzBuzz).each { |s| IO.printLine(s) }
end
stdout
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
...

Effects

2 examples

Result, Optional, and `?`

Model fallible flows with `Result`, propagate failures with `?`, and pattern-match on what comes back.

examples/error_handling.kex
examples/error_handling.kex
type ParseError = InvalidFormat(String) | Overflow | EmptyInput

let parsePort(s: String) -> Result<Int, ParseError> do
  return Error(EmptyInput) if s.empty?

  match Integer.parse(s) do
    Ok(n)    -> do
      return Error(Overflow) if n > 65535

      return Ok(n)
    end
    Error(_) -> Error(InvalidFormat(s))
  end
end

main do
  match parsePort("8080") do
    Ok(port)    -> IO.printLine("listening on ${port}")
    Error(why)  -> IO.printLine("bad port: ${why}")
  end
end
stdout
listening on 8080

Pure vs foul

Pure code can’t call foul code. The compiler rejects it before the program ever runs.

README.md
README.md
# Pure function, no side effects, can be called from anywhere
let wordCountFrom(lines: String[]) -> Integer do
  let words = lines.map do |line|
    line.split(" ").count { |w| !w.empty? }  # words per line
  end

  words.sum
end
      
# A foul, impure function with side-effect.
# Must be called from other foul functions.
foul wordCount(path: String) -> [Integer] do
  return if !File.exists?(path)

  let file_lines = File.lines(path).or([])

  let words = wordCountFrom(lines: file_lines)
  let lines = file_lines.count
  let bytes = File.size(path).or(0)

  [lines, words, bytes]
end

# ...

DSL

1 example

DSL-friendly language

Block args make library code read like language keywords — routing included.

README.md
README.md
let app = Http.routes do
  get "/" do |req|
    Response.ok("Welcome")
  end

  get "/users/:id" do |req|
    match UserService.find(req.params.id) do
      Just(user) -> Response.json(user)
      None -> Response.notFound("user not found")
    end
  end

  post "/users" do |req|
    let user = UserService.create(req.body)
    return Response.created(user) if user.ok?

    return Response.error("error while creating user")
  end
end