【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
-
打ち消しcommitだったり、そもそもが意味のないcommitだったりも含まれてしまうことも…↩
-
自分でブランチを分割したときにも発生するので、実は対応が必須↩
-
今、考えたいこと以上に複雑な問題にしない)↩
-
実際には、マージ前に
add a.txt
のレビューを行っているはず。↩ -
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は素晴らしい。 書評は以下に書いた。
また、初めてHaskellに触れた。 読めるコードが増えた。 他にも、モナドのような「聞いたことはあるけどよく知らない概念」を手に入れられたのが収穫。
- すごいHaskellたのしく学ぼう! ,Miran Lipovača (著), 田中 英行 (翻訳), 村主 崇行 (翻訳)
- ハイパフォーマンス ブラウザネットワーキング ―ネットワークアプリケーションのためのパフォーマンス最適化, Ilya Grigorik (著), 和田 祐一郎 (翻訳), 株式会社プログラミングシステム社 (翻訳)
- 実践Rust入門[言語仕様から開発手法まで], κeen, 河野 達也, 小松 礼人
- 現代暗号の誕生と発展, 岡本 龍明
- Go言語による並行処理, Katherine Cox-Buday (著), 山口 能迪 (翻訳)
- Writing A Compiler In Go, Thorsten Ball
ビジネス書
本は目標ほど数を読めなかった。 その分、d10n Labのレポートを昨年分と今年分で350本ほど読んだ。
- 一人の力で日経平均を動かせる男の投資哲学, cis
- ハートドリブン 目に見えないものを大切にする力, 塩田 元規
- エンジニアリング組織論への招待 ~不確実性に向き合う思考と組織のリファクタリング, 広木 大地
- この1冊ですべてわかる 広告の基本, 波田 浩之
- 武器になる哲学 人生を生き抜くための哲学・思想のキーコンセプト50, 山口 周
- 最小の手間で最大の効果を生む! あたらしいWebマーケティングの教科書, 西 俊明
記事
概ね読んだ順番でリスト化する。
結構忘れてしまったものも多い。少なくとも当時の自分が気に入ったのだから、もう一度読み直してもいい。
Pocketでお気に入りしたものからリスト化した。体感、50記事に1記事くらいお気に入りする。 ちょっと記事に時間取られすぎてしまったのは反省点。来年は、論文と本を読む時間を多くしたい。
ざっと眺めてみると今年から読み始めたジャンルは、DeFi、金融、法。 変わらずネットワークやGo、Rustも読んでいるので、単純にジャンルの増加が読む記事の増加に直結。 体系的な知識に落とし込んで、読まない記事を増やすべき。
- Rustのモジュールの使い方 2018 Edition版 | κeenのHappy Hacκing Blog
- 本番環境のKubernetesマニフェストに 最低限必要な 7 のこと @ Japan Container Days v18.12 / jkd1812-prd-manifests - Speaker Deck
- JavaScriptの概念たち (前編) - Qiita
- JavaScriptの概念たち (後編) - Qiita
- 心理的安全性を 0から80ぐらいに上げた話
- 「例外」がないからGo言語はイケてないとかって言ってるヤツが本当にイケてない件 - Qiita
- 個人開発をはじめる前にやること & 公開直前にやったことリスト - Qiita
- OKR推進を支える「わくわく感」と「いけそう感」について話すよ - コネヒトのタレ
- 「そこ誤解してたら辛い!」OKR勉強会でたくさんの質問に答えてわかった、みんなが誤解していたOKRのあれこれ|horie|note
- bloXroute – ブロックチェーンのスケーラビリティを向上させる分散ブロック配信ネットワーク技術 | block-chain.jp by コンセンサス・ベイス
- Biz大学生がSTO規格をまとめてみた - Eisuke Tamoto - Medium
- 初心者がGo言語のcontextを爆速で理解する ~ cancel編 ~ - Qiita
- Goのnilだけどnilじゃないちょっとだけnilな値 - Qiita
- Replicated State Machinesでのストレージ故障からのリカバリー - だいたいよくわからないブログ
- CVE-2018-1002105 の issue を読んで kube-apiserver に詳しくなろう! - Qiita
- 開発組織マネジメントのコツ - Speaker Deck
- FOLIOの画像回帰テストの裏側 - Yosuke Kurami - Medium
- 心理的安全性ガイドライン(あるいは権威勾配に関する一考察) - Qiita
- 巨大企業のサーバー構成や内部ツールを覗く - 発明のための再発明
- コンテナ未経験新人が学ぶコンテナ技術入門
- Compiling Go to WebAssembly - Blog | SitePen
- FOLIOからfreeeに転職します - itohiro73’s blog
- プログラミングを目的にしてもいいと思う | κeenのHappy Hacκing Blog
- 0->1スタートアップが エンジニアを 2->8人にするまで - Speaker Deck
- How Much Privacy is Enough? at Scaling Bitcoin 2018 - Develop with pleasure!
- 世界最先端の認証認可技術、実装者による『CIBA』解説 - Qiita Handle; Aggregation protocol for large scale Byzantine committee
- 離散対数仮定が崩れた際にConfidential Transactionチェーンのコインを保護するSwitch Commitment - Develop with pleasure!
- 【KDD2018】論文『Customized Regression Model for Airbnb Dynamic Pricing』を読んでまとめた - 港区で苦しむデータサイエンティストのメモ帳
- .appという画期的でセキュアなgTLDについて - Lento con forza
- ユビキタスデータセンターOSの文脈におけるコンテナ実行環境の分類 - 人間とウェブの未来
- xerrorsパッケージがWrapメソッドではなく : %w でラップする理由 - Qiita
- お薦めのコンパイラの本とか | κeenのHappy Hacκing Blog
- 政府によるインターネットの検閲とSNIについて - catatsuy - Medium
- Pairings for beginners - LayerX Research
- DApp Questのコントラクト開発 - Speaker Deck
- 高木浩光@自宅の日記 - Coinhive事件、なぜ不正指令電磁的記録に該当しないのか その2
- Kamuee: SRv6対応の設計と実装に関して - Speaker Deck
- バッチ処理の採用と設計を考えてみよう - Mercari Engineering Blog
- 逆に今更しか知れないBCH超技術の話 | ALIS
- セキュリティトークンのエコシステム概観 - LayerX-jp - Medium
- ZeroFormatter/MagicOnion - Fastest C# Serializer/gRPC based C# RPC
- FlatBuffers: FlatBuffers
- 「新たなICO規制についての提言」について | 一般社団法人 日本仮想通貨ビジネス協会(JCBA)
- Internet Week 2018 知っておくべきIPv6とセキュリティの話
- TLSとWebブラウザの表示のいまとこれから~EV証明書の表示はどうなるのか~
- 「エンジニア採用したい」と言う割には面接が下手な企業が多すぎるという話 - paiza開発日誌
- A generalised solution to distributed consensus – the morning paper
- 次世代Webカンファレンス 2019:HTTPSセッションが面白かった - ろば電子が詰まつてゐる
- 心理的安全性と、Veinの紹介 Psychological safety and introduction of Vein
- 自己修復的なインフラ -Self-Healing Infrastructure-
- サービス開発初期の「時間を金で買う」技術 - Speaker Deck
- Fastlyのプログラマから見たCDN - Speaker Deck
- 認証にまつわるセキュリティの新常識 - Speaker Deck
- Slim: OS kernel support for a low-overhead container overlay network – the morning paper
- Microservicesでなぜ作るのか - An Epicurean
- Microservices時代の監視設計 - An Epicurean
- 負荷試験コトハジメ
- 【動画で学ぶブロックチェーン】Confidential Transaction - 安土 茂亨氏 - YouTube
- 仮想通貨で稼ぐってどうやるの総まとめ(2019年版)BitMEX、Binance、レンディング等 - 西欧の車窓から - Medium
- <仕組み解説>Guidelines of MakerDAO&dai - Watata Crypto Medium - Medium
- MakerDAO(メーカーダオ)と分散型ステーブルコインDAI | BitPR Deepdiveレポート | DeFi シリーズ | bit-pr.com
- 【Haskell】 言葉の定義まとめ(型クラス、型コンストラクタ、値コンストラクタ、型引数など) - takafumi blog
- 「新しいものを生むチャレンジを支えたい」 日本のクラウドファンディング先駆者「Makuake」に聞く、“サービスの意義”と“トラブルから学んだこと” (1/2) - ねとらぼ
- MakerDAO入門 - BUIDL - Medium
- エンジニアの評価制度、他社は一体どうやってるの!?まとめて紹介! - paiza開発日誌
- エンジニアのための刑事手続入門 - Speaker Deck
- 手数料を使ったReorg攻撃の可能性とその対策案 - Qiita
- 次世代のコンセンサスエンジン"Tendermint"の話をしました @blockchain.tokyo #8 - Mercari Engineering Blog
- Goコンパイラをゼロから作って147日でセルフホストを達成した - Qiita
- 仮想通貨のレンディングでガバナンス投票を安価にできてしまう問題 | CoinChoice
- How to choose a good and safe Cosmos hub validator among 100? A brief guide for delegators
- クラウドファンディングを成功させたい人へのまとめ – バタフライボード 公式サイト
- Cosmos Staking Primer (+Reward Calculator)
- ためしておぼえる Rust のマクロ - Qiita
- TLS 1.3 開発日記 その22 公開鍵暗号の動向 - あどけない話
- community/stability-fee.md at master · makerdao/community
- BEAMが提供する監査機能 - Develop with pleasure!
- What are Bitcoin loans used for, and who is using Crypto backed loans?
- 【動画で学ぶブロックチェーン】Mimblewimble - 安土 茂亨氏 - YouTube
- Rustの関連型の使いどころ | κeenのHappy Hacκing Blog
- 「スクラムマスターを雇う時に聞いてみるとよい38個の質問」に答えた - この国では犬が
- 20190613コンセンサスアルゴリズム勉強会 - LayerX Research
- 敵対的生成ネットワーク(GAN)
- 「中央銀行、金融政策、暗号通貨に未来はあるのか」 特別インタビュー 早稲田大学 岩村充教授 - YouTube
- Libra勉強会@JBA
- Rust のエラーまわりの変遷 - Qiita
- 心理的安全性の構造 デブサミ2019夏 structure of psychological safety
- Polkadot(ポルカドット)とSubstrate(サブストレート)の概要と仕組み、取り巻くエコシステムに関して | CRYPTO TIMES
- 大企業アジャイルの勘所 #devlovex #devlovexd
- 卜部昌平のあまりreblogしないtumblr - 検索と挿入がともにO(1)であるようなHashを作るにはコツがいる
- マスロフ式算数がやたらに面白いんですけど - 檜山正幸のキマイラ飼育記 (はてなBlog)
- 【特別放送】仮想通貨取引所とセキュリティ対策の課題 with Bitbank CBO ジョナサン・アンダーウッドさん - YouTube
- スマートコントラクト用の高水準言語BitMLを利用した安全なBitcoinベースのスマートコントラクト開発 - Develop with pleasure!
- Rustのasync/awaitをスムーズに使うためのテクニック - Qiita
- 秒間100万リクエストをさばく - Googleの共通認可基盤 Zanzibar - 発明のための再発明
- 暗号資産の鍵を取り扱うサービスに関する調査 中間報告 - Speaker Deck
- 計算機の機構と計算理論
- 第4回:平澤直(プロデューサー) | 3DCGの未来~CGアニメとメディアリレーション~ | AREA JAPAN
- 楽天ネットワークエンジニアたちが目指す、次世代データセンターとは
- RDBMS in Action - Speaker Deck
- AWSサービスで実現するバッチ実行環境のコンテナ/サーバレス化/ Container service of batch execution environment realized by AWS service - Speaker Deck
- アーキテクチャのレビューについて - JaSST Review '18
- シャノン限界を達成しかつ実行可能な通信路符号を実現:NTT持株会社ニュースリリース:NTT HOME
- The Magic of Go Comments · jbowen.dev
- ブロックチェーンは証券決済をどのように効率化するか?|セキュリティトークン特集|Ginco Magazine - 安全に仮想通貨を管理するための情報をとどける
- 電源を切っても消えないメモリとの付き合い方 - Speaker Deck
- RDBの作成時刻や更新時刻用カラムに関するプラクティス | おそらくはそれさえも平凡な日々
- ISPバックボーンネットワークにおける経路制御設計 ~実践編~
- 質とスピード / Quality and Speed - Speaker Deck
- Rustの非同期プログラミングをマスターする - OPTiM TECH BLOG
- Snap: a microkernel approach to host networking – the morning paper
- Taiji: managing global user traffic for large-scale Internet services at the edge – the morning paper
- NFTと金融 - Google スライド
- なぜChromeはURLを殺そうとするのか? (Chrome Dev Summit 2019) - ぼちぼち日記
- これならしんどくないGit運用の考え方 - Speaker Deck
- 運用を支えるためのログを出すにはどうするか? #jjug_ccc #ccc_m3 - Speaker Deck
- オニギリペイのセキュリティ事故に学ぶ安全なサービスの構築法 (PHPカンファレンス2019)
- レンディング格付けを行うDefi Scoreとサービス比較 | TokenLab
- (リサーチラボ)中央銀行がデジタル通貨を発行する場合に法的に何が論点になりうるのか:「中央銀行デジタル通貨に関する法律問題研究会」報告書の概要 : 日本銀行 Bank of Japan
- ScaleCheck: A Single-Machine Approach for Discovering Scalability Bugs in Large Distributed Systemsを読んだ - だいたいよくわからないブログ
- キャリアを考える上で大事だと思ってること|石倉秀明@bosyu|note
- なぜプロジェクトは炎上するのか?炎上しやすい4つの傾向と、炎上を防ぐ3つの対策 - paiza開発日誌
Github
後半が少ない。これも記事消化を優先してしまったため。 来年は言い訳せずに手を動かす。
【Rust】mapとand_thenの違い
この記事は Rust Advent Calendar 2019 の9日目の記事です。
最近、すごいHaskellを読み終わりました。
HaskellやってからRustに戻ってくるとmapとand_thenの違いがわかってよいな
— さいぺ (@cipepser) 2019年10月15日
Rustに戻ってきたので、改めてmap
とand_then
の違いを整理したいと思います。モナドで一般化されますが、本記事ではOption
型を例に考えます。
環境
❯ rustup --version rustup 1.20.2 (13979c968 2019-10-16) ❯ cargo version cargo 1.38.0 (23ef9a4ef 2019-08-20)
実装の確認
まずはmap
とand_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_then
はSome
で包んで返します。
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_product
、name
から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_product
とget_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=3
でmap
とand_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_product
とget_price
の間に、別のOption
を返す関数を実装しなくてはならなくなった場合はどうでしょう。
コンビネータを差し込むだけではなく、包みを解くコードにも変更が必要です。
差し込む関数を実装するときも、前の関数が何回Option
を包んだのか意識しながら実装することになり、変更に弱くなります。
References
-
Rust by Exampleにも書いてあります。↩
GoでCount-min sketchを実装する
この記事は Go Advent Calendar 2019 の2日目の記事です。
こんにちは!さいぺです。
サムネのGopherくんは最近趣味で描いたものを、せっかくなので載せました。
オリジナルのThe Go gopher(Gopherくん)は、Renée Frenchによってデザインされました。
さて、以下の動画を見てMiniSketchなるデータ構造があることを知りました。
MinisketchってIBLTを比較してメリデメとかあるんだろうか。そもそもどういうデータ構造使ってるんだろ。
— さいぺ (@cipepser) 2019年11月13日
【動画で学ぶブロックチェーン】Bitcoinの新しいTxリレープロトコルの提案 Erlay - 安土 茂亨氏 - YouTube https://t.co/mKB18CmWAF
動画中のMiniSketchでは集合のreconcileまで述べられていますが、今回はCount-min sketchを実装します。より具体的には、指定した要素がいくつ追加されたのかを返すデータ構造を実装します。
Count-min sketchとは
Count-min sketchは、追加された要素の数を記憶するデータ構造です。
BloomFilterに似た確率的データ構造で、BloomFilterが要素の有無をbool
で返すのに対して、Count-min sketchは追加された要素の数をint
で返します。
偽陽性を許容することで、追加された要素の数を得るための最悪計算量が O(1)
、メモリサイズを固定にできます。
アルゴリズム
k
個のハッシュ関数とk
個のテーブルを利用します。
各テーブルのフィールドは0
に初期化されています。
それぞれのハッシュ関数とテーブルは1:1で対応しており、要素を追加する際にフィールドの値をインクリメントします。
ハッシュ関数の数k=3
、テーブルのサイズN=10
として、二つの要素とを追加する例を考えます。3つの各テーブルのindexは0始まりで図示しています。
1つ目の要素に対して、3つのハッシュ値を計算し、該当するフィールド(中段図の赤字)をインクリメントします。
続けて、2つ目の要素に対して、3つのハッシュ値を計算し、該当するフィールド(下段図の赤字)をインクリメントします。
要素がいくつ追加されたのか知りたいときも同じような手順をとります。
例えば上記の例でがいくつ追加されたのか知りたいとします。
このとき要素に対する3つのハッシュ値を計算します(図中の中段から下段に移動したときと同じ)。該当するフィールドの値を参照すると1
、2
、1
が得られます(下段図の赤字)。この3つの数字のうち、最小(min)の値を要素が追加された数として返すのがCount-min sketchです(今回の例では1
を返す)。
とが衝突しましたが、ハッシュ関数を3つ用意したことで、衝突していない残り2つの値が正しい答えとして、追加された要素の数を表現できていることがわかります。
実装
アルゴリズムが理解できたところで実装に移ります。 本記事ではポイントを絞って説明しますが、実装した一式はGitHubにあるので興味がある方はご覧ください。
データ構造
まずSketch
型を以下のように定義します。
ここでk
はハッシュ関数の数、N
は各テーブルのサイズです。
type Sketch [k][N]int
Addの実装
k
個のハッシュ関数は、Double-hash法で用意します。
Double-hash法については、以前の記事で触れていますのでご参照ください。
今回はDouble-hash法をこのように実装していますが、Sketchのアルゴリズムとは直接関係ないので本記事では省略します。
要素をSketchに追加するためのメソッドAdd
の実装は以下のようになります。
func (s *Sketch) Add(elem string) error { for i := 0; i < k; i++ { h, err := util.DoubleHashing(elem, i, N) if err != nil { return err } s[i][h] += 1 } return nil }
k
個のテーブルそれぞれに紐づくハッシュ関数があるので、要素のハッシュ値を計算します。
そのハッシュ値ををテーブルのindexだと思って、対応するフィールドをインクリメントするだけです。
Countの実装
要素がいくつ追加されたのかを得るためのメソッドCount
の実装は以下です。
func (s *Sketch) Count(elem string) (int, error) { min := math.MaxInt64 for i := 0; i < k; i++ { h, err := util.DoubleHashing(elem, i, N) if err != nil { return 0, err } if s[i][h] < min { min = s[i][h] } } return min, nil }
Add
と同じくk
個のハッシュ値を計算します。
k
個のハッシュ値のうち最小のものを、追加された要素の数として返します。
動作を確認する
実際に動かしてみましょう。
以下のようにa
〜e
をそれぞれ1
〜5
個追加します。
なお、ハッシュ関数の数k=3
、テーブルのサイズN=10
としています。
elements := []string{ "a", "b", "b", "c", "c", "c", "d", "d", "d", "d", "e", "e", "e", "e", "e", }
要素を追加する操作はこんな感じです。
for _, e := range elements { err := s.Add(e) if err != nil { panic(err) } }
uniques := []string{"a", "b", "c", "d", "e"}
として、すべての要素がいくつずつ追加されたのか出力させてみます。
for _, e := range uniques { cnt, err := s.Count(e) if err != nil { panic(err) } fmt.Println(e, ":", cnt) }
実行結果です。しっかり追加した要素の数を取得できていますね。
❯ go run main.go a : 1 b : 2 c : 3 d : 4 e : 5
ハッシュ衝突させてみる
ハッシュ衝突させてみたかったので、要素の数を増やしたところg
とh
で衝突しました。
elements := []string{ "a", "b", "b", "c", "c", "c", "d", "d", "d", "d", "e", "e", "e", "e", "e", "f", "f", "f", "f", "f", "f", "g", "g", "g", "g", "g", "g", "g", "h", "h", "h", "h", "h", "h", "h", "h", }
以下のようにg
(7個)とh
(8個)で15
となりました。
❯ go run main.go a : 1 b : 2 c : 3 d : 4 e : 5 f : 6 g : 15 h : 15
最後に
偽陽性はありますが、データ構造自体のサイズを固定できるのは魅力的です。
今回はテーブルサイズをかなり小さくしたので比較的容易にハッシュ衝突しましたが、もう少し大きなN
を選べば十分実用的でしょう。1
今回はベンチマークまで取れていないのでk
やN
を大きくしたときにどうなるか調べるのもおもしろそうです。
あとはMiniSketchでの集合をreconcileもいずれ実装したいです。
References
Go1.13のError wrappingを触ってみる
2019年9月3日にGo 1.13がリリースされました。
リリースノートを見ていて、Error wrapping
が気になったので触ってみます。
何はともあれ、ドキュメントを見てみます。 errorsのドキュメントを確認すると、以下4つの関数が存在します。
func As(err error, target interface{}) bool func Is(err, target error) bool func New(text string) error func Unwrap(err error) error
New
はただのコンストラクタなのでいいとして、残り3つは挙動を確認します。
Isを使ってみる
順番が前後しますが、Is
から見ていきましょう。
Go1.12以前では、以下のような(if err != nil
)エラーハンドリングをしていたと思います。
package main import ( "errors" "fmt" ) var ( MyError = myError() ) func myError() error { return errors.New("myErr") } func simpleError() error { return MyError } func main() { err := simpleError() if err != nil { fmt.Println(err) // myError } }
もしくは、switch文を用いて以下のようにするパターンもありますね。
※以降では、main
以外の共通部分は省略します。
func main() { err := simpleError() if err != nil { switch err { case MyError: fmt.Println("MyError:", err) // MyError: myErr default: fmt.Println("default:", err) } } }
Is
を用いると以下のように(if errors.Is(err, MyError)
)なります。
func main() { err := simpleError() if errors.Is(err, MyError) { fmt.Println(err) // myError } }
Is
の使い方はわかりましたが、Is
の何が嬉しいのでしょうか。
本アップデートの名前にもあるようにwrapしたときに活きてきます。
ドキュメントにあるように、wrapするには%w
を使います。
例えば、以下のような実装になります。
func wrappedError() error { err := simpleError() return fmt.Errorf("%w", err) }
実際にwrappedError
を用いて、wrapしたときのerr
の型を見てみましょう。
func main() { err = simpleError() fmt.Printf("%T", err) // *errors.errorString fmt.Println() err := wrappedError() fmt.Printf("%T", err) // *fmt.wrapError }
wrapする前は*errors.errorString
型だったものが、*fmt.wrapError
型になっていることがわかります。
先ほどのswitch
文の例を実行してみると、MyError
のエラーを捕まえることができなくなってしまいました。
func main() { err := wrappedError() // wrappedErrorに変わったことに注意 if err != nil { switch err { case MyError: fmt.Println("MyError:", err) default: fmt.Println("default:", err) // default: myErr } } }
そこでIs
の登場です。
func main() { err := wrappedError() if errors.Is(err, MyError) { fmt.Printf(err.Error()) // myErr } }
こちらはMyError
を捉えられています。
Asを使ってみる
次にAs
についてです。
個人的に最近はパーサーを書くことが多いので、以下のような例を用意しました。
Parse
に失敗したときのエラーをハンドリングする例です。
返ってきたerr
をInvalidChar
に型キャストして、エラーハンドリングしています。
package main import ( "errors" "fmt" ) type InvalidChar struct { // other fields err error } func (ic *InvalidChar) Error() string { ic.err = errors.New("INVALID CHARACTER") return fmt.Errorf("%w", ic.err).Error() } func Parse() error { return &InvalidChar{} } func main() { err := Parse() if ierr, ok := err.(*InvalidChar); ok { fmt.Println(ierr) // INVALID CHARACTER } }
As
を用いると以下のようになります。
func main() { err := Parse() var ierr *InvalidChar if errors.As(err, &ierr) { fmt.Println(ierr) // INVALID CHARACTER } }
ここまでは、Is
の例と同じなので本例でもwrappedError
を実装します。
func wrappedError() error { err := Parse() return fmt.Errorf("%w", err) }
wrapされたerr
をGo1.12以前の方法で扱おうとすると以下のようにチェックをすり抜けます。
func main() { err := wrappedError() if ierr, ok := err.(*InvalidChar); ok { fmt.Println(ierr) // 何も出力されない } }
以下のようにAs
を用いれば、正しくハンドリングすることができます。
func main() { err := wrappedError() var ierr *InvalidChar if errors.As(err, &ierr) { fmt.Println(ierr) // INVALID CHARACTER } }
Unwrapを触ってみる
最後にUnwrap
です。
例はAs
で用いたものと同じです。
func main() { err := wrappedError() fmt.Printf("Type:%T\nValue:%v\n", err, err) // Type:*fmt.wrapError // Value:INVALID CHARACTER err = errors.Unwrap(err) fmt.Printf("Type:%T\nValue:%v\n", err, err) // Type:*main.InvalidChar // Value:INVALID CHARACTER err = errors.Unwrap(err) fmt.Printf("Type:%T\nValue:%v\n", err, err) // Type:<nil> // Value:<nil> }
wrapされたエラーを順番にUnwrap
していくことができ、最後にはnil
になります。
所感
- 標準ライブラリや外部ライブラリのエラーをwrapできるようになったのはありがたい
- Go1.13以降で開発されたライブラリを用いることも考慮し、安全に倒すために
Is
、As
でエラーハンドリングしたほうがよさそう - エラー型が多くなってきたときにswitch文が使えないのが微妙
3つ目について、Go1.13からは今までのエラーハンドリングが機能しなくなるかもしれない - Qiitaでも言及されています。
以下のようにUnwrap
する案も考えましたが、多段でwrapした際にうまく動かなくなるので実用的ではないでしょう。。。
func main() { err := wrappedError() switch errors.Unwrap(err).(type) { case *InvalidChar: fmt.Println("InvalidChar:", err) // InvalidChar: INVALID CHARACTER default: fmt.Println("default:", err) } }
References
Rustでprotobufのゼロ値がどうやって実現されているか
以前、以下の記事たちを書きました。
Rustで0byteを読み込むとどうなるのか(Option
型になるのか)気になったので検証してみます。
0byteをrust-protobufで読み込む
protobufを読み込むコードは、Golangで出力したprotobufバイナリをRustで読み込む - 逆さまにしたとほぼ同じです。一点変更しているのは、読み込むファイル名で、main.rs
のgo_user.bin
をzero.bin
に変更しました。
ちなみに.proto
の定義は以下の通りです。
syntax = "proto3"; package user; message User { string name = 1; int32 age = 2; }
空ファイルを作成し、それを読み込んだ実行結果は以下の通りです。 Goでいうゼロ値でデコードできています。
❯ touch zero.bin
❯ cargo run
Name:
Age: 0
というのもproto3の仕様に以下のように書いてあるのです。
Unknown fields are well-formed protocol buffer serialized data representing fields that the parser does not recognize. For example, when an old binary parses data sent by a new binary with new fields, those new fields become unknown fields in the old binary.
つまりprotobufでは、unknow fieldsも含めて仕様化されており、パーサーは値が存在しないのか、フィールドが定義されていないのかは区別できないのです。
ゼロ値ならバイナリにすらエンコードされないので、送るデータそのものを無くすことができる効率性があります。
protocで生成したコードの実装
protobufの仕様からGoのゼロ値のようにデフォルト値が使われることがわかりました。
もう一歩踏み込んで、protoc --rustout
でgenerateした実装を見ていきましょう。
わかりやすところで、Golangで出力したprotobufバイナリをRustで読み込む - 逆さまにしたで利用した以下のget
メソッドの実装を見ていきます。
println!("Name: {}", u.get_name()); println!("Age: {}", u.get_age());
protoc --rustout
でgenerateした実装を確認すると、以下のようになっています。
pub fn get_name(&self) -> &str { &self.name } pub fn get_age(&self) -> i32 { self.age }
返り値がOption
に包まれていないですね。
Goの場合はゼロ値がありますが、Rustではどうやって実現されているんでしょうか。
ということでnew
の実装をみてみます。
impl User { pub fn new() -> User { ::std::default::Default::default() } }
予想通りですが、default
が使われています。
なお、User
の定義でもDefault
がattributeに含まれています。
#[derive(PartialEq,Clone,Default)] pub struct User { // message fields pub name: ::std::string::String, pub age: i32, // special fields pub unknown_fields: ::protobuf::UnknownFields, pub cached_size: ::protobuf::CachedSize, }
References
実践Rust入門を読んだ
実践Rust入門 言語仕様から開発手法まで, κeen, 河野達也, 小松礼人を読みました。本書の特徴は以下の3つでしょう。
- 2018 Editionに対応している
- FFIについて日本語で書かれた書籍
- 実践 を意識した内容になっている
本記事では、特に3つ目の実践的という観点で感想を述べようと思います。
Rustの言語仕様という観点で言えば、プログラミングRustのほうが網羅性は高いでしょう。 しかし、Rustは入門のハードルがとても高い言語です。1
個人的な経験でいえば、map
やfilter
でさえHaskellを勉強していなかったら、とっつきにくかったんじゃないかと思います。他言語の経験が豊富であればまだしも、いきなりRustを始めると挫折してしまうでしょう。
だからこそ、本書のような実践的な入門書は重要だと思います。「あれもこれもやらなきゃいけない」とならずに、実際に利用されるトピックから取り組めるのはとても良いことだと思います。
また、わかってる人からすると当たり前だけど初学者はわからないというようなことが、言語化されていたのもよかったです。例えばP.151では、桁あふれのメソッドについて
- 検査付き演算:
checked_
- 飽和演算:
saturating_
- ラッピング演算:
wrapping_
- 桁あふれ演算:
overflowing_
のprefixがつく、と言及されていました。他にもP.169のボックス化されたスライスでは、
使う機会はあまりなさそうですが、
と書かれていたりして、力の抜きどころもわかって良かったです。 プログラミングRustを読んでいると網羅性が高いのはいいんですが、これはいつ使うんだろう?となることがあるのでこういう配慮は助かります。
他にも、プログラミングRustにも載っている内容ではあるものの、String
とstr
の違いは最初にわからなくなりがちなところなので、メモリ表現が図解されているのもわかりやすいと思います。
また、ユニット型()
がサイズゼロだと知ってるとcollectionでSet作りたいときに()
使えば良さそうと思えるのも良かったです。以前、GoでSetライブラリを自作しようとしたときにstruct{}{}
だらけになったのでRustのほうが同じサイズゼロで実装するのにきれいに書けますね。 2
いいところばかりなのもあれなので、気になったところも述べます。
内容ではないですが、誤植が気になりました。
例えば、ch07_10_closures.rs
の let lookup = || assert!(s1.find('d').is_some());
に閉じカッコがないなど、コンパイルが通らないエラーは潰し込んでほしかったなと。
とはいえ、誤植はrust-jpのslackでも報告、対応してくださっているので、大きな問題はなかったです。
また、GitHubのコードは問題なく動くのでこちらも見ながら進めれば大丈夫でした。
構成で気になったのが、P.271の最後です。ここはgrow
を実装する前だと動かないので、初学だと動かなくてちょっと困るかもと思いました。new
しただけだとサイズ0
しかヒープに領域確保してないので、動かないと思います。一度読み飛ばして、grow
を実装後に、戻ってくれば実行できるので、順序が逆だとわかりやすそうです。
また、本書でもマクロが使われているので、簡単な紹介くらいあるともっと良かったかと思います。
特に良かった章
最後に、個人的に勉強になった・楽しかった章の感想を残します。
第7章 所有権システム
Rust特有の概念である所有権やライフタイムについて第1部の中で特に実践的な章でした。
簡易版のVec
を実際に自分の手で実装することで、利用頻度の高いVec
の内部実装がイメージできるのは大きいと思います。
また、CopyとClone、RcとArc、FnとFnMutとFnOnceの違いが書かれていて、頭の中が整理できました。
第9章 パーサを作る
本書で一番読みたかった章です。 再帰下降パーサはmonkeyで実装したことがありましたが、Annotationをつけたことがなかったのと、エラー処理をちゃんとできるのはとても楽しかったです。
実践Rust入門9章終わった。どこでエラー起きたかもわかるし最高だ。 pic.twitter.com/bIMJyxuF3i
— さいぺ (@cipepser) May 5, 2019
第10章 パッケージを作る
パッケージの作成、CI、crate.ioへの公開と一通り触れてよかったです。特にcrate.ioへの公開は初めてだったので勉強になりました。
第11章 Webアプリケーション、データベース接続
リクエストの受付からDBへの接続、レスポンスを返すところまで一式という章です。 Futuresに振れることができたのも大きな収穫でした。 また、CLIがこんなに簡単に書けるのかという驚きもありました。
References
- 実践Rust入門 言語仕様から開発手法まで, κeen, 河野達也, 小松礼人
- ghmagazine/rustbook
- プログラミングRust | Jim Blandy, Jason Orendorff, 中田 秀基
- Join rust-jp on Slack!
- cipepser/monkey
- 作者: κeen,河野達也,小松礼人
- 出版社/メーカー: 技術評論社
- 発売日: 2019/05/08
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
- 作者: Jim Blandy,Jason Orendorff,中田秀基
- 出版社/メーカー: オライリージャパン
- 発売日: 2018/08/10
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る