Масштабный дизайн в Хаскеле?
Что такое хороший способ для разработки / структурирования больших функциональных программ, особенно в Haskell?
Я прошел через кучу обучающих программ ("Пишу себе схему" - моя любимая, с "Реал Уорлд Хаскелл"), но большинство программ относительно небольшие и одноцелевые. Кроме того, я не считаю некоторые из них особенно элегантными (например, обширные таблицы поиска в WYAS).
Теперь я хочу писать более крупные программы с большим количеством движущихся частей - получать данные из различных источников, очищать их, обрабатывать их различными способами, отображать в пользовательских интерфейсах, сохранять их, общаться по сетям и т. Д. Как можно Лучше ли структурировать такой код, чтобы он был удобочитаемым, поддерживаемым и адаптируемым к меняющимся требованиям?
Существует довольно много литературы, посвященной этим вопросам для крупных объектно-ориентированных императивных программ. Такие идеи, как MVC, шаблоны проектирования и т. Д., Являются подходящими рецептами для реализации широких целей, таких как разделение задач и повторное использование в стиле ОО. Кроме того, новые императивные языки поддаются "рефакторингу" в стиле "дизайн по мере роста", к которому, по моему мнению новичка, Haskell кажется менее подходящим.
Есть ли эквивалентная литература для Haskell? Как зоопарк экзотических структур управления, доступных в функциональном программировании (монады, стрелки, аппликативные и т. Д.), Лучше всего использовать для этой цели? Какие лучшие практики вы можете порекомендовать?
Спасибо!
РЕДАКТИРОВАТЬ (это продолжение ответа Дона Стюарта):
@dons упомянул: "Монады захватывают ключевые архитектурные проекты в типах".
Я предполагаю, что мой вопрос: как следует думать о ключевых архитектурных проектах на чистом функциональном языке?
Рассмотрим пример нескольких потоков данных и нескольких этапов обработки. Я могу написать модульные парсеры для потоков данных для набора структур данных, и я могу реализовать каждый шаг обработки как чистую функцию. Шаги обработки, требуемые для одного фрагмента данных, будут зависеть от его значения и других. Некоторые из шагов должны сопровождаться побочными эффектами, такими как обновления графического интерфейса или запросы к базе данных.
Какой "правильный" способ связать данные и этапы синтаксического анализа хорошим способом? Можно написать большую функцию, которая делает правильные вещи для различных типов данных. Или можно использовать монаду для отслеживания того, что было обработано до сих пор, и чтобы каждый шаг обработки получал то, что ему нужно дальше, из состояния монады. Или можно написать в значительной степени отдельные программы и отправлять сообщения (мне не очень нравится этот вариант).
На слайдах, которые он связал, есть пункт "Вещи, которые нам нужны": "Идиомы для отображения дизайна на типы / функции / классы / монады". Какие идиомы?:)
8 ответов
Я немного говорю об этом в Проектировании крупных проектов в Haskell и в Проектировании и реализации XMonad. Инжиниринг в целом - это управление сложностью. Основные механизмы структурирования кода в Haskell для управления сложностью:
Система типов
- Используйте систему типов для реализации абстракций, упрощая взаимодействия.
- Применять ключевые инварианты через типы
- (например, определенные значения не могут выходить за рамки видимости)
- Это определенный код не вводит-выводит, не касается диска
- Обеспечить безопасность: проверенные исключения (возможно / любые), избегать смешивания понятий (Word, Int, Address)
- Хорошие структуры данных (например, застежки-молнии) могут сделать ненужными некоторые классы тестирования, поскольку они исключают, например, статические ошибки из-за пределов.
Профилировщик
- Предоставьте объективные доказательства кучи и временных профилей вашей программы.
- В частности, профилирование кучи - лучший способ избежать ненужного использования памяти.
чистота
- Значительно уменьшите сложность, удалив состояние. Чисто функциональный код масштабируется, потому что он композиционный. Все, что вам нужно, это тип, чтобы определить, как использовать какой-то код - он не будет таинственным образом разрушаться, когда вы изменяете какую-то другую часть программы.
- Используйте множество программ в стиле "модель / представление / контроллер": как можно скорее разберите внешние данные в чисто функциональные структуры данных, оперируйте этими структурами, а затем, как только вся работа будет выполнена, выполните рендеринг / очистку / сериализацию. Сохраняет большую часть вашего кода в чистоте
тестирование
- QuickCheck + Haskell Code Coverage, чтобы убедиться, что вы тестируете вещи, которые вы не можете проверить с помощью типов.
- GHC + RTS отлично подходит для проверки того, что вы слишком много времени проводите в GC.
- QuickCheck также может помочь вам определить чистые, ортогональные API для ваших модулей. Если свойства вашего кода сложно определить, они, вероятно, слишком сложны. Продолжайте рефакторинг до тех пор, пока у вас не будет чистого набора свойств, которые могут протестировать ваш код, которые хорошо сочетаются. Тогда код, вероятно, тоже хорошо разработан.
Монады для структурирования
- Монады фиксируют ключевые архитектурные проекты в типах (этот код обращается к оборудованию, этот код является однопользовательским сеансом и т. Д.)
- Например, X-монада в xmonad точно отражает дизайн того, какое состояние видно для каких компонентов системы.
Классы типов и экзистенциальные типы
- Используйте классы типов для обеспечения абстракции: скрыть реализации за полиморфными интерфейсами.
Параллелизм и параллелизм
- ябеда
par
в вашу программу, чтобы победить конкурентов с легким, составным параллелизмом.
Refactor
- В Хаскеле можно много заниматься рефакторингом. Типы гарантируют, что ваши крупномасштабные изменения будут безопасны, если вы используете типы с умом. Это поможет вашему масштабу кодовой базы. Убедитесь, что ваши рефакторинги вызовут ошибки типа до завершения.
Используйте FFI с умом
- FFI облегчает игру с иностранным кодом, но этот иностранный код может быть опасным.
- Будьте очень осторожны в предположениях о форме возвращаемых данных.
Метапрограммирование
- Немного шаблона Хаскеля или дженериков может удалить шаблон.
Упаковка и распространение
- Используйте Кабал. Не катите свою собственную систему сборки. (РЕДАКТИРОВАТЬ: На самом деле вы, вероятно, хотите использовать Stack сейчас для начала.).
- Используйте Haddock для хороших документов API
- Такие инструменты, как graphmod, могут показать структуру вашего модуля.
- Если возможно, положитесь на версии библиотек и инструментов на платформе Haskell. Это стабильная база. (РЕДАКТИРОВАТЬ: Опять же, в наши дни вы, вероятно, захотите использовать Stack для создания стабильной базы.)
Предупреждения
- использование
-Wall
держать ваш код в чистоте от запахов. Вы также можете посмотреть на Агду, Изабель или Поймать для большей уверенности. Для проверки, похожей на ворсину, см. Отличный совет, который предложит улучшения.
Со всеми этими инструментами вы можете справляться со сложностью, устраняя как можно больше взаимодействий между компонентами. В идеале у вас есть очень большая база чистого кода, которую очень легко поддерживать, поскольку она композиционная. Это не всегда возможно, но к этому стоит стремиться.
В общем: разбейте логические блоки вашей системы на наименьшие из возможных ссылочных прозрачных компонентов, а затем внедрите их в модули. Глобальные или локальные среды для наборов компонентов (или внутри компонентов) могут быть сопоставлены с монадами. Используйте алгебраические типы данных для описания основных структур данных. Разделите эти определения широко.
Дон дал вам большинство деталей выше, но вот мои два цента от выполнения по-настоящему жутких программ с отслеживанием состояния, таких как системные демоны в Haskell.
В конце концов, вы живете в монадном стеке трансформаторов. Внизу находится IO. Кроме того, каждый основной модуль (в абстрактном смысле, а не в смысле "модуль в файле") отображает свое необходимое состояние в слой в этом стеке. Таким образом, если у вас есть код подключения к базе данных, скрытый в модуле, вы пишете все, чтобы иметь тип подключения MonadReader m => ... -> m ... и тогда ваши функции базы данных всегда могут получить свое соединение без функций других модули должны быть осведомлены о его существовании. Вы можете получить один слой с вашим подключением к базе данных, другой - вашу конфигурацию, третий - различные семафоры и мвари для разрешения параллелизма и синхронизации, другой - дескрипторы вашего файла журнала и т. Д.
Сначала разберитесь с обработкой ошибок. Наибольшим недостатком на данный момент для Haskell в больших системах является множество методов обработки ошибок, в том числе паршивых, таких как Maybe (что неверно, потому что вы не можете вернуть информацию о том, что пошло не так; всегда используйте Either вместо Maybe, если вы действительно просто имею ввиду пропущенные значения). Сначала выясните, как вы собираетесь это сделать, и настройте адаптеры из различных механизмов обработки ошибок, которые используются вашими библиотеками и другим кодом, в финальный. Это спасет вас от горя позже.
Приложение (извлечено из комментариев; спасибо Lii & liminalisht) -
подробнее о различных способах нарезки большой программы на монады в стеке:
Бен Колера дает большое практическое введение в эту тему, а Брайан Херт обсуждает пути решения проблемы lift
добавление монадических действий в вашу монаду. Джордж Уилсон показывает, как использовать mtl
написать код, который работает с любой монадой, которая реализует требуемые классы типов, а не с вашим собственным видом монад. Карло Хамалайнен написал несколько коротких полезных заметок, в которых резюмируется разговор Джорджа
Разработка больших программ на Haskell ничем не отличается от разработки на других языках. Программирование в целом заключается в том, чтобы разбить вашу проблему на управляемые части и как их объединить; язык реализации менее важен.
Тем не менее, в большом дизайне неплохо бы попробовать и использовать систему типов, чтобы убедиться, что вы можете соединять свои части только так, чтобы это было правильно. Это может включать в себя новые или фантомные типы, чтобы вещи, которые выглядели одинаково, были разными.
Когда дело доходит до рефакторинга кода по мере продвижения, чистота - это большое благо, поэтому постарайтесь сохранить как можно большую часть кода в чистоте. Чистый код легко реорганизовать, потому что он не имеет скрытого взаимодействия с другими частями вашей программы.
С этой книгой я впервые узнал о структурном функциональном программировании. Это может быть не совсем то, что вы ищете, но для начинающих в функциональном программировании, это может быть одним из лучших первых шагов, чтобы научиться структурировать функциональные программы - независимо от масштаба. На всех уровнях абстракции дизайн должен всегда иметь четко организованные структуры.
Ремесло функционального программирования
Сейчас я пишу книгу под названием "Функциональный дизайн и архитектура". Он предоставляет вам полный набор методик создания больших приложений с использованием чисто функционального подхода. Он описывает множество функциональных шаблонов и идей при создании SCADA-подобного приложения "Андромеда" для управления космическими кораблями с нуля. Мой основной язык - Haskell. Книга охватывает:
- Подходы к моделированию архитектуры с использованием диаграмм;
- Анализ требований;
- Встроенное моделирование домена DSL;
- Внешний дизайн и реализация DSL;
- Монады как подсистемы с эффектами;
- Бесплатные монады как функциональные интерфейсы;
- Стрелки eDSL;
- Инверсия управления с использованием Free monadic eDSL;
- Программная транзакционная память;
- линзы;
- State, Reader, Writer, RWS, ST монады;
- Нечистое состояние: IORef, MVar, STM;
- Многопоточность и параллельное доменное моделирование;
- GUI;
- Применимость основных методов и подходов, таких как UML, SOLID, GRASP;
- Взаимодействие с нечистыми подсистемами.
Вы можете ознакомиться с кодом книги здесь и кодом проекта "Андромеды".
Я рассчитываю закончить эту книгу в конце 2017 года. Пока это не произойдет, вы можете прочитать мою статью "Дизайн и архитектура в функциональном программировании" здесь.
ОБНОВИТЬ
Я поделился своей книгой онлайн (первые 5 глав). Смотрите пост на Reddit
Пост Габриэля в блоге Стоит упомянуть масштабируемую архитектуру программы.
Шаблоны проектирования Haskell отличаются от основных шаблонов проектирования одним важным аспектом:
Обычная архитектура: объединение нескольких компонентов типа A для создания "сети" или "топологии" типа B
Архитектура Haskell: объединение нескольких компонентов типа A для создания нового компонента того же типа A, неотличимого по характеру от его замещающих частей
Меня часто поражает, что на вид элегантная архитектура часто имеет тенденцию выпадать из библиотек, которые демонстрируют это приятное чувство однородности, восходящим способом. В Haskell это особенно очевидно - шаблоны, которые традиционно считаются "нисходящей архитектурой", как правило, собираются в таких библиотеках, как mvc, Netwire и Cloud Haskell. То есть, я надеюсь, что этот ответ не будет интерпретирован как попытка заменить кого-либо из других в этой теме, просто что структурные решения могут и должны в идеале быть абстрагированы экспертами в библиотеках. На мой взгляд, реальная трудность в построении больших систем заключается в оценке этих библиотек по их архитектурному "достоинству" в сравнении со всеми вашими прагматическими соображениями.
Как упоминает liminalisht в комментариях, шаблон дизайна категории - это еще одно сообщение Габриэля на эту тему, в том же духе.
Я нашел статью Алехандро Серрано "Обучение архитектуре программного обеспечения с использованием Haskell" (pdf), полезную для размышлений о крупномасштабной структуре в Haskell.
Возможно, вам придется сделать шаг назад и подумать, как в первую очередь перевести описание проблемы в дизайн. Поскольку Haskell находится на таком высоком уровне, он может фиксировать описание проблемы в виде структур данных, действий в качестве процедур и чистого преобразования в качестве функций. Тогда у вас есть дизайн. Разработка начинается, когда вы компилируете этот код и обнаружите конкретные ошибки, касающиеся пропущенных полей, пропущенных экземпляров и пропущенных монадических преобразователей в вашем коде, потому что, например, вы выполняете доступ к базе данных из библиотеки, которой требуется определенная монада состояний в процедуре ввода-вывода. И вуаля, есть программа. Компилятор подпитывает ваши мысленные наброски и обеспечивает согласованность дизайна и разработки.
Таким образом, вы получаете выгоду от помощи Haskell с самого начала, и кодирование является естественным. Я не хотел бы делать что-то "функциональное" или "чистое" или достаточно общее, если то, что вы имеете в виду, является конкретной обычной проблемой. Я думаю, что чрезмерная инженерия - самая опасная вещь в ИТ. Все иначе, когда проблема заключается в создании библиотеки, которая абстрагирует множество связанных проблем.