1. 言語設計の特徴¶
このページでは、Kink言語の主要な設計上の特徴について述べる。
1.1. 原則: ものごとは単純に¶
Kinkの言語設計の原則は、ものごとをできるだけ 概念的に単純に することである。これは、基本的な構成要素の数をできるだけ少なくすることを意味する。
たとえば、例外、ジェネレータ、スレッドローカルストア (TLS) は基本的な構成要素ではない。そのかわり、例外、ジェネレータ、TLSのようなストアは、いずれも限定継続を基本的な構成要素として構築される。
1.2. 関数は値である¶
関数 は第一級の値である。
: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]
メソッド は、値の変数に格納された関数である。
: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. オブジェクトは単に変数の束である¶
Kinkのオブジェクトシステムには、クラスベース言語のようなクラス/インスタンス関係や、プロトタイプベース言語のようなプロトタイプ/子関係がない。つまり、継承や委譲は存在しない。オブジェクト、つまり値は、単に変数の束である。
この設計は、継承や委譲とは、最適化のための単なる重複排除に過ぎない、という観察にもとづいている。ランタイムがバックエンドで重複排除するのであれば、それが言語仕様に表れる必要はない。
データとメソッドを属性として持つ値を作るには、単に new_val を呼ぶ。
: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
見ての通り、balanceメソッドとdepositメソッドは、XとYとで共通している。クラスベース言語で、メソッドをオブジェクト間で共有するためには、クラスが使われる。これに対してKinkでは、メソッドはベクタに格納されて、new_valの引数として展開される。このようなベクタはトレイトと呼ばれる。
: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. 関数呼び出しによる代入¶
変数 の代入は 変数参照 値の op_store メソッドによって行われる。
:V <- new_val
V:Num <- 42
stdout.print_line(V.Num.repr) # => 42
上のプログラムは次と同等だ。
:V.op_store(new_val)
V:Num.op_store(42)
stdout.print_line(V.Num.repr) # => 42
したがって、Kinkの文法には左辺値のようなものはない。
1.5. 関数呼び出しによる局所的な制御¶
局所的な制御構造は関数によって実装される。たとえばif-then-elseは、事前ロード済み関数である if で提供される。
:abs <- {(:N)
if(N >= 0
{ N }
{ -N }
)
}
stdout.print_line(abs(42).repr) # => 42
stdout.print_line(abs(-42).repr) # => 42
vector の要素のループは、fold などのメソッドで行える。
:Sum <- [1 2 3 4 5 6 7 8 9 10].fold(0){(:Accum :N)
Accum + N
}
stdout.print_line(Sum.repr) # => 55
一般的に、ループは再帰的な 末尾呼び出し によって実装できる。これは、連続する末尾呼び出しがスタックオーバーフローを起こさないことが保証されているからだ。 末尾呼び出し除去 を見よ。
# 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. 限定継続による非局所的な制御¶
非局所的な制御構造、たとえば 例外 、 try-finally, ジェネレータ などはプリミティブではなく、 限定継続 のshift/reset操作によって実装される。
限定継続を実用的に使えるようにするためには、局所変数の束縛はデフォルトで事実上不変でなければならない。 let節 が導入されたのはこのためだ。
shift/resetを使って、引数から三要素のタプルを作るカリー化された関数を作ってみよう。
: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はlet節を使っている。 foo('XXX') の呼び出しがfoo_barのふるまいを変えていないことが分かる。
一方で、triple_brokenは Varref.op_store によって束縛を変更している。このため、foo_barの結果は foo('XXX') の前後で変わってしまう。