Использование монаха штата Хаскелл запаха кода?
Бог, я ненавижу термин "запах кода", но я не могу придумать ничего более точного.
Я разрабатываю язык высокого уровня и компилятор для Whitespace в свое свободное время, чтобы узнать о построении компилятора, и функциональное программирование (компилятор записывается в Haskell).
Во время фазы генерации кода компилятора я должен поддерживать данные "state" -ish, когда я пересекаю дерево синтаксиса. Например, при компиляции операторов управления потоком мне нужно создать уникальные имена для ярлыков, на которые можно перейти (метки, созданные с помощью счетчика, который был передан, обновлен и возвращен, а старое значение счетчика никогда не должно использоваться снова). Другим примером является то, что когда я сталкиваюсь с строковыми литералами в синтаксическом дереве, они должны быть постоянно преобразованы в переменные кучи (в Whitespace строки лучше всего хранятся в куче). В настоящее время я завершаю весь модуль генерации кода в государственной монаде, чтобы справиться с этим.
Мне сказали, что писать компилятор - это проблема, хорошо подходящая для функциональной парадигмы, но я нахожу, что я ее проектирую точно так же, как я бы ее проектировал на C (вы действительно можете писать C в любом язык - даже Haskell w/state monads).
Я хочу научиться думать в Haskell (скорее, в функциональной парадигме) - не в C с синтаксисом Haskell. Должен ли я действительно попытаться устранить/свести к минимуму использование государственной монады или это законный функциональный "шаблон дизайна"?
Ответы
Ответ 1
Я бы сказал, что состояние вообще не является запахом кода, если оно остается маленьким и хорошо контролируемым.
Это означает, что использование монад, таких как State, ST или настраиваемых, или просто наличие структуры данных, содержащей данные состояния, которые вы передаете в нескольких местах, не так уж плохо. (На самом деле, монады - это просто помощь в выполнении именно этого!) Однако, имея состояние, которое идет повсеместно (да, это означает, что вы, МО-монада!) - плохой запах.
Ярким примером этого была моя команда, которая работала над нашей записью для ICFP Programming Contest 2009 (код доступен в git://git.cynic.net/Haskell/МКВП-конкурс-2009). Мы получили несколько различных модульных деталей:
- VM: виртуальная машина, которая запускала программу моделирования
- Контроллеры: несколько разных наборов подпрограмм, которые считывают выходные данные симулятора и генерируют новые управляющие входы.
- Решение: создание файла решения на основе выходных данных контроллеров
- Visualizers: несколько разных наборов подпрограмм, которые читают как входные, так и выходные порты и генерируют какую-то визуализацию или журнал того, что происходило по мере продвижения моделирования.
Каждое из них имеет свое собственное состояние, и все они взаимодействуют различными способами через входные и выходные значения виртуальной машины. У нас было несколько разных контроллеров и визуализаторов, каждый из которых имел свое собственное другое состояние.
Ключевым моментом здесь было то, что внутренности любого конкретного состояния были ограничены их собственными отдельными модулями, и каждый модуль ничего не знал о существовании состояния для других модулей. Любой конкретный набор кода и данных с состоянием обычно составлял всего несколько десятков строк с небольшим количеством элементов данных в состоянии.
Все это было склеено в одну небольшую функцию около десятка строк, которые не имели доступа к внутренностям какого-либо из состояний и которые просто называли правильные вещи в правильном порядке, когда они зацикливались в симуляции и проходили мимо очень ограниченное количество внешней информации для каждого модуля (конечно, вместе с предыдущим состоянием модуля).
Когда состояние используется таким ограниченным образом, и система типов мешает вам непреднамеренно модифицировать ее, ее довольно легко обрабатывать. Это одна из красавиц Хаскелла, которая позволяет вам сделать это.
В одном ответе говорится: "Не используйте монады". С моей точки зрения, это точно в обратном направлении. Монады - это структура управления, которая, среди прочего, может помочь вам свести к минимуму количество кода, который касается состояния. Если вы посмотрите на монадические синтаксические анализаторы в качестве примера, состояние анализа (т.е. Анализируемый текст, как далеко он попал к нему, любые накопленные предупреждения и т.д.) Должны проходить через каждый комбинатор, используемый в синтаксическом анализаторе, Но будет только несколько комбинаторов, которые фактически манипулируют государством напрямую; все остальное использует одну из этих нескольких функций. Это позволяет вам четко видеть и в одном месте все небольшое количество кода, которое может изменить состояние, и более легко понять, как его можно изменить, что снова облегчает работу.
Ответ 2
Я написал несколько компиляторов в Haskell, а государственная монада - разумное решение многих проблем компилятора. Но вы хотите сохранить его абстрактным - не делайте очевидным, что вы используете монаду.
Вот пример из компилятора Glasgow Haskell (который я не писал, я просто работаю с несколькими ребрами), где мы строим графики потока управления. Вот основные способы создания графиков:
empyGraph :: Graph
mkLabel :: Label -> Graph
mkAssignment :: Assignment -> Graph -- modify a register or memory
mkTransfer :: ControlTransfer -> Graph -- any control transfer
(<*>) :: Graph -> Graph -> Graph
Но, как вы обнаружили, поддержка поставок уникальных ярлыков в лучшем случае утомительна, поэтому мы также предоставляем эти функции:
withFreshLabel :: (Label -> Graph) -> Graph
mkIfThenElse :: (Label -> Label -> Graph) -- branch condition
-> Graph -- code in the 'then' branch
-> Graph -- code in the 'else' branch
-> Graph -- resulting if-then-else construct
Вся вещь Graph
является абстрактным типом, и переводчик просто весело конструирует графики чисто функциональным образом, не осознавая, что происходит что-то монадическое. Затем, когда граф окончательно построен, чтобы превратить его в алгебраический тип данных, мы можем сгенерировать код из, мы даем ему поставку уникальных ярлыков, запускаем государственную монаду и вытаскиваем структуру данных.
Государственная монада скрыта под ней; хотя это не подвержено клиенту, определение Graph
выглядит примерно так:
type Graph = RealGraph -> [Label] -> (RealGraph, [Label])
или более точно
type Graph = RealGraph -> State [Label] RealGraph
-- a Graph is a monadic function from a successor RealGraph to a new RealGraph
С государственной монадой, скрытой за слоем абстракции, она не вонючая вообще!
Ответ 3
Вы посмотрели Атрибуты грамматики (AG)? (Подробнее о wikipedia и статья в Monad Reader)?
С AG вы можете добавлять атрибуты к дереву синтаксиса. Эти атрибуты разделены в синтезированных и унаследованных атрибутах.
Синтезированные атрибуты - это вещи, которые вы генерируете (или синтезируете) из своего дерева синтаксиса, это может быть сгенерированный код или все комментарии или что-то еще, что вас интересует.
Унаследованные атрибуты вводятся в ваше дерево синтаксиса, это может быть среда или список меток, используемых во время генерации кода.
В Университете Утрехта мы используем Система грамматики атрибутов (UUAGC) для написания компиляторов. Это предварительный процессор, который генерирует код haskell (.hs
files) из предоставленных файлов .ag
.
Хотя, если вы все еще учитесь Haskell, то, возможно, сейчас не время начинать изучать еще один слой абстракции над этим.
В этом случае вы можете вручную написать тип кода, который генерирует для вас грамматики, например:
data AbstractSyntax = Literal Int | Block AbstractSyntax
| Comment String AbstractSyntax
compile :: AbstractSyntax -> [Label] -> (Code, Comments)
compile (Literal x) _ = (generateCode x, [])
compile (Block ast) (l:ls) = let (code', comments) = compile ast ls
in (labelCode l code', comments)
compile (Comment s ast) ls = let (code, comments') = compile ast ls
in (code, s : comments')
generateCode :: Int -> Code
labelCode :: Label -> Code -> Code
Ответ 4
Возможно, вам понадобится аппликативный функтор вместо
монада:
http://www.haskell.org/haskellwiki/Applicative_functor
Я думаю, что оригинальная статья объясняет это лучше, чем wiki:
http://www.soi.city.ac.uk/~ross/papers/Applicative.html
Ответ 5
Я не думаю, что использование State Monad является запахом кода, когда он использовался для моделирования состояния.
Если вам нужно передать состояние через свои функции,
вы можете сделать это явно, взяв состояние в качестве аргумента и вернув его в каждую функцию.
Государственная Монада предлагает хорошую абстракцию: она передает государство для вас и
предоставляет множество полезных функций для объединения функций, требующих состояния.
В этом случае использование State Monad (или Applicatives) не является запахом кода.
Однако, если вы используете Государственную Монаду для подражания императивному стилю программирования
в то время как функционального решения было бы достаточно, вы просто усложняете ситуацию.
Ответ 6
В общем, вы должны стараться избегать состояния, где это возможно, но это не всегда практично. Applicative
делает эффектный код более приятным и функциональным, особенно код обхода дерева может извлечь выгоду из этого стиля. Для проблемы генерации имен теперь имеется довольно хороший пакет: value-supply.
Ответ 7
Ну, не используйте монады. Сила функционального программирования - это чистота функций и их повторное использование. Там в этой статье один мой профессор однажды написал, и он один из парней, которые помогли построить Haskell.
Бумага называется " Почему функциональное программирование имеет значение?", я предлагаю вам прочитать его. Это хорошо прочитано.
Ответ 8
будьте осторожны с терминологией здесь. Государство само по себе плохое; функциональные языки имеют состояние. Что такое "запах кода", когда вы обнаруживаете, что хотите присвоить значения переменных и изменить их.
Конечно, государственная монада Haskell существует именно по этой причине - как с I/O, это позволяет вам делать небезопасные и ненадежные вещи в ограниченном контексте.
Итак, да, это, вероятно, запах кода.