Дизайн и архитектура в ФП. Введение и Часть 1

  • Tutorial

Введение


В мире функционального программирования есть один большой пробел, а именно почти не освещена тема высокоуровневого дизайна больших приложений. Я решил для себя изучить этот вопрос. Есть ли существенные отличия дизайна приложений в ФП-мире от оного в мире императивном? Что такое «каноничный ФП-код»? Какие существуют идиомы разработки, есть ли смысл вообще говорить о паттернах проектирования в применении к ФП? Эти и другие важные вопросы часто вспыхивают то там, то здесь, но покамест мне не известно ни одной книги, аналогичной книге Банды Четырех. Вероятно, мои изыскания уже кто-то повторил, однако тем лучше: схожие результаты подтвердят правильность, иные — укажут на место в теории, которое необходимо доработать.

О материале

Основа данных статей — опыт по разработке игры «The Amoeba World». Используемый язык — Haskell. Игру я изначально делал в рамках конкурса «Ludum Dare» #27, но прицел был на прототипирование инфраструктуры для другой игры, более масштабной, имя которой «Big Space». «The Amoeba World» — лишь «кошка», на которой я исследую подходы в «функциональном» геймдеве. Конкурсо-ориентированная игра стала хорошей стартовой площадкой для дальнейших раздумий.

Код «The Amoeba World» вот здесь. Документация по «Big Space» — здесь. Кстати, давным-давно я уже делал нечто похожее. Это был «Haskell Quest Tutorial», в рамках написания которого я, гм, тоже разрабатывал игру. Если это можно так назвать. Но то был очень простой низкоуровневый материал для изучения языка Haskell, здесь же речь пойдет совсем об иных материях. Соответственно, предполагается, что читатель в достаточной мере знаком с ФП и языком программирования Haskell.

План

По крайней мере следующие темы должны быть освещены в дальнейшем:

  1. Часть 1. Архитектура ФП приложения. Нисходящее проектирование в ФП. Борьба со сложностью ПО.
  2. Часть 2. Восходящее проектирование в ФП. Идея — основа хорошего дизайна. Антипаттерны в Haskell.
  3. Часть 3. Свойства и законы. Сценарии. Inversion of Control в Haskell.
  4. Часть 4. FRP


Также данный цикл статей можно найти единым документом здесь.

Часть первая


Архитектура ФП приложения. Нисходящее проектирование в ФП. Борьба со сложностью ПО.

Немного теории

Процесс разработки ПО исхожен вдоль и поперек. Мы давно пользуемся стандартизированными техниками в нашей практике, — это и различные методологии, и приемы проектирования ПО, и инструменты моделирования. Очень известен, например, UML — язык объектно-ориентированного (по большей части) проектирования. Его используют для сквозной разработки, начиная с требований и заканчивая, если повезет, генерацией кода. А паттерны проектирования? Сегодня необходимо их знать, чтобы решать типовые проблемы в ОО-коде. И есть лишь один вопрос — насколько этот багаж знаний применим к функциональной парадигме? Можно ли придумать свой язык моделирования? Нужны ли паттерны для функциональных языков?

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

И первый по-настоящему инженерный этап — разработка архитектуры ПО, или, что то же самое — архитектурный дизайн ПО. Здесь мы решаем вопросы высокоуровневого строения и функционирования программы, чтобы поддержать все нефункциональные требования, собранные ранее. Выбор парадигмы программирования — это тоже архитектурный вопрос. Выбирая ФП, мы почти полностью отказываемся от UML в виду его объектной природы. Так, диаграммы классов, объектов и последовательности практически теряют смысл. В Haskell нет классов или объектов, нет инкапсулированной мутабельности, — а есть только процесс преобразования данных. Диаграмму последовательности, необходимую для описания взаимодействий этих самых объектов, можно было бы как-то использовать для цепочек функций, но штука в том, что сами цепочки будут более читабельны и понятны. Все прочие диаграммы, тем не менее, вполне применимы. В большой программе на ФП также имеются подсистемы или компоненты, а значит, в строю остаются диаграммы компонентов, пакетов и коммуникации. Диаграмма состояний — универсальна: процессы и конечные автоматы встречаются очень часто. Ее можно было бы применять даже в иных областях, не только в разработке ПО. Наконец, диаграмма вариантов использования вообще имеет мало отношения к дизайну ПО; она связывает бизнес-требования с системными требованиями на этапе анализа.

Но если внимательно присмотреться к «применимым» диаграммам, можно прийти к выводу, что они слабо помогут в высокоуровневом дизайне кода (который располагается ниже дизайна архитектуры), а то и вовсе могут навредить, подталкивая к императивному мышлению. Например, думая о компонентной архитектуре, мы вспоминаем Inversion of Control. IoC решает одну из главных проблем разработки ПО — помогает бороться со сложностью за счет выделения абстракций. А что если взять его на вооружение? И тут мы вспоминаем, что есть один из способов его реализации — Dependency Injection. Вероятно, его можно адаптировать под наши нужды, — в следующих частях мы рассмотрим несколько различных решений, как то: Existential Types, абстракции на уровне модулей, монадная абстракция, а также монадная инъекция состояния. Но на самом деле, имея первоклассные функции, мы можем вообще забыть о DI, так как это неидиоматичный подход, поскольку он приводит (явно или неявно) к состоянию в ФП-программе. А это не всегда хорошо. Хотя, справедливости ради, нужно признать, что IoC присутствует и в ФП.

А что еще, если не IoC, помогает в борьбе со сложностью при дизайне ПО? Из SICP мы знаем, что есть три важных методики:

  1. абстракции для скрытия деталей реализации («черный ящик»);
  2. публичные интерфейсы взаимодействия между независимыми системами;
  3. предметно-ориентированные языки (DSL, Domain Specific Languages).


Можно предположить, что первые две методики нам известны. Интерфейсы, абстракция, сокрытие деталей, — всё это об одном, и IoC — тому пример. Из-за доминирования ООП-языков у нас сложились прочные ассоциативные связи «абстракция» => «наследование» и «интерфейс» => «ООП-интерфейс». На самом деле это только часть правды. Мы слишком сужаем термины «интерфейс» и «абстракция» до ООП-понятий и тем самым отсекаем «неподходящие» парадигмы. Однако, идеи, лежащие в основе первых двух методик — более общие, они справедливы для всех миров, и знание других подходов никак не может быть лишним.

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

Стоит однако признать: специализированные языки сильно недооценены в реальной жизни. Бытует миф, что это сложно, трудно и дорого в поддержке. Его причина в том, что императивному программисту для создания внешнего DSL нужно выйти из своей зоны комфорта и представить себе иной синтаксис, семантику, и, может быть даже — иную парадигму. Не зная этого, он сможет придумать DSL лишь в собственных рамках, что автоматически приведет его к текущему коду, а следом — и к вопросу «Зачем тогда DSL?». Но и это еще не все. Как реализовать DSL? Доминирующие ООП-языки (за редким исключением) не предлагают элегантного решения в сравнении с функциональными; действительно, при традиционном подходе требуются значительные усилия, чтобы внешний DSL не увеличивал риски. Для снятия рисков нужно изучить иные парадигмы и подтянуть теорию; как следствие, сложность реализации внешнего DSL на привычном языке переносится на саму идею DSL. Данное заблуждение почему-то считается сильным аргументом «против»… И оно легко разбивается тем, что помимо внешнего DSL существует и внутрений (встроенный, embedded DSL, eDSL). Для внешнего DSL, невыразимого грамматикой текущего языка, нужны, как минимум, парсер и транслятор, — что приводит к добавочному обслуживающему коду. Но внутренний DSL находится в пределах грамматики текущего языка, значит, никаких парсеров и трансляторов не нужно. Однако, придумать иную организацию кода все-таки нужно; и опять мы пришли к тому, что без широкого программистского кругозора не обойтись. И это естественное требование к современному программисту. Что ж, Мартин Фаулер нам в помощь.

Что еще можно сказать об архитектуре в применении к ФП? Важный момент состоит в том, что в ФП побочные эффекты нежелательны. Но в любой большой программе работать с внешним миром надо; то есть, должны быть какие-либо механизмы контроля побочных эффектов. И они есть. Пресловутые монады Haskell — отличный вариант, но это скорее инструмент более низкого дизайна, чем элемент общей архитектуры. Так, код общения с внешним сервером может быть реализован в рамках монады IO, — что будет не сильно отличаться от императивщины. Второе решение — код может быть задекларирован на DSL. В таком DSL есть кирпичики с побочными эффектами, есть кирпичики с чистым поведением, но все они — лишь декларация, следовательно, весь код будет чист и менее подвержен ошибкам. Вероятно, он будет понятный, гибкий, комбинируемый и управляемый, по сути — конструктор. Исполнение этого кода можно возложить на конечный автомат, работающий над тонкой прослойкой из монадического IO-кода. И пожалуйста, мы получили вполне хорошее архитектурное решение, благодаря которому также снизили сложность формализации наших бизнес-процессов.

Для борьбы с побочными эффектами есть и другое архитектурное решение, известное как "реактивное программирование". В применении к функциональным языкам стоит говорить о FRP, то есть, о Functional Reactive Programming. Данная концепция математична, поэтому хорошо ложится на ФП. Суть FRP — в распространении изменений по модели данных. Каждый элемент такой модели — это значение, зависящее других значений по нужным формулам. Таким образом, «реактивная модель» — это дерево, где листовые значения могут изменяться во времени, возбуждая волну по пересчету вышестоящих значений. Источниками значений могут быть любые функции, в том числе и с побочными эффектами. Код модели при этом будет декларативным и компонуемым.

Вообще, в функциональном программировании идиоматичнее всего код, написанный в комбинаторном стиле, как конструктор. Идея проста: из маленьких однотипных кирпичиков складывается сколь угодно большая самоприменимая система. Чем лучше спроектирован конструктор, тем мощнее и выразительнее код. Обычно комбинаторный код тоже является некоторым eDSL, в разы снижающем сложность разработки. Многие библиотеки Haskell используют этот принцип, например: Parsec, Netwire, HaXml, Lenses… Идея монадических парсерных комбинаторов была настолько удачной, что Parsec стал известен за пределами Haskell. Существуют его порты на F#, Erlang, Java, другие языки. Любопытно, что Parsec реализует аж три идеи: комбинаторы, DSL и монады, и все это органично связано в едином стройном API.

Теперь нетрудно выделить еще одну очень мощную методику борьбы со сложностью (название авторское):

4. Предметно-ориентированные комбинаторы (Domain-specific combinators).

DSC — это eDSL, элементы которого являются комбинаторами. Наиболее удобные DSC получаются на функциональных языках за счет синтаксиса (функции — суть комбинаторы преобразований), но и в обычных языках это возможно. Конечно, спроектировать DSC даже труднее, чем простой DSL, — из-за чего, вероятно, данный подход совершенно неизвестен в мэйнстриме.

Немного практики

Переходя от абстрактных размышлений к практике, обратимся к проекту большой игры «Big Space». Если кратко, это реалистичная космическая 3D-леталка реальных масштабов с полностью программируемыми игровыми элементами и, вероятно, с возможностью путешествий во времени. Любопытные могут заглянуть в дизайн-документ и другие сопутствующие материалы для более детального знакомства, а нам этого описания уже достаточно, чтобы продумывать общую архитектуру игры. Но сперва — небольшое отступление.

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

Big Space: MM-01 'Концепция'

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

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

  1. Карта необходимости. Содержит настолько крупные блоки, насколько это возможно. Связи опциональны, блоки размещаются по тематике. Карта показывает, на чем вообще базируется игра. При проектировании нужно учесть самые общие требования; например, для требований «огромный реалистичный космос», «3D графика», «кроссплатформенность» будут присутствовать блоки Cloud Computing, OpenGL и SDL. Если нужен сторонний игровой движок, — его нужно обозначить именно здесь.
  2. Карта элементов. В вольной форме раскрываем предыдущую диаграмму, не заботясь о структуре. Элементами являются такие блоки: «подсистема», «концепция», «данные», «библиотека» «объект предметной области». Связи показывают самое общее представление о том, как система работает. Можно обозначить характер связей, например: «использует», «реализует» и другие. Блоки разделяются по слоям. Диаграмма должна учитывать большую часть концептуальных требований. Ничего страшного, если будут смешаны несколько уровней абстракции, появятся конкурирующие варианты, или что-то будет выглядеть не самым лучшим образом. Диаграмма лишь очерчивает поле деятельности и предлагает возможные решения, но не является конечной архитектурой программы.
  3. Карта подсистем. На данной диаграмме принимаются конкретные архитектурные решения из предложенных на втором этапе. Информация структурируется и размещается таким образом, чтобы отделить слои приложения. Указываются библиотеки и подходы к реализации. Диаграмма не опишет полностью высокоуровневый дизайн, но покажет важные зависимости между подсистемами и поможет их разделить по слоям.


Big Space: CM-01 Necessity Map


Big Space: CM-02 Elements Map


Big Space: CM-03 Subsystems Map


На последней диаграмме видно, что в игре три слоя: Views, Game Logic, Application (Mike McShaffry, Game Coding Complete). Слои «Views» и «Game Logic» отделены от основного кода собственными интерпретаторами команд и фасадами eDSL. При этом предполагается, что вся игровая логика, за исключением игрового состояния, будет реализована вне монады IO. Игровое состояние, хоть и относится и игровой логике, стоит отдельно, поскольку к нему требуется разделяемый доступ из представлений и со стороны приложения (для сетевого общения, для периодической подгрузки данных, для GPGPU и облачных вычислений). На диаграмме показано, что для работы с состоянием игры будет использована концепция Software Transactional Memory; также указаны библиотеки Netwire, SDL, Cloud Haskell и другие. Почти вся игровая логика, по замыслу, должна быть реализована с использованием нескольких внутренних и внешних языков. Конечно, предложенный вариант архитектуры — один из тысячи, и в диаграмме ничего не сказано о более низком уровне дизайна; нужны исследования и прототипы, чтобы найти узкие места или просчеты. Но в целом, архитектура выглядит стройной и аккуратной.

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

Заключение

Представленный в этой статье подход является нисходящим, то есть, направленным от наиболее общего к наиболее частному. Мы увидели, что UML слабо применим к ФП. Поэтому мы спроектировали верхнеуровневую архитектуру, придумав свои диаграммы и построив свою методологию. Мы также узнали о методах борьбы со сложностью, которые обязательно нам пригодятся при дизайне подсистем.
Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 41
    +1
    Посмотреть бы пример проектирования корпоративного ПО, а лучше и прямо бухгалтерского…
      0
      Для этого можно было бы наняться в банк Bank of America Merril Lynch. Не совсем бухгалтерия, но все же о деньгах.

      Как уж построена их система, мы можем только догадываться…
        0
        Не совсем бухгалтерия, но все же о деньгах.

        Слышал звон — не знаю где он
        Системы прайсинга и HFT настолько далеки от бухгалтерского ПО, насколько ваша игра от программирования реальной ракеты для полета на Марс. И там и там космос, но на столько разный…
    • НЛО прилетело и опубликовало эту надпись здесь
        +4
        Pattern matching, это больше про удобный доступ к составным данным, деструктурирование.
        Основная идея ФП, в отсутствии состояний и иммутабельности данных, с этой точки зрения ФП и ООП сильно отличаются.
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Вы забываете, что в ФП граница между данными и кодом почти отсутствует. Понимая данные как то, что можно паттерн-матчить, вы упускаете возможность создавать, напимер, список функций-мутаторов, который позднее может быть применен к данным, или даже модифицирован в другой список, или даже модифицирован в одну функцию, которая уже и будет работать с данными…
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                Что, простите, является тонкостью дизайна конкретного языка? Каррирование? Или отсутствие границы между кодом и данными? Это неотъемлемая часть Любого ФЯ и лямбда-исчисления. А JS, кстати, весьма «функционален». Я полагаю, разговора с вами не получится, так как вы не разбираетесь в теме.
                • НЛО прилетело и опубликовало эту надпись здесь
                    –1
                    Отсутствие между кодом и данными характерно для Лиспо-подобных языков и для языков семейства ML, к которым относится Haskell.

                    Функции как данные
                    module Main where
                    
                    import System.Random as R
                    import qualified Data.List as L
                    
                    alg1 [] = []
                    alg1 (p:xs) =  alg1 [ x | x <- xs, x < p]
                                   ++ [p]
                                   ++ alg1 [x | x <- xs, x >= p ]
                    
                    alg2 [] = []
                    alg2 (p:xs) = (alg2 lesser) ++ [p] ++ (alg2 greater)
                        where
                            lesser  = filter (< p) xs
                            greater = filter (>= p) xs
                            
                    quicksorts = [alg1, alg2]
                    
                    testData = take 10 $ R.randomRs (1, 100) (mkStdGen 10) :: [Int]
                    expectedData = L.sort testData
                    test = all (== expectedData) $ map ($ testData) quicksorts
                    
                    main = do
                        putStrLn $ "Test result: " ++ show test
                        print $ alg1 testData
                        print $ alg2 testData
                    



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

                    Чтобы разговаривать по существу, необходимо избегать подобных ошибок.
                    • НЛО прилетело и опубликовало эту надпись здесь
                        0
                        Haskell не является динамическим языком, а то, что вы просите, как раз можно сделать только на динамическом языке.

                        Однако, если вам хочется видеть абстрактное синтаксическое дерево Haskell-кода, можно воспользоваться функциями модуля «Language.Haskell.Parser» из состава «haskell-src», и скормить функции parseModule ваш код.

                        Либо можно построить quicksort на вычислительном AST, которое потом выполнить самостоятельно. А напечатать AST очень просто.

                        P.S. Это не спекуляции.
                        Дано:
                        Существует множество D (языки с динамической типизацией).
                        x принадлежит D.
                        Существует множество F (функциональные языки).
                        По определению, F != D.

                        Ваше рассуждение:
                        «Если для вас весьма функционален JS» -> Предположим, x имеет свойство f (весьма функционален).
                        «то любой язык с динамической типизацией» -> Следовательно, для любого l из D: l имеет свойство f.
                        «В действительности они не являются функциональными» -> Противоречие. Все l из D не принадлежат F.

                        Однако, последнее заключение не является противоречием первому, так как подобного условия не было. Следовательно, последнее заключение не относится к цепочке рассуждений, а значит, не является корректным доказательством.
                        • НЛО прилетело и опубликовало эту надпись здесь
                            –2
                            О ссылке: да, лисповая реализация именно такая, а у Haskell иная реализация, и обе имеют право на существование. Но эти две реализации в равной мере иллюстрируют концепцию «code == data», и нет оснований говорить, что существование одной реализации автоматически делает другую ущербной.

                            Спорить больше не буду.

                            И да, я сегодня несговорчив, приношу извинения.
            –2
            Боюсь, что подобная трактовка паттерн-матчинга слишком притянута за уши. И к наследованию не имеет отношения. ФП от ООП действительно отличается. Если не принять это как аксиому, у вас будет получаться снова то же ООП на функциональном языке, только выглядеть и работать будет ужасно. Знаете, в чем еще отличие? В ФП позволительно и даже важно работать с, гм, «недоопределенными» функциями, когда n-Арной функции дадены n-k аргументов. Это ведет к гораздо большей гибкости, нежели любой ООП-механизм. Код получается совершенно иной, с иным дизайном, с другой основопей идеей.
            А законыв ФП, действительно, есть, и они о другом.
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                Нет. Цель проектирования — получить результат за приемлемые потраченные ресурсы с приемлемой сложностью поддержки и доработки. А «high cohesion low coupling» — лишь важный способ этого добиться.

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

                Если бы списки и кортежи решили все мои проблемы, этой статьи бы не было.

                А названные принципы нигде и не отвергаются, читайте внимательнее. Вот только их недостаточно для проектирования. Говоря вашими словами, эти принципы не решают автоматически проблемы проектирования.
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    Ваша подозрительность не является аргументом.
                    Отсутствие этих слов в статье не приравнивает статью автоматически к разряду плохих.
                    Классические паттерны упоминались, но не назывались поименно.

                    Хорошая статья о паттернах, действительно, хорошая. Вот только появилась она буквально вчера.

                    У моей статьи будет продолжение. Но я не предполагаю переносить все или часть классиеских паттернов в мир, где они по большей части не нужны, и к тому же, это уже обсуждалось.
                      0
                      Если бы вы сделали что-то подобное на Haskell, то я бы был первым кто плюсанул вам в карму.

                      Есть старая, но хорошая и краткая статья:
                      blog.ezyang.com/2010/05/design-patterns-in-haskel/
                      • НЛО прилетело и опубликовало эту надпись здесь
                          0
                          Я, кажется, понял, откуда у вас такое желание натянуть ООП на Хаскель и другие ФЯ. Ваш опыт родом из Erlang, который сформировал у вас неверное представление об ФП, и вы пытаетесь всё подогнать под это представление. Это неверный подход. ФП имеет с ООП мало общего, и не важно, что в Erlang эти понятия имеют свою реализацию. Вот, например, Scala: и функциональный язык, и ООП-язык; Haskell — чистый функциональный язык; но что Erlang? Ответ вы найдете в статье, которую я нахожу очень точной и правдивой.
                            +1
                            На счёт Синглтонов — в Хаскеле это совершенно другое.
                            Из той ссылки, что вы дали:
                            Singleton types are a technique for “faking” dependent types in non-dependent languages, such as Haskell. ... A singleton type is a type with exactly one value.
                            Это типы с единственным значением, а не единственный экземпляр типа.

                            Согласен, несколько наивная статья, что я дал.
                            • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                Я думаю паттерны тем и хороши, что они не привязаны к языку.

                                Думаю, правильнее сказать, разные паттерны разной силой привязаны к парадигмам программирования.
                                Тот же MCV никак не связан с языками и даже парадигмами.

                                Если мы говорим о Синглтоне, то этот паттерн имеет место в языках с общим пространством данных, где мы хотим получить единственное значение переменной.
                                Применяя это на Хаскель: возможность иметь Одиночку возможна лишь в монадах состояний State, таких как IO, ST, IORef, STRef. Вне контекста состояний, ни одна функция не делит «общее пространство данных», а значит, шаблон Одиночка бессмысленен вне состояний в Хаскеле.
                  0
                  Сорри, промахнулся уровнем.
                  0
                  Ответил вам чуьть ниже (промахнулся).
                  +1
                  Будьте добры, по обмену опытом.
                  1. Какую конкретно реализацию FRP используете и как она в деле?
                  2. Я хочу простенький сервер для мультиплейер. Насколько трудоемко набросать на Haskell набело, вместо допиливания warp? Скажем несложный UDP сервер? Ну да, я читал в RWH, но то в книжке. А в деле?
                  3. Известна доктрина максимально разносить, еще на стадии дизайна, чистую и побочно-эффективную компоненты с целью изжить последнюю. Ваше мнение? Например Michael Snoyman, на мой взгляд, такой подход не разделяет. Yesod просто весь соткан из монад и трансформеров.
                  4. Persistent? STM это хорошо, но недостаточно. Вы бы рекомендовали природный Acid-State, или кондуктор к чему нибудь более SQL классичному?
                  Спасибо.
                    0
                    1. Использую Netwire в качестве FRP- основы нечистой части программы. Мне нравятся два аргумента в пользу этой библиотеки: стрелочный интерфейс как вариант, и нацеленность на игры. Из-за последнего, например, там есть работа с таймером. В простых примерах библиотека ведет себя хорошо, пользоваться удобно. Правда, другие я не испытывал, только читал о них.
                    2. Точно не скажу. Если простенький, без претензий на красивости, то, думаю, пару дней, — при условии, что вы знаете, как работают UDP серверы. У меня ушло бы неделя или около того, но посылающий и принимающий сообщения сервер на коленке можно сделать быстро. Я как-то делал сервер визуализации Хаскелля, там ничего сверъестественного не было в сетевом коде… Разве что я использовал MVar для разделяемого доступа.
                    3. И да, и нет. Если совсем не разделять, будет обычный императивный код. Но если воспользоваться тем же ФРП, то это будет очень даже хороший код.
                    4. Шут его знает… Действительно, синхронизироваться с персистентным хранилищем надо, иначе можно многое потерять при сбое. Я бы выбрал что-нибудь нереляционное, no-sql-ное, но в этом я не спец вообще. Видится, что для игры SQL неудобен. Думаю, надо читать лучшие практики и писать прототип.
                      0
                      Плюс к 3-му пункту. Монады не являются с необходимостью побочными эффектами. Это механизм абстракции, которым можно обрабатывать и побочные эффекты тоже. Например, монады Maybe и Either не являются «монадами для побочных эффектов». Но они являются структурами поведения со свойством обработки «пустых» данных (Nothing) и обработки данных с возможностью fail-сценария (Left). Монады != побочные эффекты. Побочные эффекты, в основном, сосредоточены в IO.
                        +1
                        State, MVar, STVar пожалуй-ка тоже не больно чистоплотны.
                          +1
                          State можно считать чистой монадой, так как состояние лишь моделируется. Но это уже философия.
                      +1
                      … но покамест мне не известно ни одной книги, аналогичной книге Банды Четырех

                      Знаю пару книжек:
                      functional-programming-patterns-in-scala-and-clojure — GOF через скалу/clojure + функциональные паттерны.
                      clojure-for-domain-specific-languages DSL на кложуре

                        0
                        О, спасибо! Нужно пилить то же самое для Хаскелля.
                        +1
                        Я пришел к похожим выводам на более низком уровне проектирования.
                        leventov.livejournal.com/14662.html
                          0
                          Спасибо, прочитаю.
                          +1
                          реквестую тэг «математично». (• ◡•)
                          +1
                            0
                            Благодарю, судя по содержанию, книга очень достойная. Но в ней, похоже, не затронуты вопросы высокоуровневого дизайна, — только описаны «болты и гайки». Могу ошибаться, конечно.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое