Monocleのコミッターになりました
ご無沙汰しています。最近カレーがマイブームのプロダクトグループ所属エンジニアのあおいの(@AoiroAoino)です。 私事ですが、前回書いた記事にも登場したMonocleというライブラリのコミッターになりました。 で、早速なんか記事書いてと言われました()ので、今回はとりあえず代表的な(?) Lensについて、適当に書こうかなと思います。
Lens とは?
例えば、こんな感じのデータ構造と、その適当なインスタンスがあったとします。
1 2 3 4 5 | |
このネストしgame1の一番深いところ、AoinoさんのJobのidを書き換えようとした時にみなさんどうしますか?
copyメソッドを使って
1 | |
とするか、case class Game(...) の中に
1 2 3 | |
ってメソッドを定義する感じですかね?
前者はcopyメソッドのネストで辛いし、後者はcase class Game内にPlayerやJobが持つフィールドごとにsetXXXって一々定義するの、うーん、微妙ですね。 いっそmutableにでもしてしまった方が、直感的でわかりやすくかけたりしますよね。
1 | |
とはいえ、もちろんimmutableのが良いし、でもって欲張ってmutableのような表現が欲しくなっちゃいますね。そこでLensの登場です。
とてもざっくり誤解を恐れずに言うと、Lensとはgetter/setterの性質を持った関数(みたいなもの)であり、取得したり変更したいフィールドに対する参照のようなものです。 故に、Functional Reference とかって名称で呼ばれていたりもするみたいです。
さて、ではひとまずJobに対してLensを定義してみましょう。
※MonocleでLensを定義(生成)する方法は複数ありますが、次節で詳しく説明します。
1 2 | |
このidLensがお待ちかねの「Jobのidに対するLens」です。 なので、この参照に対するgetメソッドに、Jobのインスタンスを渡すとidの値が得られますし、setやmodifyで値を書き換えることができます。
1 2 3 4 5 6 7 8 9 10 11 12 | |
さてさて、Jobに関しては良さそうですが、元々の話ではネストしているgame1のJobのidを書き換えたいっていう話でしたね?ひとまずはGameからJobに至るまでのLensを定義しておきましょう。
1 2 3 4 5 6 7 | |
単純にgetするだけであれば、playerLens.getしてjobLens.getしてidLens.getすればいいですね。
1 2 | |
で、setは…..こんな感じ…..かな…..
1 2 | |
んー、しかしながら、このままだとcopyやヘルパーメソッドを定義した時と比べて大変辛い….. あれ?でも特にgetの形、見覚えありませんか?はいそうです、関数合成の話でよく見かけるパターンですね。そう、Lensも同様に合成することができるんです。
※そもそも元になったHaskellの場合、Lens は(実装方法にもよりますが?)関数として表現され、合成関数として表されます。 しかし、残念ながらMonocleの場合、Lensはデータ構造として表現されているので、厳密には「関数の合成」ではないですけれど。
MonocleでLensを合成する場合はcomposeLensメソッドを使用します。関数を合成する時と同様、順番には注意です。
1 2 3 4 5 6 7 8 9 10 11 | |
またはcomposeLensのエイリアスメソッドである^|->を使用するともう少し短くかけます。
1 2 3 4 5 6 7 8 9 10 11 | |
さてさて、ご覧の通りスッキリ書けましたね? Lensの合成は冒頭で述べた通り「取得したり変更したいフィールドに対する参照のようなもの」であり、その参照してるフィールドに対して、値を取得したり、書き換えたりができるというのがお分かり頂けたかと思います。
このように、Lens はネストしたデータに対するimmutableで簡潔な操作を提供してくれる素敵な概念です。
Monocleを試してみよう
さて、今度はMonocleでLensを定義する方法に焦点を当ててみようと思います。 前節ではgetter/setterを渡して自前で頑張っていましたが、Lensを定義する方法はv1.2.0-M1現在、三種類あります。
自前で頑張る
前節で説明のためにgetter/setterを渡して自前で作った方法です。
1 2 3 4 | |
カリー化されていますが、第一引数がgetter、第二引数がsetterです。一応setterの方は引数の順番に注意が必要です。 定義に忠実なので理解しやすいですが、いざ自分で定義しようとするといささか面倒な方法です。
GenLensマクロを使う
恐らく、MonocleでLensを定義しようとした際に一番使われている生成方法です。 内部的には上記自前で頑張るコードをマクロで生成してるだけです。
1 2 3 4 5 6 | |
また、以下のような使い方もできます。
1 2 3 4 5 6 7 | |
1 2 3 4 | |
自前で定義するよりも楽で、次に紹介する@Lensesよりも扱いやすいので、基本的にはGenLensを使えばいいと思います。
@Lensesマクロアノテーションを使う
case class定義時に@Lensesアノテーションをつけると、コンパニオンオブジェクト内に各プロパティに対するLensが定義されます。 macro paradise を使用した機能なので、(例えばsbtで開発を行う場合は)下記プラグインを追加する必要があります。
1 | |
使い方は以下の通り。
1 2 3 4 5 6 | |
大変強力な機能ですが、そもそも macro paradise が experimental な機能であり、また、case class定義時にしか使用できない為、 以外と扱いにくいのが現状です。あくまで実験的な機能と捉えるのがいいでしょう。
おまけ
MonocleのREADMEに従って、build.sbtファイルを書けばすぐにでも使い始めることができますが、もっと簡単にMonocleを試せるようgiter8のテンプレートを作ってみました。
※giter8の導入方法に関しては、giter8のREADMEを参照してください。
使い方は簡単で、
1 | |
をコマンドラインで実行し、名前やScala, Monocleのバージョンなどを指定してやるだけです。 何も指定しない場合、ScalaやMonocleのバージョンはデフォルトで最新を指定するようになっているので、とりあえずnameだけ指定してやればいいと思います。
まとめ
今回はLensについてのみざっくりと適当に書きましたが、ほかにもPrismやTraversalなどの概念があり、Optics(LensやらPrismやらの総称)は大変興味深く非常に面白い概念です。 これがHaskellだけでなく、Scalaでも使えるなんてとてもワクワクしますね!以上、あおいの(@AoiroAoino)がお送りしました。
(´-`).oO(あ、個人的に弊社では、MonocleやOpticsに興味のあるエンジニアさんを募集しています!