Don't Repeat Yourself

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

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

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

参考資料

Shinjuku.rs#10 に参加した #Shinjukurs

ブログ枠で参加しました。なので、書きます。

forcia.connpass.com

RustでProperty-Based Testing

(自分の職場のデスクで聞いていたので、めっちゃ話しかけられて半分くらいしか聞けませんでした…)

Rust で Property-Based Testing する際にはどういったクレートが使えるか?という話でした。

Property-Based Testing というのは、ざっくりいうとシステムの仕様からテストケースを半自動で作成する手法のことです。

テストと実装は多くの場合密結合になってしまうため、たとえば実装側であるデータにフィールドを追加すると、テスト側も修正し、そのテスト内容やエッジケースの検出も隅々まで修正する必要があります。Property-Based Testing を用いると、フレームワークがテストケースを(マクロやリフレクションなどでデータ構造を読み取って)自動生成するため、テストと実装を疎結合にすることができるというのが、この手法のよいところです。

Property-Based Testing についてのプレゼンテーションで、クロネコヤマトの荷物の配送条件に関するテストを実装していく中で、Property-Based Testing を使ってテストをするという内容でした。Rust では下記の2つが候補として上がるようです。

コード例はまた後日考えて書いて載せてみます。

Crate Trendsを作ってみた

Crate Trends というサイトの作者の方が LT をしてくださいました。サイトの紹介がメインでした。Rust には数多くのクレートが存在し、たとえば Web サーバーというキーワードに対して複数のクレートが検索結果として出てくるという状況になっています。このサイトを利用すると、クレート同士のダウンロード数やライセンスなどを比較しながら検討できます。

フロントエンドは Next.js で作成し、バックエンドは actix-web で構築したとのことでした。クレートの情報そのものは crates_io_api というクレートを利用したそうです。コントリビューション等をお待ちしておりますとのことなので、ぜひ気になった機能や追加したい機能がある方はカスタマイズしていきましょう!

github.com

Rust のイテレータを使いこなしたい

最近、 Project Euler を Rust でコツコツと解いているのですが、イテレータIterator)を使いこなせるときっといい書き方ができそうだなあと思う場面が多く、イテレータに改めて入門したいと思いこの記事を書きます。

書き始めていまいちまとまりのない感じになってしまいましたが、せっかく書き始めたので記事を公開して供養しておきます。

内容は、公式ドキュメントか、『プログラミング Rust』に大きくよっています。それらからヒントを得ながら自身でまとめ直した記事です。

今回の記事は JavaScala に前提がある方向けに書いています。一方で、その他の言語出身の方でもある程度お楽しみ頂ける内容にはなっていると思います。イテレータのコンセプトは、言語間でそう大差ないはずだからです。

イテレータとは何か

イテレータとは一連の要素(ベクタ、リスト、文字列、ハッシュマップなど)に対して順番に操作を行う抽象構造をいいます。典型的には、for ループで回している処理はイテレータで表現可能です。先頭から順番に、イテレータ内の要素を捜索し終わるまで順番に連続して、指定した操作をかけていくことができます。

外部と内部

イテレータには外部イテレータと内部イテレータが存在します。まず一言で説明すると次のような説明になるでしょう:

他の言語での例を知るために、 Scala コードを見ながら実装を確認していきましょう。

外部イテレータは、Iterator インタフェースと Iterable インタフェースの両方を駆使しながら実装します。Iterator インタフェースは、次の要素が存在するかを確認し、存在すれば次の要素を取り出します。Iterable インタフェースは、そのデータがイテレーション可能な構造をもっているかどうかを示すインタフェースです。イテレータを返すメソッドをひとつもっています。

trait Iterator[E] {
  def hasNext: Boolean
  def next: E
}
trait Iterable[E] {
  def iterator: E
}

これらを組み合わせて IntegerList というイテレート可能なデータ構造と、IntegerListIterator というイテレータを実装します。IntegerList#iterator() によってイテレータを取り出し、IntegerListIterator#hasNext()IntegerListIterator#next() によって、イテレート処理そのものを実装します。

case class IntegerList(array: Array[Int]) extends Iterable[Int] {
  override def iterator: Int = IntegerListIterator(array)
}
case class IntegerListIterator(array: Array[Int]) extends Iterator[Int] {
  private var index: Int = 0
  override def hasNext: Boolean = index < array.length
  override def next: Int = {
    val r = array(index)
    index = index + 1
    r
  }
}

使う場合には、

object Main extends App {
  val list = IntegerList(Array(1, 2, 3, 4, 5, 6))
  val iterator = list.iterator
  while(iterator.hasNext) {
    println(iterator.next)
  }
}

このように、while ブロックの条件の箇所に hasNext() を使用し、true である限りはイテレータの要素を取り出すという処理を書けば、イテレータはこれで実装できます。外部イテレータでは、while や for などの制御構文の力を借りながらイテレータを実装するわけです。

一方で内部イテレータの場合は少し事情が異なります。内部イテレータの場合、イテレート可能なデータが、イテレーションに関する処理を行うメソッドを内部にもっています。具体的には forEach といった関数がそれです。内部イテレータを実装する場合には、forEach メソッドがどのような処理を行うかを外から注入する必要が出てくるため、クロージャなどの自身で環境を新たに作る機能が追加で必要になります。

イテレート可能なことを示すインタフェースは、たとえば次のように実装できます。先ほどとは違い、forEach 関数が生えていてそこに行いたい処理を投げ込むとイテレータを実装できます。

trait Iterable[E] {
  def forEach(e: E => Unit): Unit
}

case class IntegerList(ints: Array[Int]) extends Iterable[Int] {
  override def forEach(e: Int => Unit): Unit = {
    for (i <- ints) {
      e(i)
    }
  } 
}

実行時は次のようになるでしょう。その際、実行サイドでは制御構文の助けは実質借りていません。

object Main {
  val list = IntegerList(Array(1, 2, 3, 4, 5, 6))
  list.forEach(i => println(i))
}

Rust におけるイテレータ

IntoIterator と Iterator

たとえば Vec<T> 型に生えているイテレータを見てみましょう。すると、2つのイテレータの利用方法があるとわかります。into_iter() というメソッドと、iter() というメソッドの両方が利用可能です。これらには、一体どのような違いがあるのでしょうか。

iter() というメソッドは、Iterator トレイトが司っています。Iterator トレイトは、次の値があるかどうかを確認しある場合は取り出す next() メソッドを保持しています。next() は、次の値が存在すれば Some を返し、そうでない場合は None を返してそのイテレーションが終了したことを示します。

Iterator の中で今回扱う内容をおおよそ理解するために必要なシグネチャを抜き出しておきます。なお、Iteratormapfilter などの「アダプタ」と呼ばれる関数群を保有しており、これらを使いこなすとイテレータを使いこなし始めたとようやく思えるようになってきます。今回の記事ではすべては紹介しませんが、この記事では網羅的にさまざまなアダプタを紹介しています。

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
(...)
}

into_iter() というメソッドは、IntoIterator トレイトが司っています。このトレイトは、「どのようにイテレータに変換されるか」を定義するトレイトです。IntoIterator を実装するすべての型は、「イテレート可能(iterable)である」と呼ばれます。先ほどの例の対応関係では、Iterable インタフェースがそれです。このトレイトが実装されていると、Rust のループに関する制御構文(for など)を使用して、イテレータの中身を走査することができます。

IntoIterator も同様に、今回扱う内容をおおよそ理解するために必要なシグネチャを抜き出しておきます。

pub trait IntoIterator {
    type Item;

    type IntoIter: Iterator<Item = Self::Item>;

    fn into_iter(self) -> Self::IntoIter;
(...)
}

Rust の for ループは IntoIterator と Iterator の各々のメソッドの呼び出しを短く書いたものに過ぎません。ベクタ Vec<T>IteratorIntoIterator を実装していますが、これらを操作するためには次のように実装できます。下記は先ほどの外部イテレータに該当する処理です。

let fibonacci = vec![1, 1, 2, 3, 5, 8];
for i in &fibonacci {
    println!("{}", i);
}

Rust における for ループは、内部的には次のように展開されて解釈されています。

let mut fib_iter = (&fibonacci).into_iter();
while let Some(i) = fib_iter.next() {
    println!("{}", i);
}

for ループは、対象となる &fibonacciIntoIterator::into_iter() を使ってイテレータIterator)に変換し、その後 Iterator::next() を繰り返して呼び出しています。into_iter() すると、Vec はムーブされる点に注意が必要です。Iterator::next() は先ほども見たとおり、要素があれば Some、要素がなければ None を生成します。None の場合は実質終了判定のため、そこで処理が終了するというわけです。これが Rust における外部イテレータの全貌です。

Rust におけるイテレータは最適化の対象になります。アダプタも静的ディスパッチが走るため、躊躇せずに利用することができます。むしろ、最適化の観点から見ると、躊躇せずにイテレータを利用していくことが Rust では重要だと言えるでしょう。

フィボナッチ数列を内部イテレータで表現する

実際に Project Euler の問題を解いて、イテレータを理解してみましょう。今回は下記の問題を題材にします。和訳はこちらのサイトより引用しております。

フィボナッチ数列の項は前の2つの項の和である. 最初の2項を 1, 2 とすれば, 最初の10項は以下の通りである.

1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...

数列の項の値が400万以下の, 偶数値の項の総和を求めよ.

フィボナッチ数列は、前後の数を足して次の数を生成するというのが基本的なロジックです。したがって、2つの数を持つ構造体を作成し、それに対する Iterator を実装することで、フィボナッチ数列イテレータを実装できます。

struct Fibonacci {
    a: i64,
    b: i64,
}

impl Fibonacci {
    // 初期化を行います。
    fn new() -> Fibonacci {
        Fibonacci { a: 0, b: 1 }
    }
}

impl Iterator for Fibonacci {
    type Item = i64;
    // a, b の情報を元に次の数字を計算します。
    fn next(&mut self) -> Option<i64> {
        let x = self.a;
        self.a = self.b;
        self.b += x;
        Some(x)
    }
}

これでイテレータは作成できました。あとは、「数値の項が400万以下の」「偶数の項の」「総和」を、アダプタ(それぞれ左から順に、take_whilefiltersum に対応)を用いて実装すれば実装は完了です。

fn main() {
    let s: i64 = Fibonacci::new()
        // 偶数の項のみに絞る
        .filter(|&f| f % 2 == 0)
        // 400万までのフィボナッチ数列に絞る
        .take_while(|&f| f <= 4_000_000)
        // 総和を算出する
        .sum();
    println!("{}", s);
}

フィボナッチ数列のような数列もイテレータで生成できてしまいますし、さらにそこから数列の絞り込みや上限以下の数列の取得、合計の取得なども、アダプタを用いれば行うことができてしまいます。何より Rust は、こうしたアダプタにも最適化が走るというのが驚異的な点でしょう。

github.com

このリポジトリのコミット履歴をたどると、パフォーマンスがまったく出なかった最初の実装例も見ることができます。笑。

まとめ

  • Rust には Iterator トレイトと IntoIterator トレイトが用意されていて、それらを使うとイテレータを利用できる。
  • Iterator には mapfilter などのアダプタと呼ばれる関数群が用意されている。それらは最適化が走るので、追加コストを気にすることなく利用できる。
  • フィボナッチ数列Iterator を用いて表現し、アダプタで Project Euler の問題を解くというサンプルを確認した。

レッツイテレータライフ🙆‍♀️❤️

async-trait を使ってみる

完全な小ネタです。使ってみた記事です。

Rust ではトレイトの関数を async にできない

Rust では、現状トレイトのメソッドに async をつけることはできません*1。つまり、下記のようなコードはコンパイルエラーとなります。

trait AsyncTrait {
    async fn f() {
        println!("Couldn't compile");
    }
}
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

async-trait

トレイトのメソッドに async をつけるためには、async-trait というクレートを利用する必要があります。

github.com

試す前の準備

この記事では、次のクレートを用いて実装を行います。

  • async-trait: 今回のメインテーマです。
  • futures: 後ほど async で定義された関数を実行するために使用します。

使用したバージョンは下記です。

[dependencies]
async-trait = "0.1.36"
futures = "0.3.5"

async-trait とは

async 化したいトレイトに対して #[async_trait] というマクロを付け足すことで async fn ... という記法を可能にしてくれるスグレモノです。次のコードはコンパイルが通るようになり、自身のアプリケーションで利用可能になります。

use async_trait::async_trait;

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

中身はマクロ

どのような仕組みで動いているのでしょうか。#[async_trait] アトリビュートの中身を少し確認してみましょう。

// (...)
extern crate proc_macro;

// (...)

use crate::args::Args;
use crate::expand::expand;
use crate::parse::Item;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;

#[proc_macro_attribute]
pub fn async_trait(args: TokenStream, input: TokenStream) -> TokenStream {
    let args = parse_macro_input!(args as Args);
    let mut item = parse_macro_input!(input as Item);
    expand(&mut item, args.local);
    TokenStream::from(quote!(#item))
}

Procedural Macros のオンパレード*2のようです。ということで、マクロがどう展開されているのかを見てみましょう。cargo-expand という cargo のプラグインを利用すると、展開後のマクロの状況を知ることができます。

github.com

実際に使ってみると:

❯ cargo expand
    Checking async-trait-sandbox v0.1.0
    Finished check [unoptimized + debuginfo] target(s) in 0.18s

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std;
use async_trait::async_trait;
fn main() {}
pub trait AsyncTrait {
    #[must_use]
    fn f<'async_trait>() -> ::core::pin::Pin<
        Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
    > {
        #[allow(
            clippy::missing_docs_in_private_items,
            clippy::needless_lifetimes,
            clippy::ptr_arg,
            clippy::type_repetition_in_bounds,
            clippy::used_underscore_binding
        )]
        async fn __f() {
            {
                ::std::io::_print(::core::fmt::Arguments::new_v1(
                    &["Could compile\n"],
                    &match () {
                        () => [],
                    },
                ));
            };
        }
        Box::pin(__f())
    }
}

async fn f() というメソッドは、マクロによって fn f<'async_trait>() -> ::core::pin::Pin<Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>> へと展開されています。では肝心の async はどこに行ってしまったのかというと、関数の中にて async ブロックとして処理されています。そもそも async 自体が Future のシンタックスシュガーなので、こういった結果になっているわけです*3

呼び出し

実際に関数を呼び出しをしてみましょう。次のようなコードを書くと、呼び出しのチェックをできます。

use async_trait::async_trait;
use futures::executor;

fn main() {
    let runner = Runner {};
    executor::block_on(runner.f());
}

#[async_trait]
pub trait AsyncTrait {
    async fn f(&self);
}

struct Runner {}

#[async_trait]
impl AsyncTrait for Runner {
    async fn f(&self) {
        println!("Hello, async-trait");
    }
}

これでコンパイルを通せるようになります。実行してみると、

❯ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/async-trait-sandbox`
Hello, async-trait

しっかり意図したとおりの標準出力を出してくれました。便利ですね。

*1:一応 RFC は出ています→https://github.com/rust-lang/rfcs/issues/2739

*2:この記事の主題ではなくなってしまうので Procedural Macros に関する解説はしませんが、この記事で使い方をかなり詳しく解説してくれています。

*3:かなり説明を端折ってしまっています。こちらのドキュメントに詳細が書いてあります。