Web開発チームの安達です。
先日ブログでアナウンスさせていただいた「お肉を食べながらScalaのコードレビューする会」ですが、たくさんの方にご応募頂き、無事に開催することができました。今回は東京(5/14)と大阪(5/21)で開催したコードレビュー会の様子をレポートしたいと思います。
会場の様子
まずは各会場の様子からお伝えします。東京は5/14に、弊社東京オフィス近くのカーサ カステリーニさんというイタリア料理のお店で開催しました。普段から社員に人気のお店ですが、今回は「お肉多めのコース」でしっかりお肉を食べました。
大阪は5/21、梅田のmeat kitchen SAKAMOTOさんという焼肉屋さんで開催しました。せっかくテレビ付きの個室を予約したのですが、HDMIケーブル忘れてしまうというトラブルがありました。しかしお肉を焼きながらだとレビューしづらいという知見も得ることができました。
東京からすぶた職人も来ました。(参照: AWSのためのsbtプラグインを作ってみた)
盛り上がったコードレビュー
今回の課題は「MyList型を実装してみましょう」でした。スタックオーバーフローを回避するために末尾再帰を意識した実装、実装スタイルの違い、色々な解答が出揃っていたので盛り上がりました。中でも盛り上がった話題をピックアップしてご紹介します。
withFilterの実装
両会場でもっとも盛り上がったのが、withFilterの実装です。 出題の意図としては、filterの後にmapやflatMapなどの処理が続く場合に中間リストの生成を行わないようにして計算効率を上げるためです。例えば、
MyList(1, 2, 3, 4).filter(_ > 2).map(_ * 2)
のような処理を書いたとき、filter(_ > 2)
をした後に一度MyList(3, 4)
という中間リストが生成されてしまいます。
しかしListのwithFilterを使うことで、中間リストの生成を避けることができます。ではどのように実装すれば良いのでしょうか?
と、その前に。申し訳ないのですが、実はMyListのこの問題には不備があったとも言えなくないです。本家Scala(2.11)のコードと比較してみましょう。
こちらがScalaのwithFilter
def withFilter(p: A => Boolean): FilterMonadic[A, Repr] = new WithFilter(p)
こちらが課題のwithFilter
def withFilter(p: A => Boolean): MyList[A]
どうやら、本家のwithFilterはFilterMonadicという型を継承したWithFilter
という型を返しているようですね。一方、お肉を食べる会の課題では返り値の型がMyList[A]
型になってますね。確かに、中間リストが生成されないようにするには、mapやflatMap内でフィルタの関数p: A => Boolean
を評価する必要がありそうで、これを行うためには新しい型を用意し、改めてmapやflatMapを実装する必要がありそうです。
参加者の方にはこのwithFilterをあれこれ手をつくして実装していただき、いろいろな解答が揃いました。 大きく分けて3パターンの実装がありましたので、それぞれ紹介します。
1. WithFilter型を用意するパターン
まずは王道の問題をねじ曲げるパターンです。WithFilter型を用意して、withFilterの返り値をWithFilter型にすると光が見えてきます。
解答例をご紹介します。
foldRightでリストを走査するときにフィルタリングを行っていますね。このような実装によって中間リストの生成を避けることができます。
2. MyListを継承した型を用意するパターン
MyCons[A], MyNil以外に、もう一つMyList[+A]を継承した型を用意するパターンもありました。 例として、MyFilteredという型を用意された解答をご紹介します。
この解答例では、map/flatMap/withFilterメソッドの実装でパターンマッチを使ってMyFiltered型の場合の処理を分けていますね。
3. 無名クラスを用意するパターン
withFilterメソッド内で無名クラスを定義し、map/flatMap/filter/withFilterをオーバライドするという実装をされた方も1人おられました。
実装はこのようになっていました。
この解答も問題のシグネチャを変更せずに、中間リストの生成を避けることができていますね。
名前の付け方
こちらも両会場でも盛り上がった話題は、メソッド内のメソッドにどんな名前つけますか?という話題でした。 再帰的な処理を末尾再帰で書くためにメソッド内にメソッドを定義することがあると思います。例えば、このような場合です。
みなさんはどんな名前を付けますか?お肉会に参加された方からは、
- foldLeft0 (メソッド名の末尾に0をつける)
- go
- loop
といった名前が挙がってきました。色んな名前の付け方があっておもしろいですね。
startsWithの実装
startsWithは、引数で受け取ったリストがそのリストの接頭辞になっているかどうかを真偽値で返すメソッドです。ほとんどの方は次のようなタプル2(ペア)のパターンマッチを駆使した実装をされていました。
しかし、MyListにzipとforAllというメソッドを実装し、それを用いてstartsWithを実装された方がいました。まずは簡単にzipとforAllについて説明をします。
zip
zip[B](other: MyList[B]): MyList[(A, B)]
は引数で受け取ったリストと同じ位置の要素をペアにした新しいリストを返すメソッドです。
例えば、MyList(1, 2, 3).zip(MyList("a", "b", "c"))
はMyList((1, "a"), (2, "b"), (3, "c"))
を返します。
forAll
forall(p: A => Boolean): Boolean
は、リストのすべての要素が条件pを満たすかどうかを真偽値で返すメソッドです。
例えば、MyList(1, 2, 3).forAll(_ > 0)
はtrue
を返します。
startsWith
zipとforAllを用いてstartsWithを実装するとこのような実装になります。
左傾化
今回のようなMyListのメソッドを実装する課題のほとんどはfoldLeftを使って実装できますよ、という話が挙がってきました。 こちらの記事「scala を左傾化させる話」を参加者の間で共有し、もくもくと読む時間もありました。(関連ツイート)
解答例でもfoldRightを多用していますが、foldRightすらfoldLeftで実装が可能というお話でした。
まとめ
今回、東京・大阪会場ともに時間いっぱいScalaの話題で盛り上がり、色々な意見や情報交換ができました。今後またこのような学びの多い機会を設けたいと考えていますので、みなさんの参加を楽しみにお待ちしております。
また今回の課題の解答例を用意しましたので、解きはじめてみたけれど分からなかったときに参考にしていただければと思います。