1. Key design features¶
The page describes key design features which characterize Kink language.
1.1. Principle: keep it simple¶
The language design principle of Kink is to keep things as conceptually simple as possible. This means having as few kinds of basic building blocks as possible.
For example, exceptions, generators, and thread-local-stores (TLS) are not basic building blocks. Instead, exceptions, generators, and TLS-like stores are all built on delimited continuations as basic building blocks.
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.
This design is based on the observation that inheritance and delegation are just deduplication for optimization. If deduplication is done in the backend by the runtime, it does not need to 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.
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
'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
1.4. Assignment by function call¶
Assignment of a variable is done by an invocation of op_store method of a variable reference value.
: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 by functions. For example, if-then-else is provided by if preloaded function.
:abs <- {(:N)
if(N >= 0
{ N }
{ -N }
)
}
stdout.print_line(abs(42).repr) # => 42
stdout.print_line(abs(-42).repr) # => 42
Looping over elements of a vector can be done with 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, loops can be implemented by recursive tail calls. That is because consecutive tail calls are guaranteed not to cause stack overflow. See tail call elimination.
# 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
1.6. Non-local control by delimited continuation¶
Non-local control structures, such as exceptions, try-finally, and generators are not primitives, but implemented by shift/reset operations of delimited continuations.
To make delimited continuations practically usable, local variable binding must be virtually immutable by default. That is why let clause was introduced.
Let's make a curried function with shift/reset which makes a three-element tuple from the arguments.
: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.
You can see the invocation foo('XXX') does not change the behavior of foo_bar.
On the other hand, triple_broken modifies the binding by
Varref.op_store.
That is why the result of foo_bar changes after foo('XXX').