Functions
Functions are the backbone of kex. They can be single expressions or
do … end blocks, and they compose through function chaining.
Signatures
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) 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.
requests
.filter { |req| req.path.startsWith?("/admin") }
.reject(&.authenticated?)
.map { |req| SecurityEvent.from(req) }
.take(20)
# exactly equivalent:
map(requests, ~normalize) Lambdas and block arguments
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 &. 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.
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
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 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