Don't Repeat Yourself

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

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:かなり説明を端折ってしまっています。こちらのドキュメントに詳細が書いてあります。

tide 0.8.0 を試してみる

現時点で 0.8.1 まで出てしまっていますので、まとめて触ってみます。

変更の概要はこちらのリリースノートに詳しくまとまっています。0.8.0 でかなり大掛かりなモジュールの構造に対する変更が入っており、もともとドッグフーディングのために使用していたコードベースを 0.7.0→0.8.0 バージョンに上げた際に、いくつかコンパイルエラーになってしまったコードがありました。

まだまだ開発途上なので致し方ないのですが、tide はこういったバージョンアップによって既存のコードベースがコンパイルエラーしてしまうことが多いです。本番環境で利用する際には、こういった点にまだ注意が必要だと思います。

具体的に修正の入ったモジュールは、

  • tide::server サブモジュールの削除
  • tide::middleware サブモジュールの削除

でした。この中に Cookie などが含まれていましたので、使用していた方は変更が必要だった可能性があります。

前回の記事は下記です。

yuk1tyd.hatenablog.com

リリースノートで気になったもの

  • エンドポイントで ? の使用をできるようになった
  • Server-Sent Events をできるようになった
  • 静的ファイルのサービングをできるようになった

エンドポイントで ? の使用をできるようになった

リリースノート以上の解説の必要はないと思うので割愛しますが、エンドポイントの実装時に ? を用いてエラーハンドリングをできるようになりました。

use async_std::{fs, io};
use tide::{Response, StatusCode};

#[async_std::main]
async fn main() -> io::Result<()> {
    let mut app = tide::new();

    app.at("/").get(|_| async move {
        let mut res = Response::new(StatusCode::Ok);
        res.set_body(fs::read("my_file").await?);
        Ok(res)
    });

    app.listen("localhost:8080").await?;
    Ok(())
}

リリースノートのコードをそのまま引っ張ってきてしまいましたが、このように書けるようになりました。? の結果が Error だった場合は、自動的に 500 Internal Server Error が割り当てられます。

Server-Sent Events をできるようになった

Server-Sent Events というのは、サーバーからプッシュ通信を行えるようにする機能です。W3C によって提案されている内容です。HTTP/1.1 のチャンク形式を元にした機能で、チャンクの少しずつ送信するという特徴を利用して、サーバーから任意のタイミングでクライアントにイベントを通知できます。WebSocket と似ていますが、WebSocket とは HTTP を利用するという点で異なります。

送られた内容を、JavaScript の EventSource API にて取得します。詳しい仕様

今回は curl で確認できそうなので、curl で確認してみます。

コードはリリースノートのそのままなのですが…

use tide::sse;

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();
    app.at("/sse").get(sse::endpoint(|_req, sender| async move {
        sender.send("fruit", "banana", None).await;
        sender.send("fruit", "apple", None).await;
        Ok(())
    }));

    println!("Server starts");
    app.listen("localhost:8080").await
}

こういった感じで実装し、サーバーを起動したことを確認します。

tide-dog-fooding is 📦 v0.1.0 via 🦀 v1.43.0 took 3m25s 
❯ cargo run
   Compiling tide-dog-fooding v0.1.0 (~/github/yuk1ty/tide-dog-fooding)
    Finished dev [unoptimized + debuginfo] target(s) in 2.86s
     Running `target/debug/tide-dog-fooding`
Server starts

curl を投げてみた結果です。Content-Type が text/event-stream になっていて、ボディも Server-sent Events の形式に沿っていることがわかりました!

❯ curl localhost:8080/sse -i
HTTP/1.1 200 OK
transfer-encoding: chunked
date: Sat, 16 May 2020 06:46:34 GMT
cache-control: no-cache
content-type: text/event-stream

event:fruit
data:banana

event:fruit
data:apple

内部の実装的には、async-sse というクレートをそのまま利用しているようです。async-std 関係のエコシステムがかなり充実してきていますね。

静的ファイルのサービングをできるようになった

掲題の通りですが、静的ファイルを扱えるようになりました。

この修正に伴って、Route#middleware 関数に型引数が追加になり、その型引数 M に Debug を derive しているという条件が追加されました。なので、従来使用していた Middleware の独自実装のすべての構造体に対して、Debug トレイトを継承させる必要が出てきたため注意が必要です。

余談ですが、0.8.0 時点ではディレクトリトラバーサルが可能な状態のようでしたが、0.8.1 でディレクトリトラバーサル対策を行った PR がマージされています。なので、静的ファイルのサービングを行いたい場合は 0.8.0 を利用しないほうがよさそうです。

感想

PR を1つ1つ読むのがおもしろいです。HTTP サーバーフレームワークフルスクラッチした経験はないので、どのように開発が進んでいっているかが追えて楽しいです。引き続き tide の更新情報は追っていきたいです。

ただ、バージョンを上げるたびにわりと毎回破壊的な変更が入っていて、前バージョンまで使っていたコードが動かなくなる、またはコンパイルエラーの潰しこみが必要になります。なので、tide はまだちょっと本番では試せないかなという印象です。

Rust における直観に反するように見えるコード

ちょっとおもしろいツイートを見つけたので、メモがてら書きます。

次のようなコードはコンパイルが通ってしまいます。

fn main() {
    let a;
    let b;
    let z = (a = 10) == (b = 20);
    assert_eq!(z, true);
}

もちろん、a には 10 が代入されており、b には 20 が代入されており、それを比較する z は当然 10 == 20 の式になって false …と直感的には思えてしまうのですが、厳密には異なります。

C や Java などを用いていると、a = 10 の評価後、得られる値は 10 なので、10 と(b も同様に考えて) 20 が比較されていると思われます。これらの言語では、代入は文だからです。

class Main {
    public static void main(String[ ] args) {
        int a;
        int b;
        boolean z = (a = 10) == (b = 20);
        System.out.println(z);
    }
}

このサンプルコードは Java ですが、出力結果は false になります。10 == 20 が成立するからです。

一方で Rust においてはこの方も説明されている通り、a = 10b = 20 は式であり、この式の評価結果の値は ()(unit 型*1)なのです。この値同士を比べる演算が行われるため、() == () となって ztrue になるという話のようです。

これは新しい変数 c を作るとコンパイルエラーから知ることができます。

fn main() {
    let c;
    println!("c?: {}", (c = 50));
}

これをコンパイルすると、

error[E0277]: `()` doesn't implement `std::fmt::Display`
 --> src/main.rs:8:24
  |
8 |     println!("c?: {}", (c = 50));
  |                        ^^^^^^^^ `()` cannot be formatted with the default formatter
  |
  = help: the trait `std::fmt::Display` is not implemented for `()`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
  = note: required by `std::fmt::Display::fmt`

error: aborting due to previous error

このエラーの概要は、() 型に対して std::fmt::Display が実装されていないので、標準出力には用いることができませんよという話です。c = 50 という式はやはり、() として判別されているようですね。

もちろん、a, b には値は代入済みのようで、下記のようにさらに書き直しを行うとそれを確かめることができました。

fn main() {
    let a;
    let b;
    let z = (a = 10) == (b = 20);
    let another_z = a == b;
    assert_eq!(z, true);
    assert_eq!(another_z, false);
}

これは通ります。

しかし、うーん、ちょっと直観に反しますね。

*1:Rust では要素 0 個の Tuple は Unit 型として判定されます。() は要素0個のタプルの値である一方で、それ自身の型は Unit 型でもあります。

tide 0.7.0 を試してみる

Rust の async-std ベースの HTTP サーバーフレームワーク tide を触ります。0.6.0 に引き続き、先ほど 0.7.0 がリリースされていたようなので試してみたいと思います。前回の記事は下記です。

yuk1tyd.hatenablog.com

今回も私が気になるところだけメモします。リリースノートはこちら。

今回の修正で気になったもの

  • http-typesasync-h1 を一気に入れ込んだ。
  • エンドポイント単位でミドルウェアを設定可能になった。

http-types が入った

http-types というのは、HTTP の操作に関する処理をまとめて提供してくれているクレートです。Status や Method などが入っているイメージです。これまでは http クレート を使用していたようですが、http-rs が提供する http-types クレートを使用するように変更が入っています。

わりと大きめの変更で、これまで次のように Response を生成できていましたが、今回からは http-types の Status を使用して生成することになりました。

以前までは

use tide::Response;
let mut res = Response::new(200);

200 を入れておけばよかったのですが、今回からは

use http_types::StatusCode;
use tide::Response;
let mut res = Response::new(StatusCode::Ok);

このように、StatusCode::Ok を入れる必要があります。Response 以外にも、これまで http クレートに依存して処理を書いていた部分は軒並み受け取りが http-types に変更になっています。他に PR を見た感じでは、内部的にかなり修正が入っていますね。

必要なミドルウェアをエンドポイントごとに切り替えられるようになった

たとえば、

  • このアプリで扱うエンドポイント全体向けに独自ヘッダーを挿入する
  • 各エンドポイント用の独自ヘッダーを挿入する

ミドルウェアを追加してみたとします。下記のように実装できます。

use cookie::Cookie;
use futures::future::BoxFuture;
use http_types::headers;
use http_types::StatusCode;
use tide::Middleware;
use tide::Response;

struct TestMiddleware(&'static str, &'static str);

impl TestMiddleware {
    fn with_header_name(name: &'static str, value: &'static str) -> Self {
        Self(name, value)
    }
}

impl<State: Send + Sync + 'static> Middleware<State> for TestMiddleware {
    fn handle<'a>(
        &'a self,
        req: tide::Request<State>,
        next: tide::Next<'a, State>,
    ) -> BoxFuture<'a, tide::Response> {
        Box::pin(async move {
            let res = next.run(req).await;
            res.set_header(
                headers::HeaderName::from_ascii(self.0.as_bytes().to_vec()).unwrap(),
                self.1,
            )
        })
    }
}

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();

    app.middleware(TestMiddleware::with_header_name(
        "X-Global-Test-Name",
        "Global",
    ));
    app.at("/set")
        .middleware(TestMiddleware::with_header_name(
            "X-Set-Cookie-Name",
            "Set-Cookie",
        ))
        .get(|_req| async move {
            let mut res = Response::new(StatusCode::Ok);
            res.set_cookie(Cookie::new("tmp-session", "session-id"));
            res
        });
    app.at("/remove")
        .middleware(TestMiddleware::with_header_name(
            "X-Remove-Cookie-Name",
            "Remove-Cookie",
        ))
        .get(|_req| async move {
            let mut res = Response::new(StatusCode::Ok);
            res.remove_cookie(Cookie::named("tmp-session"));
            res
        });

    app.listen("127.0.0.1:8080").await?;
    Ok(())
}

期待値としては、

  • /set エンドポイント: グローバルと /set 用にセットしたミドルウェアのヘッダーの挿入結果が返却される。
  • /remove エンドポイント: /set 用にセットしたものは入ってこず、グローバルと /remove 用にセットしたミドルウェアのヘッダーの挿入結果が返却される。

です。curl で実行してみるとわかりやすいと思います。

set

❯ curl localhost:8080/set -i
HTTP/1.1 200 OK
content-length: 0
date: Sat, 18 Apr 2020 08:05:12 GMT
set-cookie: tmp-session=session-id
x-set-cookie-name: Set-Cookie
x-global-test-name: Global

remove

❯ curl localhost:8080/remove -i
HTTP/1.1 200 OK
content-length: 0
date: Sat, 18 Apr 2020 08:05:18 GMT
x-remove-cookie-name: Remove-Cookie
x-global-test-name: Global

期待した通りの値が入っていますね。今回はこのテストコードを参考に実装しました。

余談

PR をチェックしていておもしろかった一コマ。

github.com

あまり意識したことがなかったのですが、#[derive(Clone)] すると実質トレイトを継承することになり、State: Clone となってしまうんですが、不必要に State に Clone 制約をもたせることになってしまいます。それを嫌って、Clone トレイトを Service<State> に impl する形を採用していました。

こういう細かいところまで気を配ったことはなかったなあと思ったので、勉強になりました。