IOHKブログ:Plutus Tx:HaskellをPlutus Coreにコンパイリング

Cardano用スマートコントラクトアプリケーション作成の核心に迫る

2021年2月2日 Michael Peyton Jones 読了時間9分

Plutus Tx: compiling Haskell into Plutus Core

先週は、リニューアルされたPlutus Playgroundがリリースされました。これは、Plutusプラットフォームのショーケースであり、その中核となるのは1つの高次言語、Haskellでスマートコントラクトアプリケーションが作成できる機能です。

このツールチェーンは、単一のHaskellプログラムにより、ユーザーが自分のコンピューターで実行できる実行ファイルのみでなく、Cardanoブロックチェーンで実行できるコードを作成することを可能にします。これにより、ユーザーは実績のある、上質なプログラミング言語とともに、標準ツールおよびライブラリーサポートを利用することができます。必要に迫られない限り、専用のプログラミング言語と中途半端なツールを習得したいと思う人はいませんよね。

この動力となる技術はPlutus Txと呼ばれますが、これは要はHaskellからPlutus Core(チェーン上で実行される言語)へのコンパイラーであり、GHCプラグインとして提供されます。本稿では、この仕組みと、いくつかの技術的課題を紹介します。

Haskellとは

Haskellとは古い、複雑な言語なのでしょうか。悪名高いことに、これにはこの言語を実にさまざまな方向に変化させる数多くの洗練された拡張機能が備わっています。このすべてをサポートすることが可能なのでしょうか。

幸いなことに、HaskellのプライマリーコンパイラーであるGHCのデザインが、これを可能にしています。GHCは、GHC Coreと呼ばれるHaskellプログラムの非常にシンプルな表現手段を持ちます。最初の型チェック段階後、すべての複雑な表面上の言語はGHC Coreへと脱糖衣され、残りのパイプラインがこれを知る必要はありません。これが、ここでも機能します。GHC Core上で操作でき、数多くの表面上の言語のサポートも無料で得ることができます。

Haskellの別の複雑性として、型システムがあります。これは、避けるのが遥かに難しいものです。しかし、対象言語に使用する型システムを選ぶ自由があるため、ここではHaskellのサブセットを使用します。幸い、Haskellの型システムはかなり優秀です。

結局のところ、Haskellのすべてをサポートしたいわけではないということがわかりました。機能によってはニッチなものや適用不可なもの(誰もブロックチェーンでC FFIなんて必要としません)、あるいは、正直言って、単に実装が大変なものもあります。したがって現時点では、Plutus Txコンパイラーでサポートされていない機能を使用すると、ありがたいことにエラーが表示されます。最も「シンプル」なHaskellがサポートされています(中には一見シンプルに見えても、実際には苛立つほど複雑なものもありますが)。

管に落とし込む

Haskellは何にコンパイルするのでしょう。最終的には、Plutus Coreを生成しなければなりませんが、このように大きなコンパイルパイプラインは「中間言語」か「中間表現(IR)」を導入することにより細分化するというのが、いにしえのコンパイラーの智恵です。これにより、1ステップが大きすぎになるようなことはなく、各ステップは個別にテストすることができます。

コンパイルパイプラインは以下のようになります。

  1. GHC: Haskell → GHC Core
  2. Plutus Txコンパイラー: GHC Core → Plutus IR
  3. Plutus IRコンパイラー:Plutus IR → 型付きPlutus Core
  4. 型イレイサー:型付きPlutus Core → 型なしPlutus Core

ここで見られるとおり、GHC Coreの後にいくつかの段階を経ることになりますが、ここではPlutus IRに注目したいと思います。これは、GHC Coreに近くなるように設計されたPlutus Coreの拡張です。したがって、厳密にいえば、Plutus TxはPlutus Coreではなく、Plutus IRをターゲットにしており、その後に残りのパイプラインを呼び出して、残りの部分を取得します。

こうすることにより、プラグイン自体に存在すべきロジックの量が削減されます。GHCの特異性の処理に焦点を当て、データ型の処理や再帰といった明確に定義された(ただし難しい)問題をPlutus IRコンパイラーに任せて、GHCプラグインを実行せずにテストできるようにします。

パイプラインにPlutus IRがあることにより、別の利点も得られます。GHCがGHC Coreを生成する方法を完全にコントロールすることはできませんが、Plutus IRをPlutus Coreに変える方法は制御下にあります。したがって、ユーザーが自分のオンチェーンコードの完全な再現性を確保したい場合は、Plutus IRを保存して、後でリロードが可能な(比較的)可読性のあるダンプを得ればいいのです。

GHCに忍び込む

まずどのようにして、実際にGHC Coreを得るのでしょうか。GHC CoreはGHCのコンパイルパイプラインの一部です。GHCのコンパイルプロセスで、どうにかPlutus Coreにコンパイルしたプログラムの一部をインターセプトして(念押しになりますが、オンチェーンコードにコンパイルするのはプログラムの一部のみです)、これをコンパイルし、その結果生じたものを使えるものにする必要がありました。

幸いなことに、GHCはGHCプラグインの形でこのために必要なツールを提供しています。GHCプラグインはGHCのコンパイル過程で実行され、GHCがコンパイルするプログラムを好きなように変更することができます。これこそまさに私たちが必要としたものでした。

GHCがコンパイルするプログラムを変更することができ、Plutus Tsコンパイラーのアウトプットを配置するための明確な場所がある、すなわちメインHasukellプログラムに戻すことができるためです。これは適切な場所です。というのも、残りのHaskellプログラムはPlutus Coreスクリプトを含むトランザクションを送信する役割を負っているからです。しかし、残りのプログラムの観点からは、Plutus Coreは不透明であるため、単にトランザクションにそのまま入れられるバイトのプロブとして提供するだけで済みます。

これは、以下のような関数実装を意図していることを示唆しています。

compile :: forall a . a -> CompiledCode a

ユーザーの観点からすると、これは任意のHaskell式を受け取り、この式を表す不透明な値に置き換えますが、Plutus Coreプログラム(またはそのプログラムのシリアル化した表現を含むバイト文字列)にコンパイルします。実際のバージョンはもう少し複雑ですが、コンセプトとしては同じです。

しかしながら、これを通常のHaskell関数として実装することを目指しているわけではありません。通常の「compile」のシグネチャー付きHaskell関数は、a型の値を取り、実行時にこれをPlutus Coreプログラムに変換します。a型の表現式には構文木を取り、これをコンパイル時にPlutus Coreプログラムに変換したいと考えています。

どんでん返し

トリックは以下の通りです。実際には「compile」を関数として実装するわけではありません。代わりに、プラグインはプログラムをトロールして、引数に対する「compile」のアプリケーションを見つけ、アプリケーション全体をコンパイルされたコードに置き換えます。

例えば

compile 1

は次のように変換されます

<bytestring containing the serialized Plutus Core program ‘(con integer 1)’>

つまり、プログラムは総じて型コレクトであり続けます。プラグインを実行する前に、式「compile 1」は型「CompiledCode」を持ちます。その後も同様ですが、その時点では実際のプログラムがあります。

ソースを見つける

コンパイラーはプログラムのソースで動作しますが、Plutus Txでもこれは変わりません。プログラムのGHC Core構文木を処理します。では、プログラムが別のモジュールから関数を呼び出すとどうなるでしょう。Haskellは別々にコンパイルされます。通常、モジュールは別のモジュールの関数の型を見るだけで、オブジェクトコードは後から一緒にリンクされます。したがってソースはありません。

これは、実際には非常に苛立たしいことです。そして長期的には、モジュールのGHC Coreを生成されるインターフェイスファイル内に確実に保存するためのサポートをGHCに実装することを予定しています。これにより、Plutus Txの「独立したコンパイル」のようなことができるようになります。しかしながらそれまでは、「展開」を使用した回避策を取ります。

展開はGHCがモジュール間のインライン化を可能にする関数のコピーです。関数のソースを得る方法として、これらに便乗します。結果的に、Plutus Txにより推移的に使用される関数は、「INLINABLE」とマークする必要があり、これが、展開の存在を保証します。

実行時も重要

ここまではすべて問題ないように聞こえます。ところが、通常、実行時の決定に基づいて異なるバージョンのPlutus Coreプログラムを作成する必要があることを考慮すると話は別になります。アトミックトレードを実装する契約を作成している場合、参加者や金額を変更するためにプログラムを再コンパイルするのはごめんです。

しかし先に述べたとおり、実際に実行時に動作する型「a → CompiledCode a」の関数を書くのは厄介です。Haskellプログラムで式を表現するGHC Core構文木を見るかわりに、プログラムが計算する値を処理する必要があります。

これは、型クラスのペアを定義することで、通常のHaskell風に実行することができます。

  1. Typeable:Haskell型をPlutus Core型に変更する方法を示す
  2. Lift:Haskell値をPlutus Core値に変更する方法を示す

Haskellに精通している人のために、これらは、GHCがより典型的なHaskellメタプログラミングに役立つ表現にHaskellの型と値を変換するために提供する「Typeable」クラスと「Lift」クラスを、意図的に並行させています。

すべての型にこれらの型クラスのインスタンスを書くことはできません。例えば、GHC Coreを見ているとき、「\x → 1」のGHC Coreを調べてそれがラムダであること、ボディが何であるかを見ることができます。しかしコードの実行中に、関数はコンパイルされたネイティブコードのブロブとなり、これが実行できなくなる場合もあります。したがって残念ながら、実行時に関数をリフトすることはできません。

つまり、通常、Integer型や料金表を表す複雑なデータ型など、実行時にデータをリフトすることができます。次にリフトされたデータをコンパイル時にコンパイルした関数にちょっとしたヘルパーを使用して渡すことができます。 applyCode :: CompiledCode (a -> b) -> CompiledCode a -> CompiledCode b.

これは、関数アーキテクチャーの優れた例であり、コンパイル時と実行時の間のこれらの厄介な依存関係を単純な関数と引数で処理できます。

邪魔を取り去る

Plutus Txの目標は、自由にHaskellを書き、オンチェーンコードとオフチェーンコードの両方をシームレスに使用できるようにすることです。これまでこの目標に向かい大きく前進してきましたが、今後も残りのこぶを均しながら進んでいきたいと思っています。

追記:SHOW ME THE MONEY!

実際にPlutus Txはどのように使うことができるのでしょう。このコンパイラーはGoguen機能をサポートするCardano更新の一環として、Plutus Foundationとともにリリースされます。これには、Cardanoチェーン上のPlutus Coreサポートも含まれます。この時点で、ライブラリーを公開します。それまでは、Githubで私たちを応援し、新しいPlutus Playgroundの使い心地をお知らせください。