kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

WORDIAN Advent Calendar 2019/12/22: プログラミング言語Klassicの紹介

まえがき

この記事はWORDIAN advent calenderの22日目の記事です。

以前にこのブログでも触れたことがあるのですが、私が開発中のプログラミング言語Klassicについて紹介します。

Klassicの紹介

リポジトリこちらScala環境があれば、ビルド&実行できますが、まだまだ挙動変わりまくってますので、試してみたいチャレンジャーな方々以外にはお勧めしません。

さて、Klassicです。そもそも、この言語、旧称Toysといって、探偵オペラミルキーホームズシリーズ[^milky] にハマってた頃に、そこから名前を取って作った言語だったりします。当時のリポジトリを見ると、二期が黒歴史にならなければいいとかいうのがサンプルコードに書いてあったりして、懐かしいなあと思ったりしました。

それはおいといて、Klassicです。

静的型&型推論を持っている

まずは例をば。

def myFoldLeft(list) = (z) => (f) => {
  if(isEmpty(list)) z else myFoldLeft(tail(list))(f(z, head(list)))(f)
}

これは、foldLeftを実装したコードです。Klassicは私の好みもあって、静的型を持っていますが、さらに、ML系が採用しているHindley-Milner型推論を実装しています。上のコードは型宣言が一切ありませんが、次のように型が推論されます。

def myFoldLeft<A, B>(list: List<A>) = (z: <B>) => (f: (B, A) => B) => B

ML系やHaskellとかだと当たり前のようにやってくれることですね。ただ、これまで、ちゃんとした型推論を自作言語に入れたことがなかったので、その実験も兼ねて入れてみました。Algorithm Wの変種っぽいもので、適当に単一化して、ごにょごにょやってます。

def add(x, y) = x + y

みたいなのの処理はどうするか悩んだのですが、Klassicにはまだ型クラスないし、ということで、型注釈がない場合+(Int, Int) => Intだと決め打ちすることにしました(ML風)。+が実は浮動小数点数にも使えるけど、辺りもML風。

OCamlにあるレコード多相ぽいもの、というか、いわゆるrow polymorphism的なものも実装してみました。

record P {
  x: Int
  y: Int
  z: Int
}
record Q {
  x: Double
  y: Double
  z: Double
}

record T <'a, 'b> {
  x: 'a
  y: 'b
}

def add_xy(o) = {
  o.x + o.y
}
assert(3 == add_xy(#P(1, 2, 3)))
assert(3 == add_xy(#T(1, 2)))

ここで、

add_xy: {o: { x: Int, y: Int, ...}): Int

て感じに推論されるのが味噌で、必要なメンバを持っていれば、継承とか関係なしに渡せます。これは、いわゆる構造的部分型とは異なる点に注意が必要です(多相型を用いて、サブタイピングのニーズの多くを満たす方式の一つが、row polymorphismだと私は認識してますが、この辺は識者からのツッコミをお待ちしています)。

こんな感じで、Klassicは、今のところ型注釈なしにだいたいのプログラムがうまく型付けされます(健全かどうかは証明できてないですが、健全だと思いたい)。

スペース&行センシティブなリテラル

これは、他の言語にはあまり見られない特徴かなーと思います。記憶が確かなら、たぶん、Fortress辺りに影響を受けて入れたものです。

たとえば、配列(リスト)のリテラルを表記するのに、

[1, 2, 3, 4, 5]

のように書くのは一般的ですが、ここでカンマが必要なのって、これってぶっちゃけパーザの都合ですよね。ちょっとトリッキーなパーザ作ってやればスペースのみで行けるんじゃね?と思って、パーザをごりごり書いてみたら、

[1 2 3 4 5]

という風に、カンマなしスペース区切りでいけるようになった、という話です。カンマを入れてもパーズしてくれますし、スペースの代わりに改行で要素を区切ることもできます。これ、どういうときに嬉しいかというと、たとえば、表をリテラルで表記したいときに、

val tbl = [
   [1 2 3]
   [4 5 6]
   [7 8 9]
] // tbl: List<List<Int>> と推論

という感じで、より表っぽく書けるのが利点かなと思います。なお、二次元配列のときには、レイアウトから決め打ちで要素をパーズできるようにしようかと考えたことがありましたが、その方式だと二次元だけ特別扱いになってビミョーなので、止めました。マップ、セット辺りも似たような感じで書けます。

val map = %[
  "k1" : "v1"
  "k2" : "v2" 
] // map: Map<String, String> と推論
val set = %(
    1 2
    3
) // set: Set<Int> と推論

リテラルの中に複雑な式を埋め込むことも(当然)できますが、割愛。

Java FFI

Klassicは現在、Scalaで実装されていますが、そのおかげで、簡単にJavaコードを呼び出せる機能を作ることができました。たとえば、以下のプログラムを評価すると、"F"になります。

"Foo"->substring(0, 1)

Javaの型をどうKlassicの型にマッピングするか悩みどころなのですが、今のところいわゆるプリミティブだけ特殊扱いで、それ以外のJavaの参照は*って特殊な型にしています。あんまりJVMべったりにしたくはないので、色々考え中です。

プレースホルダ構文

これは、思いっきりScalaのそれに影響を受けたものなのですが、

val xs = [1 2 3]
map(xs)(_ + 1)

て書くと、

val xs = [1 2 3]
map(xs)((x) => x + 1)

と展開されます(展開ルールが抽象構文依存な辺りもScalaの影響が強い)。

ちなみに、これは思っていなかったのですが、強力な型推論プレースホルダが組み合わさったことによって、Scalaでなんとなくプレースホルダ使っても型が付かなかった式にもうまく型がつくので、結構使い勝手がいいのでは、と自画自賛しています。

その他

Klassicはある意味で、私にとっての言語の実験場みたいなものなので、思いついた機能を次の日に実装してたりすることがよくあります。プレースホルダ構文もあ、Scala-thonで1日で作ったものだったりしますし。Lispは言語を拡張できるとはいえど、言語の見た目まで変えるにはリーダーマクロを持ち出さなきゃいけないわけで、言語の見た目を色々好き勝手にカスタマイズする実験をしたい私としては、手元にこういう実験のベースとなる言語があるのが性に合ってるなと思います。

今後の予定

Klassicは、0.1.0-alpha て形で、時々、区切りのいいところで、リリースをしています。ですが、masterブランチとはかなり乖離が激しいですし、今使ってもらうのは微妙な感じです(masterのビルドしてもらった方が早いかも)。この辺は、お手軽に試せるように環境(たとえば、Scala.jsを使ってブラウザ上で処理系が動くようにあトランスパイルするとか)を整えることを考えています。あと、現在のKlassicはインタプリタなのでめちゃ遅いですが、そろそろバイトコード出力とかLLVM IR出力とかやりたいところです。

現在、既に実用的に使うために最低限の言語機能は揃っているので、処理系のクオリティアップとかREPLサポートとかもやっていきたい感じです。もし興味があればお試しいただけると幸いです(感想も送ってくれるとさらに励みになります)。