Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

CPUエミュレータをRustで自作する

この記事は Rust Advent Calendar 2020 ならびに CyberAgent Developers Advent Calendar 25日目の記事です。

今年のはじめの頃になりますが、『CPUの創り方』という本に載っている TD4 という CPU を実装してみました。TD4 は「とりあえず動作するだけの4bit CPU」の略です。この本に載っている CPU エミュレータを実際に実装してみました。ただし、本書には GUI が載っていましたが、それは省略しました。

CPUの創りかた

CPUの創りかた

  • 作者:渡波 郁
  • 発売日: 2003/10/01
  • メディア: 単行本(ソフトカバー)

「最近話題の RISC-V などの CPU エミュレータを作ってみたいものの、いきなり作るにはハードルが高い。何か簡単なもので素振りをして CPU の動作の仕組みをまずは知りたい」という方にはかなりオススメできる教材だと思います。私自身、TD4 のエミュレータを実装してみて CPU の動作原理やコンセプトがわかり、その後製作中の RISC-V エミュレータに生きているように思います。

今回私が実装したものは下記です。だいたい1日くらいで実装できました:

この記事では、まず CPU の動作原理について TD4 を通じて簡単に解説し、Rust による実装例を見ていきます。また実装したインタプリタについても簡単に解説を加える予定です。

f:id:yuk1tyd:20201225203217p:plain
加算を行う処理を動かしてみた図

今回の記事では Rust を用いた実装になっていますが、Rust 以外の言語でもまったく問題なく実装できる内容だと思っています。たとえば PythonJava 、Go のようなアプリケーション向けの言語であっても問題なく実装できるはずです。Rust のシステムプログラミング言語としての側面を利用した実装箇所は今回はないためです*1

このブログでは恒例行事になっていますが、すべての解説を盛り込んだためかなりのボリュームになっています。お好きなところをかいつまんでお読みください。

リポジトリ

github.com

方針

今回のエミュレーションの方針は、クロックごとのレジスタ内の値を再現することとしました。本書を読むと、実際は CPU を論理回路から作り上げて再現することも可能なのですが、今回は命令を書いてそれを読み込ませるとレジスタに値を読み込んで計算などを行ってくれるという部分を再現することとしました。

予備知識

TD4 のような小さな題材であっても、基本的なアイディアは実際の CPU とそう相違ないはずです。TD4 を元に、CPU に関しての予備知識を少し書いておきたいと思います。

fetch-decode-execute サイクル

CPU は、fetch-decode-execute というステップを踏みながら計算を実行します*2

  1. fetch: メモリ上からプログラムを読み出すフェーズ。
  2. decode: 取り出した命令を元に内部的にどういった処理に対応するかを紐付け、行う計算を確定するフェーズ。
  3. execute: 2で確定した計算を実際に実行するフェーズ。

このサイクルをプログラムが終了するまで実行し続けます。

今回の TD4 エミュレータも同様にこのサイクルを実行して計算を行うように実装しました。

ROM

TD4 では ROM (Read-Only Memory) と呼ばれる領域にプログラムを書き出し、そこからプログラムを1行1行読み取って計算処理を行います。

Rust ではたとえば下記のようにして表現されており、ここにプログラムの内容(今回はバイナリ表現)が入っています。

pub struct Rom {
    pub memory_array: Vec<u8>, // ここに [00000000, 00010000, 11100001] のような形でプログラムが入っている。
}

現在どの番地のプログラムを読み出しているかについては、プログラムカウンタ(pc)と呼ばれるフラグによって管理されます。先ほどの fetch-decode-execute サイクルが1サイクル終了するとプログラムカウンタが足され、次のプログラムを読み出し、ということを ROM の配列が終了するまで行うことになります。

プログラムは「オペレーションコード」と「イミディエイトデータ」と呼ばれる領域に分けて書かれています。

たとえば、00000001 という値があったときには、TD4 は 0000 をオペレーションコード、0001 をイミディエイトデータとして解釈します。ちなみにオペレーションコード 0000 は TD4 では A レジスタ(このあと説明します)の値とイミディエイトデータの ADD 命令を意味します。

レジスタ

一言で言うなら CPU における記録領域のようなものです。

TD4 では A レジスタと B レジスタという2つのレジスタが用意されています。このレジスタに計算途中の内容を出し入れしながら、計算処理を次々行っていきます。

I/O ポート

これは CPU に対して入力を与えたり、あるいは演算の結果を出力するために使用するポートです。TD4 の場合は CPU から直接 I/O が出るという構成を取っているようです。これは規模の小さい CPU で多く採用されている方式だそうです。

TD4 の命令

TD4 では加算命令(add)、レジスタの値の転送命令(mov)、入出力ポートのやりとり(in, out)、ジャンプ命令(jmp, jnc)の4つの動作が組み込まれています。

実際の CPU になってくると、これらに四則演算が追加されたり、浮動小数点演算が追加されたりと高機能になっていきます。

こうした命令はプログラム側に書かれており、CPU の役割は命令を解釈し計算処理を実行していくことにあたります。

CPU エミュレータの実装

それでは CPU エミュレータの実装の紹介に移っていきましょう。

データ構造を用意する

CPU エミュレータを作り始めた際によかった手順としては、まずデータ構造を用意し、その後に一気にそれらを紐付けていくという方法でした*3

最初に命令を表現する enum を用意しました。OpCode という enum です。なお num_derive という便利なトレイトを今回は使用しています。

use num_derive::FromPrimitive;

#[derive(Debug, PartialEq, FromPrimitive)]
pub enum Opcode {
    AddA = 0b0000,
    AddB = 0b0101,
    MovA = 0b0011,
    MovB = 0b0111,
    MovA2B = 0b0001,
    MovB2A = 0b0100,
    Jmp = 0b1111,
    Jnc = 0b1110,
    InA = 0b0010,
    InB = 0b0110,
    OutB = 0b1001,
    OutIm = 0b1011,
}

各命令の詳しい内容は省略しますが、TD4 では12個の命令を実装すればいったん動くようにできています。ということで、enum を12個用意しました。

また、レジスタも用意します。レジスタは A レジスタ、B レジスタ以外に、キャリーフラグとプログラムカウンタを持っています。

#[derive(Clone)]
pub struct Register {
    register_a: u8, // register a
    register_b: u8, // register b
    carry_flag: u8, // carry flag
    pc: u8,         // program counter
}

I/O ポートも用意します。TD4 では入力と出力をもっているので、それらを用意します。

pub struct Port {
    input: u8,
    output: u8,
}

ROM も用意します。これは先ほどもご紹介しましたがもう一度ここに貼っておきます。プログラムの内容そのものを保持しています。

pub struct Rom {
    pub memory_array: Vec<u8>,
}

エントリポイント

今回のプログラムのエントリポイントとして重要なのは CpuEmulator という構造体です。これが実質エミュレータの構造を表現しています。ここに、先ほども説明した fetch-decode-execute サイクルを表現していくとスムーズでした。

まず CPU エミュレータ自体は下記のような構造体です*4

pub struct CpuEmulator {
    register: RefCell<Register>,
    port: RefCell<Port>,
    rom: RefCell<Rom>,
}

まず fetch です。ここでは現状のプログラムカウンタの値を確認し、プログラムカウンタに該当するプログラムを ROM から読み取ります。

(...)
    fn fetch(&self) -> u8 {
        let pc = self.register.borrow().pc();
        if self.rom.borrow().size() <= pc {
            return 0;
        }

        let code = self.rom.borrow().read(pc);

        code
    }
(...)

次は decode です。ここでバイナリ形式になっているオペレーションコードを解読して、先ほども紹介した内部表現(enum Opcode)に変換をかけます。

(...)
    fn decode(&self, data: u8) -> Result<(Opcode, u8), EmulatorErr> {
        let op = data >> 4;
        let im = data & 0x0f;

        if let Some(opcode) = FromPrimitive::from_u8(op) {
            match opcode {
                Opcode::AddA
                | Opcode::AddB
                | Opcode::MovA
                | Opcode::MovB
                | Opcode::MovA2B
                | Opcode::MovB2A
                | Opcode::Jmp
                | Opcode::Jnc
                | Opcode::OutIm => Ok((opcode, im)),
                Opcode::InA | Opcode::InB | Opcode::OutB => Ok((opcode, 0)),
            }
        } else {
            // never comes
            Err(EmulatorErr::new("No match for opcode"))
        }
    }
(...)

最後は exec です。先ほどの decode の結果をもとに、その Opcode に対応した関数を実行させます。

(...)
    pub fn exec(&self) -> Result<(), EmulatorErr> {
        loop {
            let data = self.fetch();
            let (opcode, im) = self.decode(data)?;

            match opcode {
                Opcode::MovA => self.mov_a(im),
                Opcode::MovB => self.mov_b(im),
                Opcode::AddA => self.add_a(im),
                Opcode::AddB => self.add_b(im),
                Opcode::MovA2B => self.mov_a2b(),
                Opcode::MovB2A => self.mov_b2a(),
                Opcode::Jmp => self.jmp(im),
                Opcode::Jnc => self.jnc(im),
                Opcode::InA => self.in_a(),
                Opcode::InB => self.in_b(),
                Opcode::OutB => self.out_b(),
                Opcode::OutIm => self.out_im(im),
            };

            // To prevent infinite loop
            if opcode != Opcode::Jmp && opcode != Opcode::Jnc {
                self.register.borrow_mut().incr_pc();
            }

            if self.does_halt() {
                return Ok(());
            }
        }
    }
(...)

細々とした各関数の実行内容については、こちらのコードをご覧いただくと、各々の命令がどういった処理を行っているかについて概略を掴むことができるかと思います。

実行自体は、CpuEmulator::withCpuEmulator の構造体を生成したあとに exec() 関数を呼び出すことによって行われます。program という箇所に、たとえば [00110011, 00000001] のようなバイナリが入ってきて、これを解釈して計算処理が走るというのが、このエミュレータの大まかな構造です。

(...)
    let rom = Rom::new(program);
    let register = Register::new();
    let port = Port::new(0b0000, 0b0000);
    let emulator = CpuEmulator::with(register, port, rom);
    match emulator.exec() {
        Ok(_) => (),
        Err(err) => panic!("{:?}", err),
    }
(...)

命令コンパイラの実装

さてここからは、本などには載っていない独自コンテンツになります。命令をアセンブラのように書くと、中でコンパイルして CPU エミュレータ用のプログラムに変え、シームレスに計算処理まで紐付けられる実装を追加してみました。

設計

今回の TD4 エミュレータは、バイナリを渡す必要があります。これでも十分楽しめるのですが、せっかくなのでアセンブラのような書き味の形式のファイルを用意して、それを読み込むとコンパイルが走ってバイナリ表現に変えてくれる形式にしてみたら、より使いやすいエミュレータになるのではないかと思いました。

具体的には、下記のようなファイルを読み込ませると、中で加算処理を行い加算結果を返してくれるというようなものです(コメント部分は実際のファイルには入れられません)。

mov A 0001 // 0001 という値を A レジスタに転送する
add A 0001 // A レジスタの値と 0001 を加算する
mov B A // A レジスタの値を B レジスタに転送する
out B // B レジスタの内容を出力する

このファイルを sasm(simple assembly の略のつもり)という拡張子に変えたテキストファイルにして保存しておき、TD4 エミュレータに読ませると、あとは中で任意の計算処理を走らせるようにしました。

なお、出来としてはおもちゃレベルのもので、エラーハンドリングなども結構雑に作ってあります。意図しない構文を入れると普通に落ちると思います。

パース部分の実装

よくあるコンパイラのパーサーのデザインだと思いますが、下記のような構造体を用意しました。私は 9cc を作ったことがあるのですが、それを一部真似しました。

pub struct Parser {
    pos: usize, // 今どの番地を読んでいるのかを保持する
    source: Vec<String>, // ファイルから読み込んだ内容を文字列で保持する
}

このアセンブラの内容はとてもシンプルなので、空白部分 split をかければ、欲しい内容は一通り抽出できてしまいます。たとえば

mov A 0001

この場合ですと、空白で split をかけると、mov, A, 0001 という文字列が取得できます。TD4 の場合、命令がとても単純なので、たとえば mov の後ろには必ず A か B という文字列が来ているはず、といった感じでシンプルに条件分岐をしながらパースしていくと、一通り命令をパースして解釈できるようになります。

mov の場合のパースの実装例は、たとえば下記のようになっています。パース関数はこちらにあります。

(...)
    pub fn parse(&mut self) -> Result<Vec<Token>, EmulatorErr> {
        let mut result = Vec::new();

        loop {
            let op = self.source.get(self.pos);

            if op.is_none() {
                break;
            }

            let op = op.unwrap();

            if op == "mov" {
                self.pos += 1;
                let lhs = self
                    .source
                    .get(self.pos)
                    .ok_or_else(|| EmulatorErr::new("Failed to parse mov left hand side value"))?;

                self.pos += 1;
                let rhs = self
                    .source
                    .get(self.pos)
                    .ok_or_else(|| EmulatorErr::new("Failed to parse mov right hand side value"))?;

                let token = if lhs == "B" && rhs == "A" {
                    Token::MovBA
                } else if lhs == "A" && rhs == "B" {
                    Token::MovAB
                } else {
                    Token::Mov(
                        Register::from(lhs.to_string()),
                        self.from_binary_to_decimal(rhs)?,
                    )
                };

                result.push(token);
            }
(...)

最終的な結果は Token と呼ばれる enum に詰めて返しています。この enum は Opcode とは別物にしてあって、下記のような定義にしました。

pub enum Register {
    A,
    B,
}

pub enum Token {
    Mov(Register, u8),
    MovAB,
    MovBA,
    Add(Register, u8),
    Jmp(u8),
    Jnc(u8),
    In(Register),
    OutIm(u8),
    OutB,
}

そしてこの Token は、のちのコンパイルフェーズで一旦バイナリ表現に変換されます。

コンパイル部分の実装

コンパイル部分の実装も行いました。

Token ごとに所定のバイナリに変換していきます。たとえば add A 0001 とあったら、まず A レジスタ値との加算命令をしめすバイナリ 0000 とイミディエイトデータ 0001 をくっつけるイメージです。

(...)
    pub fn compile(&self, tokens: Vec<Token>) -> Result<Vec<u8>, EmulatorErr> {
        if tokens.is_empty() {
            return Err(EmulatorErr::new(
                "Failed to start to compile because token list is empty.",
            ));
        }

        let mut result = Vec::new();

        for token in tokens {
            let program = match token {
                Token::Mov(Register::A, im) => self.gen_bin_code(0b0011, im),
                Token::Mov(Register::B, im) => self.gen_bin_code(0b0111, im),
                Token::MovAB => self.gen_bin_code_with_zero_padding(0b0001),
                Token::MovBA => self.gen_bin_code_with_zero_padding(0b0100),
                Token::Add(Register::A, im) => self.gen_bin_code(0b0000, im),
                Token::Add(Register::B, im) => self.gen_bin_code(0b0101, im),
                Token::Jmp(im) => self.gen_bin_code(0b1111, im),
                Token::Jnc(im) => self.gen_bin_code(0b1110, im),
                Token::In(Register::A) => self.gen_bin_code_with_zero_padding(0b0010),
                Token::In(Register::B) => self.gen_bin_code_with_zero_padding(0b0110),
                Token::OutB => self.gen_bin_code_with_zero_padding(0b1001),
                Token::OutIm(im) => self.gen_bin_code(0b1011, im),
            };
            result.push(program);
        }

        Ok(result)
    }
(...)

あとは gen_bin_code などの関数が、指定のバイナリを生成して返すというイメージです。イミディエイトデータを受け取らない命令も TD4 にはあるのですが、その場合は 0000 で詰めればよいことに仕様上なっているので、それ用の関数も用意しました。

やってみた感想

コンピュータの内部については、これを実装してみるまでは全然知らない状態でした。何がどこから読み込まれて計算処理が行われるのかについて、あまり理解できていませんでした。

まず、CPU エミュレータを実装してみたことによって fetch-decode-execute サイクルに関して知ることができ、「このようにしてコンピュータは動いているのか!」と身を持って理解できたように思います。

コンパイラのフェーズは、単に空白を split しただけですし、登場してくる文字列もパターンがかなり定まりきったものを使っただけなので、そこまで難しいことはしていないのですが、自分で1からこうしたコンパイラインタプリタを作るのは楽しいですね。

ちょっとやってみたいなと思ったものの、実力が足りずにできなかったなと思っているものは、今回用意したアセンブラを吐くプログラミング言語を自分で設計して、そのコンパイラを実装してみるというものです。まだまだプログラミング言語そのものに対する理解や、文法の設計経験などが足りないため難しいです。が、いつか取り組めたらいいなと思いました。

次は RISC-V のエミュレータにもチャレンジしてみています。命令数ははるかに多いですし、考慮しなければならない内容やコードベースもこの TD4 エミュレータと比較すると比ではないくらいに多いのですが、がんばって作りきろうと思っています。

*1:パターンマッチがあると、命令のデコード周りをきれいに実装できるかな、というくらいですかね。

*2:fetch-decode-execute cycle: https://www.bbc.co.uk/bitesize/guides/z2342hv/revision/5

*3:ちょっと記憶がなく、そこからしっかり始めたかはもう覚えていませんが。

*4:今思うとここは RefCell である必要はなく、おとなしく &mut にするべき箇所だったかもなと思っています。

Rustでもモナドは実装できるのか?(再)

この記事は言語実装Advent Calendar 2020 25日目の記事です。

モナドに関する話題が言語実装アドベントカレンダーの範疇に入るのかわかっていませんが*1プログラミング言語がお好きな方はきっとモナドもお好きだろう…ということで、Rust におけるモナドの扱いについて記事にしたいと思います。

この記事では、Rust を使って

  • モナドを実装してみる。
  • do 記法を実装してみる。

ところまでやってみました。後半で、GATs と呼ばれる Rust の nightly に入った機能に関して少し細く説明を加えています。

なおこの記事では、モナドとは何かという話はあまり触れていません。

さらに執筆時間*2と記事の都合上、Rust 言語の細かい文法に関する解説もとくに行っていません🙇‍♀️ 登場してくる文法には、適宜注釈でコメントをできるかぎり付与するようにしましたが、不十分かもしれません。

今回の記事は、ずいぶん前に登壇した内容のアップデートです。この発表以降にさらに Rust の言語機能にアップデートがあり、その内容を使ってモナドを実装してみるという記事です。

Rust でもモナドは実装できるのか? - Speaker Deck

前提知識

解説を加えてしまうと記事の長さがとても大変なことになってしまうので今回は省かせていただきますが、いくつか詳しく解説された記事があるのでそちらをリンクさせていただきます。

高階カインド型

まず高階カインド型そのものについては、Scala による説明ですが、下記の記事が直感的でわかりやすいです。

qiita.com

また、なぜ高階カインド型が必要とされるのかについては、この記事がわかりやすいです。

keens.github.io

モナド

モナドというのはいくつか説明の仕方があるかとは思いますが、

ubiteku.oinker.me

個人的にはこの記事の説明が丁寧でわかりやすいかなと思いました。

従来のエミュレーション方法

従来のやり方では、HKT<T> と呼ばれる高階カインド型を表現するトレイトを用意し、HKT トレイトに関連型を2つ(高階を示すものと、高階の中身の型を示すもの)用意すれば、完全ではないものの近い形でエミュレーションできました。たとえば、従来のやり方ではモナドは下記のようにエミュレーションできます。

trait HKT<T> {
    type C;
    type H;
}

trait Functor<U>: HKT<U> {
    fn map<F>(self, f: F) -> Self::H
    where
        F: Fn(Self::C) -> U;
}

trait Monad<U>: Functor<U> {
    fn pure(self, m: U) -> Self::H;
    fn bind<F>(self, f: F) -> Self::H
    where
        F: Fn(Self::C) -> Self::H;
}

単純なデータ構造を例にとって、実装をしてみます。たとえば Id モナド*3を実装してみると、

struct Id<T>(T);

// 高階カインドに関するエミュレーションをここで行う。
impl<T, U> HKT<U> for Id<T> {
    type C = T;
    type H = Id<U>;
}

// ファンクタを定義する
impl<T, U> Functor<U> for Id<T> {
    fn map<F>(self, f: F) -> Self::H
    where
        F: Fn(Self::C) -> U,
    {
        Id(f(self.0))
    }
}

// モナドを定義する
impl<T, U> Monad<U> for Id<T> {
    fn pure(self, m: U) -> Id<U> {
        Id(m)
    }

    fn bind<F>(self, f: F) -> Id<U>
    where
        F: Fn(T) -> Id<U>,
    {
        f(self.0)
    }
}

といった形で、一応型パズルは解け、実際に動かしてみるときちんとそれらしい動きをしていることがわかります。残りは発表資料をご覧ください。

Generic Associated Types を用いたエミュレーション(new!)

ここからが本題です。

Rust nightly 1.50 から入った Generic Associated Types(解説は後ほど)という機能を使うと、従来の HKT トレイトを使う場合と比べ、かなり実装をきれいにすることができます。

1つ1つ型クラスを定義していきます。

型クラスを定義する

Functor を用意する

まずは Functor を用意します。Functor は変換処理の一般化です。

この FunctorFunctor の型パラメータとなる A を関連型 *4 でもたせ、高階カインド型そのものを Lifted<B> という形でもたせます。LiftedFunctor を実装してある必要があるという制約も付与しています。ここに後ほど具体的な型が入ってくることになります。

pub trait Functor {
    type A;
    type Lifted<B>: Functor;

    fn map<F, B>(self, f: F) -> Self::Lifted<B>
    where
        F: FnMut(Self::A) -> B;
}

そして、Lifted<B> の部分に、いわゆる Generic Associated Types(GATs)が使用されています。GATs については後ほど解説します。現在の stable Rust においては、このように Lifted という関連型に型パラメータを付与することはできません。

Pointed を用意する

続いて Pointed を用意します。Pointedpure 関数の一般化です。この関数は、元の型をモナドの文脈の型に変換する機能を持ちます。

PointedFunctor のトレイト境界をもちます。実装先が、Functor も実装してある必要があります。

pub trait Pointed: Functor {
    fn pure(t: Self::A) -> Self::Lifted<Self::A>;
}

Applicative を用意する

続いて Applicative を用意します。Applicative は関数適用の一般化です。apply という関数を持ちます。トレイト境界に Pointed を持ちます。

pub trait Applicative: Pointed {
    fn apply<F, B, C>(self, b: Self::Lifted<B>, f: F) -> Self::Lifted<C>
    where
        F: FnMut(Self::A, B) -> C;
}

Monad を用意する

最後に Monad を用意します。Monad は手続きの結合の一般化です。bind という関数を持ちます。トレイト境界に Applicative を持ちます。

pub trait Monad: Applicative {
    fn bind<B, F>(self, f: F) -> Self::Lifted<B>
    where
        F: FnMut(Self::A) -> Self::Lifted<B>;
}

いくつか型を実装していく

ここまで用意できたところで、実際にいくつか型を実装していきましょう。まずは最も単純な例として、先ほどもご紹介した Id モナドを実装してみます。

pub struct Id<M>(pub M);

impl<A> Monad for Id<A> {
    fn bind<B, F>(self, mut f: F) -> Id<B>
    where
        F: FnMut(A) -> Id<B>,
    {
        f(self.0)
    }
}

impl<A> Pointed for Id<A> {
    fn pure(t: A) -> Id<A> {
        Id(t)
    }
}

impl<A> Applicative for Id<A> {
    fn apply<F, B, C>(self, b: Id<B>, mut f: F) -> Id<C>
    where
        F: FnMut(A, B) -> C,
    {
        Id(f(self.0, b.0))
    }
}

impl<A> Functor for Id<A> {
    type A = A;
    type Lifted<B> = Id<B>;

    fn map<F, B>(self, mut f: F) -> Id<B>
    where
        F: FnMut(A) -> B,
    {
        Id(f(self.0))
    }
}

また、Rust にすでに存在する OptionResult も、一応今回用意したモナドにすることができます。*5

まずは Option モナドを用意してみます。

impl<A> Monad for Option<A> {
    fn bind<B, F>(self, mut f: F) -> Option<B>
    where
        F: FnMut(A) -> Option<B>,
    {
        self.and_then(f)
    }
}

impl<A> Pointed for Option<A> {
    fn pure(t: A) -> Option<A> {
        Some(t)
    }
}

impl<A> Applicative for Option<A> {
    fn apply<F, B, C>(self, b: Option<B>, mut f: F) -> Option<C>
    where
        F: FnMut(A, B) -> C,
    {
        let a = self?;
        let b = b?;
        Some(f(a, b))
    }
}

impl<A> Functor for Option<A> {
    type A = A;
    type Lifted<B> = Option<B>;

    fn map<F, B>(self, mut f: F) -> Option<B>
    where
        F: FnMut(A) -> B,
    {
        self.map(f)
    }
}

また、Result モナドは今回は Ok 側にバイアスをもたせた形で用意してみました。

impl<A, E> Monad for Result<A, E> {
    fn bind<B, F>(self, f: F) -> Result<B, E>
    where
        F: FnMut(A) -> Result<B, E>,
    {
        self.and_then(f)
    }
}

impl<A, E> Pointed for Result<A, E> {
    fn pure(t: A) -> Result<A, E> {
        Ok(t)
    }
}

impl<A, E> Applicative for Result<A, E> {
    fn apply<F, B, C>(self, b: Result<B, E>, mut f: F) -> Result<C, E>
    where
        F: FnMut(A, B) -> C,
    {
        let a = self?;
        let b = b?;
        Ok(f(a, b))
    }
}

impl<A, E> Functor for Result<A, E> {
    type A = A;
    type Lifted<B> = Result<B, E>;

    fn map<F, B>(self, mut f: F) -> Result<B, E>
    where
        F: FnMut(A) -> B,
    {
        self.map(f)
    }
}

現状実装できないもの

ただし、関数型プログラミングにおいて利用する道具が完全に実装できるというわけではありません。たとえば、

  • いわゆる flattenjoin の処理はコンパイル時にコンパイラがパニックを出して落ちる(バグの可能性がある)。
  • traverse を実装しようとするとうまくコンパイルが通らない*6

といった内容が、この記事にて報告されていました

do 記法

Haskell には do 記法、Scala には for-yield と呼ばれる構文があり、これらを用いることで次のように bind(あるいは flatMap)がいくつも重なってしまう現象を回避できたりします。

// Rust による例
Option::pure(...).bind(|o| return_option_function(o).bind(|p| return_option_function(p))

モナドの真骨頂は、こうした do 記法や for-yield などとともに利用するところにあると思うのですが、Rust にはそうした専用の構文はありません。

そこで、Rust のマクロという機能を用いて do 記法を実装しました。do は Rust においては予約語になっており使用できないので、monad-do から取って mdo というマクロを作りました。

macro_rules! mdo {
    ($i:ident <- $e:expr; $($t:tt)*) => {
        $e.bind(move |$i| mdo!($($t)*))
    };
    ($e:expr; $($t:tt)*) => {
        $e.bind(move |()| mdo!($($t)*))
    };
    (ret $e:expr) => {
        $e
    };
}

少し Rust のマクロに関して補足を入れておくと、

  • $i, $e, $t: マクロ内における変数。
  • $(...)**: 0回以上の繰り返しを意味します。
  • :ident: これはいわゆる識別子がやってくることを示します。
  • :expr: これはいわゆる式がやってくることを示します。
  • :tt: 何か単一のトークン木がやってくることを示します。

内部で軽い構文解析を行い、<- の前後を解析して式なのか識別子なのか、それ以降に続く別のトークン木なのかを見分けます。その後、取り出せた式と識別子を使ってモナドに紐づけて実装されている(はずの) bind を呼び出し、残ったトークン木を更に再帰的に mdo! マクロにかける、ということをしています。また、結果は ret と書いた後ろの式を返します。

これを用いることで、

mdo! {
    a <- {何かモナドを作る};
    b <- {何かモナドを作る};
    ret result(a +b) // 何か結果を返す
}

といった記法が可能になります。

実際 Rust でこれを使用してみると、下記のように記述可能になり、テストを通すことができました。

mod test_do {
    use crate::data::option::*;
    use crate::{applicative::Applicative, functor::Functor, functor::Pointed, monad::Monad};

    #[test]
    fn test_do_notation() {
        let actual = mdo!{
            x <- Option::pure(1);
            y <- Option::pure(2);
            ret Option::pure(x + y) // Some(3) が返るはず
        };
        assert_eq!(actual, Some(3));
    }
}

便利ですね。

Generic Associated Types(GATs)とは何か

ここからは言語実装やプログラミング言語全般の話というより Rust 特有の事情の話になってしまい大変恐縮なのですが、GATs が一体何なのか、どういう使い道があるのかについて RFC を読みながら簡単にまとめておきたいと思います。

github.com

GATs というのは先ほども軽く触れたとおり、関連型に何かしらの型パラメータ(AT みたいなもの)をもたせることができる機能です。Rust にいわゆる高階カインド型を導入するための機能です。

Rust の場合は、他の言語でもあるような型パラメータ以外にもライフタイム注釈*7をこの位置に入れることができます。つまり、型パラメータの位置に入るのは A というような型情報と、もうひとつは 'a というライフタイム注釈が来ることになります。GATs はこれら2つを関連型で扱えるようにするための機能です。

下記は RFC から拝借してきたコードですが、ライフタイムに関しては次のような記述が GATs で行えるようになります。これは Rust を書いていると多々直面する内容で、ユーザーが求めていた機能でもありました。

trait StreamingIterator {
   type Item<'a>;
}

impl<T> StreamingIterator for StreamIterMut<T> {
    type Item<'a> = &'a mut [T];
    ...
}

また、型パラメータを関連型にもたせる側では、たとえば下記のような記述が GATs を用いて行えるようになります。Rust には RcBox などのスマートポインタがいくつか存在しますが、これらは型パラメータを引数に取ります。そうしたポインタを関連型の具象実装側を切り替えることで切り替えたい場合に、GATs が役に立ちます。

trait PointerFamily {
    type Pointer<T>: Deref<Target = T>;
    fn new<T>(value: T) -> Self::Pointer<T>;
}

struct ArcFamily;

impl PointerFamily for ArcFamily {
    type Pointer<T> = Arc<T>;
    fn new<T>(value: T) -> Self::Pointer<T> {
        Arc::new(value)
    }
}

struct RcFamily;

impl PointerFamily for RcFamily {
    type Pointer<T> = Rc<T>;
    fn new<T>(value: T) -> Self::Pointer<T> {
        Rc::new(value)
    }
}

struct Foo<P: PointerFamily> {
    bar: P::Pointer<String>,
}

RFC によると、GATs が入ったからと言ってモナドを完全に実装できるようになるというわけではないと注意書きがなされています。私がモナドを実装することに関してそこまで深い知識があるわけではないので、一体どういったポイントが足りなくてそうなっているのかは今後の探求の課題だなと思いました。

リポジトリ

まだいろいろ実験中ですが、リポジトリはこちらにあります。

github.com

参考文献

*1:カレンダーに登録してみたものの、プログラミング言語そのものに関わりそうな話題としての持ちネタがこれくらいしかありませんでした🙏

*2:12/24に書き始めたのが間違いだった。もう少し時間を取るべきでしたね。

*3:恒等モナド。単に 1i32 のような値をモナド文脈の中に持ち込める存在です。これを用いるとたとえば、Id(1).map(|i| i + 1) = Id(2) のような計算を行うことができます。

*4:Rust ではトレイト内部に type という他の言語でも見られるような型エイリアスをつけることができます。これは関連型と呼ばれ、Rust を書いていると何度も登場してくる概念になります。高階カインド型のエミュレーションは、この関連型を用いて行うことになります。後ほど解説しますが、現状の stable Rust ではこの関連型に型パラメータを付与することはできません。が、nightly で型パラメータを付与することが可能になり、より関連型の一般化をしやすくなりました。

*5:Rust では実際のところ、これらの型は Iterator として一般化されているのであまりやる意味はないのですが…

*6:ただ、これは直せそうな気がしました。関連型のうちどれを使用すればいいかをきちんと明示的に書いてあげる必要があるかも?

*7:Rust にはダングリングポインタを防止するための仕組みとしてライフタイムという概念が存在します。このライフタイムというのは、ユーザーがライフタイム注釈を個別に指定してライフタイムを明示的に書くこともできるようになっています。ライフタイム注釈というのは 'a というようにシングルクオートで始まる形で、通常の型情報と同じ位置(<> 内)に入れて書きます。

RustFest Global を開催しました

この記事は Rust Advent Calendar 2020 その3の4日目の記事です。今日は Rust に関する話ではあるのですが、技術的な話ではない記事になってしまうことをお許しください。

1ヶ月ほど経ってしまいましたが、Rust の全世界的なカンファレンス RustFest Global を、11/7〜11/8で開催しました。スピーカーとして参加してくださった皆さん、また RustFest Global に遊びに来てくださった皆さん、ありがとうございました。

今日はその RustFest Global に関する話を書きたいと思います。ちなみに多分この記事を Google 翻訳にかけて読まれる海外の方も多いと思うので、先に書きます。英語にするので少しお待ち下さい!

For non Japanese speakers: I'm going to jot down my thoughts regarding RustFest Global. Unfortunately, there is no English version right now. I'm planning to translate this post into English soon. Please gimme a moment!

RustFest Global とは?

f:id:yuk1tyd:20201204143538p:plain
RustFest Global

rustfest.global

ヨーロッパの RustFest チームの声掛けで始まった、複数のタイムゾーンにまたがる Rust カンファレンス

たしか夏前だったと思いますが、RustFest チームからの声掛けで、今回 Rust.Tokyo のチームもこのカンファレンスの運営に参加することになりました。

RustFest チームというのはヨーロッパ圏の Rust カンファレンスを開くチームで、ヨーロッパで毎年カンファレンスを開催しています。前回はバルセロナだったと思います。実は去年の Rust.Tokyo にて、Rust コアチームの Florian Gilcher 氏が基調講演を行ってくれましたが、彼も RustFest チームの主催者の一人でした。現在は卒業生のようです。

Rust.Tokyo は今年は中止の判断をしていた

私たち Rust.Tokyo のチームは、今年は Rust.Tokyo を実施しないという判断をしていました。理由はご多分に漏れずコロナウィルスです。中止の判断をした4月時点では、コロナウィルスがいつ収まりを見せるか、あるいはどのように今後感染拡大していくのかがまったく見えない状態でした。周りのカンファレンスも中止の判断をしているところが多かったというのもあります。

rust.tokyo

しかし一方で、2020年になって Rust はさらに広まりを見せ、私自身も本を共著で出版させてもらうなど盛り上がりを感じていました。そのような状況でカンファレンスを今年は開けないというのは、少し歯がゆい思いがありました。

そんななかでの RustFest チームからの声掛けということもあり、Rust.Tokyo としてもぜひ、ということで運営に参加することになりました。

ラテンアメリカのチームも一緒に運営をすることに

Rust.Tokyo 以外には、日本のユーザーの方は本当にあまり馴染みがないかもしれませんが、Rust LATAM というラテンアメリカのチームも運営に参加することになりました。彼らはアルゼンチンなどに済んでいるようです。日本の真裏、12時間の時差です。

開催まで

毎週ヨーロッパとラテンアメリカと一緒にミーティング

開催までは、毎週ヨーロッパとラテンアメリカ、ならびに日本のチームが Zoom で集まってミーティングを行いました。

グローバル規模で会議をすることになって大変だったのは時差の問題です。JST では1時開始のミーティングでした。ラテンアメリカチームが入ってきて、JST で19時スタート(ラテンアメリカは朝の7時スタート)のミーティングも追加され、隔週で時間帯をラテンアメリカと交代しながら行いました。

私は普段は英語はそれなりに聞けるのですが*1、夜中の1時に第二外国語を聞くのはなかなかハードでした笑。でも、毎週のミーティングは楽しかったです :)

翻訳チームの結成

今回のカンファレンスは「グローバルである」ことにとてもこだわっていました。つまり、Rust.Tokyo は日本だけでなくアジア太平洋地域(APAC)の担当をすることが期待されていました。

本来であればアジアの様々な国の言葉に英語の文章をローカライズして提供するべきだったのですが、参加者の人口比やツテの問題などを考慮して、韓国語と中国語(簡体字繁体字)の翻訳ができる方を募集しました。

韓国語については Enwei Jin さん、中国語の簡体字については Xidorn Quan さん、繁体字については Rust.Tokyo にも参加してくれた Cheng You Bai さんと Kan-Ru Chen さんが担当してくださいました。

改めてお礼をさせてください!本当にありがとうございました。Thank you for the translation team! I appreciate your swift & professional work.

翻訳チームとは基本的にすべて英語でやりとりをしました。英語は世界の標準語になっていますね。

翻訳チームには、主に RustFest のアカウントで告知される内容の翻訳と、CfP に際してサイトに掲載された文章を翻訳していただきました。たとえばこのような感じに。

全然話が逸れますが、個人的には、実際に翻訳されてあがってきた文章を見ながら「この言語ではこういう言い方をするんだ〜」というのを知ることができてとてもおもしろかったです。私は韓国系のドラマや YouTuber を見るのが好きで、韓国語を少しだけ勉強しているというのもあり、韓国語はとくに勉強になりました。できれば英語の字幕なしでそうした番組や YouTube の動画を観られるようになりたい…。

発表が英語でなかった場合の事前字幕付け

発表者が英語話者でない場合も想定していました。その場合には事前にプレゼンテーションを録画してもらい、その録画内容に字幕をつけることで対応という形になりました。

結果、日本から参加してくださったスピーカーの方は日本語でお話いただきました。ほかは、すべて英語のセッションとなりました。

字幕自体はプロの翻訳の会社に依頼してつけてもらいました。できあがった成果物について、スピーカーが話している内容と字幕の表示される内容があっているか、また字幕が表示されるタイミングは正しいかを確認しました。

準備

準備のほとんどは RustFest チームが行ってくれました。改めて本当にありがとうございました。

  • スピーカーの募集
  • スポンサーの募集
  • アーティストの募集
  • スケッチノーターの募集
  • 使用するツールの手配
  • 翻訳業者の手配

などを行っていました。

日本のカンファレンスと圧倒的に違うと感じたのは、アーティストの募集とスケッチノーターの募集でした。アーティストによる演奏があるカンファレンスは、日本ではまったく見ませんね。海外系のカンファレンスだと結構見るのでわりと一般的だと思います。アメリカの Rust のカンファレンスの RustConf は演奏していたような気がします。

スケッチノートというのは、日本でもカンファレンスに参加した方がボランティアで SNS などに流してくださるのを見ることはあるかもしれません。一方で、このように公式に募集しているカンファレンスは、私の知っている範囲が狭いだけかもしれませんが、カンファレンスサイドが公式に募集しているのは日本ではあまり見ないように思います。

ヨーロッパ圏というか欧米圏は、このようにアートも含めて「場」を設計するところに力を注いでいるところがとても見習うべきポイントだなと個人的には思いました。神は細部に宿る、のかもしれません。

(日本におけるアートの立ち位置は、今回のイベントのように(ヨーロッパのような)積極的に「場」に入れ込んでいくという立ち位置では必ずしもないように思います。日常にアートがそこまで入り込んでいないという感じがあります。東京には美術館は多いんですけどね。カンファレンスや人が集まった際、ふとした瞬間にアートを入れようとはならないのだと思います。私だけかもしれませんが、「何かこう、気合いをいれて楽しむもの」という印象が強いかもしれません。なので、カンファレンスとアートを融合させようという発想にそもそも至りません。ヨーロッパ圏とアジア圏のアートの考え方やアートへの接し方の違いはとても興味がわきました。)

あつ森のグッズ

同じく RustFest 主催者の Pillar さんがあつ森用のグッズを作ってくれたので、記念にもらって撮影してみました。

めっちゃかわいいのである。

当日

チャットルームのモデレータ

私は主にチャットルームのモデレータをしていました。Code of Conduct に反する発言をする人がいないかを監視していました。

途中で「資本主義とはなんたるか、あなたはどう思うか」というコメントが始まって、テックイベントで政治的発言は大丈夫なのかな…と思う場面もありましたが、スピーカーの方が機転を利かせて「あとは DM しよ!」と言ってくれていて内心かなりホッとする出来事がありました笑。

結局まあまあ見てしまった

20時間にも及ぶカンファレンスでしたが、日本時間であれば、開始からラテンアメリカの途中までは見られるタイムスケジュールでした。どのセッションもおもしろく、ところどころ休憩を取りながらすべてのブロックを一通り見ることができました。

次々に日が昇っていく

運営のチャットルームがあったんですが、APAC が終わったくらいで日が昇ってくる写真がヨーロッパから投稿されるなど、ワイワイ楽しくやっていました。

日本は日本から開始すると一番早く日がのぼっている国で、ヨーロッパが次に日が昇り、ラテンアメリカが最後に日が昇るという感じでした。実はラテンアメリカの方は APAC のセッションの方の最初の方は見てくれていて、最初のプレゼンテーションの前には「gambatte!」というコメントをくれてとてもほっこりしました。

難しいと感じたこと

英語の壁

日本向けのカンファレンスではないので仕方がありませんが、やはり英語のセッションのときは如実にTwitterやチャットルームのリアクションが減ってしまったように思いました。英語、大変ですよね。私もネイティブスピーカーというわけではないので、英語を使う際は少し頭を切り替えないといけません。

日本に住んでいると、英語を使う機会がそもそもありませんよね。英語を見ない・聞かない・話さない日が大半だと思います。自分から注意深く英語のコンテンツを見るようにしないと、なかなか触れる機会がないと思います。これは、地政学的な要因や文化的な要因が絡んでいて、致し方ない問題だと思います。

英語の壁の突破策は、やはり同時通訳なのでしょう。ScalaMatsuri などでは同時通訳で日本語→英語訳、あるいは英語→日本語訳が聞けたりしますが、そうした施策はやはり有用なのだなあと思いました。こうした同時通訳の用意などは、今回感じた「完全なローカライゼーション」への課題感にもつながっていきます。

すべての人が楽しめるカンファレンスにするために、言語の障壁はできる限り取り払わなければならないというのは、去年の Rust.Tokyo でも感じたことでした。

「完全な」ローカライゼーション

ローカライゼーションというのは難しいですね。日本語訳を用意することはもちろんローカライゼーションの一つではあるのですが、それだけでは足りないことがあります。

たとえば最近あがっていた記事でおもしろかったものなのですが、海外サイトのローカライゼーションだけでも、これだけ気にしなければならないことが出てきます。デザインだけでなく、よく使われる言葉やキャッチコピーに変えるなどの対応も必要になってきます。

zenn.dev

ツールに関するローカライゼーションなどもあるでしょう。たとえばメッセージングアプリに関して言えば、海外では WhatsApp が大半ですが日本では LINE が大半といった具合にです。日本では使い慣れないツールを使う必要が出てくると、どうしても操作に慣れなくてストレスが…といったこともあるかもしれません。

要するに、言語の翻訳だけではなく文化の翻訳も本来は行う必要がある、ということです。

しかし、こうしたローカライゼーションは完全対応しようとすると、費用と人がより多く必要になります。限られた資源の中で適切なローカライゼーションを優先順位をつけて行っていく必要があるというのが現実です。的確なローカライゼーション施策を打つのはとても難しいのだなあと思いました。

来年以降

RustFest Project 始動

まだ詳細は未定の部分が多いですが、RustFest Project が始動します。

これは、たとえば今はコミュニティが存在しない国や地域で新しくコミュニティを立ち上げたい!となった際に、RustFest Project がいろいろノウハウや資金面でコミュニティの立ち上げや運営を援助できる仕組みです。

blog.rustfest.eu

予算の使途などをオープンに管理するために下記のページで今後予算状況や何に使用したかなどを知ることができます。ならびに、金銭的なコントリビューションをこのページから応募することもできます。

opencollective.com

技術とコミュニティは密接な関係にあります。その技術が、いい形で普及するかどうか、また発展するかどうかはコミュニティの力にかかっています。こうした、 Rust がコミュニティを大切にする姿勢は、Rust の好きなところのひとつでもあります。

遊びに行きたい

コロナ終わってたら来年の RustFest と Rust LATAM に遊びに行きたいです!

*1:ただ、子どものころ英語を使う環境にいただけなので、大人の使う単語は正直わからん!となることは多いです。あとはアメリカ英語がわかりません。

Mac かつ VSCode で Idris の環境構築をする

Idris Advent Calendar 2020 3日目の記事です。

2020年今年の言語に Idris を選んで年始に Hello, World していたのですが、見事にその後 Python をしていた1年でした(仕事で使うことになったので Python を覚える必要が出てきました)。

完全に Idris は初心者ですが、Idris 楽しんでいきたいと思います!

今回は Mac ユーザー向けに Idris の開発環境構築を紹介します。

Idris とは

だいぶ雑に説明するなら、依存型つき Haskell です。詳しくは2020年 Idris Advent Calendar 1日目の記事をご覧ください。

環境

VSCode 上でコードが書けると安心できますね、ということで VSCode を使用する前提でいます。Idris は REPL が使えるので、それを利用するだけでも Hello, World くらいなら結構簡単にできますが。

Idris のインストールをする

Idris 自体を入れる

2つルートがあります。今回私は Homebrew を使用しました。一方で、公式には cabal によるインストール方法が記載されています。たぶんどちらでもいいと思います。

まず Homebrew から。

$ brew install idris

これが終わった後に、

$ idris --version

しましょう。私の環境では、

❯ idris --version
1.3.3

と返ってきました。Idris は1系と2系があり、2系は開発中なんだそうです。

cabal を入れる

cabal 自体はこのあと必要になるので、別途インストールしておきましょう。

$ brew install cabal-install

これを行ったあとにパスを通す必要があります。.zshrc などに下記を書き足しましょう。

PATH = "$HOME/.cabal/bin:$PATH"

stack の場合は試したことはないです。

VSCode の Extension を入れる

プラグインのインストール

VSCodeプラグインを入れましょう。Extensions で「Idris」と検索すると出てきます。

github.com

この README にしたがって、idringen というプロジェクトマネジメントツールを入れる必要がありそうです*1。プロジェクトマネジメントツールを入れるのは少々抵抗があるかもしれませんが、初心者の場合そもそも言語的なベストプラクティスも知らないと思うので、入れて勉強しようということで私は入れてみました。

最後のメンテが4年前だけど…

idringen を入れる

github.com

cabal を経由して入れます。

$ cabal install idringen

インストール後、下記を叩きます。

$ idrin --help

help コマンドは実装されていないらしい…。メンテのしがいがありそう。

❯ idrin --help
idrin: not support subcmd --help yet
CallStack (from HasCallStack):
  error, called at src/Idringen.hs:28:24 in drngn-0.1.0.3-b556eddc:Idringen

idringen でプロジェクトを作ってみる

sandbox プロジェクトを作りましょう。ちなみに、名前に「-」を入れると生成後エラーになってビルドできません。ご注意を。

new コマンドでプロジェクトを作成できます。

❯ idrin new sandbox
Creating new project named sandbox

Hello, World

ビルドして…

❯ idrin build

実行しましょう。

❯ idrin run
hello sandbox!

これで完了です!

VSCode で開いてみる

シンタックスハイライトもしっかりついていて完璧ですね!これで開発をスタートしていきましょう!

f:id:yuk1tyd:20201202114631p:plain
VSCode で Idris プロジェクトを開いてみた

(これだけ見ると、めっちゃ Haskell ですね)

Idris の本

『Type-Driven Development with Idris』という本があって、いろいろ詳しく書かれているように思います。

PDF も存在していて、下記です。

www.pdfdrive.com

12月中に時間があったら、この本からいくつかエクササイズして記事にしてみようかなと思っています。

*1:なんとExtensionの作者が作っています

2020年。あなたとJAVA、今すぐダウンロー

ド。(いつものやつ)

2020年時点で改めてよさそうな Java のインストール方法についてまとめておきます。

ちなみにですが、

  • Scala(sbt)を利用する。
  • Scala のプロダクトでは Java 8 を使用し、プライベートでは Java 14 を使用する。
  • Open JDK そのものの縛りは特にない。
  • macOS を使用する。

という前提のもと行う環境構築です。jenv を利用してバージョンの切り替えを行いながら開発することを前提としています。IntelliJ を使用しているとほとんど意識することはないんですが。

インストール方法

brew tap をして、AdoptOpenJDK を入れます。

$ brew tap AdoptOpenJDK/openjdk

JDK 8 なら、下記のように入力すると入れられます。

$ brew cask install adoptopenjdk8

AdoptOpenJDK という選択肢

という観点から、今回は AdoptOpenJDK を使用しています。

jenv で複数バージョンを管理する

Java は最近、バージョンリリースのサイクルが速くなりました。サクッと最新版を試したいけれど、普段のプロダクトでは少し古いバージョンを使用しているということもあるかと思います。

そういったときには jenv というツールが使えます。

github.com

brew install jenv

をして、

jenv versions

で、動いているかを確認します。その後、AdoptOpenJDK で入れたバージョンたちを次のようなコマンドで追加していくと、jenv の管轄下に置くことができます。

mkdir ~/.jenv/versions

ディレクトリがないと、↓でコマンドを叩いた際にディレクトリがないよと怒られます。

echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.bashrc

などを忘れずに。パスを通さないと、java -version したときに jenv の設定が読まれないみたいです。

$ brew cask install adoptopenjdk11
$ brew cask install adoptopenjdk10
$ brew cask install adoptopenjdk8

インストールした AdoptOpenJDK の分しか入れられないので注意。

これを、jenv に追加します。

$ jenv add /Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.2.jdk/Contents/Home/
$ jenv add /Library/Java/JavaVirtualMachines/adoptopenjdk-10.jdk/Contents/Home/
$ jenv add /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/

Java 8 なら Java 8 を標準に設定して、

$ jenv global 8.0

これで、

$ java -version

をした際に、8に切り替わっていれば成功。ちなみに特定のディレクトリ配下だけ Java のバージョンを変える設定もできるみたいです。

参考記事: https://www.dhiller.de/2019/01/06/setup-multiple-jdks-with-jenv-and-adoptopenjdk.html

terraformを触り始める

画面をポチポチしながらインスタンスを立てて…といったことを考えるのが辛くなってきたので、そろそろ terraform 使いこなせるようになりたいと思い、チュートリアルをコツコツはじめてみました。

普段は CDK を使ってるんですが、最近少しだけ terraform を触るチャンスがあって、なかなかいいじゃないかと思ったのもあります。

あと基本的にインフラ(というかクラウド)が苦手なのですが、そろそろ逃げられなくなってきたのもあって、本腰入れてやらないとなと思っているというのもあります。

以下はこのチュートリアルをやってみたメモです。terraform にはチュートリアルガイドが存在し、それを一通りやるだけである程度 terraform を使いこなせるように設計されているようです。英語ですが、無料なのでオススメです。

terraform そのもののインストールコマンド等は一旦割愛します。詳しくはチュートリアルをご覧ください。私の手元の terraform のバージョンは下記です。

❯ terraform --version
Terraform v0.13.5

また1つ1つ調べていたら例のごとく長くなってしまったので、お好きなところをかいつまんでお読みください :) 私は terraform はまったくの素人なので、情報が正確でない箇所があるかもしれません。

今回のお題: Docker 上に nginx を立ち上げる

今回のお題は Docker 上に nginx を立てて localhost:8000 で Welcome to nginx! するものです。

terraform は設定内容を .tf という拡張子のファイルに書いていきます。さまざまなリソースの管理をコード形式で見られるように作られているようです。

さっそくですが、Docker 上に nginx を立ち上げる設定は下記のように書くことができます。main.tf という名前をチュートリアルに従って命名しました。

terraform {
  required_providers {
    docker = {
      source = "terraform-providers/docker"
    }
  }
}

provider "docker" {}

resource "docker_image" "nginx" {
  name         = "nginx:latest"
  keep_locally = false
}

resource "docker_container" "nginx" {
  image = docker_image.nginx.latest
  name  = "tutorial"
  ports {
    internal = 80
    external = 8000
  }
}

{} で囲まれたところをブロックと呼ぶそうです。terraform の tf ファイルの文法は比較的単純で、

<BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" {
  # Block body
  <IDENTIFIER> = <EXPRESSION> # Argument
}

というものを繰り返していくだけのようです。非常に覚えやすく、シンプルで好きです。

設定ファイルを理解していく

まず上から読んでいきます。

terraformブロック, required_providersブロック

terraform ブロックは、その terraform の実行単位でのさまざまな設定を行えるブロックです。今回は後ほど解説する required_providers のみを記載していますが、required_version のように terraform の実行バージョンを設定したりもできます*1

version を使用する場合には、たとえば

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 2.70"
    }
  }
}

といったように書けるようです。

今回記載されている required_providers ブロックは、どの「プロバイダ(provider)」を利用するかを規定するブロックになっています。プロバイダという概念については後述します。

required_provider 内には2つの識別子が存在します。

  • source address と呼ばれる required_provider にのみ定義できる識別子。
  • local name と呼ばれる terraform のモジュール内ならどこでも定義できる識別子。
  required_providers {
    docker = {
      source = "terraform-providers/docker"
    }

上記のような記述をした場合、

  • source = "terraform-providers/docker"部分にあるのが、先ほどで言うところの source address。この場合の source address は terraform-providers/docker となる。
  • docker = { 部分にあるのが、先ほどで言うところの local name。この場合の local name は docker となる。

なお名前がコンフリクトすることもあるようです。そして、terraform はどの名前空間に紐づくものかまでを詳細には推測できないようなので、できるだけがんばって名前がダブらないようにする必要があるようです。

このあたりの詳しい仕様はこちらのページに記載されています。上述ならびに以降も同様ですが、ほぼこのドキュメントの内容を日本語にまとめ直していきます。

provider ブロック

まずプロバイダ(provider)という概念を理解します。プロバイダというのは terraform 自体とは独立したプラグインです。なので、バージョン管理も terraform からは分離しており、独自のバージョニングを保持しています。

Terraform Registry というところに数多くのプロバイダが登録されており、terraform においてはそれらを利用しながら設定ファイルを組み上げていくことになります。たとえば AWS のプロバイダや、Docker のプロバイダといったメジャーどころはだいたい抑えられているように見えます。

provider で重要なことは下記です。

  • provider という宣言によって始まる。
  • local name で識別する。
  • provider ブロック自体は tf ファイルのルートに書く必要がある。

provider ブロックの基本的な文法は下記のような形でしょう。

provider "<LOCAL NAME>" { ... }

先ほど説明した local name が provider の横に入ります。つまり、required_providers で先ほど docker という local name を設定したので、provider にも docker が入る必要があります。したがって、下記のような記述になっています。

provider "docker" { ... }

これだけではわかりにくいので少し他の例も利用しておくと、provider ブロックの中には例えばリージョンの情報といった arguments が入れられます。どうやらプロバイダによって arguments は異なっているようです。たとえば AWS のプロバイダを使用してみたい場合は、次のように記述できます

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 2.70"
    }
  }
}

provider "aws" {
  profile = "default"
  region  = "us-west-2"
}

provider ブロックの中に独自の arguments が登場していることがわかります。

provider に関する詳細な仕様は、このページにあります。

resource ブロック

最後に登場してきたのは resource というブロックです。

リソース(resource)というのは、terraform が生成・管理する最小限の要素のことをいいます。たとえば先ほどの例ですと、docker image と docker container というリソースを terraform が生成し、管理することになります。

resource で重要なことは下記です。

  • リソースというのはプロバイダが提供する、インフラプラットフォームを管理するためのもの。
  • resource type というものが存在する。
  • local name が resource type ごとにさらに付与されている。

resource は次のような文法をもっています(と、推察しました)。

resource "<RESOURCE TYPE>" "<LOCAL NAME>" { ... }

今回は2つの設定を記述しました。

resource "docker_image" "nginx" {
  name         = "nginx:latest"
  keep_locally = false
}

resource "docker_container" "nginx" {
  image = docker_image.nginx.latest
  name  = "tutorial"
  ports {
    internal = 80
    external = 8000
  }
}

上から順に紐解いていくと、

  • resource type が docker_image で、local name が nginx のリソースを作りたい。
  • resource type が docker_container で、local name が nginx のリソースを作りたい。

ということがわかります。

provider と resource の関係性をコードで示すと次のようになるかもしれません。

さて、この resource type 以下の情報は一体どこに書いてあるのか、というのが気になり始めると思います。これは Terraform Registry の各プロバイダのページに(それなりに)詳しく書いてあります。なので、開発していく際にはこれらを見ながらやっていく感じになるのでしょう(まだ初心者なのでわからないですが)。

実行

上記まで理解できたところで、main.tf がおそらく完成しているはずです。あとは、

terraform init

して、

terraform apply

して設定内容を反映し、localhost:8000 にアクセスすると、無事に nginx が立ち上がっているはずです!

terraform destroy 

で、apply にて生成したものを削除することができます。

terraform init

何をしているか気になったので、さっそく叩いてみました。

terraform init --help
Usage: terraform init [options] [DIR]

  Initialize a new or existing Terraform working directory by creating
  initial files, loading any remote state, downloading modules, etc.

  This is the first command that should be run for any new or existing
  Terraform configuration per machine. This sets up all the local data
  necessary to run Terraform that is typically not committed to version
  control.

  This command is always safe to run multiple times. Though subsequent runs
  may give errors, this command will never delete your configuration or
  state. Even so, if you have important information, please back it up prior
  to running this command, just in case.

  If no arguments are given, the configuration in this working directory
  is initialized.
  • 初期ファイルを設定したり、リモートにある状態をロードしたり、モジュールをダウンロードしたりするようです。
  • terraform を実行するのに必要なローカルデータを読み込むコマンドのようです。
  • 冪等のようです。

terraform apply

同様に何をしているか気になったので叩いてみました。

terraform apply --help
Usage: terraform apply [options] [DIR-OR-PLAN]

  Builds or changes infrastructure according to Terraform configuration
  files in DIR.

  By default, apply scans the current directory for the configuration
  and applies the changes appropriately. However, a path to another
  configuration or an execution plan can be provided. Execution plans can be
  used to only execute a pre-determined set of actions.
  • 指定ディレクトリに存在する terraform の設定ファイルを読み込んでビルドするか変更を更新するかを行ってくれるようです。
  • 脳死でも適切に変更を検知して状況を適用してくれますが、別の設定ファイルや実行計画を与えたりもできるようです(これは使ったことがないので、正しい説明を私がしているかわかりません)。

感想

  • たとえば AWS CDK は非常にいいツールではあるのだが、汎用プログラミング言語で書くとなると terraform のようにシンプルな宣言的な記法にはなかなかしづらい。
  • どうしてもクラスや関数と言った、本来的に設定ファイルの記述とは関係ない部分の影響を受ける。
  • もちろん、汎用プログラミング言語で書けるというのは、たとえばアプリケーションエンジニアにとっては、普段使っている言語でそのままインフラの設定まで書けるというメリットもある。これは大きい。
  • 一方でその点 terraform は宣言的な記法でインフラ構成を記述していける。
  • 宣言的な記法は、抽象化が難しいなど一長一短ありそうだが、terraform はそのあたりは上手に解決しているはず(と信じたい)。これからの勉強次第。

*1:terraform は version 0.12 と 0.13 でも結構違うなど、まあまあ破壊的変更が入っていて大変だと聞いたことがあります。

Rust の `!` (ビックリマーク、エクスクラメーションマーク、感嘆符、never) 型

タイトルは記号のググラビリティ確保のためにわざとつけています(笑)。動作はすべて、Rust 1.47.0 (stable) です。nightly を使用した場合は nightly と文中に記載しました。

TL; DR

  • Rust には ! という「何も返さないことを示すモノ」が存在する。
  • never 型と呼ぶ。
  • 値を何も返さない計算の表現として使用される。
  • 普段は型強制される(ので、使っていることに気づかないかも)。
  • never 型は安定化されていない。

今回も記事が長くなってしまいました。ちゃんと調べると、結構いろいろ書くべきことがありボリューミーになってしまいました。好きな場所をかいつまんでお読みください。

基本

! = never 型

! は Rust では never 型と呼ばれます。

これは他のプログラミング言語型理論などではボトム型あるいは発散型と呼ぶことがあります*1。他のプログラミング言語でも同様の概念が存在しています。Scala ではボトム型は Nothing として表現されます。TypeScript では Rust と同様に never として型をつけます

たとえば、

fn diverges() -> ! {
    panic!("This function never returns!");
}

この関数は値を何も返しません。それでは unit 型と同じなのでは?と思うかもしれませんが、unit 型は Rust においては中身のない Tuple という値なので、実質的に値を返していることと同義になります。そうではなく、never 型は文字通り値を何も返さないことを示します。

このように何も返さない関数を diverging functions と呼びます。この型は、関数や計算が発散していることを示しています。関数や計算が発散しているとは、つまり値を呼び出し側に返さないことを意味します。

never 型はどこに出てくるのか

たとえば loop キーワードは never 型を返します。下記はコンパイルがしっかり通ります。

fn print_forever() -> ! {
    loop {
        println!("永遠に。。。");
    }
}

その他、Rust では breakcontinuereturnpanicunimplemented マクロ*2unreachable マクロ*3などが never 型を返します。

ここまでの例では、いくぶん暗黙的な型変換の例であったり、あるいは関数の戻り値としてしか ! を登場させてきませんでした。これは意図的で、現在の stable Rust では never 型は関数の戻り値でしか指定できません。基本編では説明しませんが、のちの発展編ではそのあたりの経緯を少し説明していますので、興味がある方は発展編まで読み進めてください。

Coercing

never 型はいろいろな型に型強制(type coercing; coercing の読み方はこの動画)できます*4。型強制は要するに、ある型と、必要とする型とが異なる場合に、自動的に型の変換を挟むことです。たとえば下記の例では、!i32 に型強制(自動的な型変換)が行われています。

fn main() {
    // 最終的な型は i32 として判定される。これは、型強制が起きているためにこうなる。
    let result: i32 = match answer() {
        Ok(v) => v, // こちらの腕は `i32` を返す
        Err(err) => panic!("unexpected error") // こちらの腕は `!` を返す
    };
    println!("{}", result);
}

enum Error {}

fn answer() -> Result<i32, Error> {
    Ok(1)
}

通常、パターンマッチの各腕の型は最終的には一致している必要があります。つまり、たとえば Err(err) => panic!(...)と書いている箇所に、Err(err) => "unexpected error"と書いて &str 型にしてしまうと、この時点でコンパイルエラーになります。

fn main() {
    // 最終的な型は i32 として判定される。これは、型強制が起きているためにこうなる。
    let result: i32 = match answer() {
        Ok(v) => v, // こちらの腕は `i32` を返す
        Err(err) => "unexpected error" // こちらの腕は `&str` を返す
    };
    println!("{}", result);
}

enum Error {}

fn answer() -> Result<i32, Error> {
    Ok(1)
}
error[E0308]: `match` arms have incompatible types
 --> src/main.rs:5:21
  |
3 |       let result: i32 = match answer() {
  |  _______________________-
4 | |         Ok(v) => v, // こちらの腕は `i32` を返す
  | |                  - this is found to be of type `i32`
5 | |         Err(err) => "unexpected error" // こちらの腕は `&str` を返す
  | |                     ^^^^^^^^^^^^^^^^^^ expected `i32`, found `&str`
6 | |     };
  | |_____- `match` arms have incompatible types

型強制は、型同士のミスマッチが起きたタイミングで一度チェックされます。先ほどもおそらく一度チェックが走っているのでしょう。結果、 &stri32 には部分型等の関係性が何もなかったために変換は起きなかったというわけです。

一方で never 型は例外的で、すべての型のボトムに存在する型のため、!i32 の部分型になっています。これによって i32 への型強制が起き、結果 i32 として型が判定されます。

たまに出てくる、すべての型の基底にあってどんな型にも強制できる型という理解で、さしあたっては大丈夫です。

発展

以下は少し発展的な内容になります。

never 型はまだ安定化されていない

実は never 型はまだ型としては「本格的には」安定化されていません*5。たとえば、次のような明示的な never 型の変数における型宣言は、まだ stable コンパイラではできません。nightly を使用し、feature を有効化する必要があります。

stable コンパイラ 1.47.0 で下記コードをコンパイルしてみます。

fn main() {
    let p: ! = panic!("never type is experimental!");
}
error[E0658]: the `!` type is experimental
 --> src/main.rs:4:12
  |
4 |     let p: ! = panic!("never type is experimental!");
  |            ^
  |
  = note: see issue #35121 <https://github.com/rust-lang/rust/issues/35121> for more information

これを回避するためには、#![feature(never_type)] を使用する必要があります。下記は nightly 1.49.0 を使用してコンパイル可能でした。

#![feature(never_type)]
fn main() {
    let p: ! = panic!("never type is experimental!");
}

std::convert::Infallible

Rust 1.34.0 から導入された std::convert::Infallible という型を使用すると、「決してそこでエラーが起こらないことを示す」ことができます(できることになっています)。決してXXXが起こらない――これは never 型が本来担うべき役割なように見えますね。

use std::convert::Infallible;

fn main() {
    // このコードはコンパイルが通る。
    // loop は `!` 型であることに注意。
    let p: Infallible = loop {
        println!("never fail but print forever")
    };
}

この std::convert::Infallible の定義は実は空の enum になっています

pub enum Infallible {}

将来的にはこの Infallible は下記のように ! のタイプエイリアスとして定義される予定のようです。

pub type Infallible = !;

ただこれは、あくまでエラーがないことを示すがための型のように思います。すべての never 型を代表するような一般性はないかもしれません。

まったく異なる Infallible という型が、なぜ先ほどのように ! の代用できるかというと、「基本」で示したように裏で型強制が起きているからなのでしょう。! はボトム型である以上実質どの型にも型強制されうるので、結果表面上は Infallible = ! なように見えるというわけです。そして、Infallible は値をもたないので、実質「値を決して返さない」never 型の機能も満たせているというわけです。

例としては Result 型に対する使用でしょう。本来エラーが返ることはない操作に対して、なにか他の実装との都合でどうしても Result 型を使用する必要がある場合に、「決してエラー側は返すことがない」を表現するために Result<A, Infallible>(A は任意の型)といった宣言をして回避するという手法が取れます。将来的には Result<A, !> としたいのです。

たとえば std::convert::Infallible のドキュメントには TryFrom の例が記載されており、TryFromOk しか返さないことを Result 型を使用しつつも表現できています。

impl<T, U> TryFrom<U> for T where U: Into<T> {
    type Error = Infallible;

    fn try_from(value: U) -> Result<Self, Infallible> {
        Ok(U::into(value))  // Never returns `Err`
    }
}

普段のアプリケーションで意図的に never 型(Infallible)を使用したいというのは稀かもしれませんが、頭の片隅に置いておきたい話ですね。

で、なんで Rust には ! がいるの?

基本編でも書いたように、最終的な挙動や意味合いは unit 型(unit 型は Rust では要素0個の Tuple (())でしたね)と似ています。ではなぜ、わざわざ never 型を区別して用意しているのでしょうか?

詳しい人が StackOverflow にて回答してくれています。簡単に要約していきます。

stackoverflow.com

まずひとつめの理由は、意味のないコード片をコンパイルタイムで拾い上げられるからです。

たとえば、Rust では下記のようなコードを書くと警告が出ます。このコード片では exit が never 型を返します。println!exit より後ろにあるため、決して呼び出されないからです。これらを解析することで、下記のような警告を出せるわけです。

fn main() {
    std::process::exit(0);
    println!("unreachable");
}
warning: unreachable statement
 --> src/main.rs:3:5
  |
2 |     std::process::exit(0);
  |     --------------------- any code following this expression is unreachable
3 |     println!("unreachable");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^ unreachable statement
  |
  = note: `#[warn(unreachable_code)]` on by default
  = note: this warning originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

Rust ではやはり他のモダンなコンパイラと同様にデータフロー解析が行われるので、そこで利用されるようです。

次は、Rust のコンパイラバックエンドとして使用されている LLVM の最適化に使用できるようです。以下は私が LLVM にまったく詳しくないので間違ったことを書くかもしれません。

never 型は上記のコンパイルエラーからもわかるとおり、到達しないコード片の解析に利用できます。同時に、 LLVM にもそのことを伝えられるようです。

先ほどのコードを LLVM IR に直してみてみると、unreachable という文字列が見られます。ドキュメントを参照してみると、unreachable という命令があります。これは、ドキュメントによるとオプティマイザにそのコード部分が使用されないことを伝える役割をもっているそうです。

; Function Attrs: nonlazybind uwtable
define internal void @_ZN10playground4main17h9866004d1365e02aE() unnamed_addr #1 !dbg !163 {
start:
; call std::process::exit
  call void @_ZN3std7process4exit17h58c2c865f4c85e96E(i32 0), !dbg !166
  unreachable, !dbg !166
}

あるいは、先ほどの例にあるように Ok しか返さない Result 型で Err 側を呼び出してしまった場合などについては、コンパイラOk のケースだけ考えればよくなるというメリットも示されています。

他の言語での never 型導入のメリットは、Rust とは少し事情が異なるかもしれません。調べてみてもおもしろいかもしれません。

Rust ではなぜ、関数の戻り値だけにしか ! を使用できなくしてあるの?

まず結論ですが、これは調べてみましたがわかりませんでした。コンパイラをもっとしっかり読んでみるべきかもしれません。読んでみてなにかわかったら追記をしようと思います。以下は調べた話を目的もなくつらつらと書きます。

なんとなくなんですが、型推論の問題があるのかなと思いました。変数の束縛時やトレイトの型引数には ! を直接宣言することは今のところ無理で、関数の戻り値のように明示的な場合はOKということは、型推論や型解決のフェーズでなにか問題があったりするということなのでしょうか。

never type は一度 revert されています。経緯に関してはこの記事がとても詳しいです。このコミットによると、まず戻されたのは stable にするためのスイッチオンの部分だけのようです。

never 型の実装の追加に関する PR はないかと少し探してみました。すると、この PR がそれに該当したようで出てきました。読んでみると、どうやら型チェック周りの実装を修正している形跡があります。そしてこの実装は、(現在のコンパイラとは大きくディレクトリ構成が違う気がするので推測になりますが)現在もコンパイラに残っているようです。つまり、スイッチさえ切り替えればオンにできる状態なのかなと思います。

えー、なんの答えにもなっていないので、私の疑問をまとめておくと:

  • 関数の戻り値と、変数の型宣言やトレイト等の型引数では、指定可能な型が異なる実装をされている?
  • なぜ、関数の戻り値には指定して OK としてあって(これは私の予想では ! は型強制を走らせられるため、最悪使う側で型を解決し、! をなくした状態で型推論に乗せられるから)、その他の箇所で自主的にユーザーが設定するのは NG としたのか(私の予想では、実装当時 !型推論アルゴリズム上なにか解決できないものが存在していたから)?
  • 実装場所どこー

です。Rust の型周りをもう少し勉強しないとわからなそうです。

*1:これは型理論の標準的な入門書とされる TaPL にも載っています。TaPL には、「Bot は空ということである。つまり、型 Bot の閉じた値は存在しない。(...)Bot が空であるからといって、役に立たないということにはならない。むしろ Bot は、ある種の操作(特に、例外の送出や継続の呼び出し)が戻ってこないはずであるという事実を表現するのに大変都合がよい方法である」(p.150)と書かれています。Bot というのはボトム型のことです。

*2:まだ実装していないことを示す際に便利なマクロです。

*3:その分岐・ブロックには決して処理が到達しないはずであることを示すマクロです。

*4:頭痛が痛いみたいな感じがしますね。

*5:んん?さっき型って言ったじゃん!と思うかもしれません(私も思いました)。いくつかのPRを見ていると、full-fledged (羽が生えきった、という意味になるんですが、日本語の文脈に直すなら、要するに完全体とかフル装備とか本格的とかそういう意味です)という単語が出てきます。現状の Rust の never 型は full-fledged ではない状態ということになります。という意味です。たしかに、関数の戻り値としては使用可能だけど、変数宣言や型エイリアス、型引数には使用できないというのは片手落ち感がありますよね。