Comments 267

А каким образом Хаскелю удаётся транслировать математические абстракции в машинный код? Например, я не понимаю, как учитывается переполнение стека при рекурсии. Любое применение функции — это Just x | bottom. И я не понимаю, как с этим жить.

Гуглить по spineless tagless G-machine.

Стека в привычном смысле, кстати, нет.

И учитывать bottom в привычном смысле не надо, потому что вы не можете осмысленно проверить, вернула функция bottom или нет.
От переполнения стека спасает ленивость. То есть функция не вычисляет все значение на своем стеке, а возвращает его недовычисленным. Рекурсивные вызовы для довычисления частей результата происходят уже после в выхода из функции и удаления фрейма стека.
Фактически, это встроенный в язык паттерн «трамплин».
Конечно, компилятор делает и оптимизации хвостовых вызовов.
Как будто если книжку по паттернам ООП почитать, все будет сильно лучше.
Для понимания собственно ООП вполне хватает здравого смысла и среднеобывательского словарного запаса.

Вот паттерны − это специфическая вещь. Начиная с того, что книжка Банды Четырёх − это продукт борьбы (как минимум троих) авторов с особенностями и недостатками языка Java. Соответственно, понятна она только в этом контексте.
Чем больше я живу и работаю с другими людьми, тем больше понимаю, что ООП не понимают ни они, ни, тем более, я. Или у всех какое-то свое понимание.

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

То ли дело матан! Там все просто и понятно.
Дык натянуть-то можно. Плюсы, я б сказал, ещё ничего. Некоторые на пайтоне синглтоны пишут. На скотском серьёзе, да.

Видимо в функциональных языках нет паттернов просто потому, что они не нужны для сниппетов на конференцию по функторам и монадам.


Какие преимущества даёт абстрактная алгебра для реального программирования всё ещё никто не продемонстрировал.

Видимо в функциональных языках нет паттернов просто потому, что они не нужны для сниппетов на конференцию по функторам и монадам.

Они есть, просто называются, например, «полугруппа» или «профунктор».


Какие преимущества даёт абстрактная алгебра для реального программирования всё ещё никто не продемонстрировал.

Проще рассуждать о коде и проще писать код. Но для того, чтобы увидеть преимущества, нужно именно рассуждать о коде и писать код в терминах абстрактной алгебры (хотя мне теоретикотиповая часть больше импонирует, если честно, и я её нахожу более полезной). А для этого приходится знать абстрактную алгебру.

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

Ага, особенно синглтон, фабрика, визитор или мультиметод. Или тот же адаптер с фасадом. Очень высокий уровень.


Переименовывают не в монады. Не монадами едиными, тащем.


И, кстати, да, помогает. Потому что заворачивание в монаду ограничивает, какая функциональность вам доступна в контексте.

Ну и где помогает-то, есть какие-то примеры из жизни? А то что-то все туториалы по монадам начинаются и заканчиваются матаном. Наверное полезно тем, у кого стоит задача программировать что-то из матана, да.

Ну и где помогает-то, есть какие-то примеры из жизни?

Да, любой eDSL поверх free/operational monad.


Или взять тот же ST, гарантирующий детерминированность stateful-вычислений.


Или взять тот же lvish для детерминированного параллелизма.


А то что-то все туториалы по монадам начинаются и заканчиваются матаном. Наверное полезно тем, у кого стоит задача программировать что-то из матана, да.

Осталось по туториалам понять, зачем нужен фасад или визитор.

eDSL и детерминированные stateful вычисления не пишут без монад? ИМХО там гораздо проще допустить какую-нибудь логическую ошибку с монадами из-за лишнего синтаксического шума.


Пример из жизни по паттернам ООП. Есть что-то подобное про монады?

eDSL и детерминированные stateful вычисления не пишут без монад?

Некоторые eDSL пишут, потому что там монады не нужны. Некоторые с монадами удобнее.


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


ИМХО там гораздо проще допустить какую-нибудь логическую ошибку с монадами из-за лишнего синтаксического шума.

А в чём шум? Пишете в do-нотации, и нет никакого шума.


Пример из жизни по паттернам ООП.

Всё равно непонятно, зачем там фабрика. Почему нельзя было это инкапсулировать в обычную функцию (или пару функций)?

А в чем недетерминированность параллелизма с мьютексами и семафорами, например?


Инкапсулировать в функцию — это имеется ввиду возвращать функцию-исполнитель из функции-конструктора, возвращающей ту или иную функцию в зависимости от контекста? Да, неплохая идея! А если нам нужно несколько функций, то можно их вернуть в виде списка «ключ-значение». И функцию-конструктор, чтобы было понятно, назвать WidgetConstructor или WidgetFactory… Постойте-ка, да мы же изобрели ООП!

А в чем недетерминированность параллелизма с мьютексами и семафорами, например?

А кто гарантирует, что результат работы программы не будет зависеть от порядка захвата и освобождения мьютексов и семафоров?


Я уже не говорю об отсутствии гарантий, ээ, отсутствия дедлоков и рейсов.


Инкапсулировать в функцию — это имеется ввиду возвращать функцию-исполнитель из функции-конструктора, возвращающей ту или иную функцию в зависимости от контекста?

Нет, зачем. Просто вместо new Widget или new Button вы пишете makeWidget() и makeButton(), вынося эту всю повторяющуюся #ifdef-ерунду в отдельную функцию.


Зачем это называть фабрикой? Меня мой первый учебник по программированию учил выносить дублирующиеся куски кода в функцию, и фабрикой это не называлось.

Что подразумевается под «гарантией»? Формальная верификация?


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

Что подразумевается под «гарантией»? Формальная верификация?

Не совсем, но оно ближе к формальной верификации, чем к тестам. Собственно, если взять тот же lvish: «We would like to tell you that if you're programming with Safe Haskell (-XSafe), that this library provides a formal guarantee that anything executed with runPar is guaranteed-deterministic.»


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

Как и раньше, только везде вместо new Widget вы заменяете вызов на makeWidget(), и далее по тексту.

Зачем это называть фабрикой?

Можете предложить более удачное название?

Любое другое незанятое, потому что на википедии, например, написано, что фабричный метод solves problems like:
How can an object be created so that subclasses can redefine which class to instantiate?
How can a class defer instantiation to subclasses?


Субклассы тут, кажется, ничего не меняют.

Любое другое незанятое

Например, какое, и чем оно будет лучше-то?

Ну ещё раз. То, что предложено в туториале по ссылке выше, не похоже на фабрику в том смысле, в котором она определена на википедии. Можно переименовать статью на википедии, можно переименовать такую функцию, но не надо называть разные вещи в одной области одинаковыми именами.
Ну ещё раз. То, что предложено в туториале по ссылке выше, не похоже на фабрику в том смысле, в котором она определена на википедии.

В каком смысле не похоже? Там же одно и то же.

Фабрика — это паттерн для, говоря простыми словами, создания объектов абстрактных классов. Напрямую нельзя, потому делается фабрика, которая тем или иным способом знает какой конкретный класс подставить.

Тех кто устраивает фабрику ради «вынесения кода в абстрактный класс» (или отдельную функцию) надо бить линейкой по голове, пока не протрезвеют.

Фабричный метод — это не фабрика, это уже паттерн «Строитель» (Builder) — декомпозиция сложного конструктора.

Не надо путать. Это разные вещи.

Кажется, в этом треде начинаются разночтения о том, что такое фабрика, что иронично.


Давайте пройдёмся ещё раз по этой ветке. Приводится пример некоей абстрактной фабрики с утверждением, что это жизненный пример. Мне непонятно, почему там нужна именно фабрика, почему нельзя это просто вынести в отдельную функцию в этом коде (это же жизненный пример), и что именно данный пример иллюстрирует. Ну и кроме того возникли вопросы о том, что такое фабрика, фабричный метод и так далее.


Предлагаемой строгостью определений и приближенностью к жизни как-то не очень пахнет.

Жизненный пример:
Есть абстрактный класс компонента и тьма тьмущая его наследников.
На вход поступает некий конфиг, допустим JSON, по данным этого JSONа однозначным образом создаётся какой-то компонент.
Вот функция, которая реализует логику выбора подходящего класса наследника компонента, создаёт и возвращает экземпляр оного класса — получила гордое название ComponentFactory )))

Вот я понять не могу, почему стоит написать «Есть абстрактный класс компонента и тьма тьмущая его наследников» — и всё, все ставят лойс и пишут «жыза))». А стоит написать «есть желание написать чистый eDSL с operational monad для типобезопасного выражения предметной области с последующей возможностью анализа, верификации и тестового прогона алгоритма» — так сразу ой всё.


Системы с абстрактными классами (интерфейс для плагинов) я писал. И пара сотен наследников там была. В рантайме выбирать ничего из этой пары сотен не надо было.

В рантайме выбирать ничего из этой пары сотен не надо было.

Вам — не надо было, а нам — надо.
С сервера приходит инструкция (ага, JSONом) «Нарисуй-ка компонент с таким именем и такими параметрами». Клиент, соответственно, проверяет, «какой-там у меня класс лежит в мапе по этому имени?..»

А зачем тут какая-то фабрика?


-- уау экзистенциальные типы вместо базового класса
data SomeComponent where
  SomeComponent :: Component a => a -> SomeComponent

-- просто создание конкретных компонентов
mkFooComp :: Params -> FooComponent
mkBarComp :: Params -> BarComponent

-- создание какого-то компонента по имени
mkComponent :: String -> Params -> Maybe SomeComponent
mkComponent tyName params = ($ params) <$> lookup tyName thePseudoMap
  where
    thePseudoMap = [ ("fooComp", SomeComponent . mkFooComp)
                   , ("barComp", SomeComponent . mkBarComp)
                   ]

Можно даже гарантировать, что Params соответствует нужному типу компонента, если у них у всех разные Params, но это на хаскеле сделать чуть сложнее (а на идрисе — чуть проще).


Я, кстати, сначала написал mkFoo :: String -> SomeComponent, но типы заставили меня исправить это.

А зачем тут какая-то фабрика?

В вашем примере — не нужна, но если вам надо будет написать какую-то ф-ю, которая сама не знает, какой объект ей надо создать (Foo или Bar), а решается это на call site — то у вас появится фабрика.

Если я правильно понял код, то там именно это и происходит: функция на основании параметров решает какого класса компонент создавать.
Это способ реализовать фабрику на функциональном языке.
функция на основании параметров решает какого класса компонент создавать.

Ну вот если она не решает, а решает внешняя ф-я — то это и будет фабрика. Сама ф-я не должна быть в курсе, какой объект она создает.

Если решает внешняя функция, то в каком виде это решение передаётся во внутреннюю?
Если решает внешняя функция, то в каком виде это решение передаётся во внутреннюю?

Вот в виде конкретной фабрики внешняя функция во внутреннюю эту информацию и передает.

Ровно за тем же самым, за чем нужна любая high-rank полиморфная ф-я.


Допустим, у вас есть ф-я f, она вызывает ф-ю g. Ф-я g во время своей работы должна сконструировать некоторый объект с интерфейсом interface (я подразумеваю сейчас интерфейс в бщем смысле, вне зависимости от деталей реализации — оопшные, тайпклассы, какие-то свои велосипеды — тут не важно). С-но, ф-я g знает интерфейс, но не знает какой конкретно это будет тип.
Мы могли бы, конечно, сконструировать просто конкретное значение в f и напрямую закинуть в g, но:


  1. возможно нам надо в g создать несколько объектов соответствующего типа и что-то с ними поделать. тогда придется внутри f все эти объекты создать и потом в g закинуть
  2. возможно при создании объектов нужны будут какие-то дополнительные параметры, за проброс которых в конструктор, опять же, будет отвечать в данном случае f

Мы, допустим, не хотим, чтобы эта логика была в f, мы как раз хотим, чтобы она была в g. Тогда вместо того, чтобы совать в g конкретное значение, мы суем туда сам конструктор.


Вот такой конструктор, который засовывается в некоторую g, и при этом g — знает, что он конструирует объекты определенного интерфейса, но не знает, какие конкретно — это и есть фабрика.

тогда придется внутри f все эти объекты создать и потом в g закинуть

Тогда непонятно, почему f вызывает g, а не наоборот. Выглядит как какой-то кривой дизайн.


Мы, допустим, не хотим, чтобы эта логика была в f, мы как раз хотим, чтобы она была в g. Тогда вместо того, чтобы совать в g конкретное значение, мы суем туда сам конструктор.

Почему? Я тут уже совсем потерялся в мотивации, можно для тупых вроде меня пример?

Тогда непонятно, почему f вызывает g, а не наоборот. Выглядит как какой-то кривой дизайн.

Потому что f нужен результат g, а не наоборот :)


Почему?

Ну по той же причине, по которой вы передаете фунарг в map, например. Хотите разделить логику. Можете, конечно, передавать map в ее аргумент, а не наоборот. Но зачем? :)

Потому что f нужен результат g, а не наоборот :)

То есть, она и объекты создаёт, и результат ей нужен? Выглядит как-то не очень (тут нарушение SRP где-то рядом, кстати, забавно его применять к ФП-дискуссиям).


Ну по той же причине, по которой вы передаете фунарг в map, например. Хотите разделить логику.

Так с map как раз отличный пример фунаргов. Но вот примера такой наркоманской фабрики всё нет, увы.

То есть, она и объекты создаёт, и результат ей нужен?

Нет, f объектов не создает. f сует в g фабрику, а g — уже создает. f возможно вообще не знает как именно надо объекты создавать (какие аргументы совать в конструктор), но знает какой именно тип объектов надо создавать. А g — знает как создавать, но не знает конкретный тип (только интерфейс).


Так с map как раз отличный пример фунаргов. Но вот примера такой наркоманской фабрики всё нет, увы.

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

f сует в g фабрику, а g — уже создает. f возможно вообще не знает как именно надо объекты создавать (какие аргументы совать в конструктор), но знает какой именно тип объектов надо создавать. А g — знает как создавать, но не знает конкретный тип (только интерфейс).

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


Вы в f берете коробку конкретных объектов (например, карандашей), суете в g и говорите, что нарисовать. Вот эта коробка — и есть фабрика, т.к. g может при помощи нее получить объект, который рисует нужным цветом (при этом f вообще может ничего не знать о рисовании и цветах). И потом возвращает вам в f рисунок, написанный нужным штрихом. Потом в f делаете с этим рисунком что вам угодно.

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

Мне сложно представить себе практическую нужность такой архитектуры.

Ну обычное разделение ответственности. Все так пишут, в том числе и на хаскеле постоянно :)


Ну так у меня там неявно третья функция, которая и есть фабрика

Это вы про какую неявную функцию?


Я всё ещё не понимаю обсуждаемой проблемы.

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

Ну обычное разделение ответственности. Все так пишут, в том числе и на хаскеле постоянно :)

Так в том и дело, что оно там необычное и неокончательное.


Это вы про какую неявную функцию?

Та коробка, которую я беру в f и сую в g.

Так в том и дело, что оно там необычное и неокончательное.

Что неокончательное, в каком смысле?


Та коробка, которую я беру в f и сую в g.

Так а где она, хоть и неявная?

Что неокончательное, в каком смысле?

В смысле, что функция отвечает и за то, что выбирает, что рисовать, и за создание коробки карандашей отвечает.


Так а где она, хоть и неявная?

Ну вот эта вот коробка отсюда: «Вы в f берете коробку конкретных объектов (например, карандашей), суете в g и говорите, что нарисовать. Вот эта коробка — и есть фабрика»

В смысле, что функция отвечает и за то, что выбирает, что рисовать, и за создание коробки карандашей отвечает.

Ну тут все правильно, ведь "чем рисовать" это часть "что".


Ну вот эта вот коробка отсюда:

Она тут вполне явная, почему неявная-то?

Мне сложно представить себе практическую нужность такой архитектуры.


Я вспомнил ещё один пример из жизни.
Есть у меня компонент, который реализует визуальный интерфейс для разработки фильта (ну, переключалки для логических операций и и кнопки составления дерева из них, окошки для ввода констант, выбора переменных и т.д. и т.п.)
Он в ходе своей работы по мере того, как юзер натыкивает мышкой фильтр, создаёт вспомогательные компоненты (представляющие элементарные фильтры, мапа «имя: класс» которых пришла с сервера) — для этого у него есть фабрика компонентов (она создана заранее и была передана в конструктор компонента-фильтра).

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

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


А в этой ветке мы вообще уже ушли в детали реализации этого паттерна на ФП.

Я понимаю, что Вы не спорите, я просто привёл реальные примеры. Первый пример — та фабрика, вокруг которой у нас весть проект построен.
Второй пример — случай, когда объект может содержать в своём поле разные фабрики, чтобы выполнять над ними (и создаваемыми ими объектами) одни и те же действия.

Я долго думал и, кажется понял. Фабрика это функция, возвращающая копроизведение

Хм, в моей интуиции она скорее выбирает конкретный морфизм в это копроизведение.

Конкретный морфизм откуда? И на основании чего происходит выбор? Учитывая каррирование, частичное применение, представимость любого хом множества в категории hask, изоморфизм между a и ()->a это всё одно и то же.

А что у вас за копроизведение?


У меня категория, где объекты — типы, а категория — почти обычная Type^{T_i}, где { T_i } — коллекция реализующих нужный интерфейс типов, с единственным отличием от совсем обычной slice в том, что морфизмы ограничены теми функциями, которые оперируют только общим интерфейсом.

В данном случае копроизведение — дизъюнктное объединение.

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

Нет, наружу торчит только mkComponent как функция и SomeComponent как тип данных (ну и Component как тайпкласс). mkFooComp, mkBarComp там только для того, чтобы представлять, о чём речь, и это деталь реализации фабрики — у них не зря даже реализаций нет.

Функция или объект с единственным методом — это нюансы реализации.

Как-то однажды знаменитый учитель Кх Ан вышел на прогулку с учеником Антоном. Надеясь разговорить учителя, Антон спросил: "Учитель, слыхал я, что объекты — очень хорошая штука — правда ли это?" Кх Ан посмотрел на ученика с жалостью в глазах и ответил: "Глупый ученик! Объекты — всего лишь замыкания для бедных."


Пристыженный Антон простился с учителем и вернулся в свою комнату, горя желанием как можно скорее изучить замыкания. Он внимательно прочитал все статьи из серии "Lambda: The Ultimate", и родственные им статьи, и написал небольшой интерпретатор Scheme с объектно-ориентированной системой, основанной на замыканиях. Он многому научился, и с нетерпением ждал случая сообщить учителю о своих успехах.


Во время следующей прогулки с Кх Аном, Антон, пытаясь произвести хорошее впечатление, сказал: "Учитель, я прилежно изучил этот вопрос, и понимаю теперь, что объекты — воистину замыкания для бедных." Кх Ан в ответ ударил Антона палкой и воскликнул: "Когда же ты чему-то научишься? Замыкания — это объекты для бедных!" В эту секунду Антон обрел просветление.


Взято https://ru-lambda.livejournal.com/27669.html

Какие преимущества даёт абстрактная алгебра для реального программирования всё ещё никто не продемонстрировал.

Ну я уже где-то приводил неплохой пример полезного математического рассуждения.
Вот есть в js генераторы. Синтаксис генераторов изоморфен синтаксису do-нотации для call/cc монады без reentrance. При этом мы знаем, что любая монада имеет каноническое выражение через call/cc — значит, мы сразу знаем, что можно использовать синтаксис генераторов для любой монады, которая применяет фунарг внутри fmap'а не более раза. Например — та же async. А вот с list — канонически не выйдет.


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

Чем больше я живу и работаю с другими людьми, тем больше понимаю, что ООП не понимают ни они, ни, тем более, я. Или у всех какое-то свое понимание.

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

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


На монады не надо смотреть как паттерны, на монады (и прочие тайпклассы) надо смотреть как на доказательство (Карри-Говард, ага) утверждения «мой тип умеет то-то и то-то».


Ну или мне так удобнее.

На монады не надо смотреть как паттерны, на монады (и прочие тайпклассы) надо смотреть как на доказательство (Карри-Говард, ага) утверждения «мой тип умеет то-то и то-то».

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


надо смотреть как на доказательство (Карри-Говард, ага) утверждения «мой тип умеет то-то и то-то»

Это паттерны доказано "умеют то-то и то-то" (с-но, в определении паттерна и написано, что он умеет). А вот если я скажу: "Х — монада", то что умеет Х? У меня нет об этом никакой информации. Вообще говоря — ничего не умеет.

А если стоит задача доказать? В чем же тут неконструктивность? Доказательство один из способов верификации решения. Или тестирование и охрана ассертами это тоже неконструктивно?
Кто-то пишет библиотеки и возится с О-большими, корректностью, верификацией и прочим "матаном", а кто-то их комбинирует, не задумываясь, а считая, что "это уже сделано".

А если стоит задача доказать?

Если стоит, то, конечно, вопросов нет. Но в 99% случаев ее не стоит.

А вот если я скажу: "Х — монада", то что умеет Х? У меня нет об этом никакой информации. Вообще говоря — ничего не умеет.

А если я скажу, что X — визитор, то что? Что он умеет? Ну куда-то там заходить и визитировать, собственно. Мне это говорит не сильно больше, чем что X умеет bind и pure с соответствующими законами.

Боролись, боролись авторы с Java и, видимо, проиграли. Пришлось примеры к книге писать на C++ и Smalltalk.
Джонсон грокал смолток, остальные трое − джависты. Влиссидис имел бэкграунд в C++, но, наверное, больше работал с джавой на момент написания книги.

Я очень часто вижу, как программисты пытаются обособить себя от своего основного ЯП. С одной стороны, через академизацию своего опыта, мол, мы не программисты, мы computer scientists, мы не изучаем языки, а создаём их. С другой стороны, через ремесленничество − best tool for the job и прочие максимы. Но на практике я ни разу не наблюдал, чтобы кому-то удалось превзойти то форматирующее влияние, которое оказывает на способ мышления его основной инструмент. Одни мыслят категориями статически типизированных языков в динамически типизированных, другие − наоборот. Третьи плодят миллионы классов, ну и т. д.
Design Patterns вышла в 1994 году, а первый релиз Java в 1995 году. Беглый взгляд в википедию не обнаружил связь всех четырех с Sun Microsystems. Поэтому ваше заявление про борьбу с Java выглядит спорным. Зачем с ней было бороться, если в 1994 это был внутренний проект компании, где они не работали?
Вы меня уделали. У меня в голове Банда четырёх была почему-то связана с ростом популярности Java, и я до сих пор не удосужился сличить даты. Позор мне.

Гамма работал в IBM, занимался VisualAge, Eclipse и JDT. Видимо, оттуда мои ассоциации с Java. Хотя было это, конечно, добрый десяток лет спустя.
>Поскольку любой проект в конечном итоге предстоит
реализовывать, в состав паттерна включается пример кода на языке C++ (иногда
на Smalltalk), иллюстрирующего реализацию
Это цитата из перевода 2001 года. И да, слова Java в тексте книги нет вообще.
Влиссидис имел бэкграунд в C++, но, наверное, больше работал с джавой на момент написания книги.


Книга вышла раньше чем Java.
В книге приводятся листинги на примере языков Smalltalk и C++, разве нет? Или у Вас экзотическое/новое издание?

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

Для справки — книга банды четырех вышла в 1994 году. Java — где-то в 1996.
Кстати на языке ООП реализовать монады можно проще и яснее.

Как реализовать MonadReader? Как потом это обобщить до стека монад, чтобы можно было совместить MonadReader и MonadWriter?

Во-первых, можно было бы начать обсуждать связь интерфейсов с тайпклассами (и заметить, что тайпклассы — более общий подход).


Во-вторых, ну так как выражается даже обычная монада-то? Только без дженериков плз, в ООП про дженерики ничего нет.

А с динамической типизацией тоже нельзя? В ООП всё про динамическую типизацию есть.

А с динамической типизацией теряется весь смысл статических гарантий управления эффектами.

Я думал монады в первую очередь предоставляют преимущества в части компоновки.

Да, монады — композабельный способ управления эффектами, но акцент именно на управлении эффектами.


При этом эффекты — это довольно широкое понятие.


Отсутствие значения — это эффект (монада Maybe).
Множество значений (или недетерминированные вычисления) — это эффект (монада List).
Окружение с конфигурационными опциями — эффект (Reader).
Логгирование — эффект (Writer).
Состояние — ну, само собой (State).
Параллелизм — эффект (Par всякие там).
Контекстно-зависимый парсинг — эффект (поэтому парсеры монадические).

На такие вопросы сложно отвечать в общем.

Когда я пишу код на хаскеле (на 3-10 тыщ строк тоже, что развернётся в 30-100 тыщ строк плюсов), мне все эти паттерны особо не нужны, как-то по-другому удаётся делать декомпозицию.

Веб-серверы писал, компиляторы писал, парсеры логов писал, статические анализаторы писал, много всякого писал.


Паттерны, получается, не нужны?

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

В теории — всё поддаётся, но это не значит, что это всегда удобно.

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

На плюсах писал, я тогда про х-ль ещё не знал :(


Представляю, как их писать на ФП. FRP там, фигак-фигак. Да и чем это принципиально отличается от веб-серверов? Точно такое «из разряда последовательной и параллельной обработки данных, то есть то, что, вообще говоря, можно представить блок-схемой с входными и выходными данными-сигналами».

Вот с-но от натягивания на глобус совиной жопы в виде "отсутствие значения — это эффект" или "множество значений — это эффект", люди потом монады и "не понимают" :)


Нет, отсутствие значений или множество значений — это не эффекты. Если только не приложить к сове очень значительное усилие :)

Чем не эффекты? Эффекты. Хотя более по-колмогоровски короткого определения, что такое эффект, чем «это то, что выражается монадой», у меня нет, гм.

Чем не эффекты?

Ничем не эффекты. Хотя, конечно, вы всегда можете растянуть сову до такого размера, что сказать "Х — эффект" будет то же самое, что ничего не сказать. Тогда тот факт, что вы называете списки или ИО эффектами, становится бессодержателен. То есть назвать что-то эффектом тогда — то же самое, что никак не назвать.

Бедная сова. Дело в том, что кто-то определился с определениями, в частности, понятия "эффект" и работает с ним, понимая что и зачем делает. А кто-то спорит о словах и применяет "совиную" аргументацию.
Замените "эффект" на "контекст", может быть, это поможет понять, зачем это нужно. Но главное — если вам монады не нужны, не ссорьтесь, а просто не используйте их.

Дело в том, что кто-то определился с определениями

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


Замените "эффект" на "контекст"

И лучше совсем не станет. Списки — это, очевидно, не контекст.


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

>Списки — это, очевидно, не контекст.
И чем это они не контекст? Выполнить некоторую функцию над всеми элементами списка (в контексте одного из них) — это одно из типовых применений монад.
А с динамической типизацией теряется весь смысл статических гарантий управления эффектами.

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

В случае монад статически гарантии часто даются самой формой интерфейса.

Так напишите без дженериков параметрического полиморфизма интерфейс монад-то.


И не важно в данном случае, какая типизация, просто невозможно применением данных ф-й напортачить, by design.

Типизация позволяет эту форму интерфейса выразить в виде кода.

Монады в функциональном программировании используются для выполнения лишь одной роли — эмуляция эффектов характерных для императивного программирования. Непосредственно эмуляция выполняется в функции 'bind' (оператор >>= в Хаскеле). Никаких других мистических качеств у монад нет.
Если мы уже находимся в императивном окружении (ООП в общем его предполагает), то зачем там нужно как-то имитировать/реализовывать монады?
Все что нужно — инкапсуляция, возможности для сайдэффектов в любой ф-ции с параметрами по ссылке, возможность создания последовательности вычислений (с их прерыванием или протаскиванием состояния или генерирования исключения и т.п.) — все что угодно, для чего используются монады в ФП можно реализовать в ООП на императивном языке идиоматично для него вообще не прибегая даже к такому понятию.
Или я не прав? :)

Дело не в эмуляции, и дело не только в эффектах, характерных для императивщины.


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

То, что вы перечислили в качестве эффектов — это не более чем элементы программной функциональности. И в качестве таких «эффектов» можно указать много чего — в зависимости от того, что требуется программисту. И в этом смысле это конечно очень широкое понятие.
Суть в том, что в императивных языках привнесение их в программу это не удел монад, в этих языках имеются куда более естественные и идиоматические средства их создания и паттерны проектирования. Поэтому когда в контексте императивного ООП языка упоминают про монады, то это по-моему только лишь для красного словца.
Кстати стоит упомянуть, что самый главный эффект, который достигается с любой из перечисленных вами монад — задание последовательности вычислений (например функция может что-то вычислить, залогировать это, и потом еще что-то довычислить, залогировать и вернуть результат) в императивных языках имеется из коробки (этот привычный эффект и поэтому всегда пропускается, но в ФП его можно добиться только зависимостью по данным).
И в Хаскеле программирование с эффектами тоже не удел лишь монад, для этого можно использовать и аппликативы (или вообще использовать какую-то свою шайтан-конструкцию). Но я уверен, что при желании можно и аппликатив начать демонстрировать как он выглядит и объяснять что это такое например на С++ или Питоне, просто не нашлось ещё желающих просветить программистскую общественность на это счёт. :)
И в качестве таких «эффектов» можно указать много чего — в зависимости от того, что требуется программисту.

Именно!


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

Более естественные и идиоматичные для императивного программирования, да.


Прелесть монад (и управления эффектами с их помощью) не в том, что если у меня функция живёт в какой-нибудь MonadWriter LogTy или State Foo, то я знаю, что она может писать логи типа LogTy или ковырять состояние типа Foo. Прелесть в том, что если она в этих монадах не живёт, то логи она точно не пишет и состояние не ковыряет. Чистое ФП нужно для того, чтобы функциям ничего лишнего не разрешать, а монады в нём позволяют разрешать только то, что нужно.


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


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

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


К слову о шайтанах, вот.

Прелесть в том, что если она в этих монадах не живёт, то логи она точно не пишет и состояние не ковыряет.

Ну никаких гарантий же у вас нет на самом деле. Точнее, они ровно того же уровня, что в логи не будет писать ф-я, у которой в имени нету log, то есть "мамой клянусь".

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


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

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

Включу Safe Haskell, и у меня уже никто ничего не сломает. Срсли, почитайте ссылку, там клёво.


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

Мне вот в исходном комментарии очень хотелось написать, что, конечно, вы можете ту же State развернуть типах функции в явном виде, но это всё равно видно в типах функции. И -> (a, LogTy) тоже видно.


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

Ну хорошо, сделайте мне на C++ или JavaScript гарантии, что данная функция не пишет в файл и не посылает ничего по сеточке. На JavaScript даже интереснее, у них там npm, всё время то биткоины майнят, то ключи какие утекают.

Включу Safe Haskell, и у меня уже никто ничего не сломает. Срсли, почитайте ссылку, там клёво.

Ну потому что просто вы не можете использовать unsafePerformIO. Типы тут при чем?
Безопасность на типах — это когда у вас есть unafePerformIO но при этом вы не можете написать с ним некорректный код.


Мне вот в исходном комментарии очень хотелось написать, что, конечно, вы можете ту же State развернуть типах функции в явном виде, но это всё равно видно в типах функции. И -> (a, LogTy) тоже видно.

Ну так в итоге нету у вас никаких гарантий-то.


Ну хорошо, сделайте мне на C++ или JavaScript гарантии, что данная функция не пишет в файл и не посылает ничего по сеточке.

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


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


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

Безопасность на типах — это когда у вас есть unafePerformIO но при этом вы не можете написать с ним некорректный код.

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


Ну так в итоге нету у вас никаких гарантий-то.

Почему? Вот функция возвращает Int, значит, она точно не пишет в лог и не ковыряет файлы.


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

Так а кто это IO выполняет-то? Или у вас просто какой-то скрипт для интерпретатора получается?

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

Но unsafePerformIO не нарушает типобезопасность. У нее вполне корректный тип. Интерпретатор прекрасно его чекает. Он вполне осмысленный, работает как надо. Все в порядке.


Почему? Вот функция возвращает Int, значит, она точно не пишет в лог и не ковыряет файлы.

Почему? Я могу внутри вызвать unsafePerofmIO, и она напишет.


Так а кто это IO выполняет-то? Или у вас просто какой-то скрипт для интерпретатора получается?

Я же сказал — как в хаскеле. То есть да, это просто какой-то скрипт для интерпретатора.


То есть, по итогу — вся безаопансость ИО в хаскеле заключается в том что вам разработчик стандартной библиотеки мамой клянется — у него все ф-и "правильные", а неправильные ф-и вы написать просто не можете, потому что любая композиция правильных ф-й — тоже правильная ф-я. Как только вы добавляете возможность получить неправильную ф-ю (unsafePerformIO), так сразу и превращается все в тыкву и типы никак не помогают.
Все в точности так же как было бы в каком-нибудь питоне.

Но unsafePerformIO не нарушает типобезопасность. У нее вполне корректный тип. Интерпретатор прекрасно его чекает. Он вполне осмысленный, работает как надо. Все в порядке.

Нарушает гарантии, даваемые типами. На unsafePerformIO вообще можно unsafeCoerce :: forall a b. a -> b сделать, например.


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


Почему? Я могу внутри вызвать unsafePerofmIO, и она напишет.

Я включил Safe Haskell, и функция теперь напишет только сообщение об ошибке посредством тайпчекера.


Все в точности так же как было бы в каком-нибудь питоне.

А кто в питоне мне статически проверяет правильность композиции функций?

Нарушает гарантии, даваемые типами.

Если гарантии, даваемые типами, у вас нарушаются так, что тайпчекер этого не замечает — то это в точности и означает, что никаких гарантий кроме "мамой клянусь" типы вам в данном случае не дают.


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

Тайпчекер никому ничего не обязан. Он просто либо дает те или иные гарантии, либо не дает. Если тайпчекер позволяет вам написать некорректный код — то он не дает соответствующих гарантий корректности. Вы сами считаете иначе?


А кто в питоне мне статически проверяет правильность композиции функций?

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


Я включил Safe Haskell, и функция теперь напишет только сообщение об ошибке посредством тайпчекера.

И какую конкретно ошибку типов вам выводит при использовании unsafePerformIO в Safe Haskell?

Если гарантии, даваемые типами, у вас нарушаются так, что тайпчекер этого не замечает — то это в точности и означает, что никаких гарантий кроме "мамой клянусь" типы вам в данном случае не дают.

Ну так зачем нужны типы — чтобы накладывать какие-то ограничения на семантику функций. Если у вас есть магические функции типа unsafePerformIO, тела которых тайпчекер по большому счёту не видит (все эти хаки с распаковкой State — это именно что хаки и торчащие в хаскель-код кишки компилятора/рантайма), то ему просто проверять нечего.


И примерно по этим причинам Safe Haskell не даёт вам иметь FFI-функции, живущие не в IO.


Если тайпчекер позволяет вам написать некорректный код — то он не дает соответствующих гарантий корректности.

Написать — ключевое слово. Вспомните доказательства типобезопасности, они все индуктивно строятся. А тут у вас ветвь недоступна, потому что магия, потому что тела функции нет.


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

Нет, в хаскеле не сможет, если не использовать магию. Разница между хаскелем и питоном в том, что в хаскеле магия даётся компилятором и легко отключается/выгрепывается, а в питоне весь язык — одна сплошная магия.


И какую конкретно ошибку типов вам выводит при использовании unsafePerformIO в Safe Haskell?

Она даже раньше выводится. Для этого кода


{-# LANGUAGE Safe #-}

import System.IO.Unsafe

tryToLog :: Int -> Int
tryToLog n = unsafePerformIO $ do
    print n
    pure n

мы получим


prog.hs:3:1: error:
    System.IO.Unsafe: Can't be safely imported!
    The module itself isn't safe.
  |
3 | import System.IO.Unsafe
  | ^^^^^^^^^^^^^^^^^^^^^^^

Лан, давайте руками напишем, чё там.


{-# LANGUAGE Safe #-}

import GHC.Base

tryToLog :: Int -> Int
tryToLog n = case act of
                  (IO m) -> undefined
  where act = print n >> pure n

Ну вот опять:


prog.hs:3:1: error:
    GHC.Base: Can't be safely imported! The module itself isn't safe.
  |
3 | import GHC.Base
  | ^^^^^^^^^^^^^^^
Если у вас есть магические функции типа unsafePerformIO, тела которых тайпчекер по большому счёту не видит (все эти хаки с распаковкой State — это именно что хаки и торчащие в хаскель-код кишки компилятора/рантайма), то ему просто проверять нечего.

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


И примерно по этим причинам Safe Haskell не даёт вам иметь FFI-функции, живущие не в IO.

Но не за счет типов. Точно так же вы можете не давать иметь ффи-функции живущие не в ИО в питоне, ведь так? Никто же вам не мешает.


Нет, в хаскеле не сможет, если не использовать магию.

Дык и в питоне не сможете, если не использовать магию. В чем разница-то?


в хаскеле магия даётся компилятором и легко отключается/выгрепывается, а в питоне весь язык — одна сплошная магия.

Вот, легко отключается/выгрепывается — это верно. Только типы тут не при чем. Еще раз, берем питон и делаем любое ИО через unsafePerormIO. И, ВНЕЗАПНО, в питоне все столь же легко будет отключаться и выгрепываться.


мы получим

Но это не ошибка типов, это ошибка того, что вы импортируете то, что импортировать нельзя. То есть это инкапсуляция. Именно инкапсуляцией гарантии ИО и обеспечиваются в хаскеле. Не типами. И вы можете точно такую же инкапсуляцию устроить в любом языке, в том числе в языке без типов (условном питоне).

Проверять надо тип.

А что его проверять? Вот он, написан после двоеточия.


Тайпчекеры же не просто типы проверяют, тайпчекеры проверяют, что терм соответствует типу. А у вас тут терма нет.


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

Я его нигде не могу написать, потому что семантика unsafePerformIO несовместима с тем, что IO — unescapable-монада.


Ну и полезные в жизни тайпчекеры заведомо консервативны, поэтому всегда, для любого тайпчекера, у вас будет либо unsafePerformIO/unsafeCoerce/assert_total/believe_me, либо вы не сможете на этом языке выразить все семантически корректные программы (хотя, на мой взгляд, на самом деле сможете, но это сильно другой разговор о балансе между сложностью ядра языка и наличием таких лазеек).


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

Мы, кроме этого, убираем саму возможность её написать самому.


По-этому гарантии ИО в хаскеле обеспечиваются не на уровне типизации. Ну что тут непонятного?

Всё непонятно, либо мы разные вещи просто понимаем под типобезопасностью. Я под типобезопасностью понимаю soundness системы типов, и soundness в хаскеле выполняется, если не брать магические предоставляемые компилятором функции.


Точно так же вы можете не давать иметь ффи-функции живущие не в ИО в питоне, ведь так? Никто же вам не мешает.

Но никто не мешает и давать.


Дык и в питоне не сможете, если не использовать магию. В чем разница-то?

В том, что в хаскеле магия сиротливо стоит в уголке и легко контролируема, а в питоне — нет.


Еще раз, берем питон и делаем любое ИО через unsafePerormIO.

А кто гарантирует, что в ИО через другие функции не будет?


Но это не ошибка типов, это ошибка того, что вы импортируете то, что импортировать нельзя. То есть это инкапсуляция. Именно инкапсуляцией гарантии ИО и обеспечиваются в хаскеле.

Мне проще о модулях рассуждать в терминах типов (и вам, возможно, тоже, вы ж тапл читали).

Тайпчекеры же не просто типы проверяют, тайпчекеры проверяют, что терм соответствует типу. А у вас тут терма нет.

Как нет? Есть. unsafePerformIO — вот ваш терм. И система типов хаскеля не позволяет присвоить этому терму такой тип, что этот терм можно было использовать гарантированно.


Дело не в том что чекер не выдает ошибку типов когда.


Я его нигде не могу написать, потому что семантика unsafePerformIO несовместима с тем, что IO — unescapable-монада.

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


Ну и полезные в жизни тайпчекеры заведомо консервативны, поэтому всегда, для любого тайпчекера

Да чего вас в сторону тайпчекера все время уносит? Мы же про монаду ИО говорит, а не про типы.


Давайте возьмем питон, уберем из него все ф-и которые пишут в сеть или в файлы, а вместо нхи будут ф-и, которые генерят лямбды, пишущие в сеть или в файл (то есть семантически пусть IO a = () -> a), причем лямбды будут врапнуты, с-но запустить через простой apply их будет нельзя, но будет специальная ф-я unsafePerformMagic, которая и сможет такие лямбды запускать. Пусть unsafePerformMagic по дефолту лежит где-то там и ее вроде как никто не использует, но у нас будет ф-я бинд, которая юзает ее внутри и клеит вычисления, а так же запускатор наших скриптов будет при запуске применять unsafePerformMagic к определенному в скрипте значению main. Назовем такой язык Pure Python.


И вот теперь, внимание — объясните мне, чем гарантии ИО хаскеля более гарантии, чем гарантии ИО для Pure Python?


Мы, кроме этого, убираем саму возможность её написать самому.

Ну ради бога, из Pure Python тоже убираем.


Но никто не мешает и давать.

Всмысле? Ну же убрали это из библиотеки, все.


В том, что в хаскеле магия сиротливо стоит в уголке и легко контролируема, а в питоне — нет.

Это так. Но, еще раз — при чем тут типы? В Pure Python магия тоже в уголке и легко контролируема, но Pure Pyhton — динамический ЯП. Там нет типов.


А кто гарантирует, что в ИО через другие функции не будет?

Разработчик языка, как и в хаскеле.


Мне проще о модулях рассуждать в терминах типов (и вам, возможно, тоже, вы ж тапл читали).

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

Как нет? Есть. unsafePerformIO — вот ваш терм.

Ну камон, терм — это то, что справа от знака равенства в лет-байндинге, а не слева.


И система типов хаскеля не позволяет присвоить этому терму такой тип, что этот терм можно было использовать гарантированно.

Ну и хорошо, потому что IO — unescapable-монада, и такого типа в общем случае и нет. ЧТД, система типов нас снова спасает!


Конечно, возможно, если бы система типов была ещё более выразительной, то для некоторых случаев (вроде паттерна с глобальным менеджером какой-нибудь ерунды типа HTTP-соединений) узкий аналог unsafePerformIO можно было бы сделать, если бы можно было доказать, что это не нарушает семантику языка, но это уже другой вопрос. У хаскеля далеко не самая выразительная система типов, и с этим никто не спорит.


Так ради бога, напишите для unsafePerformIO какой-нибудь другой тип. Но так, чтобы можно было с ней работать, а не жопу какую-нибудь.

А кто сказал, что для unsafePerformIO вообще такой тип существует с той семантикой IO и unsafePerformIO, которую мы имеем сегодня?


Но не получится ни с каким. В этом проблема.

Я не вижу проблемы. То, что недоказуемо безопасный терм (и, вернее, доказуемо опасный) нетипизируем — не проблема, а хорошее свойство системы типов, на мой взгляд.


Да чего вас в сторону тайпчекера все время уносит? Мы же про монаду ИО говорит, а не про типы.

А после того, как мы типы прочекали и удалили, монады остались только у нас в голове в нашей ментальной модели.


Ну и потому, что гарантии рантайм-поведения мне так же важны, как само рантайм-поведение.


И вот теперь, внимание — объясните мне, чем гарантии ИО хаскеля более гарантии, чем гарантии ИО для Pure Python?

Тем, что в хаскеле я могу посмотреть на сигнатуру функции и сделать вывод, что её передача в unsafePerformMagic ни к каким IO-эффектам не приведёт. Что полезно для рассуждения о коде и о семантике функций.


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


Ещё раз, у меня нет цели запретить IO, это какая-то карго-культовая цель. У меня есть цель посмотреть на функцию и сказать, если там IO/логи/nullable-семантика, или нет.


И это я уже не говорю о вещах вроде MonadIO, ST (успехов с выражением этого на пурепитоне, там мой любимый rank-2 polymorphism пригождается), PrimMonad и тому подобных.


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

Да, согласен, мы задерейлились в обсуждение импортируемости unsafePerformIO. Выше я описал, зачем там на самом деле типы (ну, про проверку, что функция чего-то не делает, опять же).

Ну камон, терм — это то, что справа от знака равенства в лет-байндинге, а не слева.

Все константы являются термами. И, кстати, если y = f(x), то замена терма y на терм f(x) в каком-то выражении (офк, когда все ок со связыванием) не обязательно будет всегда корректна.


Ну и хорошо, потому что IO — unescapable-монада, и такого типа в общем случае и нет.

Нет в хаскеле. Потому что система типов хаскеля — слабая.


ЧТД, система типов нас снова спасает!

Всмысле, как спасает? Еще раз — мы спокойно может писать некорректный код с unsafePerformIO и компилятор ничего с этим не делает. В том время как он должен зарезать некорректный код, оставляя корректный.


А кто сказал, что для unsafePerformIO вообще такой тип существует с той семантикой IO и unsafePerformIO, которую мы имеем сегодня?

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


Я не вижу проблемы. То, что недоказуемо безопасный терм (и, вернее, доказуемо опасный) нетипизируем — не проблема, а хорошее свойство системы типов, на мой взгляд.

Так терм доказуемо безопасный, об этом речь как раз.


Ну и потому, что гарантии рантайм-поведения мне так же важны, как само рантайм-поведение.

Да, но ИО тут при чем?


Тем, что в хаскеле я могу посмотреть на сигнатуру функции и сделать вывод, что её передача в unsafePerformMagic ни к каким IO-эффектам не приведёт.

Нет, не можете. Тайпчекер хаскеля не гарантирует вам, что внутри вашей ф-и с хорошим типом (без ИО), где-то внутри не вызывается unsafePerformIO. Знать тип для этой гарантии недостаточно. Более того — знание типа для этой гарантии не является и необходимым. Иными словами — гарантии ИО в хаскеле не зависят от знания вами типа ф-и.


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

Именно так.


У меня есть цель посмотреть на функцию и сказать, если там IO/логи/nullable-семантика, или нет.

Ну вот как вы в хаскеле это делаете? Видите ф-ю, смотрите на ее реализацию. Смотрите на реализации используемых в ней ф-й (и так далее рекурсивно). Если нигде не встречается unsafePerformIO — значит, все ок.
Как вы делаете это в pure python? Смотрите на ф-ю, смотрите ее реализацию. Смотрите реализации используемых ф-й. если нигде не встречается unsafePErformIO — значит, все ок.
Разница-то в чем?
И непонятно, при чем тут тип. Вам же код надо смотреть, а не тип.


Выше я описал, зачем там на самом деле типы

Ну вы написали, что якобы по типу можно понять, есть там IO или нет. Но по факту же это неправда, нельзя.

Все константы являются термами.

Так это не константа, это просто имя в байндинге. Оно будет термом внутри тела байндинга (после in, или дальше по скоупу, или как вы там дальше язык отображаете на ваш чистый STLC с расширениями).


Нет в хаскеле. Потому что система типов хаскеля — слабая.

Я написал про это дальше.


Хочу посмотреть на типизируемый терм на агде/идрисе, кстати.


Еще раз — мы спокойно может писать некорректный код с unsafePerformIO и компилятор ничего с этим не делает. В том время как он должен зарезать некорректный код, оставляя корректный.

Ну так откажитесь уже от магии, наконец, и включите -XSafe.


Еще раз — unsafePerformIO, семантически, это корректная ф-я. В ней нет ничего плохого, т.к. вы можете писать с ней корректный, осмысленный код.

Это верно для любой функции, даже unsafeCoerce : a -> b. Я поэтому писал и про заведомую консервативность любого тайпчекинга: существуют семантически корректные, но нетипизируемые программы.


И я бы не назвал unsafePerformIO семантически корректной функцией. Да, бывают контексты, в которых она корректна, но и сложение числа со строкой бывает корректно (если строка пустая).


Так терм доказуемо безопасный, об этом речь как раз.

Какой и где? id = unsafePerformIO . pure безопасно, да, но есть более простые способы написать identity function, скажем.


Да, но ИО тут при чем?

Отсутствие IO при этом.


Тайпчекер хаскеля не гарантирует вам, что внутри вашей ф-и с хорошим типом (без ИО), где-то внутри не вызывается unsafePerformIO. Знать тип для этой гарантии недостаточно. Более того — знание типа для этой гарантии не является и необходимым. Иными словами — гарантии ИО в хаскеле не зависят от знания вами типа ф-и.

Гарантирует, если вы тайпчекер не обманываете магией. А не обманывать очень просто, достаточно отказаться от (замкнутого) множества поставляемой с компилятором магии, включив -XSafe.


Ну вот как вы в хаскеле это делаете? Видите ф-ю, смотрите на ее реализацию. Смотрите на реализации используемых в ней ф-й (и так далее рекурсивно). Если нигде не встречается unsafePerformIO — значит, все ок.

Достаточно посмотреть на выведенную аннотацию модуля, safe он или не safe. А после этого уже можно ограничиться типами, а рекурсией тайпчекер займётся.

Так это не константа

Константа, конечно. С-но все "внешние байндинги" — это константы и есть. В частности, это константа может иметь значение, которое в самом языке вообще писаться не будет.


Ну так откажитесь уже от магии, наконец, и включите -XSafe.

Так сейф не режет некорректный код, она просто запрещает применение unsafePerformIO.


Это, понимаете, разница как между тайп-сейф доступом по индексу и полным запретом доступа по индексу. В первом случае у вас зависимые типы, а во втором — тот же условный питон, в котором просто нету доступа по индексу.


Это верно для любой функции, даже unsafeCoerce: a -> b

С unsafeCoerce не получится.


Да, бывают контексты, в которых она корректна, но и сложение числа со строкой бывает корректно (если строка пустая).

И что должно получиться при сложении числа со строкой?


Какой и где?

unsafePerformIO. В реализации бинда, например.


Гарантирует, если вы тайпчекер не обманываете магией.

Если вы не обманываете Pure Python, то он тоже гарантирует. Вы можете объяснить, в чем разница в гарантиях хаскеля и Pure Python?


Достаточно посмотреть на выведенную аннотацию модуля, safe он или не safe.

Отлично, в Pure Python все то же самое. В какой момент разница появляется?

В частности, это константа может иметь значение, которое в самом языке вообще писаться не будет.

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


Добавьте в Г терм типа ∀a:*. a, вообще веселуха будет, язык можно сразу выкидывать.


Так сейф не режет некорректный код, она просто запрещает применение unsafePerformIO.

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


Это, понимаете, разница как между тайп-сейф доступом по индексу и полным запретом доступа по индексу. В первом случае у вас зависимые типы, а во втором — тот же условный питон, в котором просто нету доступа по индексу.

Прекрасная аналогия с завтипами. Почему даже в этих языках есть костыли, в чём-то похожие на unsafePerformIO?


С unsafeCoerce не получится.

Почему?


Пусть у вас машина с 32-битным интом, тогда


foo = 10 :: Int
bar = (unsafeCoerce foo :: Word32) + 1
baz = foo + 1
isEq = unsafeCoerce baz == bar

абсолютно семантически корректная программа, и при разумных предположениях для представления чисел можно ожидать, что Refl : isEq = True (кек).


И что должно получиться при сложении числа со строкой?

То же число. Пустая строка ж identity :)


unsafePerformIO. В реализации бинда, например.

Можно пример?


Если вы не обманываете Pure Python, то он тоже гарантирует. Вы можете объяснить, в чем разница в гарантиях хаскеля и Pure Python?

В том, что обмануть хаскель я могу только через unsafePerformIO и подобные хаки, а обмануть Pure Python я могу как угодно, включая доступные мне IO-абстракции.

Это внешнее по отношению к тайпчекеру.

Именно так. Важно лишь, чтобы у терма был корректный тип, который бы не ломал нам все.


Технически она запрещает импорт. Но это даже неважно: я включил -XSafe, и всё, с этого момента сигнатуры функций мне (и тайпчекеру) не врут. Вообще. Никогда.

Ну отлично, в Pure Python все будет то же самое — запрещаете импорт unsafePerformMagic и гарантированно никто из ИО никогда и никак не вылезет.


Прекрасная аналогия с завтипами. Почему даже в этих языках есть костыли, в чём-то похожие на unsafePerformIO?

Почему?


абсолютно семантически корректная программа

Да? Разве хаскель вам гарантирует, что на любой возможной реализации эта программа отработает с одним и тем же результатом?


Можно пример?

Ну реализация бинда для ИО-монады в грязных языках так делается же. bind f x = f $ unsafePerformIO x


Поскольку на выходе ИО одно, то все вызовы развернутся в одну цепочку и это гарантирует корректность (вы ваши World* сольете из начального в конечный через непрерывную последовательность операций).


В том, что обмануть хаскель я могу только через unsafePerformIO и подобные хаки, а обмануть Pure Python я могу как угодно, включая доступные мне IO-абстракции.

Эм, нет, обмануть Pure Python вы можете тоже только через unsafePerformMagic. Просто потому что никаких других способов запустить ИО у вас нет, какие абстракции вы там не пытайтесь накручивать.

Гарантии есть. Их предоставляет компилятор. Чтобы ему помочь нужно приложить некоторые усилия. Стоит расслабиться и ваша программа превращается в одно большое IO, в дымке которого растворяются все парадигмы. Чтобы этого не происходило нужно потрудиться и разобраться.

Гарантии есть. Их предоставляет компилятор.

Какие гарантии ИО предоставляет вам компилятор хаскеля, но при этом не предоставляет компилятор питона (допустим, я в питоне написал bind-io, return-io и переписал все библиотечные ф-и так, что они возвращают io вместо того, чтобы сразу отрабатывать)?

Допустим я разогнался и врезался на байке в стену на скорости 200 километров в час. Тогда, действительно, никаких гарантий производитель шлема не даёт.
Повторюсь, нужно сделать усилие, и не пихать IO туда где без него можно обойтись. Тогда по сигнатуре функции будет видно что она делает и что ничего другого она сделать не может.

Причём хаскель сделан так, что писать effectful-код чуть больнее. Это не зря.

Допустим я разогнался и врезался на байке в стену на скорости 200 километров в час. Тогда, действительно, никаких гарантий производитель шлема не даёт.

Если не разгоняться, ехать по правилам и все такое — то и в питоне ничего плохого не случится.
Это тогда не гарантии, а фигня какая-то, извините.

Прелесть монад (и управления эффектами с их помощью) не в том, что если у меня функция живёт в какой-нибудь MonadWriter LogTy или State Foo, то я знаю, что она может писать логи типа LogTy или ковырять состояние типа Foo. Прелесть в том, что если она в этих монадах не живёт, то логи она точно не пишет и состояние не ковыряет. Чистое ФП нужно для того, чтобы функциям ничего лишнего не разрешать, а монады в нём позволяют разрешать только то, что нужно.
Да, я понял что вы имели ввиду когда говорили об ограничениях эффектов только теми местами, где они нужны.
К слову о шайтанах, вот.
Интересно, спасибо.
За вот это вот «монады − это просто, как раздватри» и последующую стену текста на эльфийском языке. «Ты с кем разговариваешь, папа?», хабр эдишен.

Представляете, а ведь мы так говорим не чтобы запутать, а наоборот, чтобы разобраться :) и ведь получается!

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

Впрочем, это я дразнюсь. Получается не всегда, если залезаешь в незнакомую область ничерта непонятно. Но если надо, то можно, и это круто!

Я ничего не имею против математического языка, но на Хабр захожу, чтобы получить объяснение чего-то обычным языком, in layman's terms. Видимо, я не одинок.
А «нас» — это кого? К какой категории (извиняюсь за каламбур) вы меня отнесли?
К программистам-функциональщикам, конечно. А вы про кого подумали?
Про что-то более конкретное, типа хаскелистов или даже теоретиков категорий. Ни к тем, ни к другим, себя не отношу :)
Увы, я не умею конкретизировать в эту сторону. Я подумал, мало ли, может, вас отсылка к еврейскому анекдоту насторожила.
Кстати, теоркат для меня, что называется, кликнул, когда я понял, что равенство морфизмов во всяких там аксиомах и коммутативных диаграммах имеется в виду именно как равенство элементов Hom-класса. Если учить теоркат на примере Set, то возникает соблазн задумываться, например, об экстенсиональном равенстве и тому подобной ерунде.

Но это так, мелкое замечание по формулировке понятия категории.
Обычный «нормальный» функтор F переводит морфизмы (a -> b) в (F a -> F b). Используя «однобокий правый» функтор K для перевода из морфизмов (a -> b) в (a -> K b) можно построить категорию Клейсли. Что получается при использовании «однобокого левого» функтора U из (a -> b) в (U a -> b)?
:)

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

Интересно кстати, что пустой тип void и пустой кортеж разделены. Никогда не задумывался о том что это разные сущности, наоборот казалось что пустой кортеж и void это одно и то же:)

Если вы про void, который в С-подобных языках, то это действительно то же самое, что и пустой кортеж. Т.е. то, что используется, если нам не важен результат:


void main(void) {..}

main :: IO ()

К сожалению, такая вот путаница с названиями.

Имхо, нет, void это не пустой кортеж, это пустой тип.

() — это тип, в котором есть ровно одно значение, называемое (). Например, можно написать return () или даже

x :: ()
x = ()


void — это тип, в котором нет ни одного значения. Нельзя написать return void; или void variable = void;.

В стандартной библиотеке хаскеля пустого типа нет; он есть в пакете void и называется Void :)
Если говорить о типах в Haskell, то, конечно, `Void` и `()` — совершенно разные вещи.
Если сравнивать пустой кортеж и `Void` в Haskell с тем, что обозначается словом `void` в С, то **сишный** `void` — аналог пустого кортежа в Haskell, но не аналог ненаселённого типа `Void`.
В книжке, на которую я ссылаюсь, это тоже есть: bartoszmilewski.com/2014/11/24/types-and-functions
void — это тип, в котором нет ни одного значения. Нельзя написать return void; или void variable = void;.

Это, кстати, некоторые плюсисты считают недостатком (я к ним отношусь) и пишут всякие пропозалы типа regular void (я к ним не отношусь).


Ну и вещи типа


void f() {}

void g() { return f(); }

вы написать, кстати, можете уже сейчас.


Ещё можно подумать о семантике функции, принимающей тип, в котором нет ни одного значения, или возвращающей тип, в котором нет ни одного значения.

и тем не менее в тех же плюсах
template [class F] // парсер — туды его в качель
auto g(F f) { return f(); }
void f() {}
g(f);
работает. return void в полный рост.

Это примерно тот же пример, что я привёл выше, только с темплейтами поверх.


Проблемы начинаются, когда вы захотите написать что-то вроде


template<typename F>
auto g(F f)
{
    auto res = f();
    return res;
}
Блин, прочитал
вы написать, кстати, можете
как «вы написать, кстати, не можете.» сорри
Подумал тут, а можно ли сказать что аналогом Хаскелевскому void'у будет спецификация 'noreturn', которая обозначает что из функции вообще никогда не будет возврата? Например функция всегда бросает исключение или там принудительный выход из программы?

Это будет аналогом Void'у «справа», когда Void — возвращаемое значение. И это будет абсолютно верной интерпретацией с логической точки зрения.


Аналог параметра Void (или кортежа из войдов) — константная функция.

Константная стрелка она из терминального объекта, который не воид а юнит.

Юнит — это таки финальный объект, а не начальный, поэтому стрелки в него, а не из него.


А так...


consumeVoid : Void -> Int
consumeVoid v = 42

> :doc consumeVoid
Main.consumeVoid : Void -> Int

    The function is Total

Вот то, что её вызвать не получится — это другой вопрос.

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

Я в своём идрисе %default total сделал, нет у меня undefined.


Хотя я тоже неправ, consumeVoid v = 42 и consumeVoid v = 43 равны, очевидно, поэтому морфизм у нас там тоже один.

Дико извиняюсь, перепутал константу и константную функцию.

«Монады с точки зрения программистов»… имхо, проще надо быть :) монада — это интерфейс из двух методов. И некоторый контракт на то, как эти методы должны работать вместе (ничего удивительного, вон см контракт между equals и hashCode). И больше ничего. Ну, синтаксический сахар с этим интерфейсом работает (do-нотация), к этому не привыкать (см. в яве цикл for и интерфейс Iterable).

В принципе, вся «наука» проектирования ПО (в технической части) — это про изобретение абстракций (наборов интерфейсов и их взаимодействия друг с другом). Интерфейсы бывают похуже и получше. Одна из метрик «хорошести» интерфейса — это «ортогональность», т.е. возможность при помощи малого количества простых методов выразить большое количество всяких сложных вещей (как из трёх координатных векторов можно всё трёхмерное пространство сделать). Другая / похожая метрика — composability, т.е. возможность комбинировать с большим количеством других интерфейсов множеством разных способов. Ну и есть метрика «универсальности» или «абстрактности» — насколько большое количество разных вещей могут имплементировать этот интерфейс.

Математики профессионально и целенаправленно занимаются изобретением абстракций и компоновкой из них других абстракций уже больше ста лет (если считать, например, от Гильберта; а до этого занимались тем же, но не так профессионально и целенаправленно). Они наизобретали много «хороших», т.е. «ортогональных», «композабельных» и «универсальных» абстракций — множества, функции, группы, категории… Так как эти вещи по построению абстрактны / универсальны, ничего удивительного нет в том, что множество сущностей в программировании имплементирует эти интерфейсы.

Программисты занимаются построением абстракций… ну, скажем, с 50х. Товарищи инженеры из «банды четырёх» обобщили кучу инженерного опыта за пару десятилетий и выписали десяток распространённых, «хороших» абстракций. Многие из этих абстракций являются велосипедами — математики их изобрели на полвека раньше.

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

Ну и см. «You might have invented monads» (есть перевод на хабре — habr.com/ru/post/96421).
UFO landed and left these words here
Я честно старался… Интуитивно до этого понимал, что такое монада, но когда пытался сформулировать словами, впадал в ступор. Увидел статью и решил, что сейчас-то доберусь до истины. Но на fish операторе поплыл. Тем не менее, спасибо за труд.
Ну, это не самая простая статья на тему, реально. Были тут и попроще попытки. В картинках :)
Мне помогла такая ассоциация: «Монады — это программируемые точки с запятой». Вот у нас есть монада try… catch. В обычном случае точка с запятой вызывает переход к следующей команде. А в try..catch не всегда — только если предыдущий не бросил исключение. Появилось нетривиальное ПРАВИЛО исполнения последовательных операций.

Вот штука, говорящая что точки с запятой в некоей зоне будут вести себя как-то по-другому, и правило, говорящее — КАК ИМЕННО они себя будут вести, и есть монада.

Мне всегда интересно было, почему, например, Монаду нельзя описать как некую хрень с сайдэффектом (нечто, что, например, пишет инфу в базу, в поток) или как некую защитную обертку над другими типами (Maybe вообще шикарно соотвествует Nullable из какого-нибудь C#). Почему обязательно пытаться притягивать категории, функторы? Я понимаю, что высокая наука требует применения спецтерминов, чтобы доказать свою научность. Но инженерная практика работает то с чем-то более осязаемым...


И кстати, я случаем не пропустил в статье сказочное утверждение, что в хаскеле попав в монаду, из нее нельзя выйти?

> Почему обязательно пытаться притягивать категории, функторы?
Я думаю, в основном потому, что авторы хотят показать, откуда эта абстракция исторически возникла. С другой стороны, это не вредно: категории и функторы сами по себе являются полезными абстракциями, имеющими множество имплементаций.
Представьте, что вы рассказываете про паттерн «абстрактная фабрика» студенту, который ещё ни разу не сталкивался с проблемами, которые этот паттерн решает: студент будет удивляться, зачем такая сложная штука нужна. А вот если вы будете это рассказывать программисту, который такие проблемы решал во множестве, просто по какой-то причине не знал, что этот паттерн так называется — он скажет «а, понятно, ок».

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

> в хаскеле попав в монаду, из нее нельзя выйти?
Это не является универсальным свойством монад. Из монады IO «выйти» нельзя (и то, есть всякие unsafePerformIO, но это скорее костыли); из монады State «выйти» можно стандартной функцией runState.
Я думаю, в основном потому, что авторы хотят показать, откуда эта абстракция исторически возникла. С другой стороны, это не вредно: категории и функторы сами по себе являются полезными абстракциями, имеющими множество имплементаций.

У меня ни разу не возникло возражений при подобном описании монад в учебниках по Хаскелю, написаных скорее математиками для математиков, чем инженерами для инженеров (разве что только один раз видел исключение у Шевченко (https://www.ohaskell.guide)). Но когда в заголовке вижу "Монады с точки зрения программистов" ожидаю все-таки инженерный подход. Sorry.


Представьте, что вы рассказываете про паттерн «абстрактная фабрика» студенту, который ещё ни разу не сталкивался с проблемами, которые этот паттерн решает: студент будет удивляться, зачем такая сложная штука нужна.

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

Да Maybe = Nullable. Но монада — это не maybe. Наоборот maybe — это монада.
Для шарпщика проще объяснить через линк. Пусть у нас есть дженерик тип X[T] (например IEnumerable или Nullable или Task.
реализуем для всех типов экстеншен метод SelectMany. теперь мы можем писать:
var result =
from x in X() // внезапно query syntax — это аналог do натации в хаскеле
from y in Y()
select x + y;
а проблема в том что мы не можем абстрагировать этот код от типов IEnumerable/Task/Nullable. В хаскеле можно, поэтому там дин и тот же код работает и со списками и с асинхронными знаниями и тд. в шарпе для тасков у нас будет своя функция Sum(), а для нулаблов своя.
UFO landed and left these words here
Мне всегда интересно было, почему, например, Монаду нельзя описать как некую хрень с сайдэффектом или как некую защитную обертку над другими типами.
Почему — как раз можно, и это наиболее понятный и утилитарно-обоснованный способ.
Почему обязательно пытаться притягивать категории, функторы?
Это просто хаскеллисты притягивают, поскольку многие концепции и подходы к структурированию задач в Хаскеле могут быть описаны в рамках теории категорий, т.е. математически. А это очень подкупает.
И кстати, я случаем не пропустил в статье сказочное утверждение, что в хаскеле попав в монаду, из нее нельзя выйти?
Например из монады IO выйти нельзя — «распаковать» значение без доступного конструктора или спецфункции компилятора не получится.
А зачем все это? Зачем так все усложнять? Функциональное программирование хорошо в меру. Зачем из него делать целый язык?
Мне Haskell переусложненным не кажется, он просто функциональный и все. Уложить его в голову требует некоторого ментального усилия(а у людей с математической подготовкой и не требует), но далее все просто и понятно. Мне кажется переусложненным например scala, где функционального в меру, процедурного в меру, ООП в меру, присутствуют и наследование и композиция и символические преобразования и последовательные вычисления, с миру по нитке. Это все дается ценой запутанного синтаксиса и невнятной идиоматики. То же Rust — то ли Haskell для бедных, то ли переHaskell. Haskell на мой взгляд яснее чем мультипарадигменные гибриды, проще понять что ты пишешь и прочитать написанное другими.
Такое впечатление, что Haskell придумали специально, чтобы с большим трудом решать простые задачи. Язык не надо укладывать в голову. Есть три задачи языка программирования: 1) быстро написать правильную программу 2) легко прочитать программу 3) быстро изменить программу не добавляя ошибок. Другие языки гораздо лучше для этих целей.
Какие, например?

По моему сугубо субъективному опыту, в первом и третьем пункте хаскель прям вообще сияет, во втором — весьма хорош, если автор программы не старался повыпендриваться (но это в любом языке верно).
То, что по третьему пункту хаскель сияет — это широко распространенное заблуждение. Ключевое слово там — быстро.

Типы таки очень помогают.


Хотя, конечно, и на хаскеле можно всё писать в IO и сделать жизнь адом.

UFO landed and left these words here

После семи лет ежедневной и плотной работы в Wolfram Mathematica, мне её показалось мало и в моём инструментарии появился Lisp. Ещё через пять лет мне стало тесно и в нём, так я пришёл к Хаскелю, Axiom, Agda, поскольку в них математическая мысль формулируется точнее и строже. И всё это прекрасно сочетается в работе и не мешает писать для развлечения на JavaScript :) Или вы предлагаете отменить все прочие языки, оставив лишь те, что нравятся вам?

Нет, конечно. Вам нравится одно, мне — другое. Мы обменялись мнением. Никто ничего не предлагал отменять. Просто я уверен, что все о чем написано в этой статье можно сделать намного проще в других языках.

Не всё. Монады не про ввод/вывод, а про композицию в контексте. Одним из контекстов может быть "внешний мир", но их гораздо больше: неоднозначные вычисления, вероятностные вычисления, параллельность и корутины… При этом синтаксически одна программа может выаолняться в разных контекстах. Это как ортогональные криволинейные координаты: один раз выразил дифференциальные операторы в терминах чисел Ламе и получаешь один и тот же дифур, но в разных контестах.

Зачем выращивать рис, когда есть макароны? Зачем снимать кино, когда есть книги? Зачем переусложнять и говорить по-китайски, когда есть понятный и простой румынский? Ответ прост: потому что можно. Так развивается человечество.

Да, человечество развивается именно так: попробовал, признал негодным, забыл

Или так: попробовал, понял область применения и применяешь, если нужно. Многим нужно.

Спасибо, давно скачано на волне интереса, но руки/глаза не доходят прочитать.
В превью на гитхабе мат. символы рендерятся в иероглифы (скачанный вариант ок)
А вы бы не могли улучшить русскую статью вики по теории категорий? Какими проблемами вызвано появление теории и решила ли она их? Интересные результаты? Из статьи как-то получается что это теория без теорем.

П.С. В английской написано " the goal of understanding the processes that preserve mathematical structure" и я этого не могу понять однозначно.
Вики мне улучшать не хочется, но теория категорий в первую очередь задумывалась как новый язык математики вместо теоретико-множественного. Маклейн ведь учился логике в Германии у Гильберта. На категорном языке удобно давать общематематические определения (типа «декартово произведение»). Некоторые разделы математики без этого языка невозможны (алгебраическая геометрия и топология в первую очередь). Интересные примеры тоже возникли, в первую очередь это топосы (категории, похожие на категорию множеств, но с неклассической логикой). Неожиданно оказалось, что они естественно возникают везде. Я в учебнике на этих примерах всё и объясняю.
В каком смысле алгебраическая геометрия невозможна без «теории категорий»? Помню на хабре была статья про элиптические уравнения и доказательство Вайлса ВТФ в пересказе «перечень шагов» я смотрел — не было там ни термнинов теории категорий (функторов, композиций) ни алгебраической записи характерной для нее. Что я упускаю?
За доказательство Вайлса не ручаюсь (не моя область), а вообще алгебраическая геометрия — это пучки, а пучки это функторы с определёнными свойствами. Диаграммы там рабочий язык.

Категория из одного объекта не обязана иметь только морфизм id .

Так это нигде и не утверждается… Если тот же тип Bool взять как единственный объект и функции Bool -> Bool как морфизмы, то их 4 штуки показано. Мне просто не хотелось для каждой стрелочку рисовать, чтобы рисунок не загромождать.

В вашем тексте есть утверждение, что функтор в категорию из одного объекта переводит все морфизмы в id. В общем случае это неверно.

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

Не за что. Пишите ещё. Например про комонады или про монадические трансформеры.

Да, ведь, как известно, группа — это просто группоид с единственным объектом.

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

Можно так:


io1 = do
  putStrLn "введите текст"
  x <- getLine
  putStrLn "повторите ввод"
  y <- getLine
  if x == y
    then putStrLn "Ok"
    else putStrLn "Ошибка"

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


io2 = process
      <$> (putStrLn "введите текст" >> getLine)
      <*> (putStrLn "повторите ввод" >> getLine)
      >>= putStrLn
  where
    process x y = if x == y then "Ok" else "Ошибка"
Спасибо! Тут для меня две магические функции putStrLn, getLine. За которыми скрыто очень много. Например, за getLine скрыта обработка прерываний от клавиатуры. За putStrLn в общем-то глобальное хранение состояния видео памяти.
Для меня одним из декларируемых свойств функционального программирование — это то, что выполнение функции не зависит от состояния. А тут явная зависимость вывода функции getLine от состояния вычислительной машины. Она же может никогда не отработать. Функция всегда должна при одинаковых параметрах возвращать одинаковый результат. А тут getLine всегда получает пустой параметр, а возвращает разные результаты.
Для меня, вот эти две функции, выглядят как костыли в языке, Haskell. Которые возможны только по воле создателя языка. А вот если абстрагироваться от реализации? А оперировать абстрактной вычислительной машиной.
Или в общем-то мы все равно придем к глобальному хранению состояний машины? Или программа на функциональном языке — это всегда функция? В чистом виде, без костылей. И при вызове программы мы всегда должны передать ей входные параметры, а она при одинаковых входных параметрах всегда должна возвращать одинаковый результат.
Типа $ plus 1 1
2
И в чистом виде, она никогда не должна вернуть, например 3. Хотя с функцией getLine, такое сделать можно в легкую.
То есть вопрос не про Haskell, а про философию функционального программирования.

Можно подходить к этому так. Программа с IO это чистая функция — рецепт того, что делать при конкретных входах. После чего мы подаём ей на вход разные варианты внешнего мира так же, как в интерпретаторе функции plus подаём разные числа. При этом, концептуально, одинаковым мирам всегда соответствует одинаковый ответ. getLine, в мире, в котором клавиатура всегда возвращает "123", всегда вернёт "123". миров много, но и у функции plus возможно много входных параметров.
Не воспринимайте IO-функции как нечто инородное в чистом ФП, а монады требуются не для обеспечения чистоты, с этим проблем нет, а для обеспечения порядка вычислений в ленивом языке.

Философия функционального программирования не совсем в этом. В конце концов, можно считать, что все программы на C или C++ или Rust или чём ещё живут в IO и точно в том же смысле чистые, выдавая один и тот же ответ, если состояние окружающего мира то же самое.


Философия и смысл в том, что компилятор гарантирует, что для тех функций, которые не живут в IO, их вывод от внешнего мира не зависит никак.

Абсолютно согласен. Мне хотелось показать, что ничего "магического", "неправильного" или чуждого идее ФП в функциях типа getLine нет. И вообще, разницу между императивным и функциональным подходом хотелось бы демистифицировать. Можно писать чистые функции на С, и даже выгоды от этого получать (в FORTRAN есть ключевое слово pure, помогающее компилятору трансляции чистых функций), но это акт доброй воли. Можно в Haskell запихать всё в IO, но компилятор из помощника превратится в послушного толмача. А можно балдеть от оптимизаций gcc и ghc, подавая им правильно приготовленные программы, расширяя горизонты. Кайф!

Сергей, а можно вас также попросить представить пример тоже простейшей программы на Хаскел эквивалентной следующей на С:
#include <stdio.h>

#define N 10000u

const char* compare(unsigned x, unsigned y)
{
  static const char LT[] = "LT";
  static const char EQ[] = "EQ";
  static const char GT[] = "GT";
  return (x < y ? LT : (x > y ? GT : EQ));
}

int main()
{
  for (unsigned x = 1; x <= N; ++x)
    for (unsigned y = 1; y <= N; ++y)
      printf ("%u <%s> %u\n", x, compare(x ,y), y);
}
  
именно «отделив бизнес-логику в чистую функцию».

Хороший пример, спасибо!
Идиоматичное решение здесь может быть таким: создадим чистую полиморфную функцию, которая соответствует вложенному циклу:


mkComps :: PrintfType a => Int -> [a]
mkComps n = [printf "%u <%s> %u\n" x (show (compare x y)) y
            | x <- [0..n]
            , y <- [0..n]] 

где compare :: Ord a => a -> a -> Ordering — это чистая библиотечная функция, соответствующая вашей.


Эту функцию можно вызвать в чистом контексте:


> concat (mkComps 3) :: String
"0 <EQ> 0\n0 <LT> 1\n0 <LT> 2\n0 <LT> 3\n1 <GT> 0\n1 <EQ> 1\n1 <LT> 2\n1 <LT> 3\n2 <GT> 0\n2 <GT> 1\n2 <EQ> 2\n2 <LT> 3\n3 <GT> 0\n3 <GT> 1\n3 <GT> 2\n3 <EQ> 3\n"

или в IO


main = sequence_ (mkComps 3)

> main
0 <EQ> 0
0 <LT> 1
0 <LT> 2
0 <LT> 3
1 <GT> 0
1 <EQ> 1
1 <LT> 2
1 <LT> 3
2 <GT> 0
2 <GT> 1
2 <EQ> 2
2 <LT> 3
3 <GT> 0
3 <GT> 1
3 <GT> 2
3 <EQ> 3

это был вывод в консоль.


Здесь mkComps выполняет всю работу чистым образом, "производя" данные, а в main они превращается в вывод на печать. Благодаря параметрическому полиморфизму функция printf может возвращать как "чистую" строку, так и побочное действие. А благодаря ленивости языка никакого списка в памяти при этом не создаётся, хотя выглядит он подозрительно. На моём ноутбуке откомпилированный бинарник отправил в /dev/null 10000*10000 записей за 10 секунд.

Здорово. Мой вариант для задачи, сведённой до этого упрощенного примера был таким:
mkOut :: Int -> [String]
mkOut n = [ shows x $
            showString " <" $
            shows (compare x y) $
            showString "> " $
            shows y "\n"
          | x <- xy, y <- xy ]
  where xy = [1..n]

main = mapM_ putStr $ mkOut 10000
И он меня очень не порадовал — память заканчивается, система начинает подвисать.

Такая версия — сделать список из IO-действий — не является по сути отделением логики от операции вывода:
mkOut :: Int -> [IO ()]
mkOut n = [ putStr $
            shows x $
            showString " <" $
            shows (compare x y) $
            showString "> " $
            shows y "\n"
          | x <- xy, y <- xy ]
  where xy = [1..n]

main = sequence_ $ mkOut 10000

Насколько я понял решение можно сделать таким, используя полиморфную функцию-префикс:
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}

class Out t where  out :: String -> t

instance Out String where  out = id
instance Out (IO ()) where  out = putStr

mkOut :: Out t => Int -> [t]
mkOut n = [ out $
            shows x $
            showString " <" $
            shows (compare x y) $
            showString "> " $
            shows y "\n"
          | x <- xy, y <- xy ]
  where xy = [1..n]

test :: String
test = concat $ mkOut 10000

main :: IO ()
main = sequence_ $ (mkOut 10000 :: [IO ()])
(Какие-то нюансы не знаю, что мешают, чтобы не нужно было специфицировать тип результата mkOut при вызове.)
Я правильно понял с инженерной точки зрения? Что монада нужна для того, чтобы чисто функциональный язык, типа Haskell мог взаимодействовать с машиной, которая управляется через ее состояния. В языках, которые построены на управлении состояниями, и где внедряются и рекламируются функциональный подход (C#, JavaScrip, python и т.д.), большой необходимости в монадах нет. Там прозрачней использовать родные средства, а функциональный подход использовать для организации логики на чистых функциях.

Думаю, вполне правильно. Для управления состояниями строгие и императивные языки в монадах не нуждаются. Но монады и в этих языках позволяют организовывать поток вычислений с весьма изощрённой логикой. Когда-то я определил для себя, что монады позволяют мне перегрузить операторы := и ; определив в них свою семантику. Это случилось во времена Лиспа — не чистого и строгого языка.

Некоторые функциональщики как собаки: глаза умненькие, понимающие, а объяснить не могут (но задорно пытаются).
«Первоначально он отнёсся ко мне неприязненно и даже оскорблял меня, то есть думал, что оскорбляет, называя меня собакой, — тут арестант усмехнулся, — я лично не вижу ничего дурного в этом звере, чтобы обижаться на это слово…»
ИМХО, основная беда теории категорий не в сложности, а в разболтанности и неоднозначности её языка. Тебе кажется, что ты легко воспринял вводную, но не обратил внимания на нюанс, который выстрелит в ногу сильно позже.
Категории очень легко и естественно визуализируются как ориентированные графы. В принципе, любой ориентированный граф можно достроить до категории, добавив композиции морфизмов и тождественные морфизмы, если необходимо.
«если необходимо»? То есть, можно назначать композиции уже имеющимся морфизмам? (Иногда можно) А вот попробуйте решить задачку, которую я сам себе в своё время придумал, а решая просветлился.
Постройте для произвольного графа категорию, где стрелки — всевозможные маршруты в данном графе без повторяющихся вершин, то есть почти свободная категория, но со стянутыми в id циклами, всё просто, riiight?
Внезапно, такие приколы есть и в «строгом» хаскелле, за примером далеко ходить не надо:
Существует целый класс типов, который так и называется, Functor.
Эта фраза меня просто убивает, именно потому что так говорят все, а это неверно. Тип не может быть функтором, функтором может быть конструктор типа. [a] — не тип, тип — [Int]. Это дико мешает начинающему воспринять концепцию параметрических типов, ибо тех поначалу за одинаковым синтаксисом не замечаешь. В С++ тут будет шаблон и его, по крайней мере, сразу видно.

Тяжёлое наследие сортов. [a] это тип, просто сорт у него не , а ->*.

Тип это то, что принадлежит категории Hask, то, экземпляр чего можно создать, (кроме Void).

Компилятор считает по-другому. Для совмещения теории с практикой нужна формулировка что объекты категории Hask это типы сорта *.

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

Синтаксис-то разный. Там первая буковка большая, а там маленькая.

UFO landed and left these words here
UFO landed and left these words here

Раньше не поддерживал, кстати. Спасибо, что попробовали, буду иметь в виду.

2 вывода по статье.
* Автор явно не Фейнман. На кого рассчитана статья не понятно.
* Отличный пример, что использовать монады можно понимая ничего в теоркате, а значит и углубляться в него нужно только если действительно хочется.

Монады может быть, но есть ещё профункторы, естественные преобразования, комонады,T-алгебры и коалгебры, монадические трансформеры, линзы…
Можно интегралы считать как площадь под графиком трафаретом, по клеточкам, и ныть что высшая математика это сложно и никому не нужно, потому что есть трафарет а самые частые случаи уже посчитали (это я про паттерны проектирования).

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

Чтобы мосты строить тоже снипы есть, но, почему-то, строители учат и математический анализ и механику и сопротивление

«И математика, и физика — экспериментальные науки» академик Арнольд. www.mccme.ru/edu/index.php?ikey=viarn_burbaki теория категорий, теория множеств не основы математики, не язык математики, а просто лабаротория для умтсвенного поиска особого рода. Для вычисления интегралов другие эксперименты в другой лабартории нужны, и практически, другие люди.
А какие вопросы к Арнольду? Что касается меня — то два образования одно из них университетский бакалавр в прикладной математике хотя математиком не стал, да.

У меня университетский магистр в прикладной математике, но хоть что-то в ней я начал понимать только тогда, когда сам стал ботать матлог и прочую абстрактную алгебру. Хотя сейчас я всё равно больше не понимаю, чем понимаю.

Бесконечное количество аксиоматик и утверждений. А практически, я бы лично хотел чтобы дифуры и интегральные уравнения и другая аналитика шли с первого курса (у меня было не так). Не все интересно.

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

Тогда мне было это интересней, чем передоказательство всего начиная с теории множетсв. Приведенную цитату Арнольда, я понимаю и так «в математике можно работать и с интуитивными понятиями (а на практике и с утверждениями в которые „веришь“ — они были доказаны когда-то, но сам ты это не проверял) потому что в итоге результаты просто работают на практике». И еще в итоге, мне кажется, все равно все всё «в основах» забывают — ресурс головы не безграничен — а для практика ценится будет умение посторить модель. Так лучше уж сразу и держаться моделей.

Я думал это я тугодум, но уже на магитсратуре софтинженерии — пришел на кафедру прикладников — там по каким-то дням решили публично передоказать что-то модное, прозвучавшее (уже не помню что) и раздавали доцентам по страничке подготовить перед студентами и профессурой (многодневное доказательство, странчек на 20-40, но в конкретный день — по 2-3 проходили). Это было мучительно. Каждое утверждение куда-то проваливалось и это было бесконечным углублением. И я понял почему передоказывают вообще «по кафедрам» так мало — ну никому не надо, не интересно, как использовать не ясно, а работа тяжелая. Так вот я вполне могу сказать верю я ваши бурбакизмы и быстрей вернуться к своим моделям.
Ну это всё вопрос интересов, как вы в самых первых пяти словах и написали.

Мне вот теория множеств проще, матлогика там, теория типов. Там всё просто и понятно.

А на первом курсе у нас был линал матричный анализ «линал». Мне тогда было непросто, нелогично и непонятно. Набор правил, который надо выучить, без какой-то изящной внутренней логики и структуры. Через несколько лет попалась Linear algebra done right — ба, да всё естественно и очевидно, если рассуждать алгебраически в терминах операторов, а не циферки в матричках рисовать!

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

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

Т.е. это просто математический инструментарий физика. При этом физика формально не особо-то колышет почему оно «работает», главное — как и где это применять.
Ну, да, поэтому наш курс во многом выродился в «для уравнений такого-то вида надо искать решения в такой-то форме». Скука и ремесленничество.
Ну, да, поэтому наш курс во многом выродился в «для уравнений такого-то вида надо искать решения в такой-то форме».

Да ладно, это даже часто для курса диффуров верно, чо уж там про урматы. Да и "ищем в нужной форме" это ладно еще (и хорошая, кстати, задачка — получить тот же самый результат, но естественным образом, без подгонки), вот "из естественных физических соображений бадумц!" — вот это да! :)

Диффуры я поэтому тоже не любил, но они хотя бы ещё подъёмные. А из-за урматов меня чуть не выперли.

Ну это всё вопрос интересов, как вы в самых первых пяти словах и написали.


И тут приходят профессора, всех строят под свои интересы, утверждают, вон как свидетельствует Арнольд: «ноль это положительное число… и вообще теория множеств — язык математики». А если им аккуратно сказать «я так не думаю»… влепляют минус в карму.

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


А кто, кстати, когда утверждает, что ноль — положительное число? Ни разу такого утверждения не видел.

В западной математической школе ноль «по-умолчанию» включают в множество натуральных чисел. Это можно понять как «положительность нуля»

Зачем это так понимать?


Ну и если множество натуральных чисел строить индуктивно, то ноль принадлежит этому множеству по определению. А даже если ничего индуктивно не строить и не заморачиваться такими слоожными вопросами, как что такое натуральное число, то наличие единичного элемента по сложению, например, приятно, и почему бы его не поиметь нахаляву?

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

Важно следить как это влияет на дальнейшие формулировки и доказательства.

Ага, можно начать с обоснованности принципа математической индукции.


Если я пишу


data Nat : Type where
  Z : Nat
  S : Nat -> Nat

то принцип матиндукции я получаю нахаляву, например (т. е. соответствующий тип населён и довольно просто строится).

Арнольду из указанной ссылки принадлежит высказывание про экспериментальность.
В остальном, это древний холивар про нужна ли математика подставить_профессию_по_желанию.

Автору спасибо за попытку, но, к сожалению, все-таки не для программиста(= В отличие от этой статьи: после десятка подобных вашей наконец-то пришло понимание. А все потому, что ничего лишнего в объяснении — только код.

Есть хороший критерий для самопроверки. Вы должны потерять способность объяснять что такое монады.

А я попробую объяснить "для программистов", по-проще.


И так, начальные условия. Мы что-то там начитались про функциональщину и теперь взяли строгий принцип: только чистые функции, только хардкор!


Берём чрезвычайно программистский язык С (не волнуйтесь, кода не будет). В том числе и потому что там можно всё писать чисто на чистых функциях. Ну там main получает указатель на массив строк аргументов и выходит с интовым кодом возврата (т.е. это (**char -> int) ), а вся грязь только от нас.


Отлично. Пока полёт нормальный. Хотим написать утилитку, которая берёт из аргумента имя и выводит на экран "Привет, имярек!" (чуть более сложный Hello, world!).
Тут у нас случается "упс". В **char -> int ничего нет про экран. Т.е. хотим грязного! Как работать с грязью? Правильно — абстрагироваться.


И так есть экран, чьё состояние изменится, но мы хотим оставить наши руки чистыми. Мы говорим себе — нет экран на самом деле не изменится. Точнее, что он там себе изменится внутри — нас, чистюль, не колышет, для нас он как был так и останется сам собой — экраном. Точнее, сохраняет свой тип (а конкретное значение меняется).


Какие бы мы преобразования над экраном не делали, они дадут нам экран. Другой экран уже с начерканными там словесами, но экран. А раз так, то мы — чисты, мы ничего не меняли! Это как 2+2=4 сложить. Только мы берём экран, берём операцию написания слов, применяем, получаем другой экран. С точки зрения нашего кода и нашего абстрагирования от экрана — никаких побочных эффектов.(Чтоб программа main как функция была не тривиальной ещё возвращаем из неё 1, если экран вычислился какой-то ошибочный, и 0 если нет)


Как не сложно догадаться такое понимание экрана — и есть монада.


Т.е. мы все грязные изменения состояния превращаем в абстракции над вычислениями (ой это уже сложно зашло).

Хорошо, экран — монада, но ведь монада — не экран.


Как объяснить std::optional и std::vector в рамках монады, например?

А зачем вектор в монаду засовывать? Там где гуляют монады — вектора имютабельны должны быть. Иначе — костыли городить. Нехорошо :)

С опшиналом так же как и с экраном. _Считаем_ внутренние состояния (в данном случае null — не null) просто за различные константные значения, изменения — операциями (блин, так и подмывает «функторы» и «морфизмы» писать, но я пока держусь).
Тем более там операции-то простые null с чем угодно даёт null, а что угодно с не null даёт туже операцию.

Моё описание имеет некоторую общность.

Вот монаду Promise так просто уже тяжелее будет объяснить в таких упрощённых терминах, потому и не буду.
А зачем вектор в монаду засовывать? Там где гуляют монады — вектора имютабельны должны быть. Иначе — костыли городить. Нехорошо :)

Ну это так, как аналог List.


А про иммутабельность — ну, если у вас там линейные типы, или если компилятор может что-то доказать, то иммутабельность совершенно необязательна.


Вот монаду Promise так просто уже тяжелее будет объяснить в таких упрощённых терминах, потому и не буду.

После Promise можно и Logic :)

Автору спасибо за попытку, но, к сожалению, все-таки не для программиста(= В отличие от этой статьи: после десятка подобных вашей наконец-то пришло понимание. А все потому, что ничего лишнего в объяснении — только код.

Первый пример — сильно обрезанный экземпляр монады Writer с единственной возможной функциональностью которую можно реализовать в данном случае — конкатенация вывода лог-сообщений. Третий пример (когда функции класса Employee могут возвращать None вместо значения) можно интерпретировать как монаду Maybe. Но как было уже сказано: "Монада — это не maybe. Наоборот maybe — это монада"

Во втором примере монады как таковой нет. Да, это effectful-вычисления, но ни в каком виде не монада. Что-то типа:
data Backtrace a = Backtrace {
                     getResult :: a,
                     getBacktrace :: [a]
                   }

class IdFunctor f where
  ($$) :: (a -> a) -> f a -> f a
  infixr 0 $$

instance IdFunctor Backtrace where
  ($$) f (Backtrace x b) = Backtrace (f x) (x : b)

withBacktrace :: a -> Backtrace a
withBacktrace x = Backtrace x []


f1 x = x + 1
f2 x = x + 2
f3 x = x + 3

-- обычное вычисление
result1 = f3 $ f2 $ f1 $ 0

-- вычисление с обратной трассировкой
resultWithBacktrace = f3 $$ f2 $$ f1 $$ withBacktrace 0

result2 = getResult resultWithBacktrace
backtrace = getBacktrace resultWithBacktrace
Only those users with full accounts are able to leave comments. Log in, please.