Кто-нибудь когда-нибудь сталкивался с монадным трансформатором в дикой природе?
В моей сфере деятельности - бэк-офисе ИТ для финансового учреждения - очень часто для программного компонента характерно иметь глобальную конфигурацию, регистрировать его прогресс, иметь какое-то короткое замыкание по обработке ошибок / вычислениям... Вещи, которые могут быть смоделированы с помощью Reader-, Writer-, Maybe-monads и т.п. в Haskell и составлены вместе с монадными трансформаторами.
Но есть некоторые недостатки: концепция монадных трансформаторов довольно хитрая и трудная для понимания, монадные трансформаторы приводят к сигнатурам очень сложного типа и наносят некоторый ущерб производительности.
Поэтому я задаюсь вопросом: лучше ли использовать монадные трансформаторы при решении этих общих задач, упомянутых выше?
6 ответов
Сообщество Haskell разделено по этому вопросу.
Джон Хьюз сообщает, что ему легче учить монаду-трансформеры, чем учить монады, и что его ученики лучше учатся с подходом "сначала трансформеры".
Разработчики GHC обычно избегают монадных преобразователей, предпочитая сворачивать свои собственные монады, которые объединяют все необходимые им функции. (Мне только что безоговорочно сказали, что GHC не будет использовать монадный трансформатор, который я определил три дня назад.)
Для меня монадные преобразователи во многом похожи на бессмысленное программирование (т.е. программирование без именованных переменных), что имеет смысл; в конце концов, они точно программируют бессмысленно на уровне типов. Мне никогда не нравилось бессмысленное программирование, потому что полезно вводить случайное название.
Что я наблюдаю на практике
Количество монадных трансформаторов, доступных на Hackage, очень велико, и большинство из них довольно просты. Это классический пример проблемы, когда сложнее выучить большую библиотеку, чем бросать свои собственные экземпляры.
Монады, такие как Writer, State и Environment, настолько просты, что я не вижу большой пользы для монадных преобразователей.
Где сияют монадные трансформаторы, так это модульность и повторное использование. Это свойство прекрасно продемонстрировано Ляном, Худаком и Джонсом в их знаковой статье "Трансформаторы монады и модульные переводчики".
Являются ли монадные трансформаторы наилучшей практикой при решении общих задач, упомянутых выше?
Я бы сказал, нет. Где монадные преобразователи являются наилучшей практикой, это то, где у вас есть линейка связанных абстракций, которые вы можете создать, составляя и повторно используя монадные преобразователи различными способами. В таком случае вы, вероятно, разрабатываете ряд монадных преобразователей, которые важны для вашей проблемной области (например, тот, который был отклонен для GHC), и вы (а) составляете их несколькими способами; (б) достичь значительного количества повторного использования для большинства трансформаторов; (в) инкапсулируют что-то нетривиальное в каждом монадном трансформаторе.
Мой монадный трансформатор, который был отклонен для GHC, не соответствовал ни одному из критериев (a)/(b)/(c) выше.
Концепция монадных трансформаторов довольно сложна и трудна для понимания, монадные трансформаторы приводят к сигнатурам очень сложного типа.
Я думаю, что это немного преувеличение
- Использовать определенный монадный стек трансформатора не сложнее, чем обычную монаду. Просто подумайте о слоях \ стопках, и все будет в порядке. Вам почти всегда не нужно поднимать чистую функцию (или конкретное действие ввода-вывода) более одного раза.
- Как уже упоминалось, скрыть ваш стек Monad в новом типе, использовать обобщенный вывод и скрыть конструктор данных в модуле.
- Старайтесь не использовать определенный стек Monad в сигнатуре типа функции, напишите общий код с классами типов Monad, такими как MonadIO, MonadReader и MonadState (используйте гибкое расширение контекстов, которое стандартизировано в Haskell 2010).
- Используйте библиотеки, такие как fclabels, чтобы уменьшить стандартные действия, которые обращаются к частям записи в Monad.
Трансформаторы монады - не единственные варианты, вы можете написать собственную монаду, используя продолжение монады. У вас есть изменяемые ссылки / массивы в IO (глобальный), ST (локальный и управляемый, без действий IO), MVar (синхронизация), TVar (транзакционный).
Я слышал, что потенциальные проблемы эффективности с преобразователями Monad можно уменьшить, просто добавив прагмы INLINE для привязки / возврата в исходном коде библиотеки mtl/transformers.
Недавно я "упал" на композицию монад в контексте F#. Я написал DSL с сильной зависимостью от монады состояния: все компоненты основаны на монаде состояния: синтаксический анализатор (монада анализатора, основанная на монаде состояния), таблицы соответствия переменных (более одной для внутренних типов), таблицы поиска идентификаторов. И поскольку все эти компоненты работают вместе, они полагаются на одну и ту же государственную монаду. Поэтому существует понятие состава состояний, которое объединяет различные локальные состояния, и понятие средств доступа, которые дают каждому алгоритму видимость своего состояния.
Изначально дизайн был действительно "просто одна большая государственная монада". Но затем я начал нуждаться в состояниях с локальными временами жизни, и все же в контексте "постоянного" состояния (и опять же, все эти состояния управляются государственными монадами). Для этого мне нужно было ввести преобразователи монад состояния, которые увеличивают состояние и адаптируют монады состояния вместе. Я также добавил трансформатор, чтобы свободно перемещаться между монадой состояний и монадой состояний продолжения, но я не стал его использовать.
Поэтому для ответа на вопрос: да, монадные трансформеры существуют в "диком" виде. Тем не менее, я бы категорически против использования их "из коробки". Напишите ваше приложение с простыми строительными блоками, используя небольшие мосты между вашими модулями, если вы в конечном итоге используете что-то вроде монадного преобразователя, это здорово; Не начинай оттуда.
И о типовых сигнатурах: я стал думать об этом типе программирования как о чем-то очень похожем на игру в шахматы с завязанными глазами (и я не шахматист): ваш уровень навыков должен быть таким, чтобы вы "видели" свои функции и типы подходят друг другу. Сигнатуры типов в основном заканчивают тем, что отвлекают, если только вы явно не хотите добавлять ограничения типов по соображениям безопасности (или потому, что компилятор заставляет вас давать их, например, с помощью записей F#).
Когда я изучал монады, я создавал приложение, используя стек StateT ContT IO для создания библиотеки имитации дискретных событий; продолжения использовались для хранения монадических потоков, причем StateT содержал очередь выполняемых потоков, а другие очереди использовались для приостановленных потоков, ожидающих различные события. Это работало довольно хорошо. Я не мог понять, как написать экземпляр Monad для оболочки нового типа, поэтому я просто сделал это синонимом типа, и это сработало довольно хорошо.
В эти дни я, вероятно, свернул бы свою собственную монаду с нуля. Однако всякий раз, когда я делаю это, я смотрю на "All About Monads" и источник MTL, чтобы напомнить мне, как выглядят операции связывания, так что в некотором смысле я все еще думаю о терминах стека MTL, даже если результат это пользовательская монада.
То есть что-то, что имеет тенденцию быть довольно глобальным, например, журнал или конфигурация, вы бы предложили добавить в монаду IO? Посмотрев на (по общему признанию очень ограниченный набор) примеров, я пришел к выводу, что код на Haskell имеет тенденцию быть либо чистым (то есть совсем не монадическим), либо монадой IO. Или это заблуждение?
Я думаю, что это заблуждение, только монада IO не является чистой. такие монады, как Write/T/Reader/T/State/T/ST, все еще чисто функциональны. Вы можете написать чистую функцию, которая использует любую из этих монад внутри, как этот абсолютно бесполезный пример:
foo :: Int -> Int
foo seed = flip execState seed $ do
modify $ (+) 3
modify $ (+) 4
modify $ (-) 2
Все, что это делает, - это неявное выполнение потоков / верстка состояния, то, что вы бы делали вручную явно, нотация здесь просто дает вам хороший синтаксический сахар, чтобы он выглядел обязательным. Вы не можете выполнять какие-либо действия ввода-вывода здесь, вы не можете вызывать какие-либо посторонние функции. Монада ST позволяет вам иметь реальные изменяемые ссылки в локальной области, имея чистый интерфейс функций, и вы не можете выполнять какие-либо действия ввода-вывода, там все еще чисто функционально.
Вы не можете избежать некоторых действий ввода-вывода, но вы не хотите возвращаться к вводу-выводу для всего, потому что это то, куда все может пойти, ракеты могут быть запущены, у вас нет контроля. У Haskell есть абстракции для управления эффективными вычислениями с различной степенью безопасности / чистоты, монада IO должна быть последним средством (но вы не можете избежать этого полностью).
В вашем примере я думаю, что вы должны придерживаться монадных преобразователей или монады, изготовленной на заказ, которая делает то же самое, что и их составление с помощью преобразователей. Я никогда не писал собственных монад (пока), но я довольно часто использовал монадные трансформеры (мой собственный код, а не на работе), не волнуйтесь о них так много, используйте их, и это не так плохо, как вы думаете,
Вы видели главу из реального мира на Haskell, в которой используются монадные трансформаторы?
Я думаю, что это заблуждение, только монада IO не является чистой. такие монады, как Write/T/Reader/T/State/T/ST, все еще чисто функциональны.
Мне кажется, что существует более одного понятия о термине чистый / не чистый. Ваше определение "IO = unpure, все остальное = pure" звучит похоже на то, о чем говорит Пейтон-Джонс в "Приручающих эффектах" ( http://ulf.wiger.net/weblog/2008/02/29/peyton-jones-taming-effects-the-next-big-challenge/). С другой стороны, RealWorld Haskell (на последних страницах главы Monad Transformer) сравнивает чистые функции с монадической функцией в целом, утверждая, что вам нужны разные библиотеки для обоих миров. Кстати, можно утверждать, что IO также чист, его побочные эффекты заключаются в функцию State с типом RealWorld -> (a, RealWorld). В конце концов, Haskell называет себя чисто функциональным языком (включая IO, я полагаю:-).)
Мой вопрос не столько о том, что можно сделать теоретически, а о том, что оказалось полезным с точки зрения разработки программного обеспечения. Монадные преобразователи допускают модульность эффектов (и абстракций в целом), но стоит ли ориентироваться на программирование направлений?