Масштабний дизайн в Haskell? [зачинено]


565

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

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

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

Існує досить велика література, що стосується цих питань для великих об'єктно-орієнтованих імперативних програм. Ідеї, такі як MVC, модель дизайну тощо, є пристойними рецептами для реалізації широких цілей, таких як розділення проблем та повторне використання в стилі OO. Крім того, нові імперативні мови піддаються стилю рефакторингу «дизайн у міру зростання», до якого, на мою думку початківця, Haskell виглядає менш підходящим.

Чи існує еквівалентна література для Haskell? Як зоопарк екзотичних структур управління доступний у функціональному програмуванні (монади, стрілки, додаткові програми тощо) найкраще використовуються для цієї мети? Які найкращі практики ви могли б порекомендувати?

Дякую!

EDIT (це відповідь на відповідь Дон Стюарт):

@dons згадується: "Монади захоплюють ключові архітектурні конструкції за типами."

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

Розглянемо на прикладі декількох потоків даних та декількох етапів обробки. Я можу записати модульні аналізатори для потоків даних до набору структур даних і можу реалізувати кожен етап обробки як чисту функцію. Етапи обробки, необхідні для однієї частини даних, залежатимуть від її значення та інших ". Деяким крокам слід дотримуватися побічних ефектів, таких як оновлення GUI або запити до бази даних.

Який "правильний" спосіб прив'язувати дані та кроки аналізу симпатичним чином? Можна було б написати велику функцію, яка робить правильно для різних типів даних. Або можна використати монаду, щоб відслідковувати те, що було оброблено до цього часу, і кожен етап обробки отримує все необхідне, наступне від стану монади. Або можна написати значною мірою окремі програми та надсилати повідомлення (мені ця опція не дуже подобається).

На слайдах, які він зв'язав, є куля «Нам потрібні речі»: «Ідіоми для відображення дизайну на типи / функції / класи / монади». Що таке ідіоми? :)


9
Я думаю, що основна ідея при написанні великих програм на функціональній мові - це невеликі, спеціалізовані модулі та без громадянства, які спілкуються через передачу повідомлень . Звичайно, ви повинні трохи прикинутися, тому що справжня програма потребує стану. Я думаю, що саме тут F # світить над Haskell.
ChaosPandion

18
@Chaos, але лише Haskell за замовчуванням виконує безгромадянське життя. У вас немає вибору, і доводиться наполегливо працювати, щоб запровадити стан (щоб порушити композиційність) у Haskell :-)
Дон Стюарт

7
@ChaosPandion: Теоретично я не згоден. Звичайно, в імперативній мові (або функціональній, розробленій навколо передачі повідомлень), це могло б бути саме тим, що я б робив. Але у Haskell є інші способи боротьби з державою, і, можливо, вони дозволяють мені зберегти більше "чистих" переваг.
День

1
Я трохи писав про це під "Правилами дизайну" в цьому документі: community.haskell.org/~ndm/downloads/…
Ніл Мітчелл

5
@JonHarrop не давайте забувати, що хоча MLOC є хорошою метрикою, коли ви порівнюєте проекти на подібних мовах, це не має особливого сенсу для порівняння між мовами, особливо з такими мовами, як Haskell, де повторне використання та модульність коду набагато простіше та безпечніше порівняно з деякими мовами там.
Таїр

Відповіді:


519

Я трохи говорю про це в інженерних великих проектах в Haskell і в розробці та впровадженні XMonad. Інженерія в цілому - це управління складністю. Основними механізмами структурування коду в Haskell для управління складністю є:

Тип типу

  • Використовуйте систему типу для застосування абстракцій, спрощуючи взаємодію.
  • Застосовуйте ключові інваріанти за допомогою типів
    • (наприклад, що певні значення не можуть уникнути певної області застосування)
    • Цей певний код не робить IO, не торкається диска
  • Забезпечення безпеки: перевірені винятки (можливо / будь-яке), уникайте змішування понять (Word, Int, Address)
  • Хороша структура даних (як блискавки) може зробити деякі класи тестування непотрібними, оскільки вони виключають, наприклад, статичні помилки поза межами.

Профілер

  • Надайте об'єктивні докази купівлі та часу вашої програми.
  • Зовнішнє профілювання, зокрема, є найкращим способом забезпечити непотрібне використання пам'яті.

Чистота

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

Тестування

  • QuickCheck + Покриття коду Haskell, щоб переконатися, що ви перевіряєте речі, які не можете перевірити за допомогою типів.
  • GHC + RTS чудово підходить для того, щоб побачити, якщо ви витрачаєте занадто багато часу на заняття GC.
  • QuickCheck також може допомогти вам визначити чисті, ортогональні API для своїх модулів. Якщо властивості вашого коду важко констатувати, вони, ймовірно, занадто складні. Продовжуйте рефакторинг, поки у вас не буде чистий набір властивостей, які можуть перевірити ваш код, які добре складають. Тоді код, ймовірно, також добре розроблений.

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

  • Монади фіксують ключові архітектурні конструкції у типах (цей код отримує доступ до обладнання, цей код - сеанс для одного користувача тощо)
  • Наприклад, монада X у xmonad, точно фіксує конструкцію для того, у якому стані видно, які компоненти системи.

Типові класи та екзистенційні типи

  • Класи типів використовуйте для надання абстракції: приховуйте реалізацію за поліморфними інтерфейсами.

Паралельність і паралелізм

  • Прокрадайтеся parдо своєї програми, щоб перемогти конкуренцію з легким, компонованим паралелізмом.

Рефактор

  • Ви можете зробити рефактор в Хаскелл багато . Ці типи гарантують, що ваші зміни масштабу будуть безпечними, якщо ви розумно використовуєте типи. Це допоможе вашій шкалі кодових баз. Переконайтеся, що ваші рефактори можуть спричинити помилки типу до завершення.

Використовуйте FFI розумно

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

Мета програмування

  • Трохи шаблону Haskell або generics може видалити котлован.

Упаковка та розповсюдження

  • Використовуйте Кабал. Не скочуйте власну систему складання. (EDIT: Насправді ви, мабуть, хочете зараз використовувати Stack для початку роботи).
  • Використовуйте Haddock для хороших документів API
  • Такі інструменти, як graphmod, можуть показувати ваші модульні структури.
  • Покладайтесь на версії бібліотек та інструментів платформи Haskell, якщо вони взагалі можливі. Це стабільна основа. (EDIT: Знову ж, ці дні, ймовірно, ви хочете використовувати 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

Дон дав вам більшість деталей, описаних вище, але ось два мої центи від того, щоб робити дійсно найсміливіші програми, такі як демони системи в Хаскеллі.

  1. Врешті-решт, ви живете в стені монадських трансформаторів. Внизу - IO. Над цим кожен головний модуль (в абстрактному сенсі, а не в сенсі модуля у файлі) відображає необхідний стан у шар у цьому стеку. Отже, якщо у вас код модуля підключення до бази даних схований у модулі, ви записуєте це до типу MonadReader Connection m => ... -> m ..., і тоді ваші функції бази даних завжди можуть отримати з'єднання без функцій інших модулі, які повинні бути обізнані про його існування. Ви можете отримати один шар, який містить з'єднання з вашою базою даних, інший - Ваша конфігурація, третій - різні семафори та мвари для вирішення паралелізму та синхронізації, інший обробляє файл Вашого журналу тощо.

  2. Спершу з’ясуйте свою помилку . Найбільша слабкість на даний момент для Haskell у великих системах - це безліч методів обробки помилок, включаючи такі хижі, як, можливо, (що неправильно, тому що ви не можете повернути будь-яку інформацію про те, що пішло не так; завжди використовуйте або замість, можливо, хіба що ви справді просто означають відсутні значення). З'ясуйте, як ви будете це робити спочатку, і встановіть адаптери з різних механізмів обробки помилок, якими користуються ваші бібліотеки та інший код, в остаточний. Це позбавить вас згори світу.

Додаток (витягнутий з коментарів; завдяки Lii & liminalisht ) -
більше дискусій про різні способи розрізання великої програми в монади в стек:

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


5
Два хороших моменти! Ця відповідь заслуговує на те, що вона є достатньо конкретною, що не є іншими. Було б цікаво прочитати більше дискусій про різні способи розрізання великої програми в монади в стек. Будь ласка, публікуйте посилання на такі статті, якщо у вас є такі!
Лій

6
@Lii Бен Колера дає чудовий практичний вступ до цієї теми, а Брайан Херт обговорює рішення проблеми введення liftмонадійних дій у вашу власну монаду. Джордж Уїлсон показує, як використовувати mtlдля написання коду, який працює з будь-якою монадою, яка реалізує необхідні типи класів, а не з вашою власною монадою. Карло Хамалайнен написав кілька коротких корисних записок, узагальнюючи розмови Джорджа.
liminalisht

Я погоджуюся з тим, що штати трансформаторів монади, як правило, є ключовими архітектурними фундаментами, але я дуже намагаюся не допустити їх ІО. Це не завжди можливо, але якщо ви подумаєте про те, що означає "а потім" у вашій монаді, ви можете виявити, що ви дійсно маєте продовження або автомат десь внизу, які потім можуть бути інтерпретовані в IO функцією "run".
Пол Джонсон

Як @PaulJohnson вже вказував, такий підхід Monad Transformer Stack здається в конфлікті з дизайнерською схемою
McBear Holden

43

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

Однак, у великому дизайні приємно постаратися використовувати систему типів, щоб переконатися, що ви можете вмістити свої частини разом правильним способом. Це може включати новітипові чи фантомні типи, щоб зробити речі, які, схоже, мають один і той же тип, різними.

Якщо справа стосується рефакторингу коду, то чистота - це велика користь, тому постарайтеся зберегти якомога більше чистого коду. Чистий код легко змінити, оскільки він не має прихованої взаємодії з іншими частинами вашої програми.


14
Я фактично виявив, що рефакторинг - це досить розчаровує, якщо типи даних потрібно змінити. Це вимагає стомлюючої модифікації сукупності конструкторів та відповідностей шаблонів. (Я погоджуюсь, що переробляти чисті функції на інші чисті функції того ж типу легко - до тих пір, поки ніхто не торкнеться типів даних)
День,

2
@Dan Ви можете звільнитися абсолютно безкоштовно за допомогою менших змін (наприклад, просто додавання поля) під час використання записів. Деякі можуть захотіти зробити записи
звичайними

5
@Dan Я маю на увазі, якщо ви змінюєте тип даних функції будь-якої мови, чи не потрібно робити те саме? Я не бачу, як така мова, як Java або C ++, допомогла б вам у цьому плані. Якщо ви говорите, що ви можете використовувати якийсь загальний інтерфейс, якому підкоряються обидва типи, тоді ви повинні були робити це з Typeclasses в Haskell.
крапка з комою

4
@semicon різниця для таких мов, як Java, полягає у наявності зрілих, добре перевірених та повністю автоматизованих інструментів для рефакторингу. Як правило, ці інструменти мають фантастичну інтеграцію редакторів і забирають величезну кількість виснажливої ​​роботи, пов’язаної з рефакторингом. Haskell надає нам систему блискучого типу, за допомогою якої можна виявити речі, які необхідно змінити в рефакторингу, але інструменти для здійснення цього рефакторингу (на даний момент) дуже обмежені, особливо порівняно з тим, що вже доступно на Java екосистема більше 10 років.
jsk

16

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

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

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

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


11
Як чудово, як Craft FP - я навчився Haskell від нього - це вступний текст для початківців програмістів , а не для проектування великих систем у Haskell.
Дон Стюарт

3
Ну, це найкраща книга, яку я знаю про розробку API та приховування деталей реалізації. З цією книгою я став кращим програмістом на C ++ - тільки тому, що навчився кращі способи організації свого коду. Що ж, ваш досвід (і відповідь), безумовно, кращий, ніж ця книга, але Ден, можливо, все ще стане початківцем в Haskell. ( where beginner=do write $ tutorials `about` Monads)
comonad

11

Зараз пишу книгу з назвою "Функціональний дизайн та архітектура". Він надає вам повний набір методів, як створити великий додаток, використовуючи чисто функціональний підхід. Він описує багато функціональних моделей та ідей, будуючи SCADA-подібний додаток "Andromeda" для управління космічними кораблями з нуля. Моя основна мова - Haskell. Книга охоплює:

  • Підходи до моделювання архітектури з використанням діаграм;
  • Аналіз вимог;
  • Вбудоване моделювання домену DSL;
  • Зовнішнє проектування та впровадження DSL;
  • Монади як підсистеми з ефектами;
  • Вільні монади як функціональні інтерфейси;
  • Стрілецькі eDSL;
  • Інверсія управління за допомогою вільних монадичних eDSL;
  • Транзакційна пам'ять програмного забезпечення;
  • Лінзи;
  • Держава, Reader, Writer, RWS, ST монади;
  • Нечистий стан: IORef, MVar, STM;
  • Багатопотокове та одночасне моделювання домену;
  • GUI;
  • Застосування основних методів та підходів, таких як UML, SOLID, GRASP;
  • Взаємодія з нечистими підсистемами.

Ви можете ознайомитися з кодом для книги тут , і «Андромеди» код проекту.

Я очікую , щоб закінчити цю книгу в кінці 2017. Поки цього не станеться, ви можете прочитати мою статтю «Дизайн і архітектура в Функціональне програмування» (Rus) тут .

ОНОВЛЕННЯ

Я поділився своєю книгою в Інтернеті (перші 5 глав). Дивіться пост на Reddit


Олександре, ви можете люб'язно оновити цю нотатку, коли ви закінчите книгу, щоб ми могли її виконати. Ура.
Макс

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 . Тобто, я сподіваюся, що ця відповідь не буде трактуватися як спроба замінити когось із інших у цій темі, лише те, що структурні вибори можуть і повинні в ідеалі бути відстороненими в бібліотеках експертами домену. Справжня складність у створенні великих систем, на мою думку, полягає в оцінці цих бібліотек за їх архітектурною «добротою» проти всіх ваших прагматичних проблем.

Як в коментарях згадується ліміналіст , модель дизайну категорій - це ще одна публікація Габріеля на цю тему, у подібному руслі.


3
Я б згадав ще одне повідомлення Габріеля Гонсалеса про модель дизайну категорії . Його основний аргумент полягає в тому, що те, що ми, функціональні програмісти, вважаємо «хорошою архітектурою» - це справді «композиційна архітектура» - це проектування програм з використанням елементів, які гарантовано складають. Оскільки закони категорій гарантують збереження ідентичності та асоціативності під композицією, композиційна архітектура досягається за допомогою абстракцій, для яких у нас є категорія - наприклад, чисті функції, монадійні дії, труби тощо
liminalisht


3

Можливо, вам доведеться зробити крок назад і подумати, як перекласти опис проблеми в першу чергу. Оскільки Haskell настільки високого рівня, він може захоплювати опис проблеми у вигляді структури даних, дії як процедури та чистої трансформації як функції. Тоді у вас є дизайн. Розробка починається, коли ви компілюєте цей код і знаходите конкретні помилки щодо відсутніх полів, відсутніх екземплярів та відсутніх монадичних трансформаторів у вашому коді, оскільки, наприклад, ви виконуєте доступ до бази даних з бібліотеки, якій потрібна певна монада стану в рамках процедури IO. І вуаля, є програма. Компілятор подає ваші ментальні ескізи та надає узгодженість дизайну та розробки.

Таким чином ви отримуєте вигоду від допомоги Haskell з самого початку, і кодування є природним. Мені було б байдуже робити щось "функціональне" чи "чисте" чи достатньо загальне, якщо те, що ви маєте на увазі, є конкретною звичайною проблемою. Я думаю, що надмірна інженерія - найнебезпечніша річ в ІТ. Речі йнакше, коли проблема полягає у створенні бібліотеки, яка абстрагує набір пов'язаних із цим проблем.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.