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