SCamlによるTezosプログラミング#0

SCaml という Tezos ブロックチェーン のためのスマートコントラクト記述言語とコンパイラを作って元旦にリリースしました。

このブログではSCamlを使ったTezosスマートコントラクトプログラミングについて少しずつ日本語で連載をしていきたいと思います。

初回はプログラミングではなく、SCamlの基本設計コンセプトについてです。一部、プログラミング言語設計についての知識がなければ理解できないところがありますが、読み飛ばされて構いません。

SCaml: 極小コストでそこそこ良いものを

SCamlが目指すものは単純明快で、

  • 言語開発者と言語ユーザー両者の開発、ラーニングコストを極小に保ちながら
  • そこそこ品質の良い安全なスマートコントラクトを記述できプログラミング言語

を提供することです。

DSLはコストが高い

スマートコントラクト用の言語というと Ethereum の Solidity をはじめとして、その環境専用の DSL が多い。これは言語開発者にとっては新しい言語を作るということになり、ユーザーにとっては新しい言語を学ぶということです。

沢山の機能を盛り込んだ新しい言語を作るのはプログラミング言語研究者にとっては夢の仕事ではありますが、 だからといって彼らが完成度の高い言語をゼロからサッと作れるかと言うと、全くそうではありません。 PoCレベルは簡単でも、工業レベルで使えるモノを作るには多大な労力が必要です。

ユーザーにとっては新しい言語には学習コストがかかります。このコストを下げるために多くの DSL では 既存言語に似た外見を用意します。Solidity は JavaScript に似ている、といったように。 それでも学習コストはかかる。 そしてDSLはその名の通りDomain Specificなので、せっかく学習してもそのドメイン外ではまず使えません。

言語開発者とユーザーとの間を繋ぐものが言語のリファレンスやコンパイラのマニュアルですが、 これにかかるコストもバカにできません。DSLだと一から書かねばならない。

既存の汎用言語でスマートコントラクトを

DSLの開発コスト、学習コストを下げるためには、既存の汎用プログラミング言語をそのまま使ってスマートコントラクトが書けるのが一番です。

言語開発者にとっては既存言語処理系コードを可能な限り利用可能することで、主な仕事をスマートコントラクト実行系へのコンパイラバックエンドに限ることができ、限られたリソースでも高品質な言語処理系を作ることが可能になります。(高品質なものを作る能力があればね😜)

ユーザーにとっては既存言語を知っている人はほぼゼロコストでスマートコントラクトが書けるようになり、知らない人も既刊の入門書やチュートリアルを利用して言語を比較的低コストに学ぶことができます。DSLではなく汎用ですから、スマートコントラクト開発以外にも習得した知識を使うことができます。

また、完全に既存の汎用言語と同じものを採用すれば、その言語のエコシステムツール(インデント、Linting、IDEなど)を変更なしにそのまま使えるという利点もあります。

SCamlがベースとしてOCamlを選ぶ理由

SCamlはその名前が示す通り、OCamlをベースにしており、OCamlの 完全なサブセット になっています。OCaml の Strict Subset なので SCaml。

既存の汎用プログラミング言語の中からブロックチェーンでのスマートコントラクト記述言語として何かを採用する場合、いくつかの選択基準があります:

  • 記述力
  • 計算モデル
  • 安全性
  • ポピュラリティ

スマートコントラクトプログラミングの場合、既存ライブラリの豊富さはあまり重要ではありません。既存のライブラリをそのままスマートコントラクトで使うということはあまりないからです。例えば、HTMLのパージングのライブラリをスマートコントラクトで使うということはありません。Tezosの場合それはより顕著です。World computerを標榜するEthereumと違ってprogrammable moneyを目指すTezosではスマートコントラクト内で複雑な計算をすることは推奨されていません。複雑な計算はチェーン外で(平方根をニュートン法で求め)、その結果の検証をチェーン上で行う(自乗して元の値と比べる)のが理想のスマートコントラクトでのプログラミングとなります。

記述力

型付き関数型言語の代数データ型とパターンマッチは文句なしに非常に強力な記述力を提供します。

オブジェクト指向系言語ではクラスとメソッドがその代替となりますが、巨大なライブラリを形成することがないスマートコントラクトの世界(少なくともTezosではそう)では継承の魅力は大きいとは言えません。代数データ型と比べるとどうしても冗長さが目立つことになります。

計算モデル

計算モデルについては、スマートコントラクト実行環境と適合した物を持つ汎用プログラミング言語が適しています。Tezosのスマートコントラクトには静的型付き純粋関数型スタックVMであるMichelsonが使われています。これにコンパイルされる高級言語も同じ性質を持っている方が、高級言語とVMでのプログラム意味論の類推の簡単さや、コンパイラ記述の書きやすさに大きな利点があります。

この点で論理型や、アクターモデルは排除されます(さようなら Prolog, Erlang)。 入出力のないスマートコントラクトでは、命令型言語とはいえSSA変換で十分純粋関数型に変換可能なので、命令型であることはさほど大きいデメリットではありません。

純粋関数型といってもHaskellのようなnon strictな計算モデルを持つ言語は、その意味論をそのままスタックVMに移すのは手間がかかります。StrictなHaskellは、もうそれはHaskellではないDSLになっちゃいますね。 OCamlは副作用に関する言語要素を抜けば簡単にstrictな純粋関数型言語になります。もちろんその多くのライブラリが副作用を使用していますが、上述の通りスマートコントラクトではライブラリはあまり重要ではありません。

安全性

パブリック・ブロックチェーンは換金性のあるトークンを扱わざるをえません(このことについては、私の発表でよく触れていますが、いつか小文を書いた方が良いとは思っています)。結果、その上で動くスマートコントラクトも換金性のあるトークンを操作することになります。スマートコントラクトのバグは、DAO事件などを見れば明らかなように、巨額の経済被害に直結します。

シリアスなスマートコントラクトを書くのであれば、安全性に優れた、バグの発生しづらいプログラミング言語を選ぶ必要があります。汎用プログラミング言語中では、静的型付けを持った関数型言語が比較的そういった性質を持っています。型がなければ人類は実行時型エラーの起こらないプログラムを書くことはできないし、副作用ばかりであればプログラム挙動の解析、理解は形式的にも非形式的にも難しくなります。

もちろん、静的型付けを持った関数型言語を使ったからといって、バグが完全に無くなる、などというオメデタイ話はありません。やはりバグは発生します。そこで、安全よりの汎用プログラミング言語を選択した上で、完璧な安全性を保証したければプログラム検証技術を適用することが考えられます。静的型付けのある関数型言語とプログラム検証との相性は良く、特にOCamlはCoq定理証明支援器をはじめとする検証技術の環境には最も恵まれている言語の一つです。SCaml本体はこのようなプログラム検証技術を組み込みませんが、SCamlのプログラムを検証するための道具立てはすでに揃っていると言えます。

ポピュラリティ

汎用プログラミング言語は、一から作るDSLと比べれば、言語リファレンスやチュートリアルなどがすでに存在しているという面で参入障壁が非常に低いという利点があります。が、できれば既に習得しているユーザーがいれば、その障壁さえ無いのですから、それに越したことはありません。

OCamlは広い世間では全くポピュラーな言語とは言えませんが、金融におけるトレーディングシステムや派生商品コントラクト記述言語に使われていたり、Facebook、dockerなど、decentな産業ユーザーはそれなりにいます。 さらに、日本の大学教育はOCamlをやらされた人を毎年安定して供給してくれています。感覚でモノを言いますが50人/年はいるのではないでしょうか。これらの人々はコンピュータサイエンス系の学部に所属していることが多く、プログラミングに対する能力は非常に高いと期待できます。これらの人々の大半は単位さえ取ってしまえばOCamlなどその後の人生で触れることもないでしょうが、幾人かは目の前に金を積めばOCamlを使ってくれる人もいるのではないか。

そんな人数ではポピュラリティもクソもないという方もおられるかもしれませんが、我々は全人類が有価値トークンを扱うスマートコントラクトを書くべきとは思っていない。 コンピュータサイエンスを学び、プログラムの意味論に親しんだことのある人々の中ではOCamlは十分にポピュラリティのある言語です。

だからOCaml

以上の観点からOCamlを選ぶのは正解(もしくは正解に限りなく近いと結論しないと話がはじまらない)なのです。

C系(C++/C#)のポインタ、リファレンスは必要ありません。 型の無いスクリプト言語で手軽にスマートコントラクトを書いてバグらせたくはない。 Scalaはクラス部分が余分。手続き型言語はプログラムの意味解析が人間、機械共に難しい。 Erlangのような並列計算への利便性は必要ない。 Haskellは遅延評価が相性が悪い。SMLは形式的検証にはIsabelleがあるが、OCaml x Coqと比べると人気ではどうしても劣る。Rustが提供するポインタ安全性やリソース安全性は純粋関数型スタックVMでの実行モデルにおいては価値がない。

OCamlはマルチコア対応が遅れているが、スマートコントラクトはマルチスレッド概念は不要。OCamlを使っている人々でその文法が好きという人も珍しいが、CamlP4やPPXなどで強化可能だし、いざとなれば文法を全く差し替えることも(ReasonMLCamleopardを参照)できる。Haskellが好きならHaskellそっくりの文法を持ったOCamlを作ることは全く可能です。ただ、そんなことをする酔狂な人が過去にいなかっただけですね(私に十分金を積んでくれれば一ヶ月で作ってあげます)。

さらにOCamlではコンパイラフレームワークがライブラリとして提供されており、これを最大限利用することで、パーサと型検査器は変更無しでタダで(ライセンスに同意できれば)利用できます。さらに強力なアノテーションの機能がついているので、元言語の文法仕様を変えずに追加言語仕様をアノテーションを使うことで実現することが可能。ここに下位言語へのバックエンドを追加しさえすればそれだけでスマートコントラクト用言語ができてしまう、そこまでお膳立てが出来ているのです。

OCamlを選ばなくて何を選ぶのか?

SCaml の基本設計

以上の根拠でOCamlのコンパイラフレームワークを利用できるだけ利用したTezosブロックチェーンのスマートコントラクト記述言語がSCamlです。初めてのバージョンは3000行、5日で作れました。そこにパターンマッチとユーザー定義型を追加するのに10日。SCamlは半月で完成させたことになります。

OCamlによるパースと型検査

SCamlのプログラムはまずOCamlのパーサと型検査器に処理されます。OCamlプログラムとして問題があればOCamlコンパイラと同じエラーメッセージで報告されます。OCamlのエラーが読みにくい、という方は自作言語を作ってそのエラーメッセージのお粗末さに絶望したことのない人間だ。

OCaml型付きASTからSCamlの中間言語への変換

OCamlのコンパイラフレームワークによって検査されたプログラムはOCamlの型付きASTからSCamlの中間言語"IML"のASTに変換されます。IMLは単相型付き純粋関数型。それに適合しない機能を使っているOCamlプログラムはここで拒否されます。つまり、

  • 多相型
  • 再帰
  • 副作用
  • モジュール/ファンクタ
  • クラス/オブジェクト

の使用は全て弾かれます。

多相型を弾くのは下位言語Michelsonが単相型の言語だからです。多相関数が書けなくなりますが、プログラムが小規模でライブラリも重要性がないというスマートコントラクトの特殊な環境ではそうデメリットではありません。(将来多相関数をサポートし、各使用に応じて関数定義を複数回具体化する計画はあります。)再帰を弾くのもやはりMichelsonが一般の再帰が記述できないようになっているためです。(Michelsonにはリストなどの再帰型データに関する操作プリミティブやループのためのプリミティブがあるので、それでもチューリング完全な言語です。これらのプリミティブを呼び出す関数はSCamlには用意されています。)

パターンマッチとユーザー定義型はIMLには存在しないため、この変換にはパターンマッチコンパイルとユーザー定義型のプリミティブ型への変換も含まれています(ここに10日かかった)。

IMLは単相型付き純粋関数型言語です。MichelsonスタックVMオプコードへのコンパイルは、オーソドックスなλ計算からスタックマシンへのコンパイルで、特筆すべきことはありません。 初期のMichelsonでは型付きのくせにクロージャーが無かったため、クロージャー変換が大変でしたが(クロージャー変換を知っていて、この難しさがわからない人は型付きのスタックマシンで考えてみると良いです)、MichelsonのBabylonアップグレードによりクロージャーが下位言語レベルでサポートされたので、苦労なくできるようになりました。(それ以前に頑張った2日分の自由変数を追跡するrow typingとそれを使ったクロージャー変換の労力は無駄になりました。)

驚き最小の原則と、唯一の驚き

SCamlはOCamlからMichelsonへの最も自然なコンパイル方法を選択しています。OCamlの様々な機能がSCamlでは制限されてはいますが、これらの機能はサポートされていないことを明示的に表示して拒否されるので、(OCamlプログラムとしてのエラーの意味がわからない、という初心者を除けば)謎のエラーを貰うことはありません。「驚き最小の原則」をかなり実現できているはずです。

ただ、唯一の驚きがあります。contract, operation, big_mapの型を持つ変数は関数中の自由変数として出現することができません。このようなコードを書いてしまった場合はコードの再設計を迫られます。これは、Michelsonのクロージャの環境部分にこれらの「unpackableな型」を持つ値を載せることができない制限のためです。これは高級言語側ではシステマチックに回避することは恐らくできません。具体的には次のようなoperation listを作っていくようなList.fold_leftの使用は拒否されます:

(* これはSCamlに拒否される *)
List.fold_left (fun ops -> fun kh -> 
  Contract.transfer_token () (Tz 1.0) (Contract.implicit_account kh) :: ops) 
  [] khs

ここで opsoperaion list という型を持っている変数ですが、これは List.fold_leftに 与えられた高階関数の内側の関数抽象 fun kh -> .. の内部で自由に出現しています。そのため拒否されます。

これを避けるために、SCamlでは(他のTezosの高級言語でも同じような対策が取られています)、 Curry化された高階関数を取らずUncurry化された関数を取るList.fold_left'を用意しています。 次の例では2番目の関数抽象がUncurry化により1番目と一緒になったことでopsの自由出現の問題が解消されます:

(* これはSCamlに拒否されない *)
List.fold_left' (fun (ops, kh) -> 
  Contract.transfer_token () (Tz 1.0) (Contract.implicit_account kh) :: ops) 
  [] khs

これがOCamlプログラマにとってのSCaml仕様上でのほぼ唯一の驚きだと思います。 (SCamlのバグによる驚きについては、そりゃあるでしょう。なくなるように努力していますが。)

Tezosの他のOCaml系言語との比較

TezosにはLiquidityとCamLigoというやはりOCamlに似た高級言語があります。

Liquidityの制作者はTezosと揉めた経緯があり、今は独自のブロックチェーンをやっています。 Liquidityは今でもTezosに使えることになっていますが、様々なリスクが無視できないため個人的には使わないことをお勧めしています。

この技術外のリスクを除くと、LiquidityとCamLigoはOCamlのコンパイラフレームワークをあまり使わずに作られた言語で、スマートコントラクト用の独自機能や検証機構が追加されており、OCamlと非互換な部分があることが共通しています。一方、後発であるSCamlは 厳密なOCamlの機能縮小版であり続けること を目標としています。SCamlには他のに言語のようなスマートコントラクトのための便利な機能はありませんが、その分、OCamlのコンパイラと同じ使用感とそのエコシステムツールがほぼそのまま使えるという利点があります。

SCamlにもOCamlを超えたスマートコントラクト用言語仕様や、検証機構を備えようという計画(というか夢想)はありますが、それはSCaml本体に施されることはなく、SCamlを取り巻くツールとして実装されるべきだと考えています。SCaml自体は愚直にOCamlコンパチブルな言語であった方が、言語開発者としての責任領域を下手に広げずに済みます。OCamlという既知の言語との仕様の共通性を担保することは、SCamlを対象とした新機能を作ろうと思っていただける第三者の参入障壁を下げるという点でも重要なことだと思っています。Let’s Stay Simple and Stupidraightforward.

今回はこれでおしまい

第0回はSCamlのコンセプトについて説明しました。これから少しずつSCamlによるTezosスマートコントラクトプログラミングについての解説を書いていきたいと思います…が、記事書きで本業を圧迫できないので、あまり丁寧な説明にはなりそうにありません。粗く不親切であっても全部をまず抑える方が良いかと思っています。Tezosブロックチェーンの動作機構については他に譲ります。OCamlのプログラミングについては、SCamlのコンセプト通り、既存のOCamlチュートリアルを読んでくださいな:

あと、Tezos JapanからもSCamlを使ったチュートリアルが出る予定です。「やさしさ」はそちらに任せます。