Docs / Functions

Functions

Functions are the backbone of kex. They can be single expressions or do … end blocks, and they compose through function chaining.

Signatures

fns.kex
let double(n: Integer) = n * 2

# Haskell-style type declaration, then multiple clauses that
# pattern-match on the argument:
factorial : Integer -> Integer
let factorial(0) = 1
let factorial(n: Integer) = n * factorial(n - 1)
examples/fns.kex

Function chaining

value.f(arg) is equivalent to f(value, arg). The receiver type decides which overload resolves. This is what makes fluent chains possible without methods living on the data.

ufcs.kex
requests
  .filter { |req| req.path.startsWith?("/admin") }
  .reject(&.authenticated?)
  .map { |req| SecurityEvent.from(req) }
  .take(20)

# exactly equivalent:
map(requests, ~normalize)
examples/ufcs.kex

Lambdas and block arguments

lambdas.kex
let inc = { |x| x + 1 }
let add = { |a, b| a + b }

[1, 2, 3].map(~inc)         # [2, 3, 4]
[1, 2, 3].map(&.to(String)) # method reference via &.
examples/lambdas.kex

Capturing, currying and partial application

~func captures a function as a value — on its own, ~f is just f. Each trailing (args) group partially applies it: bound arguments fill left to right, and _ marks an explicit open slot. Once enough arguments arrive to fully saturate, the function runs immediately.

curry.kex
let add(a, b) = a + b
let multiply(a, b) = a * b

[1, 2, 3, 4].filter(~even?)       # plain capture — no argument group

let inc    = ~add(1)              # {|b| add(1, b)}
let double = ~multiply(2)         # {|b| multiply(2, b)}

[1, 2, 3].map(~multiply(10))      # [10, 20, 30]
(1..100).reduce(0, ~(+))          # 5050

let sub5 = ~(-)(_, 5)             # {|a| a - 5}
sub5(20)                          # 15
examples/curry.kex

Every binary operator can be captured with ~(op), plus ~(!) for negation. ~(&&) and ~(||) are ordinary functions, so they evaluate both arguments rather than short-circuiting. The captured name may also be module-qualified, to any depth.

flags.map(~(!))                   # {|b| !b}

["a", "b"].each(~IO.printLine)
["x", "y"].map(~String.upperCase)
words.map(~Outer.Inner.Deep.shout)

Capturing is always spelled with ~. & is receiver shorthand only, as in &.method&func and &.+ are not valid syntax.

Type-directed make

Behavior attaches to a type through make. Inside, @field is shorthand for this.field, and operators overload per receiver type.

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

  let to(String) -> String do
    return "(${@x}, ${@y})"
  end
end
examples/make.kex