Люди часто реализуют это, используя шаблон построителя.
В вашем примере вы сначала заполняете значения по умолчанию, а затем позволяете пользователю переопределять некоторые поля, если он хочет. В Builder все наоборот: вы позволяете пользователю заполнять данные, которые он хочет переопределить, а затем вы заполняете остальные.
В частности, вы создаете промежуточный тип данных для хранения частично заполненной конфигурации, ConfigUnderConstruction
. Все поля необязательны. Пользователь может указать все поля, которые ему интересны, затем вы собираете конфиг, заполняя все значения по умолчанию:
module Config
where
import Data.Maybe
import Control.Monad.Trans.State
data Config = Config
{ cfgJobType :: String
, cfgJobToHtml :: String
} deriving (Show)
data ConfigUnderConstruction = ConfigUnderConstruction
{ cucJobType :: Maybe String
, cucJobToHtml :: Maybe String
}
emptyConfig :: ConfigUnderConstruction
emptyConfig = ConfigUnderConstruction
{ cucJobType = Nothing
, cucJobToHtml = Nothing
}
assemble :: ConfigUnderConstruction -> Config
assemble partial = Config
{ cfgJobType = jobType
, cfgJobToHtml = jobToHtml
}
where
jobType = fromMaybe defaultJobType $ cucJobType partial
jobToHtml = fromMaybe (defaultJobToHtml jobType) $ cucJobToHtml partial
defaultJobType :: String
defaultJobType = "default job"
defaultJobToHtml :: String -> String
defaultJobToHtml jobType = jobType ++ " to html"
Вот как вы его используете:
*Config> assemble emptyConfig
Config {cfgJobType = "default job", cfgJobToHtml = "default job to html"}
*Config> assemble $ emptyConfig {cucJobType = Just "custom"}
Config {cfgJobType = "custom", cfgJobToHtml = "custom to html"}
*Config>
Иногда люди go далее и добавьте немного синтаксиса c сахара:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
newtype Builder a = Builder
{ fromBuilder :: State ConfigUnderConstruction a
} deriving (Functor, Applicative, Monad)
setJobType :: String -> Builder ()
setJobType jobType = Builder $ modify' $ \s -> s
{ cucJobType = Just jobType
}
setJobToHtml :: String -> Builder ()
setJobToHtml jobToHtml = Builder $ modify' $ \s -> s
{ cucJobToHtml = Just jobToHtml
}
buildConfig :: Builder () -> Config
buildConfig builder =
assemble $ execState (fromBuilder builder) emptyConfig
Таким образом конструкция станет немного менее шумной:
*Config> buildConfig (return ())
Config {cfgJobType = "default job", cfgJobToHtml = "default job to html"}
*Config> buildConfig (setJobType "custom")
Config {cfgJobType = "custom", cfgJobToHtml = "custom to html"}
Добавлено: вы можете уменьшить количество шаблонов определив Config
следующим образом:
data GConfig f = Config
{ cfgJobType :: f String
, cfgJobToHtml :: f String
} deriving (Show)
type Config = GConfig Identity
type ConfigUnderConstruction = GConfig Maybe