このエントリーをはてなブックマークに追加

ここ数年の間,瞬く間に多くのプログラミング言語が登場し,話題を呼んでいます. その中でも特に人々の目を引いたものに, Rust があります. この記事では,そんな Rust を少しだけ紹介します. ここで紹介する Rust の機能は ほんの一部 であり,まだまだ優れた機能を数多く持っています!

今までのプログラミング言語との決定的な違い

多くの人々の目を引くほど話題になるからには,なにか決定的な違いがあるはずです. その一つが, 革新的で安全なメモリ管理システム です.

プログラミング言語とプログラマは,長い間メモリ管理に悩まされてきました. 例えば C では,静的なメモリ空間を確保するために mallocstatic といった機能を使います. これらで確保された空間は非常に柔軟ですが, メモリリークやバグの温床 となります. コンパイルされたプログラムにおいて静的な空間はその管理下にならないからです.

プログラムは,プログラマの記述通りに動作します. 自分で考えることも,動作を変えることもしません. プログラマがミスをすれば,それが確実にプログラムへ反映されます. 先述したようなメモリリークやバグも,もちろんこれに含まれるでしょう.

こういったプログラマのミスによって起きたメモリリークを解決する 根本的ではない 策として,様々なものが登場してきました. その一つが, ガベージコレクタ (GC) です. GC はある条件を満たすタイミングで定期的に実行され,今利用されていないメモリ空間を検索し,開放します. プログラム下にあるメモリ空間すべてを検索するわけですから,当然計算リソースを消費しますし,時間がかかります. また, GC が実行されるまでの間メモリ上に無駄な空間が存在することになります. 比較的そういったリソースに余裕が見えてきた今でこそ,あまり気にされていませんが,無駄なものを排除しない理由はないはずです.

Rust では,このような問題が起きないための様々な言語機能が提供されています. Rust で書かれたプログラム下のメモリ空間は,たとえ静的であっても堅牢に管理されます. GC が動作することもありません. つまり,プログラマがメモリ空間から逃げたことに起因する無駄を 根本から 解決することができます.

普段高レイヤを扱う人々にとって,ここまで述べてきたような話題はあまり興味がないかもしれません, 私もその一人でした. しかし, Rust は低レイヤに限られない言語です. すでに, Web をはじめとした様々な実行環境用のフレームワークがリリースされています. Rust によるメモリ管理は高レイヤでも力を発揮します. 結果として, 高機能で速く,安全なプログラム を作り上げることができるのです!

環境構築

では,さっそく Rust を書くための環境を整えていきましょう. Rust は,環境の構築も非常に簡単です. まずは, Rust のツールチェイン (コンパイラや標準ライブラリなどのセット) を管理するためのツールである rustup をインストールします.

macOS や Linux などの Unix オペレーティングシステムでは以下のコマンドでインストールできます:

curl https://sh.rustup.rs -sSf | sh -s

Windows では以下を使います:

$uri = "https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe"
$destination = "$env:TEMP\rustup-init.exe"
$client = New-Object System.Net.WebClient
$client.DownloadFile($uri, $destination)
. $destination

あとはインストーラ CLI の指示に従うだけです. 詳細なカスタマイズもできますが,とりあえず今はデフォルトでいいでしょう. インストール先に拘りのある方は,適宜調整してください.

rustc --version コマンドが正常に実行できれば,インストールは成功です!

$ rustc --version
rustc 1.46.0-nightly (8aa18cbdc 2020-07-08)

実際にプログラムを記述するためのワークスペースも作成しておきましょう. 以下のコマンドを実行するだけです:

$ cargo new <ワークスペース名>

現在のディレクトリに指定した名前のフォルダが作成され,そこがワークスペースとなります.

$ cd <ワークスペース名>
$ tree .
.
├── Cargo.toml
└── src
    └── main.rs

最初の一歩: 所有権

一般的な言語チュートリアルでは,ここで Hello, world! を標準出力に書き込むことでしょう. その言語の標準ライブラリに定義された関数を呼べばその文字列が出力されるということは誰でもわかるので,ここでは Rust の特徴的な一面から始めます.

Rust では,メモリ空間が必要でなくなった瞬間に開放されます. いつ開放されるかというタイミングが重要です. 以下のコードを考えてみます:

fn main() {
    {
        let s = String::from("Hello");

        println!("{}", s)
    }

    println!("{}", s)
}

お察しの通り,このコードは動作しません. もっと言うと,コンパイルエラーを起こすため実行すらできません. Rust では, スコープを抜けた瞬間 ,そのスコープで定義された変数により束縛されたメモリ空間が開放されます. ここまでは,他の言語と大差はありません.

次に,以下のコードを見てみましょう:

fn main() {
    let s = String::from("Hello");
    
    foo(s);
    println!("main says: {}", s)
}

fn foo(bar: String) {
    println!("foo says: {}", bar)
}

実際にコンパイルしてみると分かりますが,このコードも,実は動作しません. ここで,他のプログラミング言語との違いが見えてきます. s という変数は, main 関数のスコープで定義されたため,この関数が終了するまでは有効なように思えます. しかし, main 関数最後の println! は動作しません. cargo runcargo check コマンドを実行すると,以下のようなエラーが発生します:

error[E0382]: borrow of moved value: `s`
 --> src\main.rs:5:31
  |
2 |     let s = String::from("Hello");
  |         - move occurs because `s` has type `std::string::String`, which does not implement the `Copy` trait
3 | 
4 |     foo(s);
  |         - value moved here
5 |     println!("main says: {}", s)
  |                               ^ value borrowed here after move

foo 関数を呼び出し, s 変数を渡すと,この変数は main 関数のスコープから foo 関数のスコープへと所有権が移動します. Rust では,一度他のスコープへ所有権を渡した変数は,もう自身の所有権がないのでアクセスできません. また,エラーメッセージにもありますが, Copy トレイトを実装した型 (数値型など) ではこの問題が発生しません. 所有権を渡す前に変数に格納された値をコピーするからです.

では,同じ変数を複数回使いたいときはどのようにするのでしょうか. それを解決するための言語機能が存在します. 借用 (borrowing) です. 借用を行うことで,所有権を完全に渡さずに変数 (に束縛されたメモリ空間) へのアクセスを認可できます.

先ほどの例で,借用を使ってみます:

fn main() {
    let s = String::from("Hello");
    
    foo(&s);
    println!("main says: {}", s)
}

fn foo(bar: &String) {
    println!("foo says: {}", bar)
}

このコードは無事に実行できます.

このように, Rust では,所有権という概念を導入することで,メモリ安全性を確保しています. 所有権を持つ所有者は必ず 1 つに保たれるため,メモリの確保から開放までのプロセスがコンパイル時点で明示化されるのです. そして,スコープから外れたタイミングで破棄されることにより無駄なメモリ空間を残すことなくリソースを使用できます.

もう少し先へ: エラー処理と Result<T, E> 型

ここでは少しメモリ管理から離れます. プログラマを悩ますもう一つの要素に,エラー処理が挙げられます. 言語によっては 例外 という呼称を使います. エラーは,プログラムを記述する段階で意図しない動作を起こしたときに発生するものです. プログラマは,エラーが発生したときに場合に応じて復帰したり,エラーメッセージを出して異常終了したりします.

このようなエラー処理をきちんと行っていない場合,プログラムは突然終了します. Java のような VM 言語でない限り,エラーメッセージも出さないことが多いでしょう. 特に,メモリ関連の深刻なエラーでは SIGSEGV (Segmentation Fault) シグナルとともに終了します. しかし,これではあまりにも不親切ですし,不具合の原因特定にも寄与しません.

プログラマは,できるだけエラーが発生しないよう,また外部要因によって仕方なく発生したエラーを適切に処理する必要があります. Rust では,こういったエラー処理も非常に安全かつ柔軟に行うことができます. その際に用いられるのが, Result<T, E> 型と Option<T> 型です.

以下の例を見てみましょう:

use std::fs::File;
use std::io::Read;

fn main() {
    let file = File::open("foobar.txt");  // 変数はデフォルトで変更不可
    let mut bytes: Vec<u8> = Vec::new();  // 変更されるものは mut が必要

    file.unwrap().read_to_end(&mut bytes);
}

現在のディレクトリに foobar.txt存在しない ことを確認してから実行してください. プログラムは以下のようなエラーで異常終了します:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "The system cannot find the file specified." }', src\main.rs:8:10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\RustSandbox.exe` (exit code: 101)

これを パニックする といいます. 元のルーチンに復帰する手段が定義されていないために,終了するしかなくなるのです.

実は, File::open メソッドは File 型ではなく, Result<File, Error> 型を返します. その代わり,多くの言語にある例外という概念は存在しません.

この型は,処理が成功したか失敗したかという情報と,結果またはエラーを持ちます. read_to_end メソッドは結果側の File に存在するので,これを取り出す必要があります. unwrap メソッドを使えば強制的に取り出すことができますが,エラーの場合はそこでパニックします. したがって,上記のコードは動作するものの, Rust ではあまり推奨されません.

以下は親切なエラーメッセージを出力する例です:

use std::error::Error;
use std::fs::File;
use std::io::Read;
use std::process::exit;

fn main() {
    let result = File::open("foobar.txt");
    let mut bytes: Vec<u8> = Vec::new();

    match result {
        Ok(mut file) => {
            println!("Read {} bytes", file.read_to_end(&mut bytes).unwrap());
        },
        Err(error) => {
            println!("Failed to open foobar.txt: {}", error);
            exit(1)
        },
    }
}

実行すると以下のようなエラーが発生して終了します:

Failed to open foobar.txt: The system cannot find the file specified. (os error 2)
error: process didn't exit successfully: `target\debug\RustSandbox.exe` (exit code: 1)

親切なエラーメッセージを出力することができました. ユーザ側から見ても分かりやすく,プログラマ側から見てもデバッグしやすいメッセージが一番良いでしょう.

でも実は,このコードにはまだ危険な箇所が潜んでいます. read_to_end メソッドは,実は正常終了する場合としない場合があります. もしこれがエラーを起こした場合, unwrap メソッドを呼ぶとパニックしてしまいます. ではこれを処理するようにしてみましょう:

use std::error::Error;
use std::fs::File;
use std::io::Read;
use std::process::exit;

fn main() {
    let result = File::open("foobar.txt");
    let mut bytes: Vec<u8> = Vec::new();

    match result {
        Ok(mut file) => {
            let result = file.read_to_end(&mut bytes);

            match result {
                Ok(length) => println!("Read {} bytes", length),
                Err(error) => {
                    println!("Failed to read from foobar.txt: {}", error);
                    exit(1)
                }
            }
        },
        Err(error) => {
            println!("Failed to open foobar.txt: {}", error);
            exit(1)
        },
    }
}

なんと!ネストがとても深くなってしまいました. いくら安全といえど,ネストの深いコードは良いコードではありません. Rust には,こういったことを避けるために, Result<T, E> 型のための便利な実装がたくさんあります.

簡潔にしてみましょう:

use std::fs::File;
use std::io::Read;
use std::process::exit;

fn main() {
    let mut bytes: Vec<u8> = Vec::new();
    let result = File::open("foobar.txt").and_then(|mut f| f.read_to_end(&mut bytes));

    match result {
        Ok(length) => println!("Read {} bytes", length),
        Err(error) => {
            println!("Failed to read from foobar.txt: {}", error);
            exit(1)
        }
    }
}

and_then メソッドは,その Result<T, E> が成功 (Ok) だった場合に与えられた関数を呼び出し,その関数による新しい Result<T, E> を返します. 失敗 (Err) だった場合はそこでその Result<T, E> を返し,与えられた関数は呼び出されません.

プログラム全体で Result<T, E> 型を使うようにしておけば,エラーメッセージを表示する処理は main 関数に書くだけで済みます. エラーの種類ごとにメッセージを変えたければ,プロジェクトの名前空間に独自の Error 型を定義して, Result<T, Error> として使えばよいでしょう.

このように, Rust では,エラー処理でさえも安全に行えるようになっています. もう Segmentation Fault の文字列を見てエラー原因箇所を必死に探すことは (Rust の不具合がない限り) する必要がありません.

深渕へ: ライフタイム

最後に,私が Rust で最も難しいと感じた ライフタイム という概念を紹介します.

序文で述べたように, C などでは mallocstatic といった機能で静的なメモリ空間を確保してきました. しかし, Rust では手動で静的なメモリ空間を自由に確保することができません. すべての変数は所有権を持ち,スコープから抜けた瞬間に開放されます

ライフタイムを使えば,変数が存在するスコープを明示することができます. つまり,より長い時間参照を確保しておけるのです. その時間の長さを指定するのが ライフタイム指定子 です.

以下の例を考えてみましょう:

struct Foo {
    bar: &String,
}

fn main() {
    let s = String::from("Hello");
    let f = Foo { bar: &s };

    println!("{}", f.bar)
}

このコードは動作しません. 以下のようなコンパイルエラーが発生します:

error[E0106]: missing lifetime specifier
 --> src\main.rs:2:10
  |
2 |     bar: &String,
  |          ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 | struct Foo<'a> {
2 |     bar: &'a String,
  |

構造体に参照を保持するには,その構造体が有効な間, 参照先の値がメモリ空間上に存在している必要 があります. 構造体が有効なまま s 変数のメモリ空間が開放されてしまうと, f.bar にアクセスできなくなってしまいます. これではメモリ管理上の安全性を確保できません.

また,ここまでエラーメッセージを見てきてお気づきの方もいるかと思いますが, Rust のコンパイラは非常に丁寧なメッセージを出力します. 上記の例では解決方法も教えてくれるのです.

さて,ライフタイム指定子を書いた例がこちらです:

struct Foo<'a> {
    bar: &'a String,
}

fn main() {
    let s = String::from("Hello");
    let f = Foo { bar: &s };

    println!("{}", f.bar)
}

このコードは正常に動作し,標準出力に Hello を書き込みます. ライフタイム指定子 'a によって, Foo 構造体 (のインスタンス) と bar メンバの参照は同じライフタイムを持ちます. 言い換えると, Foo 構造体が有効な間, bar メンバの参照も有効であることが保証されるようになります.

ライフタイムが正常に機能することを確かめるために,あえて bar メンバの参照,つまり s を無効化してみましょう:

struct Foo<'a> {
    bar: &'a String,
}

fn main() {
    let f: Foo;

    {
        let s = String::from("Hello");
        f = Foo { bar: &s };
    }

    println!("{}", f.bar)
}

このコードは動作しません. 以下のようなコンパイルエラーが発生します:

error[E0597]: `s` does not live long enough
  --> src\main.rs:10:24
   |
10 |         f = Foo { bar: &s };
   |                        ^^ borrowed value does not live long enough
11 |     }
   |     - `s` dropped here while still borrowed
12 | 
13 |     println!("{}", f.bar)
   |                    ----- borrow later used here

スコープを分けることで,中のブロックから抜けたときに s 変数は破棄 (メモリ空間が開放) されます. そして, f に保持した s への参照は有効でなくなります.

このように, Rust では,ライフタイムを導入することによって決まった時間だけメモリ空間を確保した上で,その間は無効な参照が生まれないことを保証できるのです.

まとめ

今回の記事では,あえて簡単な言語事項の説明を省いた上で, Rust の特徴的な機能をいくつか紹介しました.

これを読んで, Rust が難しいと感じた方がいるかもしれません. 確かに,他の言語と違う革新的な要素が多いため,困惑することもあるでしょう. しかし,なぜその機能が存在するのかや,どのように安全性が担保されているかを理解すれば,決して難しい機能たちではないと思います.

速さの代償に難解な言語を書くわけではありません. 安全性の代償に長いエラー処理を書くわけでもありません. Rust は,速くて安全なロジックをいかにプログラマフレンドリな形で記述できるかを目指した言語だと私は考えています.

私自身も, Rust を書き始めてまだ少ししか経っていません. Rust にはまだまだ魅力的な言語機能やベストプラクティスがあると思っています. そして,これからも進化し,使われていく言語だと感じています!


このエントリーをはてなブックマークに追加

ReasonMLは新しい言語ではありません。実は言語でもありません。OCaml の構文です。2016 年に Jordan Walke(React の開発者)に作成されました。OCaml のおかげで TypeScript より丈夫なタイプシステムがあり、JavaScript のような書き方で OCaml を書いたことがない方でもすぐ学べ、NPM・Yarn でもパッケージをインストールできます。ビルド時間が TypeScript より早く、Reason の JS アウトプットは Webpack より読みやすいです。OCaml の上で動いているので、プログラムのスタートアップが素早く、データが不変、ブラウザーの JS だけではなくネーティブコードもターゲットできます。

TypeScript vs ReasonML(英語)

ReasonML は Facebook に応援されています。ReasonML チームがオフィシャル Reason と React のバインドを開発しています。React は最初に作られた時に JS ではなく、スタンダード ML で書かれていました。もし React を書くなら、ReasonML は最高です

では、コードを見ましょう

最初は、簡単な JS を見てみましょうか?

変数

var firstName = "ブランドン";
let middleName = "リー";
const lastName = "ピットマン";

ReasonML だと…

let firstName = "ブランドン"
let middleName = "リー"
let lastName = "ピットマン"

ReasonML ではletしかありません。

記号列

JS と ReasonML の記号列はあまり変わらないので、スキップします。

配列

JS…

const numbers = [1, 2, 3];

ReasonML…

let numbers = [1,2,3] //これは配列ではなく、	連結リストです

// 配列にするなら…
let numbersArray = [|1,2,3|]

// タプルもある
type myName = { name: string }
let otherStuff = ("Brandon", 36, { name: "Brandon" })

配列と連結リストの内容は均質じゃないとだめですが、タプルは不均質でも大丈夫です。

オブジェクト

ReasonML(と OCaml)には関数型プログラミング言語の特徴が多いんですが、OCaml にオブジェクトもあります。けれども、ReasonML のドキュメントを読むと JS の経験者向けではありません。オブジェクトみたいなデータが必要な時に ReasonML のレコードがおすすめです。レコードとは?

type language = Reason | Ocaml | Typescript;

type person = {
	name: string;
	age: number;
	hometown: string;
	favoriteLanguage: language
}

let me = {
	name: "Brandon",
	age: 36,
	hometown: "Cincinnati, OH, USA",
	favoriteLanguage: Reason
}

レコードを定義する前にタイプも定義しなければなりません。TypeScript のタイプの書き方とそんなに変わりませんが、ReasonML のタイプは引数を受けることができます。私の誕生日を定義しましょう。

type threeThings('a) = ('a,'a,'a)
let myBirthdate: threeThings(int) = (1983, 8,31)

/* 上記のように使えますが、あとでそのタイプをもう一回使えます。*/

let fullName: threeThings(string) = ("Brandon", "Lee", "Pittman")

上記の ReasonML こんな JS にアウトプットされます。

// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE
"use strict";

var myBirthdate = /* tuple */ [1983, 8, 31];

var fullName = /* tuple */ ["Brandon", "Lee", "Pittman"];

exports.myBirthdate = myBirthdate;
exports.fullName = fullName;
/* No side effect */

最後に出る JS は意外にきれいです。

if 文

JS と ReasonML のif文の書き方はそんなに変わりませんが、使い方が一つの大きい違いがあります。ReasonML のif文では最後の式がリターン値になります。下記の例を見てください。

let number = 1;

/* Unicodeを使うなら、下記の{j|...|j}を使わないといけない */
let result =
  if (number > 0) {
    {j|ゼロより大きい|j};
  } else {
    {j|1より少ない|j};
  };
/* resultは「ゼロより大きい」になる */

あとif文を書くとelseを絶対に書かないといけません。

Null と undefined

ReasonML にはnull又はundefinedというコンセプトはありません。その2つがないので、JS でよく起こるエラーに襲われません。もしnullみたいの値を使わないといけない時にはどうしましょうか?「パターンマッチング」を使います!ReasonML ではswitchをよく使うんですが、JS のswitchより力強いです。こういう風に使えます。

let author = Some("Brandon");

switch (author) {
| Some("Brandon") => Js.log("Hello there, Brandon.")
| Some(string) => Js.log("Welcome, " ++ string ++ "!")
| None => Js.log({j|We have no author. 😞|j})
};

きれいですね?JS のアウトプットはこんなものです。

var author = "Brandon";

if (author !== undefined) {
  if (author === "Brandon") {
    console.log("Hello there, Brandon.");
  } else {
    console.log("Welcome, " + (author + "!"));
  }
} else {
  console.log("We have no author. 😞");
}

exports.author = author;

関数

ReasonML の関数は ES6 の arrow 関数に近いです。functionというキーワードがありません。ES6 の書き方とほぼ一緒に見えます。

type person = {name: string,  age: int};
let me = {name: "Brandon", age: 36};

let greet = ({name, age}) =>
  Js.log({j|Hello, $name. I see you're $age years old.|j});
/* Unicodeのような書き方で内挿もできます。*/

JS のアウトプットは…

function greet(param) {
  console.log(
    "Hello, " + param.name + ". I see you're " + param.age + " years old."
  );
}

var me = {
  name: "Brandon",
  age: 36,
};

インストール

これで今回の簡単な紹介を終わらせますが、ReasonML を使いたいなら、どうすればいいでしょうか?下記の5ステップで初 ReasonML をコンパイルできます。アウトプットされる JS を既にある JavaScript 又は TypeScript のプロジェクトの中でもすぐ実装できます。

npm install -g bs-platform
bsb -init my-new-project -theme basic-reason
cd my-new-project
npm run build
node src/Demo.bs.js

これ以上詳しく調べるなら公式サイトまでお越しください。

ReasonML のオフィシャルサイト

未来の構文

現在はバリバリで TypeScript を書いている方が多いと思いますが、ぜひ ReasonML も調べて欲しいです。Facebook に応援されているので、すぐサポートがなくなる言語ではありません。React の開発者に開発されたので、React のように深く考えらた言語だと思えばいいです。JS の経験があって、関数型プログラミングしたい方におすすめです。今は TypeScript のタイプシステムは不十分だと思う方にもおすすめです。

OCaml の上で動いていますが、ReasonML は JS/TS を使っている皆さんにアピールしたいので、これからも JS に親しむプログラマーたちを歓迎するように向けています。2020年7月には@devブランチで新しい構文も出ています。その新しい構文はどんどん JS に近い書き方になっています。もちろん、互換性も考えられていています。.reのファイル拡張子を使えば公式にサポートされたシンタックスに制限されますが、もし新しい構文を試してみたいなら、.resのファイルが使えます。同じプロジェクトでも両方の構文が使えます。開発チームは今まで書いたコードを大事にしていますが、未来向きに ReasonML の将来を用意しています。

ぜひ使ってみてください!👋🏻


このエントリーをはてなブックマークに追加

7/4(土)にオンラインで行われた Symfony Meetup Kansai 4(Online) にて「MessageBusとは何か?」を発表しました。

内容は2019年末頃からJMSJobQueueBundleからSymfonyのMessengerコンポーネント(MessageBus機能)への移行を進める中で考えたこと・調べたことをまとめたものです。 少人数でしたが、アットホームな雰囲気でリラックスして話すことができました。皆様ありがとうございました。(そろそろ毎度zoomスライド共有で手間取るのをなんとかしようと思います…)

発表後の雑談タイム中にMessageBusの Bus はコンピューター用語の伝送路のほうの「バス」であって、「乗り物のバスでは無いのでは?」という質問をいただきましたが、その後調べてみたところ、そもそも伝送路のバスが「乗り合いバス」から名付けられたということでした。つまり伝送路の方のバスであっても、もとのイメージとしては乗り物のバスで間違っていないようです。

次回のSymfony Meetup KansaiではCD(継続的デリバリ)について話すことになっているので下調べを頑張りたいと思います!