Help us understand the problem. What is going on with this article?

HaskellでBuilderを作ろう with Lens

HaskellでBuilderを作ろう

 HaskellにはData.Defaultというものがあり、これを使うとデフォルトの値を設定することができる。
 しかしこれは部分的なデフォルトの値を用意することはできず、ちょっと込み入ったことをしようと思うと急にクソダサ関数を用意する羽目になる。

makeHoge :: Int -> Hoge
makeHoge n = Hoge { essential = n, {デフォルトの値を頑張って書く} }

 そこで、JavaなんかでみるBuilderパターン的なやつをHaskell上に作ることを考える。
 理想的には、柔軟にデフォルト値を設定したり、この値は環境変数から読み込んだりといったことを明示できるようになると嬉しい。

もくじ

  1. Stateモナドで頑張ってみる
  2. Lensで手間を削る
  3. まとめ

Stateモナドで頑張ってみる

 モナドはHaskellの言語内DSLであるという格言がある。そこで、モナドを使ってBuilderのDSLを作ることを考えよう。
 まずはサンプルとしてクソデカデータHogeを作る。

data Hoge = Hoge
    { _nParam :: Int
    , _mParam :: Maybe Int
    , _sParam :: String
    } deriving (Show)

 次に、どんなDSLを書きたいか想像する。個人的には、下のように書けたら素直でよいと思う。
 デフォルト値の設定などは見えないところでやってほしい。

build essential $ do
    nParam 10
    mParam $ Just 5
    sParam "hello"

 ビルダーモナドを作る。一から作るのはめんどいので、Stateモナドを流用する。
 最低限のDSL内の機能はmodifyを使えば簡単に実装できる。ここは単に値を設定するだけでなく、環境変数から値を読み取ったり、値をさらにビルダーで構成したり、値の取得に失敗する可能性を表現したり等、何でもやっていい場面である。

-- ビルダーモナド
type BuilderT = StateT Hoge

-- DSL内の機能
nParam :: Monad m => Int -> BuilderT m ()
nParam n = modify' $ \h -> h { _nParam = n }

mParam :: Monad m => Maybe Int -> BuilderT m ()
mParam m = modify' $ \h -> h { _mParam = m }

sParam :: Monad m => String -> BuilderT m ()
sParam s = modify' $ \h -> h { _sParam = s }

 最後にbuild関数を作る。ここではDSLで明示しなくて良い部分の全てを書く。
 ここでは以下のような仕様を考えた。

  1. nParamは必ずビルド時に与えなければならない。
  2. mParamはデフォルト値がNothingである。
  3. sParamはデフォルト値が"sParamDefault"である。しかし環境変数sParamがある場合はそちらをデフォルト値とする。
  4. いずれもDSL内で指定があればそっちを優先する。
build :: MonadIO m => Int -> BuilderT m a -> m (a, Hoge)
build n b = do
    sEnv <- liftIO $ lookupEnv "sParam"
    let mParamDefault = Nothing
        sParamDefault = fromMaybe "sParamDefault" sEnv
    runStateT b $ Hoge n mParamDefault sParamDefault

 これでビルダーのDSLを書けるようになった。

>>> (_, hoge) <- build essential $ do
>>>     nParam 10
>>>     mParam $ Just 5
>>>     sParam "hello"
>>> print hoge
Hoge {_nParam = 10, _mParam = Just 5, _sParam = "hello"}

 目的は概ね達成できたといえる。問題はクソデカデータを作るというのにDSLの機能を全て手で実装しなければならない点だ。そこでLens先生の力を借りる。

Lensで手間を削る

 クソデカデータを作るときにLensを生成するようにする。これにはTemplateHaskell拡張とlensパッケージの追加が必要だ。

{-# LANGUAGE TemplateHaskell #-}
import Control.Lens

data Hoge = Hoge
    { _nParam :: Int
    , _mParam :: Maybe Int
    , _sParam :: String
    } deriving (Show)

makeLenses ''Hoge

 驚くべきことに準備はこれだけで良い。BuilderTbuildはそのまま使うことができる。
 さっそく使ってみよう。

>>> (_, hoge) <- build essential $ do
>>>     nParam .= 10
>>>     mParam .= Just 5
>>>     sParam .= "hello"
>>> print hoge
Hoge {_nParam = 10, _mParam = Just 5, _sParam = "hello"}

 .=はLensのState Combinatorsと呼ばれるもので、Lensを使ってStateモナドの中の状態を変更することができる演算子だ。
 他にもたくさんのコンビネータがあるので、アレないかな? と思って調べればあることがある。

まとめ

 クソデカデータを作るときにビルダーモナドを作ってDSLを書くようにすると便利な場面がある。
 Lensは泥臭い実装を大幅に減らしてくれることがある。

works-hi
「はたらく」を楽しく!に向けて大手企業の人事業務から変えていく HR業界のリーディングカンパニー
https://www.works-hi.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした