elm-monocle
elm-monocle とは
- http://package.elm-lang.org/packages/arturopala/elm-monocle/latest
- 僕は http://qiita.com/miyamo_madoka/items/f225803d275019cc34c1 を読んで知りました
- scalaのMonocleのインスパイアライブラリ
- scala Monocle
- scala MonocleはHaskellのLensからインスパイア
- 複雑なレコードを操作するためのライブラリ
どんなときに便利なのか
- 複雑なレコードを操作するとき
映画は物語を持つというレコードがあるとして
type alias Story =
{ text : String
}
type alias Movie =
{ name : String
, story : Story
}
スターウォーズシリーズの外伝が作られることになりました
starwarsSpinOff : Movie
starwarsSpinOff =
Movie "starwars:SpinOff" (Story "遠い...未定")
正式タイトルが決まり、ストーリーも更新されます!
rogueOne : Movie
rogueOne =
{ starwarsSpineOff
| name = "rogueOne: A star Wars Story"
, story = { starwarsSpineOff.story | text = "遠い昔、はるか彼方の銀河系で…。" }
}
しかし、これではコンパイルが通りません。 なので以下のようにしました
rogueOne : Movie
rogueOne =
let
oldStory =
starwarsSpineOff.story
newStory =
{ oldStory | text = "遠い昔、はるか彼方の銀河系で…。" }
in
{ starwarsSpinOff
| name = "rogueOne: A star Wars Story"
, story = newStory }
}
レコード内の奥深くのフィールドを更新して新しいレコードを作ろうとすると見辛いコードが増えていきますね‥
こんなとき elm-monocleのLensを使うと見栄えがよくなる
import Monocle.Lens exposing (Lens, compose)
~~略~~
textOfStory : Lens Story String
textOfStory =
let
get story =
story.text
set text story =
{ story | text = text }
in
Lens get set
type alias Movie =
{ name : String
, story : Story
}
nameOfMovie : Lens Movie String
nameOfMovie =
let
get movie =
movie.name
set name movie =
{ movie | name = name }
in
Lens get set
storyOfMovie : Lens Movie Story
storyOfMovie =
let
get movie =
movie.story
set story movie =
{ movie | story = story }
in
Lens get set
rogueOne : Movie
rogueOne =
starwarsSpinOff
|> nameOfMovie.set "rogueOne: A star Wars Story"
>> (compose storyOfMovie textOfStory).set "遠い昔、はるか彼方の銀河系で…。"
hogeOfHuga: Lens Huge Huga
という関数が増えていてコード自体の量は増えていますが
「rogueOne」を作るコードはすっきりしていませんか?
hogeOfHuga: Lens Huge Huga のコードも単純です
レコードのフィールド一つに対して定義されていて
レコードからフィールドの値をとるget
と
レコードのフィールドを更新して新しいレコードを返すset
を定義しているだけで非常にシンプルです
starwarsSpinOff |> nameOfMovie.set "rogueOne: A star Wars Story"
はstarwarsSpinOff
のname
フィールドに"rogueOne: A star Wars Story"
をセットして新しいMovieを返しています
(compose storyOfMovie textOfStory)
にあるcompose
は名の通り合成で、Movie
をとってstory
フィールドの、Storyのtext
フィールドに対するLens
を定義(getとset)してます
なのでここではMovie
のstory
フィールドのStory
のtext
フィールドに文字列をセットして新しいレコードとして返しています
monocleのOptionalを使うともっと楽しくなる
Movieレコードに変更します
storyをMaybe Storyとしましょう
映画には物語がないようなものもあるでしょう
type alias Movie =
{ name : String
, story : Maybe Story
}
Story
レコードに変更をします
Story
にはgoodPoint
というフィールドがあるとしましょう
映画には見所があるものとないものがあるのでMaybe
で定義しましょう
type alias Story =
{ text : String
, goodPoint : Maybe GoodPoint
}
映画の見どころとは言葉で表せないこともあるのでフィールドtext
はMaybeです
type alias Text = String
type alias GoodPoint =
{ text : Maybe Text
}
ここでmonocleなしでMovie
からgoodPoint
のフィールドtext
取ってこようとすると...
rogueOne = Movie "rogueOne: A star Wars Story" (Just <| Story "遠い..." (Just <| GoodPoint (Just "イケてる俳優")))
rogueOneGoodPoint : Maybe Text
rogueOneGoodPoint =
rogueOne.story
|> Maybe.map
(\{ goodPoint } ->
goodPoint
|> Maybe.map (\{ text } -> text)
|> Maybe.withDefault Nothing
)
|> Maybe.withDefault Nothing
map
の連打、withDefault
の嵐、Maybe
が増えるほどコードがネストすることを察せます
これがset系の操作になったら
updatedRogueOne : Movie
updatedRogueOne =
let
newGoodPoint : Maybe GoodPoint
newGoodPoint =
rogueOne.story
|> Maybe.map
(\story ->
story.goodPoint
|> Maybe.map (\goodPoint -> { goodPoint | text = "かっこいい宇宙戦" })
)
|> Maybe.withDefault Nothing
newStory : Maybe Story
newStory =
rogueOne.story |> Maybe.map (\story -> { story | goodPoint = newGoodPoint })
in
{ rogueOne | story = newStory }
僕のウデマエでは非常に大変なコードになってしまいました
Optionalを使ってみよう
import Monocle.Optional exposing (Optional)
import Monocle.Common exposing ((=>))
~~略~~
textOfGoodPoint : Optional GoodPoint Text
textOfGoodPoint =
let
get goodPoint =
goodPoint.text
set text goodPoint =
{ goodPoint | text = Just text }
in
Optional get set
goodPointOfStory : Optional Story GoodPoint
goodPointOfStory =
let
get story =
story.goodPoint
set goodPoint story =
{ story | goodPoint = Just goodPoint }
in
Optional get set
--- Lensの例から定義を変更してます
storyOfMovie : Optional Movie Story
storyOfMovie =
let
get movie =
movie.story
set story movie =
{ movie | story = Just story }
in
Optional get set
rogueOneGoodPoint : Maybe Text
rogueOneGoodPoint =
rogueOne |> (storyOfMovie => goodPointOfStory => textOfGoodPoint).getOption
updatedRogueOne : Movie
updatedRogueOne =
rogueOne |> (storyOfMovie => goodPointOfStory => textOfGoodPoint).set "かっこいい宇宙戦"
LensのときのようにhogeOfHuga
のようなOptional
の定義がMaybe
なフィールドの数だけあります
とはいってもこれらの定義もシンプルですね
--- MovieからGoodPointのTextをとってくる
rogueOneGoodPoint : Maybe Text
rogueOneGoodPoint =
rogueOne |> (storyOfMovie => goodPointOfStory => textOfGoodPoint).getOption
--- MovieのGoodPointのTextを更新して新しいMovieを返す
updatedRogueOne : Movie
updatedRogueOne =
rogueOne |> (storyOfMovie => goodPointOfStory => textOfGoodPoint).set "かっこいい宇宙戦"
=>
はOptional同士のcomposeをする中置関数です
こうするとよりスッキリ
movieStoryGoodPointText =
storyOfMovie => goodPointOfStory => textOfGoodPoint
rogueOneGoodPoint : Maybe Text
rogueOneGoodPoint =
rogueOne |> movieStoryGoodPointText.getOption
updatedRogueOne : Movie
updatedRogueOne =
rogueOne |> movieStoryGoodPointText.set "かっこいい宇宙戦"
Movie
のStory
のGoodPoint
のText
を取り出すコードや、
Movie
のStory
のGoodPoint
のText
を更新して新しいMovie
を返すコード
非常にスッキリですよね
Maybe
のためのwithDefault
やmap
がコードから綺麗さっぱりいなくなっていますね
合成されたOptional
はget
やset
するためにフィールドを辿るどこかでNothing
があったとき、get
に対してNothing
を返したりset
をしないということをmonocleがヨロシクしてくれます
LensとOptionalが混ざっているときには
映画のいいところは絶対に言葉になるはずだ!と強い気持ちでフィールドtext
はMaybe
でなくなりました
type alias GoodPoint =
{ text : Text
}
rogueOne = Movie "rogueOne: A star Wars Story" (Just <| Story "遠い..." (Just <| GoodPoint "イケてる俳優"))
そうするとtextOfGoodPoint
の定義はOptional
からLens
になります
textOfGoodPoint : Lens GoodPoint Text
textOfGoodPoint =
let
get goodPoint =
goodPoint.text
set text goodPoint =
{ goodPoint | text = text }
in
Lens get set
そして実際にMovie
からGoodPoint
のText
を取り出すコードと
Movie
のGoodPoint
のText
を更新して新しいMovie
を返すコードは以下になります
movieStoryGoodPointText =
storyOfMovie => (composeLens goodPointOfStory textOfGoodPoint)
rogueOneGoodPoint : Maybe Text
rogueOneGoodPoint =
rogueOne |> movieStoryGoodPointText.getOption
updatedRogueOne : Movie
updatedRogueOne =
rogueOne |> movieStoryGoodPointText.set "かっこいい宇宙戦"
OptionalとLensを合成するためにcomposeLens
を利用するようにしました
MaybeなフィールドとMaybeでないフィールドも合成できました!
まとめる
- monocleはネストしたレコードの操作をスッキリ書ける
- LensとOptionalはレコードのある1つのフィールドのgetとsetをセットで渡して定義する
- つかうのは Lens , Optional , compose, composeLens, =>
- MaybeはOptional, それ以外はLens
- LensとOptionalは合成してレコード操作していく
最後に
- Iso, Prismなどmonocleにはまだいくつもできることがある
- ローグワンおもしろかったです