search
LoginSignup
4

More than 3 years have passed since last update.

posted at

updated at

Organization

elm-monocleでスッキリする

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

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
What you can do with signing up
4