Правильный способ обработки глобальных флагов в Haskell

Мне часто нужно сделать основную функцию, которая используется во многих местах, как-то конфигурируемая, то есть она может использовать либо алгоритм A, либо алгоритм B в зависимости от ключа командной строки; или распечатать дополнительную информацию в stdout, если флаг "debug" установлен каким-то образом.

Как реализовать такие глобальные флаги?

Я вижу 4 варианта, все они не очень хороши.

1) Прочитайте аргументы командной строки из функции - плохо, потому что для этого нужны мода IO и основные функции вычисления, все чисты, я не хочу получать там IO;

2) Передайте параметр от main/IO до конца до функции "leaf", которая должна изменить поведение - полностью непригодна для использования, так как это означает изменение десятка не связанных функций в разных модулях для передачи этого параметра, а я вы хотите попробовать такие параметры конфигурации несколько раз, не меняя каждый раз код упаковки;

3) Используйте unsafePerformIO, чтобы получить истинную глобальную переменную - чувствует себя уродливым и излишним для такой простой проблемы;

4) Прямо посередине функции есть код для обоих параметров и прокомментируйте один из них. Или имеют функции do_stuff_A и do_stuff_B и изменяете, какой из них вызывается, в зависимости от того, что говорит глобальная функция "нужнаDebugInfo = True". Это то, что я делаю сейчас для debuginfo, но его нельзя изменить без перекомпиляции, и это не должно быть наилучшим доступным способом...

Мне не нужно или требуется глобальное изменяемое состояние - я хочу иметь простой глобальный флаг, который неизменен во время выполнения, но может быть установлен как-то при запуске программы. Есть ли какие-либо варианты?

Ответ 1

Наша новая библиотека HFlags предназначена именно для этого.

Если вы хотите увидеть пример использования, как ваш пример, посмотрите на это:

https://github.com/errge/hflags/blob/master/examples/ImportExample.hs

https://github.com/errge/hflags/blob/master/examples/X/B.hs

https://github.com/errge/hflags/blob/master/examples/X/Y_Y/A.hs

Никакая передача параметров не требуется между модулями, и вы можете определить новые флаги с простым синтаксисом. Он использует unsafePerformIO внутренне, но мы думаем, что он делает это безопасным способом, и вам не придется беспокоиться об этом.

Существует сообщение в блоге об этом: http://blog.risko.hu/2012/04/ann-hflags-0.html

Ответ 2

В наши дни я предпочитаю использовать a Reader монаду для структурирования состояния приложения, доступного только для чтения. Окружающая среда инициализируется при запуске, а затем доступна на всем верхнем уровне программы.

Пример - xmonad:

newtype X a = X (ReaderT XConf (StateT XState IO) a)
    deriving (Functor, Monad, MonadIO, MonadReader XConf)

Части верхнего уровня программы выполняются в X вместо IO; где XConf - структура данных, инициализированная флагами командной строки (и переменными среды).

Затем состояние XConf может передаваться как чистые данные в нужные ему функции. При получении newtype вы также можете повторно использовать весь код MonadReader для доступа к состоянию.

Этот подход сохраняет семантическую чистоту 2. но дает вам меньше кода для написания, так как монада делает сантехнику.

Я думаю, что это "истинный" способ Haskell для настройки состояния только для чтения.

-

Подходы, которые используют unsafePerformIO для инициализации глобального состояния, также работают, конечно, но, как правило, вас укусят (например, когда вы делаете свою программу параллельной или параллельной). Они также имеют смешные семантики инициализации.

Ответ 3

Вы можете использовать монаду Reader, чтобы получить тот же эффект, что и при прохождении параметра повсюду. Аппликационный стиль может сделать накладные расходы довольно низкими по сравнению с нормальным функциональным кодом, но он все равно может быть довольно неудобным. Это наиболее распространенное решение проблемы конфигурации, но я не считаю это ужасно удовлетворительным; действительно, прохождение параметра вокруг явно часто менее уродливое.

Альтернативой является пакет reflection, который позволяет передавать общие данные конфигурации, подобные этому, через контексты типов, что означает, что ни один из ваш код должен измениться, чтобы добавить дополнительное значение, только типы. В принципе, вы добавляете новый тип параметра к каждому типу ввода/результата в своей программе, так что все, работающие в контексте определенной конфигурации, имеют тип, соответствующий этой конфигурации в своем типе. Этот тип останавливает случайное смешивание значений с использованием нескольких конфигураций и дает вам доступ к связанной конфигурации во время выполнения.

Это позволяет избежать накладных расходов при написании всего в аппликативном стиле, сохраняя при этом безопасность и позволяя вам смешивать несколько конфигураций. Это намного проще, чем кажется; здесь пример.

(Полный диск: я работал над пакетом отражения.)

Ответ 4

Другим вариантом является неявные параметры GHC. Они дают менее болезненную версию вашего варианта (2): сигнатуры промежуточного типа заражаются, но вам не нужно менять промежуточный код.

Вот пример:

{-# LANGUAGE ImplicitParams #-}
import System.Environment (getArgs)    

-- Put the flags in a record so you can add new flags later
-- without affecting existing type signatures.
data Flags = Flags { flag :: Bool }

-- Leaf functions that read the flags need the implicit argument
-- constraint '(?flags::Flags)'.  This is reasonable.
leafFunction :: (?flags::Flags) => String
leafFunction = if flag ?flags then "do_stuff_A" else "do_stuff_B"

-- Implicit argument constraints are propagated to callers, so
-- intermediate functions also need the implicit argument
-- constraint.  This is annoying.
intermediateFunction :: (?flags::Flags) => String
intermediateFunction = "We are going to " ++ leafFunction

-- Implicit arguments can be bound at the top level, say after
-- parsing command line arguments or a configuration file.
main :: IO ()
main = do
  -- Read the flag value from the command line.
  commandLineFlag <- (read . head) `fmap` getArgs
  -- Bind the implicit argument.
  let ?flags = Flags { flag = commandLineFlag }
  -- Subsequent code has access to the bound implicit.
  print intermediateFunction

Если вы запускаете эту программу с аргументом True, она печатает We are going to do_stuff_A; с аргументом False он печатает We are going to do_stuff_B.

Я думаю, что этот подход похож на пакет отражения, упомянутый в другом ответе, и я думаю, что HFlags, упомянутые в принятый ответ, вероятно, лучший выбор, но я добавляю этот ответ для полноты.