Масштабный дизайн в Haskell? [закрыто]

565

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

Я прошел через кучу уроков («Пишу себе схему» - моя любимая, с «Реал Уорлд Хаскелл»), но большинство программ относительно небольшие и одноцелевые. Кроме того, я не считаю некоторые из них особенно элегантными (например, огромные таблицы поиска в WYAS).

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

Существует достаточно большая литература, посвященная этим вопросам для крупных объектно-ориентированных императивных программ. Такие идеи, как MVC, шаблоны проектирования и т. Д., Являются подходящими рецептами для реализации широких целей, таких как разделение задач и повторное использование в стиле ОО. Кроме того, новые императивные языки поддаются «рефакторингу» в стиле «дизайн по мере роста», который, по моему мнению новичка, выглядит менее подходящим для Haskell.

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

Спасибо!

РЕДАКТИРОВАТЬ (это продолжение ответа Дона Стюарта):

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

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

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

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

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

Дэн
источник
9
Я думаю, что основная идея при написании больших программ на функциональном языке - это небольшие специализированные модули без сохранения состояния, которые обмениваются сообщениями . Конечно, вы должны притворяться, потому что настоящая программа нуждается в состоянии. Я думаю, что именно здесь F # сияет над Haskell.
ChaosPandion
18
@Chaos, но только Haskell по умолчанию обеспечивает безгражданство. У вас нет выбора, и вам нужно усердно работать, чтобы ввести состояние (чтобы нарушить композиционность) в Haskell :-)
Дон Стюарт,
7
@ ChaosPandion: я не согласен, в теории. Конечно, на императивном языке (или на функциональном языке, предназначенном для передачи сообщений) это вполне может быть тем, что я бы сделал. Но у Хаскелла есть и другие способы борьбы с государством, и, возможно, они позволили мне сохранить больше «чистых» преимуществ.
Дан
1
Я написал немного об этом в «Руководстве по проектированию» в этом документе: community.haskell.org/~ndm/downloads/…
Нил Митчелл
5
@JonHarrop давайте не будем забывать, что, хотя MLOC является хорошим показателем, когда вы сравниваете проекты на похожих языках, для межязыкового сравнения не имеет особого смысла, особенно с такими языками, как Haskell, где повторное использование кода и модульность намного проще и безопаснее. по сравнению с некоторыми языками там.
Таир

Ответы:

519

Я немного говорю об этом в Проектировании крупных проектов в 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чтобы сохранить ваш код чистым от запахов. Вы также можете посмотреть на Агду, Изабель или Поймать для большей уверенности. Для проверки, похожей на ворсину , см. Отличный совет , который предложит улучшения.

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

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

Дон стюарт
источник
8
Спасибо Дон, ваш ответ превосходен - все это ценные рекомендации, и я буду регулярно к ним обращаться. Думаю, мой вопрос возникает на шаг раньше, чем все это понадобится. То, что я действительно хотел бы знать, это «Идиомы для отображения дизайна на типы / функции / классы / монады» ... Я мог бы попытаться изобрести свой собственный, но я надеялся, что где-то может быть найден набор лучших практик - или, если нет, рекомендации для хорошо структурированного кода для чтения системы большого размера (в отличие, скажем, от специализированной библиотеки). Я отредактировал свой пост, чтобы задать этот же вопрос более прямо.
Дан
6
Я добавил текст о декомпозиции дизайна в модули. Ваша цель состоит в том, чтобы идентифицировать логически связанные функции в модулях, которые имеют ссылочно-прозрачные интерфейсы с другими частями системы, и как можно быстрее использовать чисто функциональные типы данных, насколько это возможно, для безопасного моделирования внешнего мира. Документ по дизайну xmonad охватывает многое из этого: xmonad.wordpress.com/2009/09/09/…
Дон Стюарт,
3
Я пытался загрузить слайды из проекта « Разработки больших проектов» в Haskell , но ссылка оказалась неработающей. Вот рабочий: galois.com/~dons/talks/dons-londonhug-decade.pdf
mik01aj
3
Мне удалось найти эту новую ссылку для скачивания: pau-za.cz/data/2/sprava.pdf
Риккардо Т.
3
@Heather Несмотря на то, что ссылка для скачивания на странице, которую я упомянул в предыдущем комментарии, не работает, похоже, что слайды все еще можно просматривать на scribd: scribd.com/doc/19503176/The-Design-and-Implementation-of -xmonad
Риккардо Т.
118

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

  1. В конце концов, вы живете в монадном стеке трансформаторов. Внизу находится IO. Кроме того, каждый основной модуль (в абстрактном смысле, а не в смысле «модуль в файле») отображает свое необходимое состояние в слой в этом стеке. Таким образом, если у вас есть код подключения к базе данных, скрытый в модуле, вы пишете все, чтобы иметь тип подключения MonadReader m => ... -> m ... и тогда функции вашей базы данных всегда могут получить свое соединение без функций других модули должны быть осведомлены о его существовании. Вы можете получить один слой с вашим подключением к базе данных, другой - вашу конфигурацию, третий - различные семафоры и мвари для разрешения параллелизма и синхронизации, другой - дескрипторы вашего файла журнала и т. Д.

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

Приложение (извлечено из комментариев; спасибо Lii & liminalisht ) -
более подробное обсуждение различных способов нарезки большой программы на монады в стеке:

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

user349653
источник
5
Два хороших момента! Этот ответ заслуживает того, чтобы быть достаточно конкретным, а другие - нет. Было бы интересно прочитать более подробную информацию о различных способах нарезки большой программы на монады в стеке. Пожалуйста, разместите ссылки на такие статьи, если у вас есть!
Лий
6
@Lii Ben Kolera дает большое практическое введение в эту тему, а Брайан Херт обсуждает решение проблемы liftвнедрения монадических действий в вашу собственную монаду. Джордж Уилсон показывает, как использовать mtlдля написания кода, который работает с любой монадой, которая реализует требуемые классы типов, а не с вашим собственным видом монад. Карло Хамалайнен написал несколько коротких полезных заметок, в которых резюмируется выступление Джорджа.
Liminalisht
Я согласен, что стеки монадных трансформаторов, как правило, являются ключевыми архитектурными основами, но я очень стараюсь не допускать ввода-вывода. Это не всегда возможно, но если вы подумаете о том, что означает «и затем» в вашей монаде, вы можете обнаружить, что у вас действительно есть продолжение или автомат где-то внизу, который затем можно интерпретировать в IO с помощью функции «run».
Пол Джонсон
Как уже отмечал @PaulJohnson, этот подход монадных стеков
McBear Holden,
43

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

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

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

augustss
источник
14
Я на самом деле обнаружил, что рефакторинг довольно разочаровывает, если типы данных нужно изменить. Это требует утомительной модификации арности множества конструкторов и сопоставлений с образцом. (Я согласен с тем, что рефакторинг чистых функций в другие чистые функции того же типа прост - если не касаться типов данных)
Дан
2
@Dan Вы можете полностью освободиться с небольшими изменениями (например, просто добавив поле), когда вы используете записи. Некоторые могут захотеть сделать записи привычкой (я один из них ^^ ").
MasterMastic
5
@ Я имею в виду, если вы меняете тип данных функции на каком-либо языке, разве вам не нужно делать то же самое? Я не понимаю, как язык, такой как Java или C ++, поможет вам в этом отношении. Если вы говорите, что можете использовать какой-то общий интерфейс, которому подчиняются оба типа, то вы должны были делать это с классами типов в Haskell.
точка с запятой
4
@semicon Разница для языков, таких как Java, заключается в наличии зрелых, хорошо протестированных и полностью автоматизированных инструментов для рефакторинга. Как правило, эти инструменты имеют фантастическую интеграцию с редактором и отнимают огромное количество утомительной работы, связанной с рефакторингом. Haskell предоставляет нам блестящую систему типов, с помощью которой можно обнаруживать вещи, которые должны быть изменены в рефакторинге, но инструменты для фактического проведения этого рефакторинга (в настоящий момент) очень ограничены, особенно по сравнению с тем, что уже было доступно в Java экосистема более 10 лет.
JSK
16

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

Ремесло функционального программирования

Ремесло функционального программирования

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

комонада
источник
11
Как бы ни был крафтен FP - я узнал из него Haskell - это вводный текст для начинающих программистов , а не для проектирования больших систем на Haskell.
Дон Стюарт
3
Ну, это лучшая книга, которую я знаю о разработке API и скрытии деталей реализации. Благодаря этой книге я стал лучшим программистом на C ++ - просто потому, что научился лучшим способам организации своего кода. Что ж, ваш опыт (и ответ), безусловно, лучше, чем эта книга, но Дэн, возможно, все еще новичок в Хаскеле. ( where beginner=do write $ tutorials `about` Monads)
комонад
11

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

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

Вы можете ознакомиться с кодом книги здесь и кодом проекта «Андромеды» .

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

ОБНОВИТЬ

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

graninas
источник
Александр, не могли бы вы обновить эту заметку, когда ваша книга будет готова, чтобы мы могли следить за ней. Приветствия.
Макс
4
Конечно! Пока я закончил половину текста, но это 1/3 всей работы. Так что, сохраняйте интерес, это меня очень вдохновляет!
graninas
2
Привет! Я поделился своей книгой онлайн (только первые 5 глав). Смотрите пост на Reddit: reddit.com/r/haskell/comments/6ck72h/…
graninas
спасибо за обмен и работу!
Макс
Очень жду этого!
патрики
7

Блог Габриэля. Стоит упомянуть о масштабируемой архитектуре программ .

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

  • Обычная архитектура : объедините несколько компонентов типа A для создания «сети» или «топологии» типа B

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

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

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

Рено Линдек
источник
3
Я бы упомянул еще один пост Габриэля Гонсалеса о шаблоне дизайна категории . Его основной аргумент заключается в том, что то, что мы, функциональные программисты, называем «хорошей архитектурой», на самом деле является «композиционной архитектурой» - это разработка программ с использованием элементов, которые гарантированно составляются. Так как законы категорий гарантируют, что идентичность и ассоциативность сохраняются при композиции, композиционная архитектура достигается с помощью абстракций, для которых у нас есть категория - например, чистые функции, монадические действия, каналы и т. Д.
liminalisht
3

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

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

оборота агокороны
источник