Крупномасштабный дизайн в Haskell?

Что такое хороший способ разработки/структурирования больших функциональных программ, особенно в Haskell?

Я прошел через кучу учебников (напишите сами, как моя любимая, с Real World Haskell - вторая секунда), но большинство программ относительно невелики и одноцелевые. Кроме того, я не считаю, что некоторые из них особенно элегантны (например, обширные таблицы поиска в WYAS).

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

Существует довольно большая литература, посвященная этим вопросам для крупных объектно-ориентированных императивных программ. Идеи, такие как MVC, шаблоны проектирования и т.д., Являются достойными рецептами для реализации широких целей, таких как разделение проблем и повторное использование в стиле OO. Кроме того, новые императивные языки поддаются "реставрационному дизайну", в котором, по моему новаторскому мнению, Haskell кажется менее подходящим.

Есть ли эквивалентная литература для Haskell? Как лучше всего использовать зоопарк экзотических структур управления в функциональном программировании (монады, стрелки, аппликативные и т.д.)? Какие рекомендации вы можете порекомендовать?

Спасибо!

EDIT (это продолжение ответа Дона Стюарта):

@dons упоминается: "Монады захватывают ключевые архитектурные проекты в типах".

Я думаю, мой вопрос: как следует думать о ключевых архитектурных проектах на чистом функциональном языке?

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

Каков "правильный" способ связать данные и шаги анализа красивым способом? Можно написать большую функцию, которая делает правильные вещи для разных типов данных. Или можно использовать монаду, чтобы следить за тем, что было обработано до сих пор, и каждый шаг обработки получает все, что ему нужно, от состояния монады. Или можно писать в основном отдельные программы и отправлять сообщения (мне не нравится этот вариант).

Слайды, с которыми он связан, имеют Вещь, в которой нам нужна пуля: "Идиомы для картирования дизайна на типы/функции/классы/монады ". Каковы идиомы?:)

Ответ 1

Я немного об этом расскажу в Engineering Large Projects в Haskell и в Проектирование и внедрение XMonad. В целом в области разработки находится сложность управления. Основными механизмами структурирования кода в Haskell для управления сложностью являются:

Система типов

  • Используйте систему типов для обеспечения абстракций, упрощая взаимодействие.
  • Использовать ключевые инварианты через типы
    • (например, что определенные значения не могут выйти из некоторой области)
    • Что определенный код не делает IO, не касается диска
  • Обеспечение безопасности: проверенные исключения (возможно, любой), избегайте смешивания концепций (Word, Int, Address)
  • Хорошие структуры данных (например, молнии) могут сделать некоторые классы тестирования ненужными, поскольку они исключают, например, за пределами ошибок статически.

Профайлер

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

Purity

  • Сократить сложность, удалив состояние. Чисто функциональная шкала кода, потому что она композиционна. Все, что вам нужно, это тип, определяющий, как использовать какой-то код - он не будет таинственно нарушаться при изменении какой-либо другой части программы.
  • Используйте много стилей программирования "модель/вид/контроллер": как можно скорее проанализируйте внешние данные в чисто функциональных структурах данных, оперируйте этими структурами, затем, как только вся работа будет выполнена, выполните рендеринг/очистку/сериализацию. Сохраняет большую часть вашего кода.

Тестирование

  • QuickCheck + Haskell Code Coverage, чтобы убедиться, что вы проверяете то, что не можете проверить с помощью типов.
  • GHC + RTS отлично подходит для просмотра, если вы тратите слишком много времени на GC.
  • QuickCheck также может помочь вам определить чистые ортогональные API для ваших модулей. Если свойства вашего кода трудно сформулировать, они, вероятно, слишком сложны. Храните рефакторинг до тех пор, пока у вас не будет чистого набора свойств, которые могут проверить ваш код, который хорошо скомпонован. Тогда код, вероятно, хорошо разработан.

Монады для структурирования

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

Типы классов и экзистенциальные типы

  • Используйте классы типов для предоставления абстракции: скройте реализации за полиморфными интерфейсами.

Concurrency и parallelism

  • Sneak par в вашу программу, чтобы победить в конкурсе с легким, составным parallelism.

Refactor

  • Вы можете реорганизовать в Haskell много. Типы гарантируют, что ваши масштабные изменения будут безопасными, если вы будете использовать типы с умом. Это поможет увеличить масштаб кода. Убедитесь, что ваши рефакторинги будут приводить к ошибкам типа до завершения.

Использовать FFI с умом

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

Мета-программирование

  • Немного шаблона Haskell или дженериков может удалить шаблон.

Упаковка и распространение

  • Используйте Cabal. Не сворачивайте свою собственную систему сборки. (EDIT: На самом деле вы, вероятно, захотите использовать Stack для начала работы.).
  • Используйте Haddock для хороших документов API
  • Такие инструменты, как graphmod, могут отображать структуры вашего модуля.
  • Полагайтесь на версии библиотек и инструментов платформы Haskell Platform, если это вообще возможно. Это стабильная база. (EDIT: Опять же, в эти дни вы, скорее всего, захотите использовать Stack для получения стабильной базы и запуска.)

Предупреждения

  • Используйте -Wall, чтобы ваш код был чистым от запахов. Вы можете также взглянуть на Агду, Изабель или Поймать для большей уверенности. Для проверки типа lint-like см. Большой hlint, который будет предлагать улучшения.

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

В целом: разделить логические единицы вашей системы на наименьшие ссылочные прозрачные компоненты, а затем реализовать их в модулях. Глобальная или локальная среда для наборов компонентов (или внутренних компонентов) может быть сопоставлена ​​с монадами. Используйте алгебраические типы данных для описания основных структур данных. Широко распространять эти определения.

Ответ 2

Дон дал вам больше всего подробностей выше, но здесь мои два цента от выполнения действительно nitty-gritty программ с сохранением состояния, таких как системные демоны в Haskell.

  • В конце концов, вы живете в стеке трансформатора монады. Внизу - IO. Кроме того, каждый основной модуль (в абстрактном смысле, а не в смысле "модуль в файле" ) отображает его необходимое состояние в слой в этом стеке. Поэтому, если у вас есть код подключения к базе данных, скрытый в модуле, вы пишете все это над типом MonadReader Connection m = > ... → m..., а затем ваши функции базы данных могут всегда получать их соединение без функций от других модули должны быть осведомлены о его существовании. У вас может быть один слой с подключением к базе данных, другой - с вашей конфигурацией, третий - семафорами и mvars для разрешения parallelism и синхронизации, другой - с файлом вашего журнала и т.д.

  • Сначала выведите свою обработку ошибок. Самая большая слабость на данный момент для Haskell в более крупных системах - это множество методов обработки ошибок, в том числе паршивых, таких как Maybe (что неверно, потому что вы не можете вернуть какую-либо информацию о том, что пошло не так, всегда используйте Либо вместо того, просто означают недостающие значения). Выясните, как вы это сделаете в первую очередь, и настройте адаптеры из различных механизмов обработки ошибок, которые ваши библиотеки и другой код используют в вашем последнем. Это спасет вас мир скорби позже.

Добавление (извлечено из комментариев, благодаря Lii и liminalisht) —
больше обсуждений о разных способах нарезания большой программы на монады в стеке:

Ben Kolera дает большое практическое введение в эту тему, и Brian Hurt обсуждает решения проблемы lift monadic действий в вашу обычную монаду. Джордж Уилсон показывает, как использовать mtl для написания кода, который работает с любой монадой, которая реализует требуемые типы стеков, а не ваш пользовательский вид монады. Карло Хамалайнен написал несколько коротких, полезных заметок, в которых излагаются слова Джорджа.

Ответ 3

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

Тем не менее, в большом дизайне приятно попробовать и использовать систему типов, чтобы убедиться, что вы можете только соответствовать вашим частям таким образом, чтобы это было правильно. Это может включать типы newtype или phantom, чтобы сделать вещи, которые, как представляется, имеют один и тот же тип, будут разными.

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

Ответ 4

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

Создание функционального программирования

The Craft of Functional Programming

http://www.cs.kent.ac.uk/people/staff/sjt/craft2e/

Ответ 5

В настоящее время я пишу книгу под названием "Функциональный дизайн и архитектура". Он предоставляет вам полный набор методов построения большого приложения с использованием чистого функционального подхода. В нем описываются многие функциональные модели и идеи при создании SCADA-подобного приложения "Андромеда" для управления космическими кораблями с нуля. Мой основной язык - Haskell. Книга охватывает:

  • Подходы к моделированию архитектуры с использованием диаграмм;
  • Анализ требований;
  • Встраиваемое DSL-доменное моделирование;
  • Внешний дизайн и реализация DSL;
  • Монады как подсистемы с эффектами;
  • Свободные монады как функциональные интерфейсы;
  • Стреляемые eDSL;
  • Инверсия управления с использованием свободных монадических eDSL;
  • Программная транзакционная память;
  • линзы;
  • Состояние, Reader, Writer, RWS, ST monads;
  • Нечистое состояние: IORef, MVar, STM;
  • Многопоточное и параллельное моделирование домена;
  • графический интерфейс;
  • Применимость основных методов и подходов, таких как UML, SOLID, GRASP;
  • Взаимодействие с нечистыми подсистемами.

Вы можете ознакомиться с кодом для книги здесь, а "Андромеда" .

Я планирую закончить эту книгу в конце 2017 года. Пока это не произойдет, вы можете прочитать мою статью "Дизайн и архитектура в функциональном программировании" (Rus) здесь.

UPDATE

Я поделился своей книгой в Интернете (первые 5 глав). См. сообщение в Reddit

Ответ 6

Сообщение блога Gabriel Масштабируемые архитектуры программ можно было бы упомянуть.

Шаблоны проектирования Haskell отличаются от основных шаблонов проектирования в одном важно:

  • Традиционная архитектура. Объедините несколько компонентов вместе. тип A для генерации "сети" или "топологии" типа B

  • Архитектура Haskell: объедините несколько компонентов типа A в создать новый компонент того же типа A, неотличимый в символ из его частей заместителя

Мне часто кажется, что, по-видимому, элегантная архитектура часто выпадает из библиотек, которые демонстрируют это приятное чувство однородности, снизу вверх. В Haskell это особенно очевидно - шаблоны, которые традиционно считаются "архитектурой сверху вниз", как правило, фиксируются в таких библиотеках, как mvc, Netwire и Cloud Haskell. То есть, я надеюсь, что этот ответ не будет интерпретироваться как попытка заменить любой из других в этом потоке, просто, что структурный выбор может и в идеале должен быть отвлечен в библиотеках экспертами по доменам. Реальная трудность в построении больших систем, на мой взгляд, оценивает эти библиотеки по их архитектурной "доброте" по сравнению со всеми вашими прагматическими проблемами.

Как liminalisht упоминается в комментариях, Шаблон дизайна категории другой пост Габриэля по теме, в том же духе.

Ответ 8

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

Таким образом, вы пользуетесь помощью Haskell с самого начала, и кодирование является естественным. Я бы не хотел делать что-то "функциональное" или "чистое" или достаточно общее, если то, что вы имеете в виду, является конкретной обычной проблемой. Я думаю, что чрезмерная инженерия - самая опасная вещь в ИТ. Другое дело, когда проблема заключается в создании библиотеки, которая абстрагирует набор связанных проблем.