15
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ElmAdvent Calendar 2016

Day 25

elm-monocleでスッキリする

Last updated at Posted at 2016-12-24

elm-monocle

elm-monocle とは

どんなときに便利なのか

  • 複雑なレコードを操作するとき

映画は物語を持つというレコードがあるとして


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"starwarsSpinOffnameフィールドに"rogueOne: A star Wars Story"をセットして新しいMovieを返しています

(compose storyOfMovie textOfStory)にあるcomposeは名の通り合成で、Movieをとってstoryフィールドの、Storyのtextフィールドに対するLensを定義(getとset)してます
なのでここではMoviestoryフィールドのStorytextフィールドに文字列をセットして新しいレコードとして返しています

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 "かっこいい宇宙戦"

MovieStoryGoodPointTextを取り出すコードや、

MovieStoryGoodPointTextを更新して新しいMovieを返すコード

非常にスッキリですよね

MaybeのためのwithDefaultmapがコードから綺麗さっぱりいなくなっていますね
合成されたOptionalgetsetするためにフィールドを辿るどこかでNothingがあったとき、getに対してNothingを返したりsetをしないということをmonocleがヨロシクしてくれます

LensとOptionalが混ざっているときには

映画のいいところは絶対に言葉になるはずだ!と強い気持ちでフィールドtextMaybeでなくなりました

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からGoodPointTextを取り出すコードと

MovieGoodPointTextを更新して新しい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にはまだいくつもできることがある
  • ローグワンおもしろかったです
15
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?