Монады с точки зрения программистов (и немного теории категорий)

    Введение


    Как узнать, что человек понял, что такое монады? Он сам вам об этом расскажет в первые 5 минут общения и обязательно попробует объяснить. А ещё напишет об этом текст и по возможности где-нибудь его опубликует, чтобы все остальные тоже поняли, что такое монады.


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


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


    Моё изложение во многом основывается на книге Бартоша Милевски "Теория категорий для программистов", которая создавалась как серия блогпостов, доступна в PDF, а недавно вышла в бумаге.


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



    Категории


    Определение


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


    Используется следующая нотация:


    • ObC — объекты категории C;
    • HomC(A, B) — морфизмы из A в B;
    • g ∘ f — композиция морфизмов f и g.

    В определении категории на морфизмы накладываются дополнительные ограничения:


    1. Для пары морфизмов f и g, если f — морфизм из A в B (f ∈ Hom(A, B)), g — морфизм из B в C (g ∈ Hom(B, C)), то существует их композиция g ∘ f — морфизм из A в C (g ∘ f ∈ Hom(A, C)).
    2. Для каждого объекта задан тождественный морфизм idA ∈ Hom(A, A).

    Существуют два важных свойства, которым должна удовлетворять любая категория (аксиомы теории категорий):


    1. Ассоциативность композиции: h ∘ (g ∘ f) = (h ∘ g) ∘ f;
    2. Композиция с тождественным морфизмом: если f ∈ Hom(A, B), то f ∘ idA = idB ∘ f = f.

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



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


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


    Типы и функции


    Рассмотрим категорию, в которой объектами являются типы языка Haskell, а морфизмами — функции. Например, типы Int и Bool — это примеры объектов, а функция типа Int -> Bool — морфизм.


    Тождественным морфизмом является функция id, возвращающая свой аргумент:


    id :: a -> a
    id x = x

    Композиция морфизмов — это композиция функций, причём синтаксис этой операции на Haskell почти идентичен математической нотации:


    f :: a -> b
    g :: b -> c
    g . f :: a -> c
    (g . f) x = g (f x)

    Категория, в которой объекты представляют собой множества, а морфизмы — отображения, называется Set. Типы также можно рассматривать как множества допустимых значений, а функции — как отображения множеств, но с одной оговоркой: вычисления могут не завершиться. Для обозначения такой ситуации вводится специальный тип bottom, обозначаемый _|_. Считается, что любая функция либо возвращает значение типа, указанного в сигнатуре, либо значение типа bottom. Категория типов языка Haskell, в которой учитывается эта особенность, называется Hask. Для упрощения изложения мы будем работать с ней так же, как с категорией Set. Любопытно, что в этой категории морфизмы между двумя объектами в свою очередь образуют множество, которое является объектом в этой же категории: HomC(A, B) ∈ C. Действительно, функциональный тип a -> b — это тоже тип языка Haskell.


    Рассмотрим несколько примеров типов с точки зрения теории категорий.


    Пустому множеству соответствует тип Void, у которого нет значений (такой тип называется ненаселённым). Существует даже полиморфная функция absurd, которую можно определить, но невозможно вызвать, поскольку для вызова ей нужно передать значение типа Void, а таких значений нет:


    absurd :: Void -> a

    Множеству из одного элемента соответствует тип Unit, единственное значение которого — пустой кортеж, также обозначаемый (). Полиморфная функция unit возвращает это значение, принимая на вход любой другой тип:


    unit :: a -> Unit
    unit _ = ()

    Множество из двух элементов — логический тип Bool:


    data Bool = True | False

    Построим категорию, объектами в которой являются типы Void, Unit и Bool.


    Из Void выходят только морфизмы, соответствующие функции absurd, один в Bool, один в Unit. В обратную сторону морфизмов нет, поскольку мы никак не можем получить значения ненаселённого типа Void, просто потому что их не существует, а значит и не можем определить тело этой функции.


    Функция типа Bool -> Unit может быть только одна, unit, поскольку независимо от входного значения мы можем вернуть только пустой кортеж. А вот для реализации функции типа Unit -> Bool возможны варианты. Такая функция в любом случае будет принимать значение (), но вернуть можно либо True, либо False. Таким образом, получили два морфизма из Unit в Bool:


    true, false :: a -> Bool
    true _ = True
    false _ = False

    Морфизмы из Bool в Bool — это булевы функции от одного аргумента, которых 4 штуки (в общем случае количество булевых функций от n аргументов — 22n): id, true и false, которые мы уже определяли выше, а также функция not:


    not :: Bool -> Bool
    not True = False
    not False = True

    Добавив тождественные морфизмы, получим следующую категорию:



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


    Функторы


    Функтор — это отображение категорий. Для двух категорий, C и D, функтор F осуществляет два вида преобразований. Во-первых, функтор преобразует объекты из категории C в объекты из категории D. Если a — объект из категории C, то F a — соответствующий ему объект из категории D, полученный в результате применения функтора. Во-вторых, функтор действует на морфизмы: морфизм f :: a -> b в категории C преобразуется в морфизм F f :: F a -> F b в категории D.



    Кроме того, должны выполняться "законы сохранения" композиции и тождественного морфизма:


    1. Если h = g ∘ f, то F h = F g ∘ F f.
    2. Если ida — тождественный морфизм для объекта a, то F ida = idF a — тождественный морфизм для объекта F a.

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


    Функторы можно комбинировать аналогично функциям. Для двух функторов, F :: C -> D и G :: D -> E можно определить композицию G . F :: C -> E. Кроме того, для каждой категории несложно построить тождественный функтор, который будет переводить как объекты, так и морфизмы, в самих себя. Обозначим их IdC, IdD и IdE. Функторы, действующие из категории в неё саму, называются эндофункторы.



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


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


    Конструктор типа Maybe является примером такого функтора, он преобразует тип a в тип Maybe a (сам по себе Maybe не является типом!):


    data Maybe a = Nothing | Just a

    Для преобразования морфизмов, необходимо уметь получать из функции f :: a -> b функцию F f :: Maybe a -> Maybe b. Это действие можно записать в виде функции высшего порядка fmap. Обратите внимание, что тип этой функции как раз наглядно описывает преобразование морфизмов (из одной функции получаем другую):


    --          f                F f
    --      /------\    /------------------\
    fmap :: (a -> b) -> (Maybe a -> Maybe b)
    fmap _ Nothing = Nothing
    fmap f (Just x) = Just (f x)

    Конечно, Maybe — не единственный функтор. Существует целый класс типов, который так и называется, Functor. Единственным методом этого класса является функция fmap, показывающая, как преобразуются морфизмы (а преобразование объектов — это применение конструктора типа к конкретным типам):


    class Functor f where
      fmap :: (a -> b) -> f a -> f b

    В более прикладном смысле функторы — это контейнеры, которые хранят значения других типов, а функция fmap позволяет преобразовывать объекты внутри этих контейнеров. Если опустить скобки вокруг f a -> f b, как это сделано в определении выше, то такая сигнатура лучше подойдёт для описания функторов как контейнеров.


    Монады


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


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


    В качестве примера возьмём функции для преобразования строк: upCase, переводящую все символы строки в верхний регистр, и toWords, разбивающую строки на слова. Пока что их реализация всего лишь использует встроенные функции toUpper и words:


    upCase :: String -> String
    upCase = map toUpper
    
    toWords :: String -> [String]
    toWords = words

    Типы функций позволяют составить их композицию:


    processString :: String -> [String]
    processString = toWords . upCase

    Чтобы не отвлекаться на сложности реализации, в качестве представления лога возьмём просто строку. Мы хотим, чтобы после применения функции processString в логе содержалась запись "upCase toWords".


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


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


    newtype Writer a = Writer (a, String)

    Заметим, что Writer — это функтор, мы легко можем описать для него функцию fmap:


    instance Functor Writer where
      fmap f (Writer (x, s)) = Writer (f x, s)

    Преобразуем функции upCase и toWords и сделаем так, чтобы они возвращали значение, "завёрнутое" в тип Writer:


    upCase :: String -> Writer String
    upCase s = Writer (map toUpper s, "upCase ")
    
    toWords :: String -> Writer [String]
    toWords s = Writer (words s, "toWords ")

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


    compose :: (a -> Writer b) -> (b -> Writer c) -> (a -> Writer c)
    compose f g = \x -> let Writer (y, s1) = f x
                            Writer (z, s2) = g y
                        in Writer (z, s1 ++ s2)

    Реализация функции processString принимает вид:


    processString :: String -> [String]
    processString = compose upCase toWords

    Вернёмся к понятиям теории категорий. Мы можем заменить все функции (морфизмы) типа a -> b на a -> Writer b и получить новую категорию, в которой такие функции являются новыми морфизмами между a и b. Осталось только определить тождественный морфизм, т.е. функцию типа a -> Writer a:


    writerId :: a -> Writer a
    writerId x = Writer (x, "")

    Таким образом, мы получили новую категорию, основанную на Hask. В этой категории объектами являются те же типы, но морфизмом между типами a и b являются функции типа не a -> b, а a -> m b, т.е. возвращаемый тип "завёрнут" в какой-то другой фиксированный конструктор типа m. Такие функции мы будем называть обогащёнными (embellished). Композиция морфизмов и тождественный морфизм зависят от m, реализация для Writer — лишь частный случай.


    Обобщим сказанное выше для любой категории C и эндофунктора m. Построим категорию K, состоящую из тех же объектов, что и категория C, т.е. ObK = ObC. Морфизм a -> b в категории K соответствует морфизму a -> m b в исходной категории C: HomK(a, b) = HomC(a, m b). Для типов и функций это будет означать, что функции, работающие с типами в категории K — обогащённые функции в категории C.


    Для того, чтобы получить настоящую категорию с такими морфизмами, необходимо определить ассоциативную композицию морфизмов и тождественный морфизм для каждого объекта таким образом, чтобы соблюдались аксиомы теории категорий. Тогда полученная категория называется категорией Клейсли, а функтор m — монадой. На Haskell это определение выглядит так (везде далее используются типы из категории Hask):


    class Monad m where
      -- композиция морфизмов категории Клейсли
      (>=>)  :: (a -> m b) -> (b -> m c) -> (a -> m c)
      -- тождественный морфизм
      return :: a -> m a

    Тип оператора >=>, который иногда называют "fish", показывает суть монад: это способ композиции обогащённых функций. Побочные эффекты, работа с состоянием, исключения — всё это лишь частные случаи, они не определяют, что такое монады, но являются хорошими примерами их использования. Writer — тоже монада, определённая нами функция compose — это композиция морфизмов >=>, а функция writerId — тождественный морфизм return.


    Начнём реализовывать оператор >=> в общем виде. Результатом композиции двух обогащённых функций должна быть тоже функция, поэтому в реализации используем лямбда-выражение. Аргумент этого выражения имеет тип a, и у нас есть функция f, которую к нему можно применить, а дальше работать с результатом её вычисления во вспомогательной функции, которую назовём bind:


    f >=> g = \a -> let mb = f a
                    in (bind mb g)
      where
        bind :: m b -> (b -> m c) -> m c

    Функция bind получает значение типа b "в контейнере" m и функцию, которая принимает на вход аргумент типа b и возвращает m c. Тип результата у неё совпадает с типом результата >=>. Мы получили следующую сигнатуру типов: m b -> (b -> m c) -> m c. Эту функцию необходимо реализовать для каждой монады, чтобы описать способ композиции обогащённых функций. Мы подошли к "классическому" определению монад в Haskell в виде класса типов с методами >>=, или bind, и return:


    class Monad m where
      (>>=)  :: m a -> (a -> m b) -> m b
      return :: a -> m a

    Теперь попробуем продолжить реализацию в общем виде, для чего каким-то образом нужно применить функцию типа b -> m c к значению типа b, но у нас есть только значение типа m b. Вспомним, что изначально мы работаем с эндофунктором m, поэтому можем воспользоваться функцией fmap, которая в данном случае будет типа (a -> m b) -> m a -> m (m b). Для завершения реализации >>= необходимо преобразовать значение типа m (m b) к m b, "схлопнув" контейнеры, но это уже невозможно сделать в общем виде. Выразим это преобразование как функцию join:


    ma >>= g = join (fmap g ma)
      where
        join :: m (m a) -> m a

    Например, для типа Writer реализация будет следующей:


    join :: Writer (Writer a) -> Writer a
    join (Writer ((Writer (x, s2)), s1)) = Writer (x, s1 ++ s2)

    Мы пришли к третьему определению класса Monad:


    class Functor m => Monad m where
      join   :: m (m a) -> m a
      return :: a -> m a

    Здесь присутствует явное требование на то, чтобы m был функтором. Это ограничение не было нужно для предыдущих определений, поскольку функция fmap реализуется через >>=:


    fmap :: (a -> b) -> m a -> m b
    fmap f ma = ma >>= (\a -> return (f a))

    Практическое применение монад


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


    Недетерминированные вычисления


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


    Композиция обогащённых функций должна иметь тип (a -> [b]) -> (b -> [c]) -> (a -> [c]). Достаточно опытному программисту реализация может быть понятна из одной этой сигнатуры:


    (>=>) :: (a -> [b]) -> (b -> [c]) -> (a -> [c])
    f >=> g = \x -> concat (map g (f x))

    Типы практически не оставляют возможности сделать неверный шаг. Получив значение типа a, единственное, что можно с ним сделать — это применить функцию f и получить список [b]. Далее всё, что мы умеем делать со значениями типа b — применить функцию g к каждому из них в списке и получить в результате список списков: map g (f x) :: [[c]]. Чтобы получить нужный результат, конкатенируем их.


    Тогда оператор >>= можно записать так:


    (>>=) :: [a] -> (a -> [b]) -> [b]
    xs >>= f = concat (map f xs)

    Осталось реализовать функцию return :: a -> [a]. Здесь реализация снова выводится из типа:


    return :: a -> [a]
    return x = [x]

    Резюмируем эти реализации в классическом определении класса Monad:


    instance Monad [] where
      xs >>= f = concat (map f xs)
      return x = [x]

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


    Исключения


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


    Для этого подойдёт, к примеру, уже рассмотренный функтор Maybe. При штатной работе функции будут возвращать значение в конструкторе Just, при возникновении ошибки — значение Nothing. Композиция таких обогащённых функций должна работать таким образом, чтобы каждая следующая функция не вызывалась, если ошибка уже произошла в предыдущем вызове:


    (>=>) :: (a -> Maybe b) -> (b -> Maybe c) -> (a -> Maybe c)
    f >=> g = \x -> case f x of
                      Just y  -> g y
                      Nothing -> Nothing

    Определение методов класса Monad для Maybe:


    instance Monad Maybe where
      (Just x) >>= f = f x
      Nothing  >>= f = Nothing
      return x = Just x

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


    data Either a b = Left a | Right b

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


    type WithException a = Either String a

    Композиция функций описывается аналогично случаю с использованием Maybe:


    (>=>) :: (a -> WithException b) -> (b -> WithException c) -> (a -> WithException c)
    f >=> g = \x -> case f x of
                      Right y -> g y
                      err     -> err

    Определение экземпляра класса Monad уже не должно вызывать трудностей:


    instance Monad WithException where
      (Right x) >>= f = f x
      err >>= f = err
      return x = Right x

    Состояния


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


    a -> s -> (b, s)

    Объявим синоним типа для работы с состоянием:


    newtype State s a = State (s -> (a, s))

    Фиксируем тип состояния s и покажем, что State s является функтором. Нам понадобится вспомогательная функция runState:


    runState :: State s a -> s -> (a, s)
    runState (State f) s = f s

    Реализация класса Functor:


    instance Functor (State s) where
      fmap f state = State st'
        where
         st' prevState = let (a, newState) = runState state prevState
                         in (f a, newState)

    Таким образом, обогащённые функции из a в b, в которых ведётся работа с состоянием, имеют тип a -> State s b, причём State s — функтор. Попробуем превратить его в монаду, реализовав их композицию:


    (>=>) :: (a -> State s b) -> (b -> State s c) -> (a -> State s c)
    f >=> g = \x -> State (\s -> let (y, s') = runState (f x) s
                                 in runState (g y) s')

    Отсюда получим реализацию класса Monad. Тождественный морфизм, функция return, ничего не делает с состоянием, а просто добавляет свой аргумент в пару-результат:


    instance Monad (State s) where
      stateA >>= f = State (\s -> let (a, s') = runState stateA s
                                  in runState (f a) s')
      return a = State (\s -> (a, s))

    Функция для чтения состояния должна всегда возвращать текущее состояния, ей не нужны аргументы. Можно сказать, что это морфизм из Unit в s в категории Клейсли, поэтому функция должна иметь тип Unit -> State s s:


    get :: Unit -> State s s
    get _ = State (\s -> (s, s))

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


    Запись состояния, наоборот, принимает новое значение состояния и записывает его. Нам важен только побочный эффект, но не возвращаемое значение, поэтому соответствующий морфизм идёт в обратную сторону, из s в Unit, а функция имеет тип s -> State s Unit:


    put :: s -> State s Unit
    put s = State (\_ -> ((), s))

    Реализация работы с состоянием вышла чуть более сложной, чем предыдущие примеры, но на её основе можно создать формальную модель для работы с вводом/выводом. В ней предполагается, что "окружающий мир" реализован в виде некоторого типа RealWorld, о котором неизвестно никаких подробностей. При общении с внешним миром программа получает на вход объект типа RealWorld и производит какие-то действия, в результате которых получает нужный ей результат и изменяет внешний мир (например, выводит строку на экран или читает символ из потока ввода). Эту абстрактную концепцию можно реализовать с помощью состояния:


    type IO a = State RealWorld a

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

    Поддержать автора
    Поделиться публикацией

    Комментарии 267

      –1

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

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

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

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

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

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

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

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


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

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

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


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

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

                        –1

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

                          +4

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


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


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

                            –3

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

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

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


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


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


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

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

                                –1

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


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

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

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


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


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

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


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

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

                                    –3

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


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

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

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


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


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

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


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

                                        0

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


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

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

                                          Не совсем, но оно ближе к формальной верификации, чем к тестам. Собственно, если взять тот же 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(), и далее по тексту.

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

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

                                            0

                                            Любое другое незанятое, потому что на википедии, например, написано, что фабричный метод 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?


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

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

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

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

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

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

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

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

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

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


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


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

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

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


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

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

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

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


                                                          -- уау экзистенциальные типы вместо базового класса
                                                          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, но типы заставили меня исправить это.

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

                                                            Название такое.
                                                              0
                                                              А зачем тут какая-то фабрика?

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

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

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

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

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

                                                                        0
                                                                        Тогда зачем эта ничего не решающая функция нужна?
                                                                          +1

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


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


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

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


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

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

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


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

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

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

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


                                                                              Почему?

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

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

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


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

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

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

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


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

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

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

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


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

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

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

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


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

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


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

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

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

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


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

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

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

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


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

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

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

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


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

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

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

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


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

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

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


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

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

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


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

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

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

                                                                                        0

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

                                                                                          0

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

                                                                                            0

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


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

                                                                                              0

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

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

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

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

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


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


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


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

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

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


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

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

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

                                              +1

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


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


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

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

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


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

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

                                                  0

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

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

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

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

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

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

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

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


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

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

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

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

                                                          0
                                                          По-сути монада — это интерфейс с парой методов.
                                                            0

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


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

                                                              0

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

                                                                0

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

                                                                  0

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

                                                                    +2

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


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


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

                                                                      0
                                                                      Получается они от паттернов ООП никак не избавляют.
                                                                        0

                                                                        Да, от паттернов ООП избавляют не монады.

                                                                          0
                                                                          А от них вообще можно избавиться на современном этапе?
                                                                            0
                                                                            На такие вопросы сложно отвечать в общем.

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

                                                                              Может дело в специфике решаемых задач?

                                                                                0

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


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

                                                                                  0

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

                                                                                    0

                                                                                    А что не поддаётся, например?

                                                                                      0

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

                                                                                        0

                                                                                        Так вопрос в том, когда ООП прям ппц удобно.

                                                                                        +1

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

                                                                                          0

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


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

                                                                          +1

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


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

                                                                            0

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

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

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

                                                                                0

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

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

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


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

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


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

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

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

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

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


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

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

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

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


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

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

                                                                      Именно!


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

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


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


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


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

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


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

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

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

                                                                          0

                                                                          Есть, компилятор не пропустит.

                                                                            –3

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


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

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

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


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

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


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

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

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

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


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

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


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

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


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


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

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

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


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

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


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

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

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

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


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

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


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

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


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

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

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


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


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

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


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

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

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

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


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

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


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

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


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

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

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

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

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


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

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


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

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


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

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


                                                                                            мы получим

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

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

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


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


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

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


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


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

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


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

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


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

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


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

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


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

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


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

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

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

                                                                                                Как нет? Есть. 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 — динамический ЯП. Там нет типов.


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

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


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

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

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

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


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

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


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


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

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


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

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


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

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


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


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

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


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


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


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


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

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

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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

                                                                                                    Именно так.


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

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


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

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

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

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


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

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


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


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

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


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

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


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


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

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


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

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


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

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


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

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

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

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


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

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


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


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

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


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

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


                                                                                                        Какой и где?

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


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

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


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

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

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

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


                                                                                                          Добавьте в Г терм типа ∀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-абстракции.

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

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


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

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


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

                                                                                                            Почему?


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

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


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

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


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


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

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

                                                                            0

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

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

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

                                                                                0

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

                                                                                  0

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

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

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

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

                                                                    За что, простите?

                                                                      +14
                                                                      За вот это вот «монады − это просто, как раздватри» и последующую стену текста на эльфийском языке. «Ты с кем разговариваешь, папа?», хабр эдишен.
                                                                        0

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

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

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

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

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

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

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

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


                                                                              void main(void) {..}
                                                                              
                                                                              main :: IO ()

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

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

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

                                                                                x :: ()
                                                                                x = ()
                                                                                


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

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

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


                                                                                    Ну и вещи типа


                                                                                    void f() {}
                                                                                    
                                                                                    void g() { return f(); }

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


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

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

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


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


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

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


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

                                                                                          0

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

                                                                                            0

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


                                                                                            А так...


                                                                                            consumeVoid : Void -> Int
                                                                                            consumeVoid v = 42

                                                                                            > :doc consumeVoid
                                                                                            Main.consumeVoid : Void -> Int
                                                                                            
                                                                                                The function is Total

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

                                                                                              0

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

                                                                                                0

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


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

                                                                                              0

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

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

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

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

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

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

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

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

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


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

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

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

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

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


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

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

                                                                                              +1
                                                                                              Да 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(), а для нулаблов своя.
                                                                                              • НЛО прилетело и опубликовало эту надпись здесь
                                                                                                0
                                                                                                Мне всегда интересно было, почему, например, Монаду нельзя описать как некую хрень с сайдэффектом или как некую защитную обертку над другими типами.
                                                                                                Почему — как раз можно, и это наиболее понятный и утилитарно-обоснованный способ.
                                                                                                Почему обязательно пытаться притягивать категории, функторы?
                                                                                                Это просто хаскеллисты притягивают, поскольку многие концепции и подходы к структурированию задач в Хаскеле могут быть описаны в рамках теории категорий, т.е. математически. А это очень подкупает.
                                                                                                И кстати, я случаем не пропустил в статье сказочное утверждение, что в хаскеле попав в монаду, из нее нельзя выйти?
                                                                                                Например из монады IO выйти нельзя — «распаковать» значение без доступного конструктора или спецфункции компилятора не получится.
                                                                                                  –2
                                                                                                    +1
                                                                                                    Правильная ссылка на книжку здесь. А то версия 1.2.0 уже устарела…
                                                                                                      0
                                                                                                      Спасибо, поправила ссылку.
                                                                                                      0
                                                                                                      А зачем все это? Зачем так все усложнять? Функциональное программирование хорошо в меру. Зачем из него делать целый язык?
                                                                                                        +2
                                                                                                        Мне Haskell переусложненным не кажется, он просто функциональный и все. Уложить его в голову требует некоторого ментального усилия(а у людей с математической подготовкой и не требует), но далее все просто и понятно. Мне кажется переусложненным например scala, где функционального в меру, процедурного в меру, ООП в меру, присутствуют и наследование и композиция и символические преобразования и последовательные вычисления, с миру по нитке. Это все дается ценой запутанного синтаксиса и невнятной идиоматики. То же Rust — то ли Haskell для бедных, то ли переHaskell. Haskell на мой взгляд яс