Don't Repeat Yourself

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

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 ではない状態ということになります。という意味です。たしかに、関数の戻り値としては使用可能だけど、変数宣言や型エイリアス、型引数には使用できないというのは片手落ち感がありますよね。

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 の入門書をラインナップにもっておらず、今回の企画が立ち上がったようです。秀和さんだけかもしれませんが。

riscv-gnu-toolchain を macOS にインストールする

RISC-V のエミュレータを実装する際などに使用する riscv-gnu-toolchain を macOS にインストールしてみたので、メモを残しておきます。

github.com

インストール手順

git clone と make linux でかなりの時間を要します。時間に余裕をもって実行することをおすすめします。

1. clone する

ディレクトリはどこでも構わないので、上記の riscv-gnu-toolchain リポジトリを手順通り git clone します。

$ git clone --recursive https://github.com/riscv/riscv-gnu-toolchain

手元のネット環境にもよりそうですが、おおよそ1時間くらいかかりました。

2. ビルドの前準備

最終的には make でビルドしますが、その前に必要なツールキットをインストールしておきます。

$ brew install python3 gawk gnu-sed gmp mpfr libmpc isl zlib expat

Python3 以外は各ツールをまったく知らなかったので、ちょっと調べてみました。

  • gawk: awkGNU 実装。GNU + awkgawk かな?
  • gnu-sed: 名前の通りだが、GNUsed
  • gmp: GNU Multiple Precision (Arithmetic Library)←頭文字を取って gmp多倍長整数などを扱うライブラリ。
  • mpfr: 多倍長浮動小数点演算を扱うライブラリ。
  • libmpc: 多倍長整数を扱うライブラリ。
  • isl: Integer Set Library の頭文字を取ったもの。整数の集合に関する演算を扱うライブラリみたい。
  • zlib: データの圧縮を扱うライブラリ。
  • expat: XML パーサー。

3. 参照ディレクトリを設定する

今回は、/opt/riscv というディレクトリに成果物をビルドしてもらうことを想定しています。下記コマンドを叩くと、どこを見るかを指定できます。

$ ./configure --prefix=/opt/riscv

ちなみに、今回私が作っている RISC-V エミュレータでは、rv64g というアーキテクチャに対するクロスコンパイルを行いたいので、configure は下記のように設定します。

$ ./configure --prefix=/opt/riscv --with-arch=rv64g

4. Newlib と Linux のクロスコンパイラをインストールする

手順に従ってインストールしていきます。

$ make & make linux

これもかなり時間を要します。30分〜1時間くらいかかったように思います。

5. bin に PATH を通す

PATH を通しましょう。下記のように export するか、

$ export PATH=$PATH:/opt/riscv/bin

.zshrc.bashrc に下記のように設定します。

PATH=$PATH:/opt/riscv/bin

おまけ: アセンブラをバイナリに変える

私が今回やりたかったのは、下記の RISC-V アセンブラをバイナリに変えることでした。なので、最後にバイナリを作成しましょう。

main:
  addi x29, x0, 5
  addi x30, x0, 37
  add x31, x30, x29

上記を add-addi.s として保存しているものとします。elf-gcc で elf を作り、その後 elf-objcopy で Plain Binary に変換します。

riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o add-addi add-addi.s
riscv64-unknown-elf-objcopy -O binary add-addi add-addi.bin

これでエミュレータに通すことができました。

参考資料