Как получить "зависимые" значения по умолчанию, которые может изменить пользователь? - PullRequest
4 голосов
/ 05 мая 2020

У меня есть следующая функция из моей библиотеки odd-jobs job-queue. Существует множество параметров конфигурации, реализация которых по умолчанию зависит от другого параметра конфигурации. Например:

  • cfgJobToHtml зависит от cfgJobType, которое по умолчанию равно defaultJobType. Однако после вызова defaultConfig пользователь может выбрать переопределение значения для cfgJobType без изменения cfgJobToHtml. Ожидаемое поведение состоит в том, что cfgJobToHtml теперь должно использовать значение, предоставленное пользователем, вместо defaultJobType.
  • Точно так же cfgAllJobTypes зависит от cfgJobTypeSql, которое, в свою очередь, по умолчанию принимает defaultJobTypeSql. Опять же, после вызова defaultConfig, если пользователь переопределяет значение для cfgJobTypeSql, тогда cfgAllJobTypes должен использовать переопределенное значение, а не defaultJobTypeSql.

Приведенный ниже код не работает. как я этого и жду. Если вы измените cfgJobType, изменение не будет принято cfgJobToHtml. Точно так же для cfgJobTypeSql.

Как лучше всего иметь эти «зависимые» значения по умолчанию?

-- | This function gives you a 'Config' with a bunch of sensible defaults
-- already applied. It requires the bare minimum arguments that this library
-- cannot assume on your behalf.
--
-- It makes a few __important assumptions__ about your 'jobPayload 'JSON, which
-- are documented in 'defaultJobType'.
defaultConfig :: (LogLevel -> LogEvent -> IO ())  -- ^ "Structured logging" function. Ref: 'cfgLogger'
              -> TableName                        -- ^ DB table which holds your jobs. Ref: 'cfgTableName'
              -> Pool Connection                  -- ^ DB connection-pool to be used by job-runner. Ref: 'cfgDbPool'
              -> ConcurrencyControl               -- ^ Concurrency configuration. Ref: 'cfgConcurrencyControl'
              -> (Job -> IO ())                   -- ^ The actual "job runner" which contains your application code. Ref: 'cfgJobRunner'
              -> Config
defaultConfig logger tname dbpool ccControl jrunner =
  let cfg = Config
            { cfgPollingInterval = defaultPollingInterval
            , cfgOnJobSuccess = (const $ pure ())
            , cfgOnJobFailed = []
            , cfgJobRunner = jrunner
            , cfgLogger = logger
            , cfgDbPool = dbpool
            , cfgOnJobStart = (const $ pure ())
            , cfgDefaultMaxAttempts = 10
            , cfgTableName = tname
            , cfgOnJobTimeout = (const $ pure ())
            , cfgConcurrencyControl = ccControl
            , cfgPidFile = Nothing
            , cfgJobType = defaultJobType
            , cfgDefaultJobTimeout = Seconds 600
            , cfgJobToHtml = defaultJobToHtml (cfgJobType cfg)
            , cfgAllJobTypes = defaultDynamicJobTypes (cfgTableName cfg) (cfgJobTypeSql cfg)
            , cfgJobTypeSql = defaultJobTypeSql
            }
  in cfg

Ответы [ 2 ]

5 голосов
/ 05 мая 2020

Люди часто реализуют это, используя шаблон построителя.

В вашем примере вы сначала заполняете значения по умолчанию, а затем позволяете пользователю переопределять некоторые поля, если он хочет. В 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
4 голосов
/ 05 мая 2020

Этого также можно добиться с помощью открытой рекурсии. Конфигурация в настоящее время определяется рекурсивно (let cfg = mkConfig cfg in cfg). Идея состоит в том, чтобы определить только эту нерекурсивную функцию mkConfig и позволить пользователю применять свои собственные logi c перед тем, как завязать узел.

Таким образом, вместо

defaultConfig :: X -> Y -> Z -> Config
defaultConfig x y z =
  let cfg = Config {  ...  }
  in cfg

define

mkConfig :: X -> Y -> Z -> Config -> Config
mkConfig x y z cfg =
  Config {  ...  }

, чтобы пользователь мог установить свои собственные параметры как

userConfig = defaultConfig {  ...  }          -- override defaultConfig
  where defaultConfig = mkConfig x y z userConfig   -- tie the knot

. Вы также можете скрыть рекурсию от пользователей, взяв функцию Config -> Config, которую они неявно определили выше, вернуться к стилю, более похожему на вашу первоначальную версию:

mkConfig :: X -> Y -> Z -> (Config -> Config) -> Config
mkConfig x y z mkCfg =
  let cfg = mkCfg $ Config {  ...  } in -- defaults here, using cfg recursively
  in cfg

userConfig :: Config
userConfig = mkConfig x y z \defaultConfig ->
  defaultConfig {  ...  }   -- override defaultConfig
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...