kex

Programming Language

Pragmatically Functional for BEAM and More!

Type like Haskell, code like Ruby, scale like Erlang.

Just without the nulls and nils.

v0.1-prealpha · MIT licensed · C++20

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)

Why Kex?

Balance is the key. The best baked together. OOP style tidiness, Haskell-style typing, Ruby-like syntax and DSL-ability.

Universal Dots

Compose plain functions into fluent chains.

Uniform Function Call Syntax means v.f(arg) is always f(v, arg). Plain functions chain like methods, no monkey-patching required.

Purity

Side effects stay visible in the type everywhere.

Functions are pure unless marked foul, and pure code cannot call foul code. No more funny business (unless fun is mandatory).

Types

Express intent through the types, let it rule

Model data directly with product records and type unions. Get the expressivenes of Haskell typing without the hassle. Make the compiler work for you, not against you.

Traits

Contracts with defaults, behavior without classes.

Declare required methods and ship default implementations. make X implement: Trait pulls them in and you override individual methods where you need.

Pattern matching

Multi-clause functions, match expressions, destructuring, and guards. Branching reads top to bottom, and the compiler checks for exhaustiveness.

fizzbuzz.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
...

Purity, and effects you can see

Functions are pure unless marked foul, and pure code can’t call foul code. main is the effect boundary; ? propagates failures without try/catch.

purity.kex
# 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

# ...

Traits and make blocks

Collect logically similar functions into one place. Use them just like an object, but better! Define common behaviour with traits, implement them under make blocks. Mimic inheritance just like in OOP without the mess.

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

Build it from source

The compiler is written in C++20 with minimal external runtime dependencies. Requires CMake 3.20+ and a C++20 compiler.

Full install guide
bash
git clone https://github.com/kexhq/kex.git && cd kex
make build
make run F=examples/fizzbuzz.kex
make repl