Don't Repeat Yourself

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

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 する形を採用していました。

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

社会人5年目が終わった

社会人5年目が終わってしまいました。社会人4年目の終わりにも記事を書いたので、毎年恒例にしていきたいと思って今年も書きます。

今年できたこと

5年目で目標としていたもの

データサイエンスは、部署の変更に伴って触れる機会が多くなり、また自分でも一通り Kaggle の問題を解けるようになりました。ひとまずデータサイエンティストがどういう手順で処理をしていくかを知ることができて、私の中では満足した結果を得られたと思います。

分散システムについては、今年何冊か本を読むことができました。『Data Intensive Applications Design』『Designing Distributed Systems』という本を以前おすすめしていただいて読み、これらの概念を実際に業務でいくつか使って実装できました。今年はさらにマイクロサービス関係の知識を深めていきたいと思ったので、1冊か2冊くらいは深堀りできそうな本を読んで、また実際のプロダクトのアーキテクチャにいかしたいなと思っています。

関数型プログラミングの基礎分野については、今年大幅に進歩したと思いました。離散数学に入門して群論などを勉強できたのと、それで少し数学に慣れて、圏論の本も読み通すことができました。関数型プログラミングにおいては多くのモナドを組み合わせて実装を行っていくのですが、実際にモナドをどのように使ったらよいのかという話は、Scala関数型プログラミングの側面を使って DDD アプリケーションを構築する『Functional Reactive and Domain Modling』という本を通じて知識を深めることができました。これもまた、実際の業務に少し導入できました。

アルゴリズムについては、今年もまたサボってしまいました。というか、あまり興味がわかないのかもしれません。ただ、外資系のテック企業に就職する際には必須になってくるので(それに加えて英語もなんですけど)、キャリアを逆算するとかならず必要になると思います。どこかで絶対時間を作ってやります。どうやったらモチベーションが湧くかなー。

総じて今年は仕事が忙しく時間が以前ほどは取れませんでした。が、取れなかったなりに多くを勉強できた気がするので、私の中では満足です。もっとやれた、と思うと切りがなくなってしまいますし、ライフバランスが崩れてしまいそうなので無理はいけません。

仕事

  • テックリード2年生
  • 1からサービスを実装してリリースまで通した

まず、テックリード2年生でした。が、後述するように開発していたサービスに時間を大きく持っていかれることになってしまい、結果としてテックリードらしいことがあまりできなかったなと思っています。一方で、この4月からまた別のプロダクトを見ることになったので、そこでようやくテックリードらしい仕事ができるんじゃないかと思っています。

テックリードも2年生になると、見る幅が広がってきます。とくに職位(グレード)が、実は今年は通年で2段階上がったんですが、求められる仕事のレイヤーが変わってきた気がします。4月からは新人研修の担当になりました。また、全社の採用イベントに呼ばれることも増えました。部署の代表として何かに参加する機会も増えてきました。自分のプロダクトの成果とそういった組織全体の成果の評価比率のすり合わせをしていかないといけません。

仕事の大きな成果としては、やはり1からサービスを実装してリリースする経験をできたことだと思います。私はもともとアプリケーションエンジニアなので、AWS はあまりよくわかっていなかったし、またすでにリリースされているサービスをエンハンスするキャリアパスが多かったため、CI/CD などを自分で用意するという経験には正直乏しかったです。が、1からサービスをリリースしたことで、Docker コンテナを自分で考えて作って、Kubernetes の Pod の構成とかを考えて、Kubernetes 上の Pod に上手にアプリケーションを CD するところまで考えてやりきることができました。もちろん、人の手を借りはしたものの、0から100まで通して見ることができたのは、とても大きい経験でした。EKS を使った実際の構成はこの記事に書いてもらいました。構成の素案を私で考え、記事を書いてくれた彼と一緒にブラッシュアップしていきました。

Web のアプリケーションについては一通り見られるという自信はつきました。また、もともと得意だった JVM 上のパフォーマンスチューニングなどの強みも見つけられました。しかし、私はあくまでスペシャリストを目指したいと思っているので、一体どこに強みを置くかは最近の悩みでもあります。

自分の思う強みは他人に言われたものが正しいと思うものの、今年発見できてなおかつ自覚のある強みが1つあります。私はたぶん、抽象的に物事を考えたり、抽象度の高いものに言葉や名前を与えて整理するのが人よりも得意な人種の可能性が高いです。この強みが活かせる部分は、おそらくソフトウェアアーキテクチャ(システムアーキテクチャ)の考案なんだと思います。よりよいシステムのアーキテクチャを考えられるエンジニアになるために、もう少しコードを書く時間を増やしたいです。今年は理論の強化をしたので、実装の強化がしたいなと思っています。

個人で好きにやったもの

Rust.Tokyo を主催しました。プログラミング言語のカンファレンスを開くというのは初めての経験だったので、いろいろ戸惑いましたがなんとか無事終えることができてよかったです。みなさまに感謝。

Rust の LT での登壇については…今年は上述したとおり仕事が不意に忙しくなることが多く、「登壇するぞ!」と1ヶ月前に意気込んでも、その後2週間くらい大変で時間が取れず、結果的に発表が中途半端になってしまったなと思うことが何度も何度もありました。これは、素直にごめんなさいです。

Rust で登壇していたのは、そもそも私が仕事で Rust を使ってみたいなと思っていて、自分が登壇していればそれを口実に技術を導入しやすくなるからという裏の目的がありました。実際、仕事で Rust を使う機会が増えてきていて、この裏の目的は達成できています。それもあって、今年はちょっと登壇の回数を減らすと思います。

Rust のコミュニティには新しい人がたくさん増えてきていて、盛り上がりを感じます。今年度も Rust を楽しんでいきましょう!

また、多くの車輪の再発明もしました。具体的にはコンパイラ、CPU エミュレータ、ちょっとしたブラウザなどです。とりあえず疑問に思ったら実装してみて理解するというやり方、楽しいのでありだと思います。

プライベートについては、関わる人も増えてきて、またいろいろ変化もあったりして、自分ひとりの人生ではなくなってきたなという思いが強いです。今後も周りの方にいい影響を与えられる人間でありたいので、日々の研鑽を怠らないようにがんばります。

今年度(6年目)にやりたいこと

仕事は仕事でもう目標を立ててしまったので、主に自分の勉強の話を今年も書いておきたいと思います。

  • Write Code Everyday する: ちょっとやってみたいんですよね。ただ私、会食とかが多くて帰るのが0時とかそういった生活なんですが、どこまでやれるか…本当の Everyday はできないかもしれないんですけど、少しでもいいから業務以外のコードを書く日を増やしたいです。Project Euler とかなら目標立てやすいかな?あとは Leet Code を1日1問解くなど。いろいろ考えられますね。
  • いろいろ論文読みたい: なにかの役に立つわけでもなく、また、研究者でもないので完全に趣味ですが、そろそろ基礎知識がついてきたと思うので、論文読んで入門以上の知識を深めていきたいです。技術書を買っていると、永遠に入門者から抜けられない気持ちにたまになるので…
  • RISC-V エミュレータ作りたい: 作ってる人がいて、実際に xv6 が動いたようなので、自分もやってみたいなと思いました。興味があります。
  • 群論をはじめとする代数学を深めたい&余力があったら機械学習周りの数学やりたい: 証明込みで楽しみたいです。証明読むの好きなんですよね…。ということで、群論については興味をもったので、本格的な数学書を手元に用意しました。

あとは、その頃に発売した技術書を買っては読むみたいなことは、引き続きやっていきたいと思っています。

今さらだけど dotfiles を書いた

会社で使っている PC を変えてもらったんですが、環境構築が大変だなあと思ったのでいろいろ同僚に教えてもらいながら dotfiles を整えてみました。

とりあえず言われたことは「oh-my-zsh を使うんじゃねえ」だそうです🤣 詳しいことはわかりませんが、メンテナンスのされ具合や脆弱性の放置度合いがあまりよくないのだそう。

(シェルは適当に Mac デフォルトのものを使っていたり、エディタの設定も大してやらないなど、そういうものにまったく無頓着だったのを少し反省しました。IDE 上で普段生きているため、コードさえ書ければいいやの人種でしたので…)

今回の目的としては、私の Mac さえ設定できればいいというものです。実際 Mac 以外の OS は使わないし、家に Raspberry Pi もあったりするんですけど、Mac で開発したものを転送して動かすということしかやらないので、実質 OS はひとつかなあと思いそうしています。他の方は、様々な開発環境向けに中身を用意していてすごいと思います。

近々自宅用に Mac mini も買おうと思っているので、ちょうどいい機会だと思って整備しました。

構成

  • deploy.sh: .xxx という名前のファイルなりディレクトリにエイリアスを貼ってくれるものです。
  • brew_install.sh: brew install や brew cask install で入れるものを書いておくシェルです。
  • initialize.sh: brew install で済ませられないものを入れておくシェルです。
  • .zshrc: zsh 向けの設定をいくつか仕込んであります。エイリアスもここに設定しています。
  • .gitconfig: git 関係の設定。
  • .vimrc: vim 向けの設定(Vim ユーザーなためです)。
  • .config: このディレクトリを使用するツール向けのものです。

便利だと思ったもの

brew install 系をまとめるシェルを用意できるらしい

いろいろ調べてみたところ、brew install 系をまとめることができるらしく、それを試してみました。たしかにまとまりきってすっきりしたと思います。

https://github.com/yuk1ty/dotfiles/blob/master/brew_initialize.sh

調べてみると、たとえば IntelliJ とかこれまでサイトからダウンロードして使っていた多くのアプリケーションに brew cask install 対応がされていて、これをシェルひとつですべてセットアップできるのはとても楽ちんだと思いました。

ミドルウェアを全部 docker-compose で立ち上げる

ローカルで製作物を動かす際に、MySQL 5.7 を想定して作っているアプリケーションなのに brew install mysql すると今は8系が入ってしまって…というめんどうくささがあったのを、新人さんなどのメンターを通じて前から把握していました。せっかくなので、ミドルウェアは全部 Docker 上で立ち上げてしまうことにしました。

クライアントは Table Plus を使うことにしました。MySQL 以外にも、Redis なんかにも対応しているので。Memcachedtelnet で見られるので問題なし。DynamoDB は、たしかブラウザから見られたはずなのでいいと思います。最悪、毎回捨てコンテナを立ち上げてアクセスすれば大丈夫。

https://github.com/yuk1ty/dotfiles/blob/master/docker-compose.yml

おかげさまで、少し brew install する量が減りました。

もちろん、毎回 docker-compose のあのコマンドを打つのは少し大変なので、エイリアスを仕込んであります。

https://github.com/yuk1ty/dotfiles/blob/master/.zshrc#L28

iTerm2 のテーマサイトがある

あるんですね。

iterm2colorschemes.com

前から Atom のカラーパレットかわいいなーと思っていたので、Atom を入れてみました。すき。

startship

Starship というプロンプトをご存知でしょうか。Rust 製のプロンプトで、普段遣いのシェルがいい感じになります。

git のブランチ今どこにいるとかも表示できて、シェルもかなりいろいろカスタマイズできるので、結構オススメです。

starship.rs

Rust 系のモダンなコマンド

以前 Qiita の記事で話題になっていたかと思いますが、Rust 製のモダンな Linux コマンドの置き換え OSS がいくつか存在しています。せっかくなので、これをいい機会と思って入れてみました。シェルがとてもカラフルになってかわいいです。テンション上がります。

こういったコマンドは UI もいくつか既存のコマンドと比べて改善されていることが多く、色遣いに慣れてきたらすごく生産性が上がりそうだなと思います。

  • bat: cat コマンドのモダン版。カラースキームが設定された状態で cat を表示できるなど。
  • exa: ls コマンドのモダン版。カラースキームが設定された状態で ls を表示できるなど、可視性が高くなっています。
  • ripgrep (rg): 高速な grep コマンド。
  • hexyl: hexdump のモダン版。ダンプに色をつけられます。
  • procs: ps のモダン版。ps コマンドがものすごくきれいになります。
  • ytop: top のモダン版。もはや別物。

https://github.com/yuk1ty/dotfiles/blob/master/.zshrc#L22

わからないこと

  • Python は結局 env 系は何を入れたらいいんでしょうか?pyenv?pipenv?Anaconda?

まとめ

dotfiles を用意しきったので、これで急な買い替えにもバッチリ対応できそうです。よかった。

ただ実はまだ、Kubernetes 周りの環境整備を終えておらず、そのあたりの設定も追加したいと思っています。時間を見つけて追加しておきたいです。

結果はこちらにまとめました。

github.com