いつもお世話になっております。わいとん(@ytnobody) です。
新年も25日が経過し、月日の流れの速さを感じるこの頃です。
さて実は2~3日前くらいからCalcium という言語を作り始めておりまして、今回はその紹介をしておこうということで筆をとった次第です。
Calciumという言語の特徴
ではCalciumの特徴をばばっと列挙しましょう。
100%AIドリブン開発
Goで作られている
バイトコードへのコンパイル
動的型付け・関数型
イミュータブル
制約という仕組み
副作用の可視化
制御構文は基本matchとmapだけ
関数の制限
イベントドリブンな待ち受け構文
100%AIドリブン開発 Calciumの開発は100%Claude Opus 4.5で行われております。というか、AI以外にコミットさせるケースはほぼないです。今後もほぼ100%AIによってコードが書かれることになります。
これには大変重要な理由があり、もちろんAIもミスをしますが、人間である私はさらにしょっちゅうミスをするから、というものです。人間の中でも私は極めておっちょこちょいですから(注意力3万しかない)、これを信用仕切るのはリスクが大きすぎる。それならまだAIのほうが信用できるというものですし、AIのミスを私が指摘するということもすでに行われていますので(当然逆のほうが多いけど)、昭和的な言い方をするなら、常にダブルチェック体制で開発されているということになります。
あと、AIのほうがコードを素早く作成します。私のように40代半ばの体力が衰えているおっさんなどよりもはるかに素早い。なので、自分の役割はAIに仕様を伝えて設計の伴走をするということに徹しております。
Goで作られている コンパイラ、トークナイザー、パーサー、レクサー、などなど…いま疲れてるのでアルファベットを打ち込むのが面倒でついついカタカナで書いちゃいましたが、これらのものをすべてGo言語で実装してあります。単純にいろんなOSやCPUアーキテクチャに対応させるうえで、Goは手早く開発できるという経験則があります。
また、AIにとっても比較的書きやすく、学習量も相応にあります。成果物も単体バイナリとなりますから、配布する上で非常に好都合。実行速度も比較的軽快な傾向にありますし、プログラミング言語を作るうえで大変実用的な言語だなあと。私にとってはそれがGoだったのです。
バイトコードへのコンパイル Calciumは専用VM上で動作する言語として開発をはじめました。そのため、ソースコードをバイトコードへコンパイルする機能があります。
一応REPLもありますし、その気になれば単一実行バイナリを生成することも可能です。
バイトコードへのコンパイル時に、実行速度やコンパイル後の重複コードの最適化も行われます。そのため、一見ムダが大きそうな以下のような処理もほどほど悪くない速度で動作するようになる、と思われます。
1 2 3 4 5 6 7 8 9 use core.io! ;func add(x, y) = x + y; func my_calc(x, y) = add(x, y) * add(x, y); [2 , 3 ]... |> my_calc !> io.say;
動的型付け・関数型 Calciumは関数型言語でして、いっぽう今さらながら動的型付け言語です。そう、Elixirなんかと同じですね。
でも動的型付けにしたのには私なりに考えがあるんですよ。私、普段はGoやTypeScriptをAIに書かせることがほとんどなんです。しかしですね…PerlやRubyなんかも気まぐれにAIに書かせてみると、これが予想以上にスルスルと意図した通りのコードを生成してくれる。おまけにトークン消費量がTypeScriptやGoなんかよりもだいぶ少ないんですよ。
これは私の体感によるものかなあと思っていたのですが、実際のところ大マジメに検証した人がいるらしく、その結果が以下のポストにチャートとして表わされていました。
ということで、AIフレンドリーにしたいなあ、という欲もありましてこのような選択をしたわけです。
イミュータブル 変数への再代入も厳に禁じました。関数型言語にした理由とも被るところがありますが、やはりAIフレンドリーにしたいという点もありますし、そもそも状態をプログラムの責務として偏在させたくないという思いがあります。
なお、どうしても状態管理(というかイベントドリブンな待ち受け)が必要なシーンではどうするべきか、というのは後で紹介する機能 async.stay でカバーさせることにしました。
制約という仕組み Calcium言語は動的型付け言語ですから、無対策では関数にどんな値が入ってくるのかわからず、コンパイルする利点がだいぶ薄れてしまいます。
ところが変数への再代入を禁じた上でこの「制約」という仕組みを利用することで、型とは違う方式で安全性を担保しつつ、コンパイル時にも最適化が働くといううまみを得ることを狙いました。
1 2 3 4 5 6 7 8 9 10 use core.io! ;constraint Positive(n) = n > 0 ; 100 |> Positive? !? { success(v) => "OK: ${v}" !> io.say failure(e) => "NG: ${e}" !> io.say };
なんだよ、ただの名前付きバリデーションじゃないか、と思ったそこのあなた!実に察しがよろしい、その通りなんです!でも、これなら型パズルでAIトークンを消耗することもないですし、ビジネスロジックをそのまま制約として記述できますよね。
ただ、今のところ関数の引数に対してこの制約を適用する機能は未実装です(実装予定はあります)。
副作用の可視化 この言語の結構大きな特徴のひとつがこれ。副作用 を可視化することで、ビジネスロジックを純粋に保つことが可能となります。
そのための手段として、関数定義自体がfuncとfunc!の2種類ありまして、!が付いている方は副作用があります!という風になっているというわけです。
パイプライン演算子についても|>と!>があり、こちらも!が付いている方は右辺に副作用があることを示します。
さらに、モジュール名にも副作用が含まれる場合には!が付く。さっきから例示しているコードにあるuse core.io!;という行ですが、これにも末尾に!が付いている通り、副作用が含まれるモジュールとなります。だいたい標準出力も副作用ですからね。このあたりはとても厳格です。
1 2 3 4 5 6 7 8 use core.io! ;func add(x, y) = x + y; func my_calc(x, y) = add(x, y) * add(x, y); [2 , 3 ]... |> my_calc !> io.say;
上記の例ではio.sayのところを普通のパイプライン演算子でコールすると以下のように怒られます。
1 2 3 4 5 6 $ ./calcium run ./foo.ca ./foo.ca: error line 8, column 5: |> io.say; ^ cannot use '|>' with effect function; use '!>' instead
制御構文は基本matchとmapだけ この言語の非常にとがった特徴がこのあたりでしょう。if, else, for, while, switch。これらは全部ありません 。
if, else, switchのかわりにmatchがありますから、そちらを使っていただくということで頑張っていくという言語です。
またwhileはasync.stayで代用が可能(なんとコアモジュールに追い出されています!)でして、forのかわりにmapを使ってください、ということです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 use core.io! ;func fizz(n) = match n % 3 0 => "Fizz" _ => "" ; func buzz(n) = match n % 5 0 => "Buzz" _ => "" ; func fizzbuzz_or(result, n) = match result "" => to_string(n) _ => result; func fizzbuzz(n) = fizz(n) + buzz(n) |> fizzbuzz_or(n); "FizzBuzz (1-15):" !> io.say;range(1 , 16 ) |> map(fizzbuzz) !> io.say;
上記の例ではFizzBuzz問題を解いていますが、場合分け処理が必要なfizz,buzz,fizzbuzz_orの3つの関数でifなどではなくmatchがつかわれています。
また、数値配列に対して順番に処理を行うためループ処理を行いますが、forではなくmapをつかっています。
関数の制限 なんと、Calciumでは関数はシングルステートメントに限定されます。つまり!2つ以上の処理をしたければパイプラインを使うか、関数を分けるしかないということになります!
なぜそんな制限を設けているのかというと、マルチステートメントをサポートすることで関数自体の複雑度が爆上げしてしまうので、それをなくしたいという思いからこのようになっています。
パイプラインであれば、どこからが副作用でどこまでが純粋なのか一目で把握できます。ビジネスロジックに副作用が入り込んでいることがわかれば、ビジネスロジックのテスタビリティを向上させるために副作用を分離したくなるというものでして、そのようなリファクタを行いやすくするためなのです。
イベントドリブンな待ち受け構文 言語の基本機能ではなくコアモジュールの機能ではあるのですが、いわゆるwhileループの代わりになるのがasync.stayです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 use core.io! ;use core.schedule! ;use core.async! ;result = async.stay(wait: 5000) { src = schedule.timeout(wait); handler = async.expects(ev => async.leave("completed" ), src); handler.ready() }; result !? { success(v) => io.println("Result: " + v) failure(e) => io.println("Error: " + e) };
上記の例ではasync.stayがwhileループのような役割を行っています。async.stayブロックには引数としてkey: valueの形式で複数の値を状態変数として渡すことが可能です。
なお、ひとつのasync.stayに対して複数のasync.expectsを設定することが可能です。async.leaveがコールされた時点で、特段の指定がなければasync.stayは処理を終了します。
複数のイベントをこの構文で待ち受け可能ということです。
Calciumの現状 はっきり言ってまだ作り始めてから数日ですから、粗もバグもあると思います。しかし、いったん動作させることには成功していると言える状態には持ってきたと思います。
また、エコシステムも現状開発中でして、近いうちにモジュールレジストリboneyardとモジュールマネージャーboneが連携して動作するようになる予定です。
バイトコードへのコンパイルとバイトコードの実行は現時点で概ね動作していますが、外部モジュールのバンドル等についてはまだ現時点では実装ができていません。
まとめ ひとまず、AIに色々指示を出してCalciumを作っている、という状況です。また進展があったらエントリを書くと思います。気長にお待ちいただけますと幸いです。