Don't Repeat Yourself

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

社会人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 は一方で、暗黙的にインポートをいくつか起こすため、可読性を下げるという批判もよく目にします。可読性というのは主観なので、人や状況、立場によって異なるため、この批判にも一理あると思います。

Mac OS X から EC2 インスタンス上に存在する JVM に VisualVM を接続する

EC2 インスタンス上に存在する Web サーバーを VisualVM 接続してで見たいと思った際に、どのようにして見たらよいかをまとめておきます。

なお、ローカル環境が Mac OS X での場合です。

手順

  1. java の起動コマンドに引数をいくつか追加する。
  2. SOCKS プロトコルで EC2 インスタンスに接続できるようにしておく。
  3. VisualVM の起動時設定に↑を使用できるようなオプションを追加する。
  4. JMX 接続を使って、リモート接続を開始する。

1. java の起動コマンドに引数をいくつか追加する

今回はポート番号 3333JMX リモート接続用に開放しておき、そこに接続してもらうように設定します。下記の引数を追加し、アプリケーションを起動しておきます。

-Dcom.sun.management.jmxremote.port=3333 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false

2. SOCKS プロトコルで EC2 インスタンスに接続できるようにしておく

下記コマンドを実行し、SOCKS プロトコル *1 を有効にします。

ssh -i {pem_file_path} -fND 10000 ec2-user@{ec2-private-ip}

{pem_file_path} には、自身の PC に保存してある pem ファイルの居場所を指定します。たとえば、~/.ssh/visualvm_con.pem などです。

{ec2-private-ip} には、接続したい EC2 インスタンスのプライベート IP アドレスを入れておきます。

オプションについては、

  • -i: 秘密鍵ファイルを指定します。
  • -f: バックグラウンド実行を意味します。
  • -N: リモートコマンドを実行しません。
  • -D: ローカルホスト側におけるアプリケーションの動的なポート伝送を指定します。

3. VisualVM の起動時設定に↑を使用できるようなオプションを追加する

起動時の設定に SOCKS プロトコルの設定を追加して、jvisualvm を実行します。

jvisualvm -J-DsocksProxyHost=localhost -J-DsocksProxyPort=10000 -J-DsocksNonProxyHosts=

あるいはGUI から設定する場合は、「File > Preferences > Network」の順に開き、下記の画像のように設定値を入力しておきます。 SOCS Proxy 向けの設定の箇所に、localhost10000 を入れておきます。

f:id:yuk1tyd:20200929234137p:plain

4. JMX 接続を使って、リモート接続を開始する

JMX 接続を追加」を押すと、追加用のダイアログが立ち上がるので、そこに先ほどの {ec2-private-ip} と同じ IP アドレスとポートを書きます。ポートは JMX 用のポートのため、3333 を使用することに注意が必要です。

f:id:yuk1tyd:20200307194717p:plain
JMX 接続を追加」を押す

f:id:yuk1tyd:20200307194817p:plain
プライベート IP アドレスとポート番号を書く

上記の状態にして、「了解」ボタンを押すと、EC2 上のサーバーとの接続が開始されます。接続に成功すると、いつも見る VisualVM の画面が立ち上がってくれるはずです。

*1:あるポートにバインドした通信路へのソケットを提供してくれ、TCP/IP の肩代わりをするプロトコルです。ここで SSH を経由して JMX を通します。

Rust の HTTP クライアント surf を試してみる

async/await に対応した HTTP サーバーの tide を先日紹介し、先日も記事を書きました。

同様に async/await に対応した HTTP クライアントの surf というライブラリがあるようなので、それを軽く紹介したいと思います。執筆時点での surf のバージョンは 1.0.3 です。

surf

github.com

async/await に対応した HTTP クライアントです。HTTP サーバー用のライブラリ tide と同様に、async_std を非同期処理ランタイムに使用しています。ちなみに、Rust の HTTP クライアントライブラリで扱いやすいものとしては、 reqwest が有名で、私自身もよく利用します。reqwest は非同期ランタイムに tokio を使用しています。

tide と合わせて「波乗り」になっているのがおもしろいですね。ネットサーフィンの surf から取ってきているのだと思います。

機能

対応している代表的な機能は、ドキュメントによると下記の通りです。

  • もちろんですが一通り HTTP メソッドに対応している。
  • TLS/SSL はデフォルトで対応している。
  • ストリーム処理に対応している。
  • Client インターフェースを介して接続を再利用できる。
  • Logger などのミドルウェアを拡張することができる。
  • HTTP/2 が標準で入っている。

今回は、下記の機能を試してみたいと思います。

準備

Cargo.toml に下記を追加します。

[dependencies]
surf = "1.0.3"
serde = { version = "1.0", features = ["derive"] }
async-std = { version = "1.5.0", features = ["attributes"] }

serde は Rust における JSON シリアライズ/デシリアライズ用のライブラリです。

また、async_std の attributes という features を今回は使用します。詳細は後ほど解説します。

GET リクエストを送る

README にしたがって、GET リクエストを送ってみましょう!

素直に書くと次のようになります。

use async_std::task;

fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
    task::block_on(async {
        let mut res = surf::get("https://httpbin.org/get").await?;
        dbg!(res.body_string().await?);
        Ok(())
    })
}

一方で、task::block_on は少しノイズが多いと感じるかもしれません。私は次のように記述するのが好きなので、以下では、task::block_on をすべて下記のコードのように読み替えていきます。

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
    let mut res = surf::get("https://httpbin.org/get").await?;
    dbg!(res.body_string().await?);
    Ok(())
}

#[async_std::main] を利用して fn main()async fn main() に変更し、task::block_on ブロックをなくしました。

結果は次のようになりました。{deleted_by_author} は私が手を加えて消したものです。

❯❯❯ cargo run
   Compiling surf_example v0.1.0 (surf_example)
    Finished dev [unoptimized + debuginfo] target(s) in 1.64s
     Running `target/debug/surf_example`
[src/main.rs:4] res.body_string().await? = "{\n  \"args\": {}, \n  \"headers\": {\n    \"Accept\": \"*/*\", \n    \"Accept-Encoding\": \"deflate, gzip\", \n    \"Host\": \"httpbin.org\", \n    \"Transfer-Encoding\": \"chunked\", \n    \"User-Agent\": \"curl/7.64.1 isahc/0.7.6\", \n    \"X-Amzn-Trace-Id\": \"Root={deleted_by_author}\"\n  }, \n  \"origin\": \"{deleted_by_author}\", \n  \"url\": \"https://httpbin.org/get\"\n}\n"

JSON を送る POST リクエス

JSON を送ってみましょう。同様に README から拝借しています。

use serde::{Deserialize, Serialize};

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
    #[derive(Deserialize, Serialize)]
    struct Ip {
        ip: String,
    }

    let uri = "https://httpbin.org/post";
    let data = &Ip {
        ip: "129.0.0.1".into(),
    };
    let res = surf::post(uri).body_json(data)?.await?;
    assert_eq!(res.status(), 200);

    let uri = "https://api.ipify.org?format=json";
    let Ip { ip } = surf::get(uri).recv_json().await?;
    assert!(ip.len() > 10);
    Ok(())
}

assertion が通れば、無事動作していると言えます。実際に動かしてみると、

~/dev/rust/surf_example via 🦀 v1.41.1
❯❯❯ cargo run
   Compiling surf_example v0.1.0 (surf_example)
    Finished dev [unoptimized + debuginfo] target(s) in 1.89s
     Running `target/debug/surf_example`
~/dev/rust/surf_example via 🦀 v1.41.1
❯❯❯

すごくわかりにくいかもしれませんが、assertion が落ちることなく確かに動いていました。

送った後にレスポンスボディを取り出すことも可能で、

use serde::{Deserialize, Serialize};

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
    #[derive(Deserialize, Serialize, Debug)]
    struct Ip {
        ip: String,
    }

    let uri = "https://httpbin.org/post";
    let data = &Ip {
        ip: "129.0.0.1".into(),
    };
    let mut res = surf::post(uri).body_json(data)?.await?;
    let body = res.body_string().await?;
    println!("{}", body);

    Ok(())
}

上記のように書くと、実際のリクエストボディを取得することができます。今回はしませんでしたが、body_json() も実装されていて、serde を使って構造体に復元することも可能です。

❯❯❯ cargo run
   Compiling surf_example v0.1.0 (surf_example)
    Finished dev [unoptimized + debuginfo] target(s) in 1.83s
     Running `target/debug/surf_example`
{
  "args": {},
  "data": "{\"ip\":\"129.0.0.1\"}",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "deflate, gzip",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "Transfer-Encoding": "chunked",
    "User-Agent": "curl/7.64.1 isahc/0.7.6",
    "X-Amzn-Trace-Id": "Root={deleted_by_author}"
  },
  "json": {
    "ip": "129.0.0.1"
  },
  "origin": "{deleted_by_author}",
  "url": "https://httpbin.org/post"
}

ミドルウェア

ミドルウェアは少し大変です。今回は、自分の好きなロギングライブラリを処理の途中にはさむということをやってみたいと思います。

まず、Cargo.toml に下記のクレートを追加します。

[dependencies]
surf = "1.0.3"
serde = { version = "1.0", features = ["derive"] }
async-std = { version = "1.5.0", features = ["attributes"] }
log = "0.4.8" # ロギング関係のもの。追加。
simple_logger = "1.6.0" # ロギング関係のもの。追加。
futures = "0.3" # ミドルウェアを記述するために必要。追加。

Middleware というトレイトに、リクエスト時にログを書いて、リクエストを送って、かかった時間をロギングして返すというミドルウェアの実装を追加したいと思います。Middleware というトレイトは handle という関数が実装可能な状態になっていますね。

/// Middleware that wraps around remaining middleware chain.
pub trait Middleware<C: HttpClient>: 'static + Send + Sync {
    /// Asynchronously handle the request, and return a response.
    fn handle<'a>(
        &'a self,
        req: Request,
        client: C,
        next: Next<'a, C>,
    ) -> BoxFuture<'a, Result<Response, Exception>>;
}

書いてみます。

use futures::future::BoxFuture;
use log::{info, Level};
use std::time::Instant;
use surf::middleware::{HttpClient, Middleware, Next, Request, Response};
use surf::Exception;

struct HttpReqLogger;

impl<C: HttpClient> Middleware<C> for HttpReqLogger {
    fn handle<'a>(
        &'a self,
        req: Request,
        client: C,
        next: Next<'a, C>,
    ) -> BoxFuture<'a, Result<Response, Exception>> {
        Box::pin(async move {
            info!("Request: {}", req.uri());
            let now = Instant::now();
            let res = next.run(req, client).await?;
            info!("Request completed: {:?}", now.elapsed());
            Ok(res)
        })
    }
}

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
    simple_logger::init_with_level(Level::Info).unwrap();
    surf::get("https://httpbin.org/get")
        .middleware(HttpReqLogger {})
        .recv_string()
        .await?;
    Ok(())
}

Box::pin[*1] の中に、ミドルウェアの処理を書いていきます。今回は simple_logger というクレートにロギングを行わせてみました。中でリクエストを投げてレスポンスを得るまでの時間経過をプリントしています。

これを実行してみました!

❯❯❯ cargo run
   Compiling surf_example v0.1.0 (surf_example)
    Finished dev [unoptimized + debuginfo] target(s) in 2.13s
     Running `target/debug/surf_example`
2020-02-29 15:09:33,558 INFO  [surf::middleware::logger::native] sending request
2020-02-29 15:09:33,559 INFO  [surf_example] Request: https://httpbin.org/get
2020-02-29 15:09:34,486 INFO  [surf_example] Request completed: 927.326608ms
2020-02-29 15:09:34,486 INFO  [surf::middleware::logger::native] request completed

期待通りの挙動を示していますね!

まとめ

  • surf という async_std ベースの HTTP クライアントライブラリがある。
  • HTTP クライアントとして使う際にやりたいことは、ドキュメントを読んだ感じでは一通り揃っている。

*1:Box::pin(std::pin::Pin)について本当に簡単にですが補足しておきましょう。async/await によって Rust にはジェネレータが投下されることになったのですが、その際自己参照する構造体を扱う必要が出てきました。しかし、Rust の move とは相性が悪く、普通に自己参照を move しようとすると、ポインタが古い領域を示してしまい、不正な状態に陥ることになるという問題がありました。それを解決するために導入されたのが、Pin という概念でした。詳しい解説はこちら

tide 0.6.0 を試してみる

tide 0.6.0 がリリースされていたようなので、使ってみます。リリースノートはこちらです。

Cookie、CORS と Route にいくつか新しい関数が足されたというリリースでした。Cookie と CORS はどちらも Web アプリを作る上では必須要件なので、追加されて嬉しいです。

使用する準備

tide を使えるようにしましょう。下記のように Cargo.toml を用意することで利用可能になります。

[package]
name = "tide-example"
version = "0.1.0"
authors = ["Author"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tide = "0.6.0"
serde = { version = "1.0", features = ["derive"] }

[dependencies.async-std]
version = "1.4.0"
features = ["attributes"]

Cookie

cookie クレートを使った Cookie 実装をできるようになりました。

まずは、cookie クレートを追加しましょう。ただ、完全に調べきれていない状態で書きますが、cookie の最新版である 0.13.3 を使用すると、Cookie 構造体のもつライフタイムが不整合になってしまっており、set_cookieremove_cookie といった関数でコンパイルエラーが発生します。したがって、0.12.0 を指定する必要がある点に注意が必要です *1

(...)
[dependencies]
tide = "0.6.0"
serde = { version = "1.0", features = ["derive"] }
+ cookie = { version="0.12.0", features = ["percent-encode"]}
(...)

今回は下記のようなエンドポイントを追加して実験するものとします。

  • GET /set: Cookie をセットするためのエンドポイントです。レスポンスヘッダに set-cookie が入っていることが期待値になります。
  • GET /remove: Cookie を削除するためのエンドポイントです。

たとえば下記のように実装できます。

use cookie::Cookie;
use tide::Response;

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

    app.at("/set").get(|_req| async move {
        let mut res = Response::new(200);
        res.set_cookie(Cookie::new("tmp-session", "session-id"));
        res
    });
    app.at("/remove").get(|_req| async move {
        let mut res = Response::new(200);
        res.remove_cookie(Cookie::named("tmp-session"));
        res
    });
    app.listen("127.0.0.1:8080").await?;
    Ok(())
}

/set を実行すると、Cookie に期待通りのものが設定されていることが確認でき、

❯❯❯ curl localhost:8080/set -i
HTTP/1.1 200 OK
set-cookie: tmp-session=session-id
transfer-encoding: chunked
date: Sat, 08 Feb 2020 11:18:26 GMT

/remove を実行すると、一旦 200 OK が返ってきていることがわかります。ブラウザで挙動を確認したところ、正しく指定の Cookie が削除されていました。

❯❯❯ curl localhost:8080/remove -i
HTTP/1.1 200 OK
transfer-encoding: chunked
date: Sat, 08 Feb 2020 11:28:49 GMT

CORS

おなじみ CORS も実装できるようになりました。いつも Web をしていると通る道なので、実装が追加されて大変嬉しいです。

use http::header::HeaderValue;
use tide::middleware::{Cors, Origin};
use tide::Response;

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

    let rules = Cors::new()
        .allow_methods(HeaderValue::from_static("GET, POST, OPTIONS"))
        .allow_origin(Origin::from("*"))
        .allow_credentials(false);

    app.middleware(rules);
    app.at("/portfolios").post(|_| async {
        Response::new(200)
    });

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

    Ok(())
}

tide::middleware::Corstide::middleware::Origin を使って設定します。これは他の言語のライブラリとも特に使い心地に差はなく、CORS をすぐに実装できていいですね。

…と思ったんですが、

❯❯❯ cargo build
   Compiling tide-example v0.1.0 (/Users/a14926/dev/rust/tide-example)
error[E0277]: the trait bound `http::header::value::HeaderValue: std::convert::From<http::header::value::HeaderValue>` is not satisfied
  --> src/main.rs:22:10
   |
22 |         .allow_methods(HeaderValue::from_static("GET, POST, OPTIONS"))
   |          ^^^^^^^^^^^^^ the trait `std::convert::From<http::header::value::HeaderValue>` is not implemented for `http::header::value::HeaderValue`
   |
   = help: the following implementations were found:
             <http::header::value::HeaderValue as std::convert::From<&'a http::header::value::HeaderValue>>
             <http::header::value::HeaderValue as std::convert::From<http::header::name::HeaderName>>
             <http::header::value::HeaderValue as std::convert::From<i16>>
             <http::header::value::HeaderValue as std::convert::From<i32>>
           and 6 others
   = note: required because of the requirements on the impl of `std::convert::Into<http::header::value::HeaderValue>` for `http::header::value::HeaderValue`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `tide-example`.

To learn more, run the command again with --verbose.

コンパイルエラー😇 リリースノートに載っていたサンプルコードをそのままコピペしてビルドしてもやはりコンパイルエラーだったため、まずリリースノートのコードは合っていない気がします。これもあとで Issue 上げておこうかな…。もしわかる方いたら教えていただけますと🙏🏻

Nesting

エンドポイントをネストしやすくなりました。たとえば次のように実装すると、/api/v1/portfolios というエンドポイントを定義できます。

use tide::Response;

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut portfolios = tide::new();
    portfolios.at("/portfolios").get(|_| async { Response::new(200) });

    let mut v1 = tide::new();
    v1.at("/v1").nest(portfolios);

    let mut api = tide::new();
    api.at("/api").nest(v1);

    api.listen("127.0.0.1:8080").await?;

    Ok(())
}

curl を投げると、正常に動作していることが確かめられました。

❯❯❯ curl localhost:8080/api/v1/portfolios -i
HTTP/1.1 200 OK
transfer-encoding: chunked
date: Sat, 08 Feb 2020 11:53:50 GMT

不具合と思われる事象については、これから調査してみます!

*1:これは原因がわかっていて、0.12.0 時点では Cookie<'static> を返す new 関数が生えていて、それを前提として set_cookie や remove_cookieCookie<'static> を受け取るように設計されていました。しかし、0.13.3 時点では new 関数は Cookie<'c> のライフタイムを返すようになっていて、ライフタイムの不整合が発生していますね。CookieBuilder を変更した関係でそうなったように読めます→https://github.com/SergioBenitez/cookie-rs/compare/0.12.0...0.13.0

【競プロノート】グラフを Rust で実装してみる

先日の Rust LT 会でグラフを実装してみる話がありました。そこではおそらく隣接リストを説明していたと思うのですが、そういえば Rust で実装してみたことはなかったなと思ったので、実装してみました。

『みんなのデータ構造』という本を参考にして実装しています。12章にグラフの章があります。何言語かで書かれたサンプルコードも存在します。

今回は、本書に掲載されていた下記のような「典型的な操作」を実装しました。今回はグラフ G = (V, E) を考えるものとします。一般的な話通りで、V は頂点で、E は辺です。

  • addEdge(i, j): 辺 (i, j) を E に加える
  • removeEdge(i, j): 辺 (i, j) を E から除く
  • hasEdge(i, j): (i, j) ∈ E かどうかを調べる
  • outEdges(i): (i, j) ∈ E を満たす整数整数 j のリストを返す
  • inEdges(i): (j, i) ∈ E を満たす整数整数 j のリストを返す

隣接行列

いきなりですが、隣接行列の実装は意外と大変です。Rust は、ポインタを使った操作 (参照先を書き換えてゴニョゴニョする操作) をしようとすると少々めんどくさくなるように言語が設計されています。隣接行列は行列 (2次元の配列) をもったデータ構造なのですが、その行列を状態とみなして書き換えていく操作を発生させるため、Rust 的にはとても面倒な実装になります。

今回実装した隣接行列は、(i, j) ∈ E のときに true となり、そうでない場合は false となる要素が含まれている行列です。

速度面などの細かい話を気にせず、RefCell を使用して実装してみました。ちなみに、std::mem::replace でも実装できると思いますし、実際 RefCell#replace はそれを使用して実装されているため、もしかすると RefCell を使用するのは大げさだったかもしれません。

use std::cell::RefCell;

pub struct AdjacencyMatrix {
    dimension: usize,
    matrix: Vec<Vec<RefCell<bool>>>,
}

impl AdjacencyMatrix {
    pub fn new(dimension: usize) -> AdjacencyMatrix {
        let mut matrix = vec![];
        for _ in 0..dimension {
            let mut tmp = vec![];
            for _ in 0..dimension {
                tmp.push(RefCell::new(false));
            }
            matrix.push(tmp);
        }

        AdjacencyMatrix { dimension, matrix }
    }

    pub fn dimension(&self) -> usize {
        self.dimension
    }

    fn get(&self, i: usize, j: usize) -> &RefCell<bool> {
        self.matrix.get(i).unwrap().get(j).unwrap()
    }

    pub fn add_edge(&self, i: usize, j: usize) {
        self.get(i, j).replace(true);
    }

    pub fn remove_edge(&self, i: usize, j: usize) {
        self.get(i, j).replace(false);
    }

    pub fn has_edge(&self, i: usize, j: usize) -> bool {
        *self.get(i, j).borrow()
    }

    pub fn in_edges(&self, i: usize) -> Vec<usize> {
        let mut edges = vec![];
        for j in 0..self.dimension {
            if *self.get(j, i).borrow() {
                edges.push(j);
            }
        }
        edges
    }

    pub fn out_edges(&self, i: usize) -> Vec<usize> {
        let mut edges = vec![];
        for j in 0..self.dimension {
            if *self.get(i, j).borrow() {
                edges.push(j);
            }
        }
        edges
    }
}

隣接リスト

逆に、隣接リストはスムーズに実装できます。これは、単純にエッジを追加する際にリストの末尾に値を入れていけばよいためです。

pub struct AdjacencyList {
    dimension: usize,
    list: Vec<Vec<usize>>,
}

impl AdjacencyList {
    pub fn new(dimension: usize) -> AdjacencyList {
        AdjacencyList {
            dimension,
            list: vec![vec![]],
        }
    }

    pub fn dimension(&self) -> usize {
        self.dimension
    }

    pub fn add_edge(&mut self, i: usize, j: usize) {
        if i > self.dimension() {
            panic!("add: greater than dimension");
        }

        match self.list.get_mut(i) {
            Some(v) => v.push(j),
            None => self.list.insert(i, vec![j]),
        }
    }

    pub fn remove_edge(&mut self, i: usize, j: usize) {
        match self.list.get_mut(i) {
            Some(v) => v.retain(|v0| *v0 != j),
            None => (),
        }
    }

    pub fn has_edge(&self, i: usize, j: usize) -> bool {
        match self.list.get(i) {
            Some(v) => v.contains(&j),
            None => false,
        }
    }

    pub fn in_edges(&self, i: usize) -> Vec<usize> {
        let mut edges = vec![];
        for j in 0..self.dimension() {
            if let Some(list) = self.list.get(j) {
                if list.contains(&i) {
                    edges.push(j);
                }
            }
        }
        edges
    }

    pub fn out_edges(&self, i: usize) -> Vec<usize> {
        match self.list.get(i) {
            Some(v) => v.to_vec(),
            None => vec![],
        }
    }
}

ポイントは、隣接リストの場合は初期状態の Vec<Vec<usize>> で、たとえば行を取得したい場合に、行が None として返ってきてしまうパターンが考えられるため、その点について留意しなが実装する必要があったという点です。Vector 関連でもっといい操作があれば知りたいところですが、思いつく限りでは None がどうしても発生してしまうため、都度パターンマッチして慎重に取り出す操作を行っています。

もっとも単純なグラフの実装がこれでわかりました。