1. Key design features

The page describes key design features which characterize Kink language.

1.1. Principle: keep it simple

The principle of the design of Kink is to keep things as simple as possible conceptually. It requires the number of fundamental building blocks as small as possible.

For example, exceptions, generators, and thread-local-stores (TLS) are not fundamental building blocks of Kink. Instead, Kink adopts delimited continuation as a primitive building block, and uses shift/reset operations of delimited continuations to implement exceptions, generators (ITER.from_generator), and TLS-like stores.

1.2. Function is a value

A function is a first class value.

:double <- {(:N) N * 2 }
stdout.print_line(double(42).repr) # => 84

:Doubled_vec <- [1 2 3].map($double)
stdout.print_line(Doubled_vec.repr) # => [2 4 6]

A method is a function stored in a variable of a value.

:new_dog <- {
  new_val(
    'bark' { 'Bow' }
    'howl' { 'Wow' }
  )
}

:Dog <- new_dog
stdout.print_line(Dog.bark) # => Bow
stdout.print_line(Dog.howl) # => Wow

1.3. Object is just a bunch of variables

The object system of Kink does not include class-instance relationship like class based languages, nor prototype-child relationship like prototype based languages. So, there is no inheritance nor delegation. An object, or a value, is just a bunch of variables.

The rationale of the design decision is that inheritance and delegation are just means of deduplication. Thus, if deduplication can be done by the runtime as optimization, it need not appear in the language specification.

You can just call new_val function to make a value with data and methods.

:new_account <- {(:Initial_balance)
  new_val(
    'balance' {[:A]() A.Balance }
    'deposit' {[:A](:Delta) A:Balance <- A.balance + Delta }
    'Balance' Initial_balance
  )
}

:X <- new_account(100)
stdout.print_line(X.balance.repr) # => 100
X.deposit(20)
stdout.print_line(X.balance.repr) # => 120

:Y <- new_account(300)
stdout.print_line(Y.balance.repr) # => 300

Apparently, balance and deposit methods are the same for both X and Y. In class based languages, a class is used to share methods among objects of the same type. In Kink, methods can be stored as a vector and spreaded as arguments of new_val. Such a vector is called a trait.

:new_account <- {(:Initial_balance)
  new_val(
    ... Account_trait # spreading the vector of Account_trait as arguments
    'Balance' Initial_balance
  )
}

:Account_trait <- [
  'balance' {[:A]() A.Balance }
  'deposit' {[:A](:Delta) A:Balance <- A.balance + Delta }
}

:X <- new_account(100)
stdout.print_line(X.balance.repr) # => 100

Obviously, the runtime can optimize invocation of new_val to deduplicate storage of the methods in the trait.

1.4. Assignment by function call

Assignment of a variable is done by an invocation of op_store method of a varref object.

:V <- new_val
V:Num <- 42
stdout.print_line(V.Num.repr) # => 42

The program above is equivalent to the following:

:V.op_store(new_val)
V:Num.op_store(42)
stdout.print_line(V.Num.repr) # => 42

So, there is nothing like left-hand-side value in the syntax.

1.5. Local control by function call

Local control structures are implemented as functions. For example, if-then-else is provided by if preloaded function (see kink/BINDING).

:abs <- {(:N)
  if(N >= 0
    { N }
    { -N}
  )
}

stdout.print_line(abs(42).repr)   # => 42
stdout.print_line(abs(-42).repr)  # => 42

Loop for vec elements are provided as its methods such as fold.

:Sum <- [1 2 3 4 5 6 7 8 9 10].fold(0){(:Accum :N)
  Accum + N
}
stdout.print_line(Sum.repr)  # => 55

In general, any loop can be implemented by recursion as follows:

# returns the sum of nums in Vec
:sum <- {(:Vec)
  :loop <- {(:Accum :I)
    if(I < Vec.size
      { :N = Vec.get(I)
        loop(Accum + N I + 1)
      }
      { Accum }
    )
  }
  loop(0 0)
}
:Sum <- sum([1 2 3 4 5 6 7 8 9 10])
stdout.print_line(Sum.repr)  # => 55

To make it possible, it is guaranteed that successive tail calls do not cause stack overflow. See tail call elimination.

1.6. Non-local control by delimited continuation

Non-local control structures, such as exceptions, try-finally (see CONTROL.with_finally), and generators (see ITER.from_generator), are not primitives, but implemented by shift/reset operations of delimited continuations.

Kink adopts delimited continuation rather than call/cc, because delimited continuation is much easier to use.

To make first class continuation practically usable, local variable binding must be virtually immutable by default. That is why let clause was introduced.

As an example, let's try to emulate a curried function with shift/reset which takes three arguments and return a tuple of them.

:KONT_TAG.require_from('kink/')

:triple_correct <- {(:A0)
  :Tag = KONT_TAG.new
  Tag.reset{
    :A1 = Tag.shift{(:k1) $k1 }
    :A2 = Tag.shift{(:k2) $k2 }
    [A0 A1 A2]
  }
}

:triple_broken <- {(:A0)
  :Tag = KONT_TAG.new
  Tag.reset{
    :A1 <- Tag.shift{(:k1) $k1 }
    :A2 <- Tag.shift{(:k2) $k2 }
    [A0 A1 A2]
  }
}

:test <- {(:triple)
  :foo <- triple('foo')
  :foo_bar <- foo('bar')

  stdout.print_line(foo_bar('baz').repr)
  foo('XXX')
  stdout.print_line(foo_bar('baz').repr)
}

test($triple_correct) # => ["foo" "bar" "baz"] ["foo" "bar" "baz"]
test($triple_broken)  # => ["foo" "bar" "baz"] ["foo" "XXX" "baz"]

triple_correct uses let clauses, and it works as expected.

On the other hand, when using triple_broken, the result of the second invocation of foo_bar is broken. This is because the continuation foo shares the local binding with the continuation foo_bar, and invocation foo('XXX') changes the local binding destructively.