2020年振り返り

2020年の振り返りとしてやったことをまとめる。

目標管理

四半期ごとの見直し、月次の進捗確認で運用をした。

1,4,7,10月に目標の見直しを行い、次四半期の目標を立てる。
2,3,5,6,8,9,11,12月は進捗を確認する運用だった。

大項目として以下5つを設け、四半期ごとに小項目での目標管理をした。括弧内は小項目のうち、達成できた数を記載(次四半期で取り返せたものは達成扱い)。

  • 技術(5/9)
  • 読書(9/15)
  • 健康(7/9)
  • 英語(4/7)
  • 趣味(9/12)

全体として65%の達成率だった。

目標管理を開始して3年目であったが、運用が確立され、目標管理シートのフォーマットも定常化した。一方でサボることは少ないながら上述の達成率だった点はネガティブ。特に技術と読書。原因は目標が過大なことなので、2021年は本年のVelocityをベースとした目標策定へ改める。

論文

日本語の論文や金融機関のレポート、決算資料も読んでいるが、ここでは含めない。
隔週で担当しているLayerX Newsletter1により継続的に論文を読む習慣ができた。一方で開催される会議ベースで読んでいるので、過去の論文を読む習慣を付けることが課題。

書籍

  • HashHub Researchレポート: 311
  • 漫画や雑誌等を含めた書籍: 180

技術書

技術書を読む数が減った。増やしていくべき。今年は意識的に一年一言語を新規に学ぶ目標を停止したが、来年は復活させる。

ビジネス書

特によかったのは、「失敗の本質」と「決済システムのすべて」の2冊。いずれも重たい本ではあるが、組織、決済システムの文脈では必読書だと感じた。また、百人一首孔子といった教養も意識した選書となった。2021年は美術と舞台照明・演出に関する本を読みたい。

記事

論文を読む時間が増えたので記事を読む時間は減った。なお、これはポジティブだと捉えている。

以下では、Pocketでお気に入りしたものからリスト化する。いずれも2020年12月31日時点で有効なリンクのみを対象とした。残念ながら「読んでよかった」と思ったもののリンクが切れてしまっている記事もいくつか存在した。
概ね読んだ順であり、「2020年に読んでよかった記事」のため2019年以前に公開されたものを含む。体感、去年よりお気に入りにする頻度が下がった。
ざっと眺めてみるとジャンルは、Rust、CBDC、STO、組織論か。コンテナランタイムなど低レイヤの記事をお気に入りにすることも増え、基礎から重厚に議論を進めている記事を「読んでよかった」と思うようになった。ジャンルについて、改めて自分が何をおもしろいと感じたのか可視化できるので来年も振り返ろう。
リスト上は多くないが、翻訳にかけてもいいので英語や中国語の記事を読むようになった一年であった。

Github

f:id:cipepser:20201231170835p:plain

昨年の873 contributionsに対して、+155となった。

Scrapbox

privateのScrapboxの運用を開始した。

  • 0→765pages

趣味のほうは更に別に存在し、84pagesだった。


  1. 頑張って書いてるので、ぜひ購読してください!(宣伝)

【Go】Sodiumで認証付き公開鍵暗号

この記事は Go 2 Advent Calendar 2020 の23日目の記事です。

Sodiumとは

Sodium[1]は使いやすさを目的に開発された暗号学ライブラリです。 暗号の誤用に関する研究分野では、誤りの検出や復元のため技術とそれに伴う影響が議論されています。 一方で誤用そのものを防ぐような方法についての議論は比較的少数です[13]。 Sodiumは使いやすさからこの課題を解決するライブラリの一つです。

また使いやすさだけでなく、portable、cross-compilable、installableであることも謳われています。 awesome-cryptography[2]で検索してみた所、以下のように多様な言語で利用することができます。

C

C#

JavaScript

Java

PHP

Rust

Swift

GoでSodiumを使う

GoでもSodiumを扱うことができます。packageはnacl/ · pkg.go.devです。[1][14]にあるようにSodiumはNaClのforkで、Goはnacl packageとして準標準ライブラリに用意されています。

今回はnacl/boxが提供している、公開鍵を使ったメッセージの認証と暗号化を試してみようと思います。内容は[16]をベースとしています。

実装に移る前にSodiumの特徴や注意点をいくつか述べておきます。
まず、メッセージは暗号化されますが、メッセージの長さは秘匿されません。暗号文の長さから元の平文が推測できてしまうようなアプリケーションには向きません。
また、Sodiumではメッセージにnonceを付与します。メッセージごとに異なるnonceを用いることは呼び出し側の責任です。
その他の注意点としては、[16]にも書かれていますが、メッセージ全体を処理するためにメッセージをメモリ上に保持する必要があるため、小さなメッセージが推奨されています。なお、暗号化のオーバーヘッドも存在しますが、8KB程度のメッセージでは十分に償却されるとのことです。

では本題の実装に入っていきましょう。 今回はclientからserverを送信するメッセージの暗号化を行います。

まず、GenerateKeyを用いて、鍵ペアを生成します。 認証のためclient、serverの両方で鍵ペアを生成します。

// client
clientPublicKey, clientPrivateKey, err := box.GenerateKey(crypto_rand.Reader)
if err != nil {
    panic(err)
}

// server
serverPublicKey, serverPrivateKey, err := box.GenerateKey(crypto_rand.Reader)
if err != nil {
    panic(err)
}

次にメッセージに付与するnonceを生成します。nonceの長さは24 bytesです。 nonceの使い回しは厳禁です。メッセージごとに生成しましょう。

var nonce [24]byte
if _, err := io.ReadFull(crypto_rand.Reader, nonce[:]); err != nil {
    panic(err)
}

いよいよ暗号化です。暗号化にはSealを用います。 暗号化対象のメッセージをmsg := []byte("sodium msg")とし、認証付きで公開鍵暗号を施します。

encrypted := box.Seal(nonce[:], msg, &nonce, serverPublicKey, clientPrivateKey)

clientの秘密鍵clientPrivateKeyだけでなく、serverの公開鍵serverPublicKeyを渡していることに注意してください。 復号にはサーバの秘密鍵が必要であるとともに、clientの公開鍵によってclientからのメッセージであることを認証します。

ちなみにnonceは暗号文の先頭に付与されます。 筆者の環境で実行した結果は以下のようになりました。 encryptedの先頭24 bytesがnonceとなっていることがわかります。

nonce:     [176 240 87 14 190 92 224 60 245 16 119 163 71 18 1 177 57 118 139 46 141 4 99 117]
encrypted: [176 240 87 14 190 92 224 60 245 16 119 163 71 18 1 177 57 118 139 46 141 4 99 117 65 153 138 25 206 247 18 42 0 162 85 88 223 68 203 63 241 28 35 232 242 176 184 56 97 177]

また上記のlen(encrypted)は50 bytesです。 msgsodium msg)が10 bytesなので、nonceの24 bytesを差し引きいても16 bytesのオーバーヘッドがあります。このオーバーヘッドは認証のために付与されたもので、暗号文とはオーバーラップしない設計となっています。

では復号してみましょう。 実際のアプリケーションではclientからserverへencryptedが送信されてからの復号となりますが、ここではserverで受信済みとして先に進みます。

Sealされたメッセージの復号にはOpenを用います。 Openシグネチャは以下のようになっています。

func Open(out, box []byte, nonce *[24]byte, peersPublicKey, privateKey *[32]byte) ([]byte, bool)

2つ目の引数に注目いただきたいのですが、nonceが必要です。 encryptedの先頭24 bytesがnonceになっていたことを思い出し、encryptedから取り出します。

var decryptNonce [24]byte
copy(decryptNonce[:], encrypted[:24])

必要な引数がすべて揃ったので、serverの秘密鍵serverPrivateKeyとclientの公開鍵clientPublicKeyを用いてOpenを実行します。

decrypted, _ := box.Open(nil, encrypted[24:], &decryptNonce, clientPublicKey, serverPrivateKey)

正しいserverの秘密鍵とclientの公開鍵があれば、認証と復号に成功し、元のmsgと同一のdecryptedを得ることができます。

まとめ

Sodiumの紹介と、Goで認証付き公開鍵暗号の暗号化/復号を試してみました。認証付きで公開鍵暗号を使えるので正しい相手から受信したことを保証したい場合などに有用でしょう。また利用できる言語が多いことも魅力的ですね。筆者はRustとjsのcompatibilityを手元でも動作確認しました。機会があれば、いつか記事にしたいと思います。Goとのcompatibilityも試してみたいですね。

また、同じようなプロジェクトは他にもMonocypher[17]、Themis[18]、Tink[19]もあるようです。特にTinkはGoogleのプロジェクトであることからも気になっています。APIのインターフェースが違うようなので、使い勝手といった視点も含めて触っておきたいところです。

References

Rustの可変長引数関数とHListの話

この記事は Rust Advent Calendar 2020 の5日目の記事です。

背景

RFCs#2137にあるようにRustでは、可変長引数関数を直接的に書くことはできません。とはいえ全くできないわけではありません。C言語から可変長引数関数呼び出しを実現するため、stub関数を記述することは可能です。例えば、以下のような関数をRustで実装します。

pub unsafe extern "C" fn func(arg: T, arg2: U, mut args: ...) {
    // do something
}

このような関数はuse extern "C"の中で使われ、unsafeを付与する必要があります。

別の実現方法としてはマクロを利用する案もあり、以下のようなマクロで実現できるでしょう。以下はargarg2を処理し、再帰的に可変長引数に相当するargsを処理する例です。

macro_rules! func {
    ( $arg:expr, $arg2:expr, $($args:expr), + ) => {
        do_something_for_arg($arg);
        do_something_for_arg1($arg2);
        func!(@inner $($args), +);
    };

    (@inner $tail:expr ) => { $tail };

    (@inner $head:expr, $($cons:expr), + ) => {
        do_something_args($head);
        func!(@inner $($cons), +)
     };
}

HListについて

今回は可変長引数関数を実現する方法の一つとして、HListを紹介します。HlistはHeterogenous Listの略で、値に異なる型を取れるリストです。例えば、[1, "a", true]のような[u32, &str, bool]型の値を持つことができます。
似たようなデータ構造としてはVec<T>やタプルが考えられますが、Vec<T>は同一の型Tを持つ点で異なります。一方、タプルは(u32, &str, bool)のように異なる型を持つこともできますが、HListを用いることでコンパイル時まで要素の数を決めないでいられます。

frunk

RustでHListを扱うために、frunk crateを利用します。frunkはgeneric functional programmingをRustで扱えるようにするためのcrateで、以下をサポートしています。

  • HLists (heterogeneously-typed lists)
  • LabelledGeneric, and Generic
  • Coproduct
  • Validated (accumulator for Result)
  • Semigroup
  • Monoid

今回は上記の中から、HListを利用します。frunk crateのHListはHNilHConsで表現されます。実装は以下のようになっています。

pub struct HNil;

pub struct HCons<H, T> {
    pub head: H,
    pub tail: T,
}

frunk crateにはhlistマクロが定義されており、hlist![1, "a", true]の返り値の型はHCons<i32, HCons<&str, HCons<bool, HNil>>>となります。このようにHNilHConsを組み合わせて、HListを表現します。

作るもの

可変長引数を取る関数の代表的な例として、print関数を実装します。 使い方をみたほうがイメージが湧きやすいと思うので、先にテストを書きます。

#[test]
fn my_print() {
    use frunk::hlist;
    use super::Printer;

    let args = hlist!["hello ", "world. ", 1, " == ", 2.0, " is ", false];
    let got = args.print();
    assert_eq!(got, "hello world. 1 == 2 is false");
}

let args = hlist!["hello ", "world. ", 1, " == ", 2.0, " is ", false];に注目してください。&stri32f64boolという4つの異なる型を含んだListとなっています。本当はargsを引数に取るような可変長引数関数を直接実装したかったのですが、今回は擬似的にargsに対するメソッドとして実装します。Implementing a Type-safe printf in Rustでは、プレースホルダFVarとして表現し、引数としてHListを受け取るformat関数を実装しています。ぜひこちらもご覧ください。

実装

まず、Printer traitを定義します。
Stringを返すようなprint関数を持ちます。

pub trait Printer<Args> {
    fn print(&self) -> String;
}

HNilHConsに対して、Printer traitを満たすように、print関数を実装していきます。

まずは、HNilから実装します。

impl Printer<HNil> for HNil {
    fn print(&self) -> String {
        "".to_string()
    }
}

HNilとなった時点でHListは末尾に到達しているので、空文字を返します。シンプルですね。

一方、HConsは少し複雑です。

impl<T, Args, PrintList> Printer<HCons<T, Args>> for HCons<T, PrintList>
    where
        PrintList: Printer<Args>,
        T: ToString,
{
    fn print(&self) -> String {
        self.head.to_string() + &self.tail.print()
    }
}

Printer<Args>ArgsHCons<T, Args>としています。TheadArgstailです。headはそのままStringに変換したいため、ジェネリック境界としてToStringを課しています。tail再帰的に処理を進めるため、PrintList: Printer<Args>とし、printを呼び出します。この処理はHNilに到達するまで繰り返されます。

このようにして、HNilHConsに対して、print関数を実装しました。

動作確認

改めて、今回実装したコード全体は以下のようになります。

use frunk::hlist::{HNil, HCons};

pub trait Printer<Args> {
    fn print(&self) -> String;
}

impl Printer<HNil> for HNil {
    fn print(&self) -> String {
        "".to_string()
    }
}

impl<T, Args, PrintList> Printer<HCons<T, Args>> for HCons<T, PrintList>
    where
        PrintList: Printer<Args>,
        T: ToString,
{
    fn print(&self) -> String {
        self.head.to_string() + &self.tail.print()
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn my_print() {
        use frunk::hlist;
        use super::Printer;
        
        let args = hlist!["hello ", "world. ", 1, " == ", 2.0, " is ", false];
        let got = args.print();
        assert_eq!(got, "hello world. 1 == 2 is false");
    }
}

テストの実行結果です。

❯ cargo test
   Compiling rust-variadic-artgument v0.1.0 (/Users/cipepser/work/github.com/cipepser/rust-variadic-artgument)
    Finished test [unoptimized + debuginfo] target(s) in 0.47s
     Running target/debug/deps/rust_variadic_artgument-3442cb5a09929580

running 1 test
test tests::my_print ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests rust-variadic-artgument

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

いい感じですね。

最後に

可変長引数関数の話から始まり、HListでprint関数を実装しました。

今回記事を執筆するにあたり、frunc crateの存在を初めて知りました。CoproductやSemigroup、Monoidも扱えるようなので、折を見て遊んでみようと思います。

References

【git】開発とレビューのバランスを求めて 〜commitマージ編〜

以下記事の続き。

cipepser.hatenablog.com

「commitをまとめたい」を見ていく。

やりたいこと

開発中は細かくcommitし、レビュワーに依頼する際にcommitをまとめたい。

やり方

git rebase -iを使う

(準備)commit用のファイルを用意する

前回記事からc.txtd.txtを増やした。

ls
README.md a.txt     b.txt     c.txt     d.txt

txtファイルの中身はファイル名が一文字書いてある。 例えばa.txtならaとだけ書いてある。適当に用意しただけなので、意味はない。

❯ cat a.txt
a

初期状態

以下の状態から始める1

❯ git log --pretty=oneline
f17ee3b566b60c4dad9250d9d67b7ae157c1d5e1 (HEAD -> master) Merge branch 'add-b'
bceca8ee93f256fb1d4083798c1aed1e3b1c8c16 (add-b) add b.txt
b1abe76c7f481cecf64934a98dcd1971a13496e6 (add-a) add a.txt
635c5d5570a6c0492be7a924dda3df314bfd13b8 first commit

add-c-and-dブランチ

add c.txtadd d.txtで別々のcommitを行う。

❯ git checkout -b add-c-and-d

❯ git add c.txt
❯ git commit -m "add c.txt"

❯ git add d.txt
❯ git commit -m "add d.txt"

❯ git log --pretty=oneline
338cb6e2a328bebb420da2628fffa73f677da071 (HEAD -> add-c-and-d) add d.txt
2dfb9b998e3331cd1b6bf35badad9f362f04f6f1 add c.txt
f17ee3b566b60c4dad9250d9d67b7ae157c1d5e1 (master) Merge branch 'add-b'
bceca8ee93f256fb1d4083798c1aed1e3b1c8c16 (add-b) add b.txt
b1abe76c7f481cecf64934a98dcd1971a13496e6 (add-a) add a.txt
635c5d5570a6c0492be7a924dda3df314bfd13b8 first commit

(本題)commitをまとめる

masterへマージするにあたり、上記2commit(add c.txtadd d.txt)をまとめたくなった。

git rebase -iで対応していく。add c.txtの1つ前のcommitを指定していることに注意。

❯ git rebase -i f17ee3b566b60c4dad9250d9d67b7ae157c1d5e1

2つのcommitについて、それぞれreword(commit messageの修正)とfixup(まとめてしまうのでcommit messageは捨てる)にする。

reword 2dfb9b9 add c.txt
fixup 338cb6e add d.txt

上記について:wqすると、commit messageを修正するエディタが別途立ち上がるので、修正2

add c.txt and d.txt

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Sun Apr 26 16:10:59 2020 +0900
#
# interactive rebase in progress; onto f17ee3b
# Last command done (1 command done):
#    reword 2dfb9b9 add c.txt and d.txt
# Next command to do (1 remaining command):
#    fixup 338cb6e add d.txt
# You are currently editing a commit while rebasing branch 'add-c-and-d' on 'f17ee3b'.
#
# Changes to be committed:
#       new file:   c.txt

結果(2つのcommitがまとめられている)

❯ git log --pretty=oneline
e1db2b2b2082e379300d190faf634b015c6ba18c (HEAD -> add-c-and-d) add c.txt and d.txt
f17ee3b566b60c4dad9250d9d67b7ae157c1d5e1 (master) Merge branch 'add-b'
bceca8ee93f256fb1d4083798c1aed1e3b1c8c16 (add-b) add b.txt
b1abe76c7f481cecf64934a98dcd1971a13496e6 (add-a) add a.txt
635c5d5570a6c0492be7a924dda3df314bfd13b8 first commit

最後にmasterにマージして終わり3


  1. commit hashが前回記事と異なるのは、しくじって歴史を改変したため

  2. gitのeditorをvimにしています。

  3. 適宜、不要なブランチは削除(git branch -D <branch name>)する

【git】開発とレビューのバランスを求めて 〜ブランチ分割編〜

開発時、どの粒度でcommitするか考えながらコードを書きたくない。
特に、開発中は試行錯誤しながら実装を進めているので、最初から設計しきれていない場合も多い。怠慢と言われてしまうと弱いが、開発スピードも考慮すると、試行錯誤するのが早い。
確かにそんな状態で開発を進めていると、commit履歴が汚れてしまうのも現実だ。このままレビュワーにレビュー依頼をすると、レビュワーの側の負担が大きくなってしまう1

要件の整理

以下ができると、あとはチームで運用ルールを定めればよさそうだ。

  • 後で、ブランチを分割したい
  • 後で、commitをまとめたい

  • (ついでに)masterが進んでしまった場合に、新しいHEADからbranchを分けたい2

これらを実現するため、試してみたやり方をメモとしてまとめる。

本記事では、「ブランチを分割したい」を検証する。

「commitをまとめたい」は以下の記事で別にした。

https://cipepser.hatenablog.com/entry/git-operation-merge-commitcipepser.hatenablog.com

前提

  • conflictするパターンは考えない3

やりたいこと

commitが積み重なり、変更が大きくなったブランチを考える。レビュワーの負担減を目的に、このブランチを分割したい。

やり方

git cherry-pickを使う

(準備)commit用のファイルを用意する

ls
README.md a.txt     b.txt

txtファイルの中身はファイル名が一文字書いてある。 例えばa.txtならaとだけ書いてある。適当に用意しただけなので、意味はない。

❯ cat a.txt
a

後続に、git diffしている箇所があるので、git logを補足(これが初期状態)。

❯ git log --pretty=oneline
635c5d5570a6c0492be7a924dda3df314bfd13b8 (HEAD -> master) first commit

大きくなりすぎたブランチを作る

大きくなりすぎた(a.txtとb.txtを同時に追加した)ブランチをadd-a-and-bブランチとする。

❯ git checkout -b add-a-and-b

a.txtを追加したcommitと、b.txtを追加したcommitを行う。
(「2つもcommitあるなんて大きすぎるよ」の状態)

❯ git add a.txt 
❯ git commit -m "add a.txt"

❯ git add b.txt
❯ git commit -m "add b.txt"

git cherry-pickでcommit idを使うので、git logを確認しておく。

❯ git log --pretty=oneline
451fcd8e75608ca6364fd2ad9f9699adf8a8b3fb (HEAD -> add-a-and-b) add b.txt
41295c0d3043f560a61f57a0f44d4785056b4269 add a.txt
635c5d5570a6c0492be7a924dda3df314bfd13b8 (master) first commit

(本題)ブランチを分割する

a.txtを追加するadd-aブランチと、b.txtを追加するadd-bブランチに分ける

add-aブランチ

masterブランチからadd-aブランチを作成する。

❯ git checkout master
❯ git checkout -b add-a

add a.txtのcommitをpickする。

# `first commit`から`add a.txt`のcommit idを指定
❯ git cherry-pick 635c5d5570a6c0492be7a924dda3df314bfd13b8..41295c0d3043f560a61f57a0f44d4785056b4269

結果(add-aのcommitだけpickできた)

❯ git log --pretty=oneline
b1abe76c7f481cecf64934a98dcd1971a13496e6 (HEAD -> add-a) add a.txt
635c5d5570a6c0492be7a924dda3df314bfd13b8 (master) first commit

add-bブランチ

masterブランチからadd-bブランチを作成する。

❯ git checkout master
❯ git checkout -b add-b
Switched to a new branch 'add-b'

add b.txtのcommitをpickする。

# `add b.txt`のcommi idを指定
❯ git cherry-pick 451fcd8e75608ca6364fd2ad9f9699adf8a8b3fb

結果(add-bのcommitだけpickできた)

❯ git log --pretty=oneline
bceca8ee93f256fb1d4083798c1aed1e3b1c8c16 (HEAD -> add-b) add b.txt
635c5d5570a6c0492be7a924dda3df314bfd13b8 (master) first commit

masterにマージする4

add-aブランチのマージ

masterブランチに戻る。

❯ git checkout master

masterブランチにadd-aブランチをマージする。実運用ではGitHub上でプルリクを送ることになると思う。

❯ git merge add-a
Updating 635c5d5..b1abe76
Fast-forward
a.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 a.txt

結果

❯ git log --pretty=oneline
b1abe76c7f481cecf64934a98dcd1971a13496e6 (HEAD -> master, add-a) add a.txt
635c5d5570a6c0492be7a924dda3df314bfd13b8 first commit

add-bブランチのマージ

こちらもgit mergeでマージする5

念のため、事前状態を記載しておく。

❯ git checkout add-b
Switched to branch 'add-b'

❯ git log --pretty=oneline
bceca8ee93f256fb1d4083798c1aed1e3b1c8c16 (HEAD -> add-b) add b.txt
635c5d5570a6c0492be7a924dda3df314bfd13b8 first commit

マージする。

❯ git checkout master
❯ git merge add-b
Merge made by the 'recursive' strategy.
b.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 b.txt

結果(add-aブランチとadd-bブランチを分割できている)

❯ git log --pretty=oneline
298b644f77788e7f2db45f21bfc56731fc46470d (HEAD -> master) Merge branch 'add-b'
bceca8ee93f256fb1d4083798c1aed1e3b1c8c16 (add-b) add b.txt
b1abe76c7f481cecf64934a98dcd1971a13496e6 (add-a) add a.txt
635c5d5570a6c0492be7a924dda3df314bfd13b8 first commit

「commitをまとめたい」については、以下に書いた。

https://cipepser.hatenablog.com/entry/git-operation-merge-commitcipepser.hatenablog.com


  1. 打ち消しcommitだったり、そもそもが意味のないcommitだったりも含まれてしまうことも…

  2. 自分でブランチを分割したときにも発生するので、実は対応が必須

  3. 今、考えたいこと以上に複雑な問題にしない)

  4. 実際には、マージ前にadd a.txtのレビューを行っているはず。

  5. add-aブランチをmasterにマージしたことで、add-bブランチから見るとmasterが先に進んでしまっている。git rebase masterだと、一番最初にbranchを分けた時点からのdiffになってしまい、レビュワーの負担が減らなかったため、git mergeを使っている。conflictするパターンも含めて深堀りが必要か。あくまで本記事の対象は、「レビュー負担軽減を目指し、conflictがない範囲でブランチ分割を行う」こととした。

2019年読んで良かった本、記事

2019年に読んだ本と読んで良かった記事のリスト。自分用。
リストは、必ずしも今年に発表されたものではなく、自分が読んだタイミングが今年。 以前は年度ごとに振り返っていたが、ここ2年ほどやらなくなってしまったので再開(年末のほうが他の方に触発されてモチベ維持できそう)。

また、各本の感想を書くと、頑張り過ぎなので今回からやめる。

その分、読んでよかった記事を増やした。 記事を一覧化すると、自分がどんなことに興味があったのか可視化できてよかった。 何年か継続できたら振り返りに使いたい。

技術書

全体的に豊作だった。 特にWriting A Compiler In Go, Thorsten Ballは素晴らしい。 書評は以下に書いた。

cipepser.hatenablog.com

また、初めてHaskellに触れた。 読めるコードが増えた。 他にも、モナドのような「聞いたことはあるけどよく知らない概念」を手に入れられたのが収穫。

ビジネス書

本は目標ほど数を読めなかった。 その分、d10n Labのレポートを昨年分と今年分で350本ほど読んだ。

記事

概ね読んだ順番でリスト化する。
結構忘れてしまったものも多い。少なくとも当時の自分が気に入ったのだから、もう一度読み直してもいい。

Pocketでお気に入りしたものからリスト化した。体感、50記事に1記事くらいお気に入りする。 ちょっと記事に時間取られすぎてしまったのは反省点。来年は、論文と本を読む時間を多くしたい。

ざっと眺めてみると今年から読み始めたジャンルは、DeFi、金融、法。 変わらずネットワークやGo、Rustも読んでいるので、単純にジャンルの増加が読む記事の増加に直結。 体系的な知識に落とし込んで、読まない記事を増やすべき。

Github

後半が少ない。これも記事消化を優先してしまったため。 来年は言い訳せずに手を動かす。

f:id:cipepser:20191225235520p:plain

【Rust】mapとand_thenの違い

この記事は Rust Advent Calendar 2019 の9日目の記事です。

最近、すごいHaskellを読み終わりました。

Rustに戻ってきたので、改めてmapand_thenの違いを整理したいと思います。モナドで一般化されますが、本記事ではOption型を例に考えます。

環境

❯ rustup --version
rustup 1.20.2 (13979c968 2019-10-16)

❯ cargo version
cargo 1.38.0 (23ef9a4ef 2019-08-20)

実装の確認

まずはmapand_thenがどのように実装されているか見てみましょう。以下は、libcore/option.rsの実装です。

pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U> {
    match self {
        Some(x) => Some(f(x)),
        None => None,
    }
}

pub fn and_then<U, F: FnOnce(T) -> Option<U>>(self, f: F) -> Option<U> {
    match self {
        Some(x) => f(x),
        None => None,
    }
}

クロージャFシグネチャに注目します。
FnOnceを無視すると以下のようになっています。

  • map: T -> U
  • and_then: T -> Option<U>

例として、値を2倍にするようなクロージャを考えると、以下のような感じでしょうか。 mapは値をそのまま返し、and_thenSomeで包んで返します。

fn main() {
    let m = Some(1).map(|x| x * 2);
    println!("{:?}", m); // Some(2)

    let a = Some(1).and_then(|x| Some(x * 2));
    println!("{:?}", a); // Some(2)
}

上記の例のように単純に値を写像させたいだけの場合は、mapが便利なのですが、複数の(Optionを返す)関数をチェインさせたい場合は、and_thenが真価を発揮します。1

複数の関数をチェインさせる例として、以下のようなDBに対して、idからnameを引き、nameからpriceを得ることを考えます。

PRODUCTSテーブル

id name
0 desk
1 chair
2 whiteboard
3 pencil

PRICEテーブル

name price
desk 300
chair 100
whiteboard 20

実装

DBの代替にHashMapを使います。

lazy_static! {
    static ref PRODUCTS: HashMap<u32, &'static str> = {
        let mut m = HashMap::new();
        m.insert(0, "desk");
        m.insert(1, "chair");
        m.insert(2, "whiteboard");
        m.insert(3, "pencil");
        m
    };

    static ref PRICE: HashMap<&'static str, u32> = {
        let mut m = HashMap::new();
        m.insert("desk", 300);
        m.insert("chair", 100);
        m.insert("whiteboard", 20);
        m
    };
}

idからnameを得る関数をget_productnameからpriceを得る関数をget_priceとします。 DB上にレコードが存在しない可能性があるので返り値がOption型であることに注意してください。

fn get_product(id: u32) -> Option<&'static str> {
    match PRODUCTS.get(&id) {
        Some(u) => Some(u),
        None => None,
    }
}

fn get_price(product: &'static str) -> Option<u32> {
    match PRICE.get(&product) {
        Some(u) => Some(*u),
        None => None,
    }
}

get_productget_priceをチェインさせる

まずはmapを使った場合です。

fn main() {
    let product_id = 1;
    let price = get_product(product_id)
        .map(get_price);
    println!("{:?}", price); // Some(Some(100))
}

Someが二重に包まれてしまいました。値を取り出したいときに何度もunwrap()しないといけないですね。

一方でand_thenはどうでしょう。

fn main() {
    let product_id = 1;
    let price = get_product(product_id)
        .and_then(get_price);
    println!("{:?}", price); // Some(100)
}

Someに包まれるのは一回だけです。

失敗するパターン

もう一度DBのテーブルたちを再掲します。

PRODUCTSテーブル

id name
0 desk
1 chair
2 whiteboard
3 pencil

PRICEテーブル

name price
desk 300
chair 100
whiteboard 20

PRODUCTSテーブルには、pencilが存在しますが、PRICEテーブルには存在しません。

id=3mapand_thenの挙動を比較してみましょう。

fn main() {
    let product_id = 3;
    let price = get_product(product_id)
        .map(get_price);
    println!("{:?}", price); // Some(None)
}
fn main() {
    let product_id = 3;
    let price = get_product(product_id)
        .and_then(get_price);
    println!("{:?}", price); // None
}

mapでは、Some(None)が、and_thenではNoneとなりました。

まとめ

関数をチェーンさせる場合、mapを使うと多重にOptionで包まれてしまうことを確認しました。 mapを連続で使っていると最終的に何回包まれたのか覚えておかなくてはなりません。

さらに、何かしらの理由でget_productget_priceの間に、別のOptionを返す関数を実装しなくてはならなくなった場合はどうでしょう。 コンビネータを差し込むだけではなく、包みを解くコードにも変更が必要です。 差し込む関数を実装するときも、前の関数が何回Optionを包んだのか意識しながら実装することになり、変更に弱くなります。

References


  1. Rust by Exampleにも書いてあります。