Доступ к среде в функции

В main я могу прочитать мой файл конфигурации и поставить его как runReader (somefunc) myEnv просто отлично. Но somefunc не нуждается в доступе к myEnv, который поставляется читателем, и следующей цепочке в цепочке. Функция, которая нуждается в чем-то из myEnv, является крошечной функцией листа.

Как получить доступ к среде в функции без пометки всех промежуточных функций как (Reader Env)? Это не может быть правильно, потому что иначе вы просто передадите myEnv в первую очередь. И передача неиспользуемых параметров через несколько уровней функций просто уродливая (не так ли?).

Есть множество примеров, которые я могу найти в сети, но все они, похоже, имеют только один уровень между runReader и доступом к среде.


Я принимаю Криса Тейлора, потому что он самый тщательный, и я вижу, что он полезен другим. Спасибо тоже Heatsink, который был единственным, кто пытался прямо ответить на мой вопрос.

Для тестового приложения, о котором идет речь, я, возможно, просто полностью разорву читателя и передам окружение. Он ничего мне не покупает.

Я должен сказать, что меня все еще озадачивает мысль о том, что предоставление статических данных для функции h изменяет не только ее сигнатуру типа, но и функцию g, которая вызывает ее, и f, которая вызывает g. Все это, несмотря на то, что фактические типы и вычисления не изменяются. Похоже, что детали реализации протекают по всему коду без реальной выгоды.

Ответ 1

Вы даете все возвращаемый тип Reader Env a, хотя это не так плохо, как вы думаете. Причина, по которой все эти теги нужны, заключается в том, что если f зависит от среды:

type Env = Int

f :: Int -> Reader Int Int
f x = do
  env <- ask
  return (x + env)

и g вызывает f:

g x = do
  y <- f x
  return (x + y)

тогда g также зависит от среды - значение, связанное в строке y <- f x, может быть различным, в зависимости от того, какая среда была передана, поэтому соответствующий тип для g равен

g :: Int -> Reader Int Int

На самом деле это хорошо! Система типов заставляет вас явно распознавать места, где ваши функции зависят от глобальной среды. Вы можете сэкономить немного боли при наборе текста, указав ярлык для фразы Reader Int:

type Global = Reader Int

так что теперь ваши аннотации типа:

f, g :: Int -> Global Int

что немного читаемо.


Альтернативой этому является явная передача окружения всем вашим функциям:

f :: Env -> Int -> Int
f env x = x + env

g :: Env -> Int -> Int
g x = x + (f env x)

Это может работать, и на самом деле синтаксически это не хуже, чем использование монады Reader. Трудность возникает, когда вы хотите расширить семантику. Предположим, вы также зависеть от наличия обновляемого состояния типа Int, которое учитывает функциональные приложения. Теперь вам нужно изменить свои функции на:

type Counter = Int

f :: Env -> Counter -> Int -> (Int, Counter)
f env counter x = (x + env, counter + 1)

g :: Env -> Counter -> Int -> (Int, Counter)
g env counter x = let (y, newcounter) = f env counter x
                  in (x + y, newcounter + 1)

что явно менее приятно. С другой стороны, если мы принимаем монадический подход, мы просто переопределяем

type Global = ReaderT Env (State Counter)

Старые определения f и g продолжают работать без особых проблем. Чтобы обновить их, чтобы иметь семантику подсчета приложений, мы просто меняем их на

f :: Int -> Global Int
f x = do
  modify (+1)
  env <- ask
  return (x + env)

g :: Int -> Global Int
g x = do
  modify(+1)
  y <- f x
  return (x + y)

и теперь они отлично работают. Сравните два метода:

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

  • Использование монадического интерфейса потребовало изменения трех строк - и программа продолжала работать даже после того, как мы изменили первую строку, что означает, что мы могли бы делать рефакторинг постепенно (и тестировать его после каждого изменения), что уменьшает вероятность того, что рефактор вводит новые ошибки.

Ответ 2

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

В принципе, все, что использует монаду Reader, даже если оно очень далеко, должно быть Reader. (Если что-то не использует монаду Reader и не вызывает ничего другого, это не должно быть Reader.)

Тем не менее, использование монады Reader означает, что вам не нужно явно передавать среду вокруг нее - она ​​автоматически обрабатывается монадой.

(Помните, что это просто указатель на окружающую среду, проходящую мимо, а не на окружающую среду, поэтому она довольно дешевая.)

Ответ 3

Это действительно глобальные переменные, так как они инициализируются ровно один раз в main. Для этой ситуации целесообразно использовать глобальные переменные. Вы должны использовать unsafePerformIO для их записи, если требуется IO.

Если вы читаете только файл конфигурации, это довольно просто:

config :: Config
{-# NOINLINE config #-}
config = unsafePerformIO readConfigurationFile

Если есть некоторые зависимости от другого кода, так что вам нужно управлять при загрузке файла конфигурации, это сложнее:

globalConfig :: MVar Config
{-# NOINLINE globalConfig #-}
globalConfig = unsafePerformIO newEmptyMVar

-- Call this from 'main'
initializeGlobalConfig :: Config -> IO ()
initializeGlobalConfig x = putMVar globalConfig x

config :: Config
config = unsafePerformIO $ do
  when (isEmptyMVar globalConfig) $ fail "Configuration has not been loaded"
  readMVar globalConfig

См. также:

Ответ 4

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

Ответ 5

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