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') の前後で変わってしまう。