Don't Repeat Yourself

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

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 の `!` (ビックリマーク、エクスクラメーションマーク、感嘆符) 型

タイトルは記号のググラビリティ確保のためにわざとつけています(笑)。動作はすべて、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 ではない状態ということになります。という意味です。たしかに、関数の戻り値としては使用可能だけど、変数宣言や型エイリアス、型引数には使用できないというのは片手落ち感がありますよね。

ScalaMatsuri 2020に行ってきた! #ScalaMatsuri

2020/10/17-2020/10/18で開催されていた ScalaMatsuri に参加してきました。お忘れかもしれませんが本職は Scala エンジニアです。会社のスポンサー枠で参加しました。

聞いたセッション

スライドリンク集はこちらに。ありがとうございます。

自由、平等、ボックス化されたプリミティブ型

日本語のセッションでした。Java の歴史をさらって問題意識などを振り返りつつ、Scala の等価性の問題を知ることができました。これは Java 側の実装との根深い問題で、それをよい方向に進ませるためにこれまでいくつか修正が行われてきたようです。

dotty に、等価性に関連する新しい機能が追加されていてなんでだろうと思っていたんですが、Scala の等価性判定には不健全性が含まれていて、それに対する対応を行うために行われた機能追加だったのだということをしることができました。

eed3si9n.com

Caliban: Functional GraphQL Library for Scala

英語のセッションでした。Scala で GraphQL なライブラリ Caliban の設計思想などが紹介されていました。

ZIO がなんか最近あつい気がします。私も使ってみたい。

www.slideshare.net

Dotty ではじめるマルチステージプログラミング入門 / An introduction to multi-stage programming with Dotty

日本語のセッションでした。Dotty の新機能マルチステージプログラミング(MSP)。Dotty はこのあたりがすごく強化されていて、使うの楽しみです。

Tagless-final のところが非常に勉強になりました。私は F[_] くらいの理解しかしていませんでしたが、もともとは埋め込み DSL を構築するための手法で、研究者さんなどは便利だからこれを使うというような話がおもしろかったです。

github.com

Akka Streams vs Spark Structured Streaming

日本語のセッションでした。ちょうど Akka Streams を導入してログのストリーミングをしようとしていたのですが、Spark Structured Streaming という選択肢もあることを知りました。ただ、Spark の方は実質マイクロバッチで、使う際には注意が必要そうに感じました。

www.slideshare.net

モダンなJVMマルチスレッド / Modern JVM Multithreading

英語のセッションでした。JVM のマルチスレッドプログラミングに関するいい復習になりました。ただ、わたしは Quill は推しません。

pjurczenko.github.io

オンラインカンファレンスとしての工夫

非常に完成度の高いオンラインカンファレンスでした。運営の方にはぜひ知見をご共有いただきたいです…!

Rust.Tokyo も今年11月7日〜8日に RustFest Global を開催予定なので、オンラインカンファレンスでどのような工夫をしているのかを見ていました。

  • セッション自体は Zoom Webinar を使用して開催。
    • 各部屋にモデレータの方がいて、会の進行を管理していました。
    • セッション中のチャットは Discord にて行われていました。
  • Webinar 上にて同時通訳を配信。
    • 英語か日本語か選べて、日本語セッションときは裏で英語に同時通訳していました。Rust.Tokyo も本当はやりたい。
  • Discord をフル活用。
    • いわゆる認証を通すと、隠し Discord チャンネルが表示されるようになる。
    • スポンサーブースも Discord 上に存在。
    • チャンネルは参加者が自由に作っているようだった(会の途中で #functor なるチャンネルが作成されるなど笑)。
  • 懇親会は Discord 上にて実施。
    • 私は参加していませんが、ちらっと覗いた感じトークルームを使用して開催されていたようです。

Discord は普段友人とゲームのボイチャをするために使っていたくらいだったので、こんなに機能があったのかと衝撃を受けました(笑)。

最近の社内の Scala の状況

最後に。

個人ブログに会社のことを書いても仕方ないかもしれませんが、まとめておくと:

  • 完全に Go に押されている: 私のいる部署はもともと Scala が強い部署だったのですが、最近では新しいプロダクトが立ち上がるとなると、Go が採用されることの方が増えてきています。Go は社内での導入事例がたくさんあり、また他社でのはやりもあって多く採用されています。
  • 新人さんは Scala を難しいと感じるらしい: 「Scala がわからない」とよく言われます*1インターンを多く受け入れているのでわかるのですが、Java の経験がまったくない方が多く来ます。他のアルバイト先やインターン先は Rails を使っていますという方が多く、まず Java のインタフェースなどの概念の習得に苦戦している印象があります。次に苦戦しているのは Future か implicit。

です。

Web 系で、なおかつうちのようにプロダクトの興亡が激しい会社の場合、「そのときはやっている技術」を使ってプロダクト作った後、とくに更改することなくプロダクトが畳まれてしまうということがあります*2。この「そのときはやっている技術」に Go が入っているので、結果的に Go の採用事例が増えているようには思います。逆に、Scala 製のプロダクトが畳まれて、結果的に採用数が減っているように見えるといったところでしょうか。

新人さんを見ていての苦戦ポイントは、彼ら彼女らは Java に登場する概念に馴染みがなく、結果的にまず Java のエコシステムの習得に少し苦労しているようだという印象です。キャリアの中で Scala第一言語としてやる際に苦戦しているのはまず traitabstract class の違いや、それ以外のポイントですと Future の理解や implicit の理解もなかなかの苦戦ポイントのようです。

プロダクトでは Scala を中心に実装されているので、引き続き Scala エンジニアの採用や育成などは肝になってくるのですが、一方で社内勢力が減ってきているのも事実という現状です。さて、これからの技術戦略をどうしていこうかなというのが私が抱える課題になっています。なんとか Scala の社内での盛り上がりを作っていきたいです。

*1:わからない、をわかるように教えるのが教える側の責任なので、これは教える側に問題があります

*2:これは経営戦略の違いなので、長期間続くプロダクトを作れることとどちらが優れているという話には帰結しません。

My Rust 2021

It will be 3 years since I started to code in Rust. I had started to code 6 years ago, so half of my career have been dedicated to write code in Rust. Over recent years, I organised a Rust conference happened in Japan, and I'm now organising RustFest Global with amazing organisers (big thank you to Flaki, Jan-Erik, and other warm people. We're now working hard, stay tuned on Nov 7th-8th!).

I decided to jot down about Rust 2021. My expectations for the future of Rust are the following:

  1. Support async function in trait (and impl).
  2. Describe the difference between tokio and async-std more clear.
  3. Work on more materials for intermediate / advanced Rustaceans.

Support async function in trait (and impl)

Refs to Support `async fn` in trait methods (async/await follow-up) · Issue #2739 · rust-lang/rfcs · GitHub.

Now the Rust users try to implement the following code, we should use async-trait crate. For instance, the following one will be compile error.

trait AsyncTrait {
    async fn f() {
        println!("Couldn't compile");
    }
}

Then we will get an error:

async-trait-sandbox is 📦 v0.1.0 via 🦀 v1.44.0 on ☁️  ap-northeast-1
❯ cargo check
    Checking async-trait-sandbox v0.1.0
error[E0706]: functions in traits cannot be declared `async`
  --> src/main.rs:8:5
   |
8  |       async fn f() {
   |       ^----
   |       |
   |  _____`async` because of this
   | |
9  | |         println!("Couldn't compile");
10 | |     }
   | |_____^
   |
   = note: `async` trait functions are not currently supported
   = note: consider using the `async-trait` crate: https://crates.io/crates/async-trait

To avoid this error for a meantime, we should add a crate async-trait. Add the following line to Cargo.toml.

[dependencies]
async-trait = "0.1.36"

Add #[async_trait] attribute then we will be able to pass the strict compiler :)

use async_trait::async_trait;

#[async_trait]
trait AsyncTrait {
    async fn f() {
        println!("Could compile");
    }
}

async-trait is an awesome crate that enable us to async code being more flexible. However, I want Rust to support async fn in trait.

In developing web applications, we're used to build them with Dependency Injection (DI). When we're to use DI, interfaces and implementations are decoupled by using trait. For instance, I'm usually architecting components that fetch data from persistence layers as bellow:

#[async_trait]
pub trait TodoRepository {
    fn get(&self, id: String) -> Result<Option<Todo>, AppError>;
    fn insert(&mut self, todo: Todo) -> Result<Option<Todo>, AppError>;
    fn update(&mut self, todo: Todo) -> Result<Option<Todo>, AppError>;
    fn delete(&mut self, id: String) -> Result<(), AppError>;
}

#[async_trait]
impl TodoRepository for TodoRepositoryImpl {
    fn get(&self, id: String) -> Result<Option<Todo>, AppError> {
        // implementation
    }

    fn insert(&mut self, todo: Todo) -> Result<Option<Todo>, AppError> {
        // implementation
    }

    fn update(&mut self, todo: Todo) -> Result<Option<Todo>, AppError> {
        // implementation
    }

    fn delete(&mut self, id: String) -> Result<(), AppError> {
        // implementation
    }
}

The #[async_trait] attribute needs to set both on trait and impl but that's a bit noisy. I wanna define async fn directly as below.

pub trait TodoRepository {
    async fn get(&self, id: String) -> Result<Option<Todo>, AppError>;
    async fn insert(&mut self, todo: Todo) -> Result<Option<Todo>, AppError>;
    async fn update(&mut self, todo: Todo) -> Result<Option<Todo>, AppError>;
    async fn delete(&mut self, id: String) -> Result<(), AppError>;
}

impl TodoRepository for TodoRepositoryImpl {
    async fn get(&self, id: String) -> Result<Option<Todo>, AppError> {
        // implementation
    }

    async fn insert(&mut self, todo: Todo) -> Result<Option<Todo>, AppError> {
        // implementation
    }

    async fn update(&mut self, todo: Todo) -> Result<Option<Todo>, AppError> {
        // implementation
    }

    async fn delete(&mut self, id: String) -> Result<(), AppError> {
        // implementation
    }
}

I feel it more natural in case I use async fn than if use the handy crate.

Describe the difference between tokio and async-std more clear

Every time I introduce Rust to my coworkers (they're web developers as well), I always feel tough to describe the difference between tokio and async-std because there seems to be no clear description or documentation regarding the difference of two runtimes. I can find related posts on Reddit, on the other hand can't find official statements.

Work on more materials for intermediate / advanced Rustaceans

Move on to the next topic. The next one is about Rust books and guides.

Now we have a lot of introduction guides thanks to the community and enthusiastic users but mostly for beginners. We have few kinda advanced guide we can find out the same in other languages as "Effective XXX".

As mentioned in the Rust survey 2019 results, those who have finished "The Rust Programming Language" and used Rust for a long time seem to hope to read advanced material.

The topic will be about (these are a homage to "Effective Python" lol):

  • Rustic Thinking (how to design Rust application with trait and struct, tackling on clone(), hacking with smart pointers, and so on)
  • Functions (introduce some useful functions)
  • Testing and debugging (mocking, integration tests and how to use gdb on Rust, and so on)
  • Speeding up our Rust application

That's just a thought but looks good doesn't it?

That's it

That's it. Good luck everyone!

x.py について

x.py とは

Rust コンパイラ向けのツールです。Python が事前にインストールされている必要があります。

Rust コンパイラのビルド

とっても簡単。下記コマンドを実行しましょう。

まずはクローンします。

git clone https://github.com/rust-lang/rust.git

ビルドに必要なツールが README に記載されているので、それらをインストールする必要があります。私は macOS でビルドしたのですが、macOS の場合はだいたいデフォルトで入っているはずです。ただ、makecmakeninja はインストールが必要かなと思います。

https://github.com/rust-lang/rust#building-on-a-unix-like-system

回線の速度にもよりますが、まずまず時間がかかるので、紅茶でも飲みながら待ちます。

cp config.toml.example config.toml
./x.py build && ./x.py install

build を開始するとまずまず時間がかかるので、新しく紅茶を作って待ちます。ちなみに、この build を成功させるためには、10-15 GB 程度の空き領域が必要になります。

ちょっとした小ネタ

ただ、逐一ビルドしていると時間がかかって仕方がないので、そのような場合には ./x.py check が有効です。これは実質 cargo check コマンドのようなもので、ちょっとしたリファクタリングコンパイル結果をチェックするなどといった用途に使用できます。

テストは ./x.py test コマンドで実行できます。

その他使うコマンドとして、./x.py fmt コマンドがあります。これも同様に cargo fmt のように Rust のコードをフォーマットするものです。Rust のプルリクエストのレビューを見ていると、たまにコミット前に fmt を実行し忘れて指摘されているものを見かけます。コミットする前に忘れずに回しておきましょう。

TwitterFuture の Future#collect と並列処理

よく忘れるのでメモします。Scala です。

Future#collect とは

com.twitter.util.Future についている便利関数で、Seq[Future[A]] を受け取り、Future[Seq[A]] を返すことができる。別名 sequence と呼ばれる処理をします。

この関数はリストになった Future を受け取って処理をしていくのですが、途中で1つでも Future が例外状態になった場合は、そこで処理が中断されます。もし、中断せずに処理を続行させたい場合には、Future#collectToTry という関数を利用できます。

この関数のシグネチャ的に、並列処理を裏側でやってくれそうな気がするのですが、通常通り Future を投げ込む限りでは並列処理はやってくれません。いくつか実験して調べてみましょう。

実験内容

下記の実験を今回は行います。

  • タスクを3つ用意する(A, B, Cと名前をつける)
  • 1秒に1回「ticking task N」(N には A, B, C のどれかが入る)と標準出力する。
  • 期待値としては
    • 逐次実行の場合は、A が終了してから B、B が終了してから C と順にタスクが走る。
    • 並列実行の場合は、A, B, C が同時にスタートする。

実験1: 逐次実行することを確かめる

次のようなテストを書くと、その挙動を確かめることができます。テストライブラリには ScalaTest を使用しています。

import com.twitter.util.{Await, Future}
import org.scalatest.FunSpecLike

class ParalleliseTest extends FunSpecLike {

  describe("1: 逐次実行するパターン") {
    it("正しく実行されること") {
      val taskA = Future {
        for (_ <- 0 until 10) {
          println("ticking task A")
          Thread.sleep(1000)
        }
      }

      val taskB = Future {
        for (_ <- 1 until 10) {
          println("ticking task B")
          Thread.sleep(1000)
        }
      }

      val taskC = Future {
        for (_ <- 1 until 10) {
          println("ticking task C")
          Thread.sleep(1000)
        }
      }

      Await.result(Future.collect(Seq(taskA, taskB, taskC)))
    }
  }
}

結果は下記のようになります。IntelliJ の計測だと、逐次実行しているので 29s かかって終了しているようです。

ticking task A
ticking task A
ticking task A
ticking task A
ticking task A
ticking task A
ticking task A
ticking task A
ticking task A
ticking task A
ticking task B
ticking task B
ticking task B
ticking task B
ticking task B
ticking task B
ticking task B
ticking task B
ticking task B
ticking task B
ticking task C
ticking task C
ticking task C
ticking task C
ticking task C
ticking task C
ticking task C
ticking task C
ticking task C

実験2: 並列実行することを確かめる

FuturePool を使用して処理を記述すると並列処理させることができます。これを利用して実装します。val pool = FuturePool.unboundedPool にて、FuturePool を用意しています。

import com.twitter.util.{Await, Future, FuturePool}
import org.scalatest.FunSpecLike

class ParalleliseTest extends FunSpecLike {

  describe("2: 並列処理するパターン") {
    val pool = FuturePool.unboundedPool

    it("正しく実行されること") {
      val taskA = pool {
        for (_ <- 0 until 10) {
          println("ticking task A")
          Thread.sleep(1000)
        }
      }

      val taskB = pool {
        for (_ <- 1 until 10) {
          println("ticking task B")
          Thread.sleep(1000)
        }
      }

      val taskC = pool {
        for (_ <- 1 until 10) {
          println("ticking task C")
          Thread.sleep(1000)
        }
      }

      Await.result(Future.collect(Seq(taskA, taskB, taskC)))
    }
  }
}

結果は下記のようになります。

ticking task A
ticking task B
ticking task C
ticking task A
ticking task C
ticking task B
ticking task A
ticking task B
ticking task C
ticking task A
ticking task C
ticking task B
ticking task A
ticking task C
ticking task B
ticking task A
ticking task B
ticking task C
ticking task A
ticking task B
ticking task C
ticking task A
ticking task B
ticking task C
ticking task A
ticking task C
ticking task B
ticking task A

並列処理(というか、並行処理?)をしていることがわかります。IntelliJ の計測だと 10s 弱かかって終了しているようです。結果の Seq に詰め込まれる要素順はこの場合、必ずしも保証されないようなので注意が必要そうです。

余談: sequence 3兄弟

TwitterFuture には、Seq[Future[A]] (あるいは、Seq[A]A => Future[B] にリフトさせる関数)を受け取り Future[Seq[B]] を返す関数が3つあります。

  • collect
  • collectToTry
  • traverseSequentially

このうち、collect と collectToTry は Seq を受け取った後、中で iterator に変換します。Seq の数が増えてくると遅延評価を利用したくなりおそらく Stream にしたくなると思うのですが、collect に Stream を入れたとしても iterator にて一度展開されるため、遅延評価にならず意味がなくなってしまうように思います。

この点に注意が必要で、私も実際プロダクトで使っていて、メモリに一気に載せているような挙動を示していました。traverseSequentially にしたら、Stream らしい挙動を示しました。

collect は fs.iterator にて確かに Iterator を呼び出しています。

def collect[A](fs: AnySeq[Future[A]]): Future[Seq[A]] =
    if (fs.isEmpty) emptySeq
    else {
      val result = new CollectPromise[A](fs)
      var i = 0
      val it = fs.iterator

      while (it.hasNext && !result.isDefined) {
        it.next().respond(result.collectTo(i))
        i += 1
      }

      result
    }

実装はこちら

  def collectToTry[A](fs: AnySeq[Future[A]]): Future[Seq[Try[A]]] = {
    //unroll cases 0 and 1
    if (fs.isEmpty) Nil
    else {
      val iterator = fs.iterator
      val h = iterator.next()
      if (iterator.hasNext) {
        val buf = Vector.newBuilder[Future[Try[A]]]
        buf.sizeHint(fs.size)
        buf += h.liftToTry
        buf += iterator.next().liftToTry
        while (iterator.hasNext) buf += iterator.next().liftToTry
        Future.collect(buf.result)
      } else {
        Future.collect(List(h.liftToTry))
      }
    }
  }

実装はこちら

一方でどうやら、traverseSequentially の場合はそのような事態は起こらないように見えます。as.foldLeft をすれば、たとえば as が Stream だった場合は、Stream の foldLeft 実装が呼び出されるはずだからです。

def traverseSequentially[A,B](as: Seq[A])(f: A => Future[B]): Future[Seq[B]] =
    as.foldLeft(Future.value(Vector.empty[B])) { (resultsFuture, nextItem) =>
      for {
        results    <- resultsFuture
        nextResult <- f(nextItem)
      } yield results :+ nextResult
    }

実装はこちら

まとめ

  • Future#collect は逐次実行をする。
  • FuturePool と合わせると並列実行になる。

『実践Rustプログラミング入門』を書きました

すごく今更感がありますが、先週末出版しました。

私のプライベートがとても忙しくしばらく書けませんでした。書籍を書きましたのでご報告です。

ちなみに、著者、まだ現物を受け取っていません。書店で現物を触りたいなと思って見に行きましたが、今週末は在庫切れで本屋さんにありませんでした。

電子書籍は調整中です。

私の担当は1章、3章の一部、11章の一部です。

他の共著者のみなさんの記事

(2020/09/22 追記)

共著なのにこの記事のタイトルを「書きました」としてしまったので、私もだぞ、とみなさんがタイトルで煽ってきています(違

どのような本か?

他のプログラミング言語である程度経験を積んだジュニア(業界経験5年未満程度)のソフトウェアエンジニアやプログラマが、次に新しくRustを学ぶ際の道標となることを目指して書きました。今回は、ジュニア向けにわかりやすい内容になっているかを重視したため、フォルシアさんの新卒〜若手のエンジニアの方に監修に入っていただきました。みなさんありがとうございました。

一方で、Rustに関する突っ込んだ説明*1はほぼ省略しているため、プログラミング言語がとても好きな方や、業界歴が20年となるようなシニアの方にはすこし物足りない内容になっているかもしれません。そうした説明は、『プログラミング Rust』や『実践 Rust 入門』が詳しいかなと思っています。

プログラミングRust

プログラミングRust

しかしそういった方でも別の側面で楽しめるように、具体的なアプリケーションの作り方について、かなり多くの解説を加えました。「実践編」以降は各章で1つのアプリケーションを作り上げていく構成にしてあります。とくに国内の本ですと、GUI 、WebAssembly や組み込み開発の解説が入っている本はまだないかと思います。私のような普段 Web しか触らないエンジニアが、組み込み開発もすこしかじってみるなどの用途にも利用できると思います。

余談ですが、書名が組み換えパズルのようになっていて一部で話題になっています。Rust の和書は出版されている限りで、

といった感じで、「Rust」「プログラミング」「入門」「実践」の4つが順番を変えただけのような書名です…*2。もはやネタです笑。

Rust の書名メーカーなんていうものまで出てきました。著者の一人である @qnighy が作ったようです。よかったら遊んでみてください😌

shindanmaker.com

この本の愛称についてですが、表紙に歯車が多く描かれていることから、著者的には「歯車本」なんて呼ばれると嬉しいねという話をしていた(たしか)のですが、SNS を見る限りでは歯車本と自然に呼んでいただけているようです。ありがとうございます!😃💕

著者陣について

フォルシアさんの主催する勉強会で登壇したエンジニアが著者になりました。詳しくは巻末の著者紹介をご覧ください。昨年12月末に各著者に声掛けがあり*3、打ち合わせをしました。

フォルシア社による著者インタビューも公開されています。

www.forcia.com

わたしと Rust

Rust は2017年の初頭くらいから使い始めました。今では 2015 Edition と呼ばれるころの Rust です。

日本の Rust コミュニティへの初参加は、たしか2017年の10月くらいにあった Rust のハンズオンの会が最初だったと思います。その頃は転職が決まっていて時間に余裕がありました。また、後にたまに LT 会などで登壇していました。

LT 会がきっかけで、Rust.Tokyo のオーガナイザーや今回の書籍執筆にお声掛けいただきました。どちらかというとコミュニティを作っている側かもしれません。

書き方、執筆期間について

計画は1月に立てられ、その後 COVID-19 の流行にともなって、著者同士が連絡を取らない期間が4ヶ月ほど続きました。本の企画はよく立ち消えになることもあると聞いていたので、立ち消えになるかなと思いましたが、5月いっぱいくらいで作業をし、6月中旬に脱稿しました。

原稿そのものは GitHub を使用しました。GitHub 上にマークダウン形式のファイルを置いて、そのファイルを書き換えながら原稿を書き上げていきました。

脱稿後は、まず Dropbox で編集者が pdf ファイルを送信し、私たちは Dropbox 上で当初作業していました。しかし Dropbox の閲覧機能があまりに重く、Google Drive にアップロードし直して作業をしました。Google Drive は圧倒的に速かったです。

脱稿→一校→二校→念校→印刷という流れでレビューは行われました。これは他の出版社でも同じでしょうか。各フェーズの間は、だいたい2週間程度です。印刷まではあっという間でした。

本を書くということ

多くの本を書く人がそうなのかもしれませんが、私にとってもやはり、知識を体系的にまとめ直すいい機会だったように思います。たとえば Rust については、これまでの登壇や記事でいくつか書いてきていたため、断片的に知っていることは多々ありました。発表してある程度形になっていたものに加えて、周辺知識を体系的にまとめ直すことができました。その成果は、先日発表したこのスライドに実を結んだように思います。

さらに、体系的な知識のみならず読者層に合わせて文章を構成するという作業が入ります。知っていることを再度まとめ直しつつ、多くの識者が言っていることを参考資料として組み込みつつ、読者が求めている形に構成を組んで文章に落とす必要があります。これが意外に大変な作業でした。

とくに読者のニーズに応えるというのが難しかったです。1章は一度、前の原稿がボツになっています。読者層と合わないと判断したためです。このボツになった原稿は、プログラミング言語をそれなりに触ってきた方(しかも、C/C++ ならびに Haskell などの関数型まで含む様々な言語を)向けに構成されていました。しかし、この本の想定読者は、どちらかというとフロントエンドで JavaScript をメインに触っていましたという方や、PythonJava を普段書いていますという方です。なので、その方たちに興味をもってもらえるように一度書き直しました。ボツになった原稿は近々公開したいと思っています。

結果的に SNS での反応を見る限りでは、1章の構成は違和感なく受け入れられているようで本当によかったです。

私の個人的な話になりますが、本を書くのは小さい頃からの夢でした。大学生の頃はジャーナリストか戦略コンサルタントになりたくて、その分野でいつか本を書く人間になれたらいいなあと思っていたのですが、なんと予想もしなかったソフトウェアエンジニアとして出版することになりました。人生何があるかわかりませんね。

今は個人の時代と言われ、誰でも個人のメディアを持てる時代になってきています。出版も、自費出版や技術書典などのすばらしい企画によって、一部の特権的な人々だけの存在ではなくなってきたように思います。これは本当にすばらしいことです。人間は文字を使って、後世に知識を使える形で残してきたからこそ発展してきました。多くの人が、自身の学んだことをより多くの人に役立ててもらおうと伝えようとすること――これはとても尊い行為だと思います。

しかし、やはり出版社から出版するというのは、それらとは違い「プロのクオリティ」を求められることになります。ステークホルダーの数が自分ひとりのメディアで記事を書くのとは桁違いに多くなります。それだけ情報発信に責任も伴うことになります。その緊張感を味わえたこともまた、大きな人生経験になりました。機会をいただきほんとうにありがとうございました。

次回作

今回気づいたのですが、私は文章を書くのはそこまで苦ではないタイプです。なので、技術書典などに出版してみようかなと思いました。

私は Web を扱う企業で、技術選定の際に Rust が候補のひとつにあがることを夢見ているので、『Rust による Web アプリケーション開発』(元ネタ)という本を次は書こうかなと思っています。async/await に関する話も、そちらでもっと突っ込んで書くことにしようと思います。

*1:たとえば rustc がどのように AST 以降を解釈していくかや、トレイトの理論的な細かい説明など

*2:書名については当初、編集者さんがすでにある程度決めていて、その時点で「Rust プログラミング入門」という名前がついていました。でも、ダブっていると著者が主張して、頭に何かつけようかという話になり、実践的なアプリケーションの開発を重視した本だから頭に「実践」をつけようとなりました。

*3:編集者さんがポロッと言っていたことで印象的なことがあるのですが、業界的に、たとえば Rust なら競合がいるから入門書の出版をやめようか、となる感じになるのではなく、その出版社内でのラインナップの関係で決めるようです。秀和システムさんが Rust の入門書をラインナップにもっておらず、今回の企画が立ち上がったようです。秀和さんだけかもしれませんが。