matsuです。初めての投稿でドキドキです。よろしくお願い致します。
先日社内で、真面目にやるのもいいけども、食事をしながら聴くような勉強会もしたいねということで「ゆるふわ勉強会」が開催されました。
そこで発表された中から、トップバッターで出て来てくれました@kazzさんの『クソコードを使おうとしたら変態コードになった話(実は関数型プログラミングの話)』を紹介します。
ちなみに、ここで言う「クソコード」とは「メソッドを利用する人のことを考えないコード」を指しています。(※2016/08/04 「利用者のことを考えないコード」から文言変更)
この発表は以下の方を対象にしています。
- 「クソコード」=「メソッドを利用する人のことを考えないコード」に苦しめられている方
- 関数型プログラミングに興味があるかもという方
- 何かにつけて寛容な方
そして、この発表の中では、以下の4つがキーワードになります。
- Ruby
- lambda
- カリー化
- 部分適用
それでは、内容に参りましょう!
コード紹介
ある日、@kazzさんはこんなコードに出会いました。(本当はもっとクソなコードだったそうです。)
整理すると
- def initialize(input)
-
- @input,@resultがattr_accessor(以後アクセサ)になっている
- @inputは入力した値を持ってくる
- @resultは初期化をされている
- def validate
-
- @inputは引数チェックをするため0より大きくないとダメ
- def execute
-
- validate 読みこんで条件分岐させる
- validateが通っていれば、様々な処理をして重要なオブジェクトの結果をresultに値を入れてtrueを返す
- 通らなければfalseを返す
本当に@kazzさんが出会ったコードは、さらにメンバ変数が10個以上あってすべてアクセサとして指定されているのに外部に公開するべきでないプロパティだったり、長い長い業務のロジックではそれらのメンバ変数をクラス内グローバル変数として使用していたりと、ちょっとでも変更するとバグが大量に発生しそうなソースコードだったそうです。
つまるところ、コードの記述に問題が大アリでした。(※2016/08/04 追記)
中身もアンタッチャブルなのですが、実際にこのコードが使われているところでは・・・
こんな感じになっていました。
そういった思いから
書き換えたい
@kazz「上のスライドのように書かせて!」
service=Service.new(100)なんてせずに、executeした結果をビジネスロジックに渡すということですね。
こう書けるとコードを読む時間も減って、管理も楽ですね!
本来Serviceクラスの中身をリファクタリングすると良いのですが、それをするには時間がかかりすぎてしまうことと、executeで返す結果がtrueかfalseであることを前提としているコードがたくさんあるため、@kazzさんが使う部分をうまく書いてみようとしています。
しかし、今回executeで返す値はtrueかfalseのためこのように書けません。
(result取りたいと思っても、executeをしないと値がセットされない残念…)
なので、今回のお題は
Serviceクラスを変えずにexecute後のresultをワンライナーで取得する
さて、どのようにコードが変わっていくのでしょうか。
begin-end
最初に思いついた方法がbegin-end。
ローカル変数sをService.new(100)にしてexecute。
sにexecuteされた値が入っているので、最後にs.resultを書くことによって、結果がbegin-endの戻り値になるのですが、ローカル変数sが残ってしまい無駄ができてしまいます。
これは上手くないとのことで没です。
Array
二つ目の方法がArray。
Service.new(100)のインスタンスをArrayで囲んで、eachでそれぞれの配列にexecuteを行います。
executeされた結果が配列として戻ってきますが、そもそも配列にはService.new(100)しか入っていないため、最初(first)のインスタンスをとってresultをします。
しかし、これも一つの情報を処理するために配列を使ってしまったり、パッと見て何がやりたいかわからないので没です。
lambda
三つ目の方法がlambda。
中身はbegin-endで書いたものをそのまま書いてあります。
lambdaは関数なので、外から値が与えられるということで、
sにService.new(100)を入れてそれをexecuteした後にresultする
↓
executeしてresultするlambdaに外()からService.new(100)を渡す
こうすることで、lambdaの処理と使用するインスタンスが分けられていて、スッキリしますね!
ちなみにRubyではシンタックスシュガーにより、lambda を '->' と書けるそうです。
これでワンライナーになりました!
前二つよりわかりやすい記述で且つ本来の目的のワンライナーを達成したので、これで終了ですね! @kazzさん!
@kazz「…しかし、せっかくlambdaを使ったので…」
!?
これが変態コードへ成長します。
<
h2 class="article">変態化
メソッドも外から与える
先ほどは中にexecuteやresultを書いてあげていましたが、lambdaでメソッドすら外から与えてあげようと、右側()にService.new(100)、:execute、:resultを入れました。
中身のlambdaは引数3つをとって、Serviceインスタンスをメソッド1,2(m1,m2)と順繰りに読んでメソッド2の戻り値がlambdaの評価になります。
さらに
m1,m2はどういうことだ!? となることを考えてメソッドを可変引数にして集約してしまいます。
mapを使っていますが、Serviceのインスタンスを実行して保持されている結果から最後の値を取ります。
@kazz「だんだん変態になってきました。ニッコリ」
カリー化と部分適用
lambdaや関数型プログラミングには、カリー化というものがあるそうです。
主な役割として、先ほど引数を3つ取っていましたが、これを1個ずつ渡してあげることができます。
つまり
(Service.new(100),:execute,:result)を渡す
↓
Service.new(100)を渡す→:executeを渡す→:resultを渡す
になります。
ちなみに、カリー化をしない一個前のスライドの記述では、引数を3個一気に渡してあげないと、引数がないと怒られます。
そして、カリー化で可変長引数の場合に注意しないといけないことは、最終的に引数が何個になるか指定しなければなりません。
これは何故でしょう。
まず、スライドで上に書かれているlambdaには意味があり、オブジェクトのメソッドを逐次実行して最後の結果を返しており、引数の数をメソッドで指定するのはいくつでも良いという関数になっています。
対してカリー化にしてあげると、lambdaの引数を1個ずつ取れるようにできます。(可変引数の指定3を書いておく)
- 1個目
- Sevice.new(100)インスタンスを渡しますが、処理をしません。
- 2個目
- :executeを渡しますが、まだ実行されません。
- 3個目
- :resultを渡すことで、初めて実行されます。
というように、必要な結果を入れる引数が渡されるまで処理・実行待ちをします。引数の指定がないと続いてしまうため、それを判断するタイミングを指定してあげる必要があります。
※ちなみに関数型プログラミングでは、遅延実行と言われ肝になるらしいです。
「じゃあ、カリー化するとメリットはなんなのか」と気になるところですね。
それはずばり、部分適用ができるという点になります。
まず最初に、カリー化で引数を一つ一つ渡せるため、lambdaによってService.new(100)をexecuteするservice_100_excutedを事前に作っておけます。これが実行部分になり、2行目で:resultをビジネスロジックに渡します。
出てきているlambdaはそのままではありますが、引数を2個しか渡していないため、実行はされないですが準備はできている状態です。:resultを待つだけですね。この形を部分適用といいます。
ここで、別の方法の部分適用をします。
こうすることにより、先にメソッドを決めてからインスタンスを渡してあげるという書き方ができます。(Service.new()が後から決まっても大丈夫)
こちらのほうが、使う側としては、実行する処理と値を入れられるインスタンスと分かれていて、わかりやすいですね!
ようやく変態化が完了したようです。
まとめ
お題はワンライナーに書き換えたい! ということで、振り返ってみるとlambdaを使用することによってクリアされていましたね。
そして、好奇心からどんどん変態化が進むにつれて、「プログラムを読むことはできるけど、書くことができない」の逆をいく「プログラムを書くことはできるけど、これは読めない」となるコードに変わっていく恐ろしいこととなりました。ソースコードレビューでも突っ込みがくるだろうとのことです。
しかし、部分適用をうまく使って名前を付けてあげると、定義のほうは変態でも使い側にしたら読んでいけるコードになりました。
今回は、関数型プログラミングのさわりのほうの内容になりましたが、Java8でもlambdaが出てきたりするようで、こんなコードが出てきて慌てないように頭の片隅に入れていただきたいとのことです。
今回の発表から、短くスマートに書くことの大切さを学ばせていただきました。
使い側のことを考えて、コードを書くことができるよう腕を磨いていきたいと思いました。
参考サイトはこちらです。
- 手続き脳のためのラムダ式のはなし
http://d.hatena.ne.jp/mirichi/20141008/p1 - Procを制する者がRubyを制す(嘘)
http://melborne.github.io/2014/04/28/proc-is-the-path-to-understand-ruby/ - Rubyのカリー化を日本語で説明してみる ? - Qiita
http://qiita.com/lnznt/items/0b20440cecd6f4e348b9 - 「関数型Ruby」という病(3) - カリー化(Proc#curry, Proc#flip)
http://yuroyoro.hatenablog.com/entry/2012/08/10/232443
※この内容は@kazzさんの発表要約であり、私に特定の宗派を支持する意図はありません。(※2016/08/04 指示から支持に誤字修正)
@kazzさんコメント (※2016/08/04 追記)
@matsuさんには私の拙いセッションを見事な記事にしていただいて非常に感謝しています。
クソコードとはいかがなものかね!というご意見をたくさんいただきました。
もちろんクソコードとかぬかしているのは私でして@matsuさんは関係ありません。
言葉遣いの大切さに改めて気づかされています。
このセッションの隠れたオチとして、
「クソコードとかディスっているおまえは変態じゃないか!」
みたいなニュアンスが伝わるといいなーと思っている訳なのですが
セッション中の超簡易版のソースコードが意外と普通のコードになってしまっていて
もともとのソースコードに対する私の荒ぶる怒りが共有できなかったのは反省点です。
一方で関数型は変態だ!などと言っているようにも聞こえてしまうのですが、
馴染みのない関数型をウハウハ書いてやったぜ!
でも俺には力不足で同じコードは読めないぜ!
と征服感と絶望感の狭間でニヤニヤしている私が単なる変態なだけであります。
そんな変態はさておいても、この記事で関数型プログラミングに興味を持ってもらえる方が少しでも増えたなら幸いです。