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.