🎉

alphaリリース前の新言語Hyloを試してみてmutable value semanticsを知った記録

2023/12/24に公開

Hyloについて書こうと思った経緯

zreactorです。普段からMLops、データエンジニアリング、サーバサイド周りのエンジニアをやっています。(自然言語は弱く日本語が怪しい部分があるかもしれません。。)

今の時代で当たり前なことだけど、StackoverflowやGithubのコード例、そして最近はますますGithub Copilotに頼って日々のコーディングを進めています。こんなツールに助けられる度になんとなく気になるのは、こんなオンラインリソースが豊富ではなかった時代にコードを書いていた人や、今の時代でもあまりドキュメントされていないライブラリーや言語を使っている人はどうやってdebugしているんだろう、ということです。(幸いなことに?残念なことに?自分は日常的にほぼメジャーな言語やライブラリーしか扱っていないので、documentされていないバグや仕様を追求するために長々とソースコードを辿っていくような純粋なエンジニアらしい経験をする機会が少ない。)

久々にそういうマイナーな技術に触れて、仕様書を読み解きながら理解していくという泥臭いdiscovery プロセスをしたくなり、それでAdvent Calendarと言う良い機会がありました。関数型プログラミングのパラダイムが好きなので、せっかくだからその方向性で。

それで、ちょうど試してみたい言語に出会いました。最近出てきたC++の後続的な言語(Zig、Carbonなど)について色々調べながら、その中でもHylo(元Val)と言う言語の世界観が気になり、調べてみると日本語どころか、英語の記事すらまだほとんど出ていなく、ドキュメンテーションも簡易的なものしかないです(そもそも言語としてまだ未完成で、alphaリリースが2024の予定と公式サイトに書かれている)。

これは触ってみるしかないな、と思いました。

Hyloとはどんな言語?

簡単に言うと、mutable value semanticsという概念を中心にした実験的言語です。

関数型プログラミングにも関連が深いvalue-oriented programmingというパラダイムの特徴を活かしながら、更に不要なメモリー割り当てを削減し、機械の物理的な制限を考慮してパーフォマンスを良しなにしてくれるのが狙いだそうです。更に、generic programming を簡単にできるようになっています。

立ち位置として、C++の後任者や代替の言語の一つとして開発されている様子です。

言語の基礎概念であるmutable value semanticsについて少しだけ説明していきます。

まず、value semanticsとは?

簡単にいうと、ポインターなどのreferenceを使わず、あらゆるオブジェクトをprimitive型みたいに、value(値)として扱う考えです。

例えば、Pythonの例で見ていきます。

多くの言語と同じく、Pythonではintなどのprimitive型はvalue semanticsを使っています。以下のように、int1 というintを作って、新しい変数 int2 にアサインしてみると:

int1 = 1
int2 = int1
int2 = 500
print("int2 is:", int2)
print("int1 is:", int1)

出力:

int2 is: 500
int1 is: 1

新しい変数 int2int1 と独立であることが分かります。int2 の値を500に変えても、int1 の値が1のまま。これは、裏では int2int1 へのポインター/referenceではなく、int1 のvalueのコピーで、int1 と違うメモリーアドレスに住んでいるからです。これは、value semanticsのある型の基本的な動き方です。

逆に、Pythonでreference semanticsを使っている型の動きを見ていくと:

list1 = [1, 2, 3]
list2 = list1
list2[0] = 500
print("list2 is:", list2)
print("list1 is:", list1)

出力:

list2 is: [500, 2, 3]
list1 is: [500, 2, 3]

list2 の中身を変えようとすると、list1 の中身まで変更されてしまいました。これは、Pythonでは、listはreference semanticsを使っているからです。list2list1 へのreferenceであり、そしてPythonのリスト型は裏でdynamic arrayとして実装されているため、各要素がそのデータが入っているメモリーアドレスへのreferenceになっています。そのため、さっきの list2[0]list1 の0番indexのメモリーアドレスへのreferenceに過ぎないです。

こういうふうに、Pythonでは、primitive型以外はreference semanticsを使っているので、オブジェクトの値そのものをコピーしたい場合はdeepcopyを使うなりの工夫が必要です。

それと一方で、Hyloでは全体的にvalue semanticsを使っています。型関係なくdeepcopyがdefaultの動作になっています。既存のデータを新しい変数にアサインする時でも、必ずデータのコピーが作られます。これをやることによって、簡単に変数をimmutableなものにできるのです。

では、mutable value semanticsは?

普通(immutable)のvalue semanticsでは、オブジェクトを一回作ったら、あとはそれを変更できません。一部だけ変更したい場合でも、一回丸ごとコピーを作って、そのコピーに変わった部分を反映する、みたいなプロセスが必要。つまり、一から作り直すこと。in-placeで編集できません。

immutability (不変性)からくる安全性と動作の予測の楽さというベネフィットがある一方で、コピーが大量に増えてメモリーをいっぱい使うとか、更にコピーに使う時間も必要なので、不便な部分もある。value semanticsのコアな思想を保ちながら、immutability の部分を少し妥協して利便性を上げられないか、という考えがmutable value semanticsです。

mutable value semanticsの特徴として:

  • in-placeで値を編集できるようにする
  • オブジェクトの間に状態を共有できる

in-placeで編集できるようになることによって、大量のコピーを作ることのメモリーコストを軽減できる。

簡単に言うと、書く側の体験として全てが値のコピーとして動いていて、referenceなどを一切意識しなくて良いが、裏で実はコピーを必要最低限にしか作らないなどの最適化をコンパイラー側で良しなにしてくれる。

そもそも、このパラダイムの何が良いか?

主に以下のベネフィットがある:

  • 関数の予想されていない副作用を削減できるのでruntimeエラーを避けられる
  • コードを読めば何が起きているか追いやすくしている
  • referenceの使い方から発生する開発者エラーや混乱を防ぎ、より安全にする
  • 複雑なライフタイム管理をユーザー側でする必要ない(コンパイラーが良しなにやってくれる)
  • referenceを使わないため、reference countingをする必要なく、GCを使わない

つまり、値xを変更すると、他の値に予測していない変更が行われない。また、他の値への操作によってxが読まれたり書かれたりできない。コンパイルさえ通れば非常に動作が予測しやすいコードになります。

その分、コードの性能はほぼほぼコンパイラーのoptimizerがやってくれる必要があるので、かなりその最適化に依存する様子です。

Hyloの今の状態

現時点、alphaリリース前の状態です。公式ページによると、まだ開発途中で、利用される段階ではないようです。2024に言語のv1.0を出す予定だそうです。

Hylo is under active development and is not ready to be used yet.
Hylo’s implementation is at a very early stage. Key components of the compiler are still to be implemented before a first version of the language can be used.

一つの指標として、VSCodeのextensionでval/Hyloをサポートするものはまだないレベルです。

具体的に、言語機能の実装状況は以下の通り。基本的に、コンパイラーの基本の部分だけ出来ている感じ。

Parsing (100%)
Type checking (50%)
IR lowering (30%)
IR analysis and transformations (30%)
Machine code generation (20%)

まだ利用できない状態でも、実際にどれぐらい動くか気になります。

実際触ってみる

まだ色々動かない前提ですが、実際にコードを触っていきます。

setup

基本的にGithubレポのREADMEの通りにやったら動きました。ちなみに、環境はM2のMacBook AirでOSはVentura 13.2.1を使っています。(VSCodeのdevcontainerを使って開発をするというオプションもあるけれど、この記事では直接mac環境にinstallしてみる。)

手順:

  1. Hyloのコンパイラーのレポをcloneする
    → submoduleをinitするのを忘れずに

  2. コンパイラーをビルドするためのライブラリーを入れる
    まず、Swift環境が入っていることを確認する。

$ swift

Welcome to Swift! みたいなメッセージが表示されていればOK。Xcodeのcommand line toolsを一度入れたことがあれば、その中に入っている認識。

次はllvmを入れます。
Brewでもインストールできます:

$ brew install llvm

pathを通すために、.zshrcなどに以下を追加しました。

.zshrc
export PATH=$PATH:/opt/homebrew/opt/llvm/bin

次はmake-pkgconfig ツールを取ってくる。

$ cd hylo
$ swift package resolve

それでllvmのためのpkg-configファイルを生成します。

$ .build/checkouts/Swifty-LLVM/Tools/make-pkgconfig.sh llvm.pc

出力は以下のような感じです。llvmライブラリーを使うためのmetadataを便利に書き出してくれます。

llvm.pc
Name: LLVM
Description: Low-level Virtual Machine compiler framework
Version: 17.0.6
URL: http://www.llvm.org/
Libs: -L/opt/homebrew/Cellar/llvm/17.0.6/lib -L/opt/homebrew/opt/zstd/lib -lc++ -lLLVM-17
Cflags: -I/opt/homebrew/Cellar/llvm/17.0.6/include

最後に、環境変数を設定します。コンパイラーを使いたいdir(プロジェクトdirなど)から、shellの中から以下の環境変数をexportする。

$ export PKG_CONFIG_PATH=$PWD
  1. コンパイラーをビルド:
$ swift build -c release

.build/release/hcというファイルが作られる。これがHyloのコンパイラーです。

コンパイラーが動いていることをテストする:

$ swift test -c release --parallel

そして、コンパイラーバイナリーをPATH下に置くと便利です。

$ mv .build/release/hc /usr/local/bin/

hello world!

Hello.hyloで、シンプルなhello worldプログラムを作ります。

syntaxハイライトはまだサポートされていないけれど、シンタックスがSwiftに近い感じということで、VSCodeでLanguage Modeを「Swift」にしたらそれっぽくなりました。

docsに従って、プログラムのentrypointになるmain関数を作成する。入力パラメーターも出力値も指定しない。

Hello.hylo
public fun main() {
  print("Hello, World!")
}

さっき作成した hc のバイナリーで以下のようにプログラムをコンパイルして、後に実行してみます。

$ hc Hello.hylo -o hello
$ ./hello
Hello, World!

よしよし。コンパイラーが動いていることを確認できました。

面白い言語機能をピックアップして見ていく

ここからは公式docにある色々なコード例を見て、面白そうなものや代表的なものをピックアップしていく。動かせるものは動かしてみる。

binding

bindingとは一般的に言うと変数の名前とその変数の持つ値の間の関係性のことです。Hyloではさらに変数がmutableなもの、もしくはimmutableなものか、または型をbindingで表現しています。

bindingのmutability/immutability、intやfloatなどの型などの特性は宣言時に決まります。

以下では pi と言うimmutableなbindingを作ってみます。

public fun main() {
    let pi = 3.14159
    &pi = 11.0    // 変数piを別の値に設定しようとする
}

let と言うキーワードで、その変数の値をimmutableなものだ、と宣言します。

そして、&pi = 11.0 で、値を変更しようとする。(多くの言語と違ってHyloでは & はポインター型が使うシンタックスではなく、値をin-placeでmutateすることを意味しています。mutable value semanticsのmutableなとこです。)

そうすると、immutableなオブジェクトなのに変更しようとするな、とコンパイラーに怒られる:

$ hc Main.hylo -o main
Main.hylo:3.5-15: error: illegal mutable access
    &pi = 11.0 
    ~~~~~~~~~~

つまり、let なのにJSの const みたいな感じです。逆に、mutableのものは var で指定する。(JSのletキーワードはむしろmutableなものを指して真逆の意味なので、正直ややこしいと思ったw)

ここまでは他の言語にもよくあるものでしたが、Hyloだと更に inout と言う特殊なbindingがあります。docにあるコードの例を見ていくと、

public fun main() {
  var point = (x: 0.0, y: 1.0)
  inout x = &point.x
  &x = 3.14
  print(point) // (x: 3.14, y: 1.0)
}

xpoint.x へのreferenceみたいに扱っているように見えますが、実はreferenceと少し違っていて、projectionと言う概念を使っています。

projectionとは?

Hyloでmutableなものを作るに当たって重要な概念です。ここでは値の一部(ここではtuple pointのフィールドx)をmutableなものとしてproject (投影)し、それを介して値を編集しています。referenceみたいに自由に使えるものではなく、元々の変数pointの一部のviewみたいになっていて、このプロジェクションからしか編集できなくなっています。投影している値の完全な所有権を持っています。

一つ重要なポイントとして、他の言語と違って、一つのmutableな値を複数のところからアクセスできないようになっています。例えば、同じメモリーアドレスを二つの別々の変数で指すことができず、mutableなものは同時に一つのbindingからしかアクセスできません。

例えば、

public fun main() {
  var point = (x: 0.0, y: 1.0)
  inout x = &point.x
  inout y = &point.x
}

みたいなことができない。

(ちなみに、以上のtupleを使った例は実は、コンパイルされなかった。現時点でtupleは定義できるけれど、メンバーへのアクセスが対応されていない様子。)

Main.hylo:7.21-22: error: type '{x: Int, y: Int}' has no member 'x'
   inout x = &point.x
                    ^

パラメーター引渡しの conventions

関数にパラメーターの渡し方を指定できる書き方です。

let, inout, sink, setと、四つがあって、以下のように関数のパラメータータイプの前に書きます:

fun offset_let(_ v: let Vector2, by delta: let Vector2) -> Vector2 {
  (x: v.x + delta.x, y: v.y + delta.y)
}

let → immutableなものを渡す、by valueで渡す
inout → mutableなものを渡す、呼び出し先によって変更可能
sink → 呼び出し先に所有権を引き渡しながら渡す
set → まだ初期化されていないものを渡し、呼び出し先によって初期化できるようにする

この当たりは色々とまだ実装されていない様子ですが、計画とコード例はdocにあります。

subscripts

ここで言うsubscriptは、オブジェクトの値、もしくは値の一部をyieldで返す純粋高階関数です。関数みたいに値を返すのではなく、一時期呼び出し元に所有権を渡してyieldした値にアクセスできるようにしています。(この概念、関数型プログラミングに出るlensと言うものに近いです。)

こういうふうに書きます。ここではyieldが使われないためminは値を返していない状態です。

subscript min(_ x: Int, _ y: Int): Int {
  if x > y { y } else { x }
}

public fun main() {
  let one = 1
  let two = 2
  print(min[one, two]) // 1
}

値を呼び出し元mainからyieldでアクセスする書き方はこうなります:

subscript min(_ x: Int, _ y: Int): Int {
  print("enter")
  yield if x > y { y } else { x }
  print("leave")
}

public fun main() {
  let one = 1
  let two = 2
  let z = min[one, two] // enter
  print(z)              // 1
                        // leave
}

ちなみに、動かそうとしてみるとコンパイラーに大量に怒られます。型推論などはまだ開発途中だからそれはそうか。。。

(値がconsumeされたと出ているのでlifetime周りは先に実装されていそう。)

Main.hylo:1.17-18: error: parameter was consumed
subscript min(_ x: Int, _ y: Int): Int {
                ^
Main.hylo:2.25-26: note: escape happens here
  if x > y { y } else { x }
                        ^
Main.hylo:1.27-28: error: parameter was consumed
subscript min(_ x: Int, _ y: Int): Int {
                          ^
Main.hylo:2.14-15: note: escape happens here
  if x > y { y } else { x }

並行・並列処理

計画を立てている、段階ですが、実装はまだ先だそうです。

気になったところ

ここは自分個人の疑問やまだよく分かっていない部分をメモします。
今後調べて答えが出たら書き足していくかも。

  1. 同時アクセスから発生するデータ競合状態を起こさない理由

公式ページに以下のように書いてあるけれど、

Hylo’s foundation of mutable value semantics ensures that ordinary code is memory safe, typesafe, and data-race-free.

in-placeで同じオブジェクトのデータを変更できるmutable value semanticsはむしろデータ競合が起きやすいのでは?

現時点シングルスレッド前提の構成なので、複数スレッドがデータにアクセスできないようにしている、と言うのが現時点のソリューションらしい。今後マルチスレッド対応をしていく予定らしいので、それを裏でどう実現するか気になります。

  1. mutable value semanticsで、referenceを使わずinplaceでデータを変更するのはどう実現できているの?

おそらく上記に説明したprojectionやinoutの構造を使っていることによって実現できています。まだ、ここが完全に分かっていない気がする。

  1. 今ある言語でも同じことができるのでは?

確かにRust、C++、Swiftでもできそうな部分が多いです。自分個人として、RustやSwiftは詳しくないので、何をどこまで出来るか把握していない。ここはRustやSwiftを普段書いている人の観点から是非記事を書いて欲しいと思っています。

  1. mutable value semantics自体の良さについて。

せっかく全ての変数をimmutableの値にして安全性と副作用防止を担保しようとしているのに、あえて一部をmutableにすることによって、もともと防ぎたい問題は防げなくなるのでは?と気になったりします。mutable value semanticsの弱点としてデータ競合や予期していない変更があるものらしいので、それを防ぐ仕組みは何?とまだ完全に把握していない。

今のところの理解として、mutabilityを限定されている方法だけで露出し、mutableとimmutableな部分を明示的に定義させることによって、副作用が起きやすいところを意識させているところです。ここは、実際コードを書いて触ってみればより分かってくる気がします。

最後に

関数型プログラミングとその理念が大好きな人間として、参照透過性とimmutabilityの良さがよく響きます。今回Hyloを調べている時に、value-oriented programmingと言う概念が関数型言語以外でもこんな使い方があるんだ、と言った。また、Mutable Value Semanticsと言う概念は、この概念から担保される安全性や読みやすさと、メモリー管理の効率との興味深い妥協だと思いました。

概念を読み解いていくと、関数型プログラミングに似ている部分はピンと来ていたけれど、lifetimeマネージメントなどの概念は若干不慣れがあって、Rustを勉強すればHyloをもっと分かってくるのではないか、と思いました。

手元で動かして色々まだ動かない状態でしたが、Alphaリリースされたらすぐ使って何かを作ってみたいです。特に、自分がたまに書くHaskellと比較をしてみたいです。また、Devcontainerなどの中の開発を試して、現時点でももっと動かせる部分がないか、今後試してみたいです。

何よりも、一つの概念を突き詰めた言語がカッコ良いな、と思いました。純粋な関数型プログラミングならHaskell、mutable value semanticsならHylo、みたいなゴールドスタンダードになってもらいたいですね。

とりあえず2024にくるHyloの発展に期待します!

Discussion