Don't Repeat Yourself

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

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

Scalaのtrait、Rustのtrait、そしてScalaのimplicit

Scala の trait と Rust の trait は微妙に使い方が異なる、とよく質問を受けます。たしかに、使い心地は微妙に異なるかもしれません。Scalaオブジェクト指向を中心に設計された言語ですが、Rust はそれを中心に設計されているとは言えません*1。こういった言語設計の差が、trait の使い心地の違いを生み出していると私は思っています。

両者の trait には、共通した特徴もあります。共通した処理をまとめあげるという意味では同じ目的をもっているといえますし、また、「犬は動物である」「猫は動物である」の共通性を示すことで、共通したものをひとまとめに処理しきることもまた可能です。

Scala には implicit という強力な機能が存在します。これは柔軟でスケーラブルなソフトウェアデザインを可能にする Scala の特徴のひとつです。非常にすばらしい機能です。この機能を利用すると、型に応じて実装を切り替えたり、あるいは既存の型に対して機能を追加できたりします。これは、Rust においては trait という概念ひとつに集約されています。ここがおそらく、もともと Rust を触っていた人が Scala を触った際に抱く違いのひとつなのでしょう。

今回は、Scala のエンジニアが Rust に取り組んだ場合、あるいは Rust のエンジニアが Scala に取り組んだ場合によく耳にするこの話について、私の理解の範囲で Scala と Rust の実装上の関係性を少し記しておきたいと思います。ひとつ注意点ですが、あくまで直感的な理解を目的としているので、実装上はこうなるという話のみを記しています。

Scala と Rust は、言語デザインも目的も大きく異なる言語です。たとえば、ScalaJVM 上で動作しそうさせることを目的として最適化された言語なので、裏側が LLVM の Rust とはその時点で設計がまったく異なります。単純比較すること自体、実はナンセンスかもしれません。なのでそういった意味でも、今回は直感的な理解に重きを置くことにします。

型の理論の関係上厳密にはどうしても異なるという話がありましたら、こっそり Twitter などで教えて下さい。

共通する振る舞い

今回見ていきたいのは、次のような挙動についてです。

  • インタフェースで Animal というものを定義する。
  • その具象型として DogCat を定義する。
  • DogCatAnimal という集合に属する存在なのだから、List<Animal> というひとつの型に押し込められる。
  • また、Animal とは全然別の Machine というインタフェースを定義する。具象型として Robot を定義する。
  • Animal を要求している関数に Machine に属する Robot を入れようとしても、ちゃんとコンパイルエラーになる。

同じカテゴリのものを同じように扱えるようにする

よくある「犬は動物である」「猫は動物である」を双方の言語で表現しましょう。これを表現するためには、インターフェースで動物を用意し、各種類の動物を具象型で実装すればよいです。Scala ではすぐに実現可能ですが、Rust ではひと手間必要になります。

Scala で実装する

Scala では次のようになります。

trait Animal {
  def name: String
  def bark(): Unit
}

class Dog(override val name: String) extends Animal {
  override def bark(): Unit = println(s"$name: ワンワン")
}

class Cat(override val name: String) extends Animal {
  override def bark(): Unit = println(s"$name: ニャーニャー")
}

DogCat は、Animal という型と関係性をもつことになるため、次のように List に追加したとしても、コンパイルエラーにはならないはずです。

  val petShop: List[Animal] =
    List(new Dog("ポチ"), new Dog("マル"), new Cat("太郎"), new Cat("花子"))
  petShop.foreach(_.bark())

これを部分的型付け(サブタイピング)と呼びます。interface と実装の関係性以外に、JavaScala ではクラス同士の継承関係を利用しても同様の結果を得られます。

Rust で実装する

ただし、Rust ではそのままではうまくいきません*2。Rust では、下記はコンパイルエラーになります。

fn main() {
    let pet_shop: Vec<Animal> = vec![
        Dog {
            name: "ポチ".to_string(),
        },
        Dog {
            name: "マル".to_string(),
        },
        Cat {
            name: "太郎".to_string(),
        },
        Cat {
            name: "花子".to_string(),
        },
    ];
    pet_shop.into_iter().for_each(|a| a.bark());
}

trait Animal {
    fn bark(&self);
}

struct Dog {
    name: String,
}

impl Animal for Dog {
    fn bark(&self) {
        println!("{}: ワンワン", self.name);
    }
}

struct Cat {
    name: String,
}

impl Animal for Cat {
    fn bark(&self) {
        println!("{}: ニャーニャー", self.name);
    }
}
❯❯❯ cargo check
    Checking pol-prac v0.1.0
warning: trait objects without an explicit `dyn` are deprecated
 --> src/main.rs:2:23
  |
2 |     let pet_shop: Vec<Animal> = vec![
  |                       ^^^^^^ help: use `dyn`: `dyn Animal`
  |
  = note: `#[warn(bare_trait_objects)]` on by default

error[E0277]: the size for values of type `dyn Animal` cannot be known at compilation time
 --> src/main.rs:2:19
  |
2 |     let pet_shop: Vec<Animal> = vec![
  |                   ^^^^^^^^^^^ doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `dyn Animal`
  = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
  = note: required by `std::vec::Vec`

(...)

Animaldyn trait にして、さらに Sized トレイトを実装するとコンパイルが通るようです。要するに、コンパイラに「メモリサイズを教えてくれ」といわれています*3。なるほど、メモリのサイズがわからないんですね。こういうときは、Box が使えます。dyn trait にして、ヒープに寄せてしまいましょう。

fn main() {
    let pet_shop: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog {
            name: "ポチ".to_string(),
        }),
        Box::new(Dog {
            name: "マル".to_string(),
        }),
        Box::new(Cat {
            name: "太郎".to_string(),
        }),
        Box::new(Cat {
            name: "花子".to_string(),
        }),
    ];
    pet_shop.into_iter().for_each(|a| a.bark());
}

trait Animal {
    fn bark(&self);
}

struct Dog {
    name: String,
}

impl Animal for Dog {
    fn bark(&self) {
        println!("{}: ワンワン", self.name);
    }
}

struct Cat {
    name: String,
}

impl Animal for Cat {
    fn bark(&self) {
        println!("{}: ニャーニャー", self.name);
    }
}

これならば、コンパイルは通ります。Scala 側と得られる出力結果は同じになります。

重要なことは、Rust の trait においては、Scala では求められなかったメモリサイズに関する情報を求められるということです。これは、Rust がシステムプログラミング言語であり、ハイパフォーマンス性を保つために多くの物事を静的に解決したい言語であるという点に依拠しています。言語デザインや言語の目的の差からやってくる違いと言えるでしょう。

違うカテゴリのものはちゃんと弾く

さて、両者の言語に共通する trait の使い方として、トレイトに対して境界を設定できるという点があります。

Scala で実装する

たとえば、Scala で次のような関数を書いたとしてみましょう。

object animal {
  def feed[A <: Animal](animal: A): Unit = {
    animal.bark()
  }
}

A <: Animal は、AAnimal を上限境界とするという意味です。

さらに、わかりやすさのために、Animal でない別の種類の存在 Machine を追加しましょう。

trait Machine {
  def name: String
}

class Robot(override val name: String) extends Machine

実際にこれらを使ってみましょう。

  val pochi = new Dog("ポチ")
  val taro = new Cat("太郎")
  val atom = new Robot("アトム")

  animal.feed(pochi)
  animal.feed(taro)
  animal.feed(atom) // Compile Error!

pochi と taro は Animal に属する Dog または Cat なので、feed 関数の実引数として使用可能です。しかし、atom は Animal に属さない(Robot という trait に属する)型なので、feed 関数の実引数としては型変数の制約を満たさないために使用不可能です。

Rust で実装する

Rust でも似たようなことは表現できて、

fn feed<T: Animal>(animal: T) {
    animal.bark()
}
trait Machine {}

struct Robot {
    name: String,
}
fn main() {
    let dog = Dog { name: "ポチ".to_string() };
    let cat = Cat { name: "太郎".to_string() };
    let atom = Robot { name: "アトム".to_string() };

    feed(dog);
    feed(cat);
    feed(atom); // Compile Error!
}

同様にコンパイルエラーです。トレイト境界を満たさないため、関数の引数としては使用できませんといった主旨のコンパイルエラーが検出されます。

~/dev/rust/pol-prac via 🦀 v1.41.1
❯❯❯ cargo check
    Checking pol-prac v0.1.0
error[E0277]: the trait bound `Robot: Animal` is not satisfied
  --> src/main.rs:8:10
   |
8  |     feed(atom); // Compile Error!
   |          ^^^^ the trait `Animal` is not implemented for `Robot`
...
11 | fn feed<T: Animal>(animal: T) {
   |    ----    ------ required by this bound in `feed`

error: aborting due to previous error

たしかに、似たようなことはでき、得られる結果は同じになりました*4

implicit と trait

さて、ここまで長々と説明してきてしまいましたが、ここから implicit がはじめて登場します。Scala の implicit は、次の2つの役割を現在では多く利用しているといっていいでしょう。

  • ある型に対して機能を追加するものとして利用する(Enrich My Library などと呼ばれています)。
  • 型クラス*5として利用する(implicit parameter として Scala には登場します)。

Enrich My Library

Enrich My Library というのは Scala でよく使われる手法で、要するに何かある型があったとして、その型にあとから機能を追加するという手法です。

今回確認したいのは下記のような挙動についてです。

  • Int なり i32 なり、標準で入っている数値型に対して機能を拡張する。

Scala で実装する

適切な例がすぐに思い浮かばなかったので、こちらの記事に掲載されているコードを引用させていただきます。与えられた回数分 A という文字列を標準出力します。

object EnrichMyLibrary extends App {

  implicit class RichInteger(self: Int) {
    def times(block: => Unit): Unit = {
      var n = 0
      while(n < self) {
        block
        n += 1
      }
    }
  }

  3.times {
    print("A")
  }
}

Scala では、このように Int のような特定の型に対して、後付けで機能を追加できる方法があります。拡張メソッドとも呼ばれるようです。3.times というように、3 という Int 型には、従来 times というメソッドは存在しないのですが、新たにメソッドを外から追加しています。

Rust で実装する

少し強引かもしれませんが、Rust では仮に実装し直すとすると、次のような方法が1つ考えられます。

fn main() {
    3.times(|num| {
        let mut n = 0;
        while n < num {
            print!("A");
            n += 1;
        }
    });
}

trait RichInteger<T: Sized> {
    fn times<F>(self, block: F) where F: FnOnce(T) -> ();
}

impl RichInteger<i32> for i32 {
    fn times<F>(self, block: F) where F: FnOnce(i32) -> () {
        block(self)
    }
}

Rust でもやはり、i32 というもともとある型には times という関数は存在しません。しかし、trait を用いて実装を用意してあげることによって、外から機能を追加することができました。

重要なことは、Rust では trait を使うと Scala でいうところの Enrich My Library を実現でき、Scala では implicit を用いるとそれを実現することができるという点です。

implicit parameter

Scala の implicit のもうひとつの重要な機能である implicit parameter もエミュレートしていきましょう。今回確認したい挙動は下記です。

  • 「モノイド」を定義できること。
  • 定義したモノイドを用いて、リストの畳み込み*6を実装できること。
  • 型に応じて呼ばれるモノイドが暗黙的に切り替わること。

モノイド

ここでひとつ Scala や Rust そのものとは関係のない用語を投入させてください。モノイドという用語をこれから使っていきます。これは型クラスの説明の際に使用されることの多い概念のひとつで、もっとも理解の助けになるため使用します。

モノイドというのは、もともとは数学に存在する概念で、次の条件(モノイド則といいます)を満たすものをモノイドと呼んでいます*7

  1. 結合律を満たす: たとえば、演算 + について、(a+b)+c = a+(b+c) が成立しますよね。
  2. 単位元を持つ: たとえば、演算 +単位元は 1 + 0 = 0 + 1 より、0であることがわかります。直感的な理解をするならば、ある元 a に対して e という別の元を与えた際に、a に対して e は一切影響を与えない存在と言えるでしょう。

さしあたっては、「いい感じに2つの物事を足し算させるために必要な概念」だと思ってください。

モノイドは、1あるいは2の両方を満たしさえすれば、どのようなデータ型に対しても適用可能な概念です。つまり、Int 型であっても String 型であっても、1と2のルールを満たすように実装すれば、それはモノイドにできるということです。

そして、型に応じた実装さえ切り替えれば、使用する側は1つのシグネチャで済むようになってきます。コードの抽象化をさらに一段押し進めることができるようになるのです。これを Scala なら implicit、Rust なら trait を使って表現可能です。

Scala で実装する

モノイドを用意しましょう。op が結合律を満たす関数で、unit単位元を満たす関数です。

trait Monoid[A] {
  def op(lhs: A, rhs: A): A
  def unit: A
}

今回は、Int 型と String 型についてのモノイドを定義します。モノイド則を満たしていきましょう。

object IntMonoid extends Monoid[Int] {
  override def op(lhs: Int, rhs: Int): Int = lhs + rhs
  override def unit: Int = 0
}

object StringMonoid extends Monoid[String] {
  override def op(lhs: String, rhs: String): String = lhs + rhs
  override def unit: String = ""
}

さてここで、モノイドを上手に使うためのちょっとしたテクニックを使います。implicit parameter という存在です。先に、書きたい関数のシグネチャを示してみます。

object list {
  def sum[A](list: Seq[A])(implicit monoid: Monoid[A]): A =
    list.foldLeft(monoid.unit)(monoid.op)
}

このようにモノイドを活用して、リストの畳込み処理を行っていきたいと思っています。implicit monoid: Monoid[A] とありますね。これが implicit parameter です。

implicit parameter に該当の型に対するモノイドの実装を与えられるように、implicit の定義を行います。

object monoids {
  implicit val intMonoid: Monoid[Int] = IntMonoid
  implicit val stringMonoid: Monoid[String] = StringMonoid
}

monoids を import しさえすれば、あとは implicit parameter は型を解決してよしなに必要な方を使用してくれます。

では、使用する側を定義してみましょう。

object MonoidMain extends App {
  import monoids._

  val intList = Seq(1, 2, 3)
  list.sum(intList)

  val stringList = Seq("a", "b", "c")
  list.sum(stringList)
}

list.sum という関数は、引数が Seq[Int] なり Seq[String] ではあるものの、内部で必要な Monoid の呼び出しが implicit parameter の解決によって切り替えられます。これにより、使用者は実装の細かい切り替えを気にすることなく、ただ monoids の中身を import しておきさえすればよいというものです。

次は、これを Rust で表現してみましょう。

Rust で実装する

まず最初は、Scala 側の implicit parameter のパターンにそろえて実装をしてみます。

モノイドの型クラスを定義しましょう。

trait Monoid<T> {
    fn op(&self, lhs: T, rhs: T) -> T;
    fn unit(&self) -> T;
}

たとえば、i32 という型をモノイドにしたいとします。Scala と同じように、一旦実体化する必要があるので、次のような構造体を用意します。

struct I32Monoid;

この構造体に対して、opunit の内容を実装します。

impl Monoid<i32> for I32Monoid {
    fn op(&self, lhs: i32, rhs: i32) -> i32 {
        lhs + rhs
    }

    fn unit(&self) -> i32 {
        0
    }
}

比較のために、String 型に対しても同様にモノイドを用意しておきます。

struct StringMonoid;

impl Monoid<String> for StringMonoid {
    fn op(&self, lhs: String, rhs: String) -> String {
        lhs + &rhs
    }

    fn unit(&self) -> String {
        "".to_string()
    }
}

リストの畳込みを行い、最終的な結果を得るための関数を用意します。ポイントは、i32 型でも String 型でも受付可能なように、T 型を使用している点です。これによって、どちらの型についてのモノイドがやってきたとしても、中でリストの畳み込みを行うことができます。

fn sum<T>(list: Vec<T>, monoid: impl Monoid<T>) -> T {
    list.into_iter().fold(monoid.unit(), |acc, n| monoid.op(acc, n))
}

では、これを使用する側を用意してみましょう。

fn main() {
    let int_result = sum(vec![1, 2, 3], I32Monoid);
    println!("{}", int_result);

    let string_result = sum(
        vec!["a".to_string(), "b".to_string(), "c".to_string()],
        StringMonoid,
    );
    println!("{}", string_result);
}

結果は、i32 に対する演算は 6 になり、String に対する演算は "abc" となります。

ただし、Scala 側にあった「使用者はモノイド演算の切り替えを考慮せずにただ使うだけでいい」というよさがなくなってしまっています。

さらに、sum の引数に毎度 Monoid<T> を代入する必要があり、補助計算向けのコンテキストとリスト本体のコンテキストを同時に引数として引き回すことになります。Scala の implicit parameter はその点、処理本体のコンテキストのみを関数の引数として暗黙的に受け取ればいいです。概念(あるいは抽象度、カテゴリ)の異なるもの同士の実装の切り離しという観点で見たとき、異なる概念が一度に引き回されることがなく、スッキリした見た目になるように感じます*8

Rust には implicit parameter がありません。でも、実装をスッキリさせたい。どうすれば…。

trait をもう少し活用しましょう。Rust では、先ほど見たように i32 という型に対して直接関数を生やすことができていましたね。これを使えば、実装をもう少しスッキリさせることができます。実際にやってみましょう!

あらためて Monoid<T> を定義し直します。ポイントは、unit という関数を static にしておくことです。

trait Monoid<T> {
    fn op(&self, rhs: T) -> T;
    fn unit() -> T;
}

i32 型のモノイド則を満たしていきましょう。

impl Monoid<i32> for i32 {
    fn op(&self, rhs: i32) -> i32 {
        self + rhs
    }

    fn unit() -> i32 {
        0
    }
}

impl Monoid<String> for String {
    fn op(&self, rhs: String) -> String {
        self.to_string() + &rhs
    }

    fn unit() -> String {
        "".to_string()
    }
}

リストを畳み込む関数は、少し様子が変わってきます。先ほどの引数には impl Monoid<T> が存在していましたが、今回は必要ありません。なぜなら、trait がもはや、自身の型の内容を利用して処理を継続できるように実装されているためです。

fn sum<T: Monoid<T>>(list: Vec<T>) -> T {
    list.into_iter()
        .fold(T::unit(), |acc, n| acc.op(n))
}

unit を static にしておいたのが、ここで生きてきました。T 型は Monoid<T> を実装していることを前提としています。その前提のもと T のリストを受け取り、内部で畳み込みを行います。これで畳み込み処理は実装完了です。使用する側の実装も少し変更してみましょう。

fn main() {
    let int_result = sum(vec![1, 2, 3]);
    println!("{}", int_result);

    let string_result = sum(
        vec!["a".to_string(), "b".to_string(), "c".to_string()]
    );
    println!("{}", string_result);
}

先ほどまであった I32MonoidStringMonoid はなくなりました。結果も 6"abc" を得られ同等でした。

Scala の例と同じように、使う側ではまったくどの処理を呼ぶかについて意識していません。ただ、sum 関数を呼んでいるだけです。sum 関数の内部で適用される trait が切り替えられ、それに応じたモノイドの演算が行われるだけです。Scala 側でできていたことは、やはり Rust の trait によって実現可能なのでした。

余談ですが、Scala の implicit parameter は下記のようにも記述可能です。

object list {
  def sum[A: Monoid](list: Seq[A]): A =
    list.foldLeft(implicitly[Monoid[A]].unit)(implicitly[Monoid[A]].op)
}

こうすると、Rust 側で定義した関数のシグネチャと同等になりますね。

まとめ

実は書いている本人も結局 Scala の implicit と Rust の trait の関係性を一言でズバッと言い表せずにモヤモヤしています。Scala の trait と Rust の trait は、もちろん同等の機能を持っている面もあります。

それは、最初に示した Java の interface のような trait の使い方を通して学びました。

一方で、Rust の trait は Scala の trait 以上の機能をもっています。Scala においては implicit を用いて実現されていた機能が、実は Rust では trait ひとつで実現可能なのでした。

ところで、今回は意図的にパラメトリック多相やアドホック多相、型クラスという言葉をあえて避けて通ってきました。実装の結果を通じて結果が等価であることを確認しながら進んできましたが、裏側にはこういった概念が見え隠れします。下記の記事などが参考になると思いますので、裏側の概念が気になる方はぜひご参照ください。

また、Scala の implicit については下記の記事を大いに参考にさせていただきました。

*1:たとえば、Rust には class の継承が存在しません。

*2:なぜうまくいかないのかというこのあたりの事情は、Rust nomicon のサブタイプの章に詳しく書いてあります。

*3:trait の状態では Animal というトレイトが一体どのくらいのメモリを確保する必要があるかわからないからです。たとえば Dog と Cat で保持するデータの量が変わってくると、当然 Animal というトレイトが確保しなければならないメモリそのものが変化します。

*4:このあたりの裏側についてあまり詳しくないのですが、「表現できる」と書いたものの「等価とは限らないかもしれない」という点に注意が必要です。似たようなことはできるけれど、たとえば集合の扱いのレベルで同じなのかは、よくわかりません。Scala の方はサブタイピングの境界範囲を定義しているのに対して、Rust の方はトレイト境界を定義しているという点で厳密には異なりそうだからです。詳しい人にお任せしたいと思います。

*5:型クラスというと Monad や Functor などが使えそうな気がしてくるかもしれませんが、残念ながら Rust では for-yield などのモナド用構文が用意されているわけではなく、また関数型プログラミング用の有力なライブラリが存在しているわけでもありません。何より高階カインド型が存在していないため、できることは相当限られています。

*6:たとえば 1, 2, 3 のリストがあって、それに対して足し算の畳込みを定義すると、合計値 6 を受け取るなど。畳み込みには「初期状態」「結合演算」の2つの要素が必要になりますが、モノイドを使うとそれらをスッキリ実装できます。

*7:以前記事を書いたことがあります: 群、モノイド、半群 - Don't Repeat Yourself

*8:implicit parameter は一方で、暗黙的にインポートをいくつか起こすため、可読性を下げるという批判もよく目にします。可読性というのは主観なので、人や状況、立場によって異なるため、この批判にも一理あると思います。