Пользователь
0,1
рейтинг
4 февраля 2009 в 12:11

Разработка → Классы типов, монады

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

Предыдущие статьи:
Типы данных, паттернг матчинг и функции
Основы

Классы типов


Предположим, вы написали свой тип данных и хотите написать для него оператор сравнения. Проблема в том, что перегрузки функций в Хаскеле нет и потому использовать для этих целей (==) простым способом не получится. Более того, придумывать для каждого типа новое имя — не вариант.
Но вы можете определить класс типов «сравниваемый». Тогда, если ваш тип данных принадлежит к этому классу, значения этого типа можно будет сравнивать.
class MyEq a where
    myEqual :: a -> a -> Bool
    myNotEqual :: a -> a -> Bool
MyEq — это имя класса типов (как и типы данных, оно должно начинаться с заглавной буквы), a — некий принадлежащий к данному классу тип. Параметров у класса может быть несколько (FooClass a b c), но в данном случае только один.
Тип a принадлежит к классу MyEq, если для него определены соотстветствующие функции.
В классе можно дать определение функции по умолчанию. Например, функции myEqual и myNotEqual могут быть выражены друг через друга:
    myEqual x y = not (myNotEqual x y)
    myNotEqual x y = not (myEqual x y)
Такие определения приведут к бесконечной рекурсии, но в экземпляре класса достаточно определить хотя бы одну из них.
Теперь напишем экземпляр класса для Bool:
instance MyEq Bool where
    myEqual True True = True
    myEqual False False = True
    myEqual _ _ = False
Определение экземпляра начинается с ключевого слова instance, затем вместо переменной типа a в определении самого класса мы пишем тот тип, для которого определяется экзмепляр, т.е. Bool.
Определяем одну функцию myEqual и теперь можно проверить в интерпретаторе результат:
ghci> myEqual True True
True
ghci> myNotEqual True False
True
ghci> :t myEqual
myEqual :: (MyEq a) => a -> a -> Bool
Видим, что тип функции myEqual накладывает ограничение (constraints) на тип — он должен принадлежать к классу MyEq.
Такие же ограничения можно накладывать и при объявлении самого класса:
class (MyEq a) => SomeClass a where
    -- ...

Классы чем-то похожи на интерфейсы — мы объявляем фукнции, для которых потом предоставляем реализации. Другие функции могут использовать эти функции только если для некоторого типа есть такая реализация.
Однако есть существенные отличия как в возможностях, так и в реализации самого механизма.
  1. Как видно по функции myEqual, она принимает два значения типа a, тогда как виртуальная функция принимает только один скрытый параметр this.
    Даже если у нас есть instance MyEq Bool и instance MyEq Int, вызов функции myEqual True 5 приведёт к неудаче:
    ghci> myEqual True (5::Int)

    <interactive>:1:14:
        Couldn't match expected type `Bool' against inferred type `Int'
        In the second argument of `myEqual', namely `(5::Int)'
        In the expression: myEqual True (5 :: Int)
        In the definition of `it': it = myEqual True (5 :: Int)
    ghci>
    Компилятор (интерпретатор) знает, что параметры myEqual должны иметь один тип и потому предотвращает такие попытки.
  2. Экземпляр класса может быть определён в любой момент, что также очень удобно. Наследование от интерфейсов же указывается при определении самого класса.
  3. Функция может потребовать принадлежность типа данных сразу к нескольким классам:
    foo :: (Eq a, Show a, Read a) => a -> String -> String
    Как это сделать, например, в С++/C#? Создавать композитный IEquableShowableReadable? Но от него не отнаследуешься. Передавать аргумент три раза с приведением к разным интерфейсам и полагать внутри функции, что это один и тот же объект, а ответственность лежит на вызывающей стороне?

Раз уж упомянул C++, то заодно скажу, что в новом стандарте C++0x concept и concept_map есть суть классы типов, но используемые на этапе компиляции :)

Но есть и недостаток. Как нам завести список объектов, которые, к примеру, принадлежат к классу Show (функция show :: a -> String)?
Способ есть, но он нестандартен. В начало файла необходимо добавить соответствующие опции GHC:
{-#
  OPTIONS_GHC
  -XExistentialQuantification
#-}

-- Не знаю, почему, но при выравнивании TAB'ом GHC опции игнорировал
Теперь мы можем объявить такой тип данных:
data ShowObj = forall a. Show a => ShowObj a
(Прочитать подробнее про existential types)
И заодно определить для него экземляр класса Show, чтобы он сам также к нему принадлежал:
instance Show ShowObj where
    show (ShowObj x) = show x
Проверим:
ghci> [ShowObj 4, ShowObj True, ShowObj (ShowObj 'x')]
[4,True,'x']
Хотя функцию show я явно не вызывал, интерпретатор использует именно её, так что можно быть уверенным, что всё работает.

Как это всё реализуется?


В функцию, которая накладывает ограничения на тип, передаётся скрытый параметр (на каждый класс и тип — свой) — словарь со всеми необходимыми функциями.
Фактически, instance — это определение экземпляра словаря для конкретного типа.
Чтобы было проще это понять, я просто приведу псевдо-реализацию для класса Eq на Haskell'е и на C++:
-- class MyEq
data MyEq a = MyEq {
    myEqual :: a -> a -> Bool,
    myNotEqual :: a -> a -> Bool}

-- instance MyEq Bool
myEqBool = MyEq {
    myEqual = \x y -> x == y,
    myNotEqual = \x y -> not (x == y)}

-- foo :: (MyEq a) => a -> a -> Bool
foo :: MyEq a -> a -> a -> Bool
foo dict x y = (myEqual dict) x y
-- foo True False
fooResult = foo myEqBool True False
То же самое на C++:
#include <iostream><br/>
 <br/>
// class MyEq a<br/>
class MyEq<br/>
{<br/>
public:<br/>
    virtual ~MyEq() throw() {}<br/>
    // принимаем void const *, так как сам тип в базовом классе неизвестен<br/>
    virtual bool unsafeMyEqual(void const * x, void const * y) const = 0;<br/>
    virtual bool unsafeMyNotEqual(void const * x, void const * y) const = 0;<br/>
};<br/>
 <br/>
// Шаблонная обёртка, знающая о типе и потому определяющая<br/>
// безопасные виртуальные функции<br/>
template <typename T><br/>
class MyEqDictBase : public MyEq<br/>
{<br/>
    virtual bool unsafeMyEqual(void const * x, void const * y) const<br/>
    { return myEqual(*static_cast<const *>(x)*static_cast<const *>(y))}<br/>
    virtual bool unsafeMyNotEqual(void const * x, void const * y) const<br/>
    { return myNotEqual(*static_cast<const *>(x)*static_cast<const *>(y))}<br/>
public:<br/>
    virtual bool myEqual (const & x, T const & y) const { return !myNotEqual(x, y)}<br/>
    virtual bool myNotEqual (const & x, T const & y) const { return !myEqual(x, y)}<br/>
};<br/>
 <br/>
// Экземпляры классов. Определяться будут через специализацию.<br/>
template <typename T><br/>
class MyEqDict;<br/>
 <br/>
// Создать словарь для определённого экземпляра класса<br/>
template <typename T><br/>
MyEqDict<T> makeMyEqDict() { return MyEqDict<T>()}<br/>
 <br/>
// instance MyEq Bool<br/>
// Экземпляр класса MyEq для bool<br/>
template <><br/>
class MyEqDict<bool> : public MyEqDictBase<bool><br/>
{<br/>
public:<br/>
    virtual bool myEqual(bool const & l, bool const & r) const { return l == r; }<br/>
};<br/>
 <br/>
// Функиця принимает словарь и два параметра<br/>
// То, что эти параметры на самом деле bool, гарантируется компиляторов Haskell'я<br/>
bool fooDict(MyEq const & dict, void const * x, void const * y)<br/>
{<br/>
    return dict.unsafeMyNotEqual(x, y)// myNotEqual<br/>
}<br/>
 <br/>
// Вспомогательная функция<br/>
// foo :: (MyEq a) => a -> a -> Bool<br/>
template <typename T><br/>
bool foo (const & x, T const & y)<br/>
{<br/>
    return fooDict(makeMyEqDict<T>()&x, &y);<br/>
}<br/>
 <br/>
int main()<br/>
{<br/>
    std::cout << foo(truefalse) << std::endl// 1<br/>
    std::cout << foo(falsefalse) << std::endl// 0<br/>
    return 0;<br/>
}

Некоторые стандартные классы и синтаксический сахар


Enum — перечисление. Определяет функции для получение следующего/предыдущего значения, а также значения по номеру.
Используется в синтаксическом сахаре для списков [1 .. 10], фактически, это означает enumFromTo 1 10, [1,3 .. 10] => enumFromThenTo 1 3 10

Show — преобразование в строку, основная функция show :: a -> String.
Используется, например, интерпретатором для вывода значений.

Read — преобразование из строки, основная функция read :: String -> a.
Не знаю, почему выбрали возвращение значения, а не Maybe a (опциональное значение), чтобы сигнализировать об ошибке «чистым» способом, а не «грязным» исключением.

Eq — сравнение, операторы (==) и (/=) (как бы перечёркнутый знак равенства)

Ord — упорядоченные типы, операторы (<) (>) (<=) (>=). Требует принадлежности типа к классу Eq.

Более полный список различных классов можно посмотреть здесь.

Functor — функтор, функция fmap :: (a -> b) -> f a -> f b.
Чтобы было понятно, приведу примеры применения этой функции:
ghci> fmap (+1) [1,2,3]
[2,3,4]
ghci> fmap (+1) (Just 6)
Just 7
ghci> fmap (+1) Nothing
Nothing
ghci> fmap reverse getLine
hello
"olleh"
Т.е. для списка это просто map, для опционального значения Maybe a функция (+1) применяется, если само значение есть, а для ввода-вывода функция применяется к результату этого ввода-вывода.
Подробнее про ввод-вывод я напишу далее, сейчас можно просто запомнить, что getLine не возвращает строку, так что применить к нему reverse напрямую не получится.

Чтобы каждый раз не определять экземпляры для вновь написанных типов данных, Haskell умеет делать это автоматически для некоторых классов.
data Test a = NoValue | Test a a deriving (Show, Read, Eq, Ord)
data Color = Red | Green | Yellow | Blue | Black | Write deriving (Show, Read, Enum, Eq, Ord)
ghci> NoValue == (Test 4 5)
False
ghci> read "Test 5 6" :: Test Int
Test 5 6
ghci> (Test 1 100) < (Test 2 0)
True
ghci> (Test 2 100) < (Test 2 0)
False
ghci> [Red .. Black]
[Red,Green,Yellow,Blue,Black]

Великие и ужасные монады


Класс Monad представляет собой «вычисление», т.е. он позволяет описывать способ комбинирования вычислений и результатов.
Вряд ли я напишу статью лучше, чем есть уже написанные, так что я просто дам ссылки на лучшие (по моему мнению) статьи для понимания, для чего эти монады нужны.
1. Монады — статья с SPbHUG о монадах на русском языке и с аналогиями на привычных императивных языках.
2. IO inside — статья на английском о вводе-выводе с использованием монад
3. Yet Another Haskell Tutorial — книга по Хаскелю, в разделе Monads очень хорошо написано на примере создания класса Computation, который суть и есть монада.

Здесь я напишу очень вкратце, чтобы можно было потом подглядеть.
Допутим, мы хотим описать последовательные вычисления, где каждое следующее зависит от результатов предыдущего (некоторым не заданным наперёд способом). Для этого можно определить соответствующий класс, в котором должны быть как минимум две функции:
class Computation c where
    return :: a -> c a
    bind :: c a -> (a -> c b) -> c b
Первая функция по сути принимает некоторое значение и возвращает вычисление, которые при выполнении просто вернёт это же самое значение.
Вторая функция принимает 2 аргумента:
1. вычисление, которое при выполнении вернёт значение типа a
2. функцию, которое примет значение типа a и вернёт новое вычисление, возвращающее значение типа b
Результатом будет вычисление, которое вернёт значение типа b.

Зачем всё это может быть нужно, я покажу на примере опционального значения
data Maybe a = Just a | Nothing
Допустим, у нас есть несколько функций, каждая из которых может завершиться неудачей и вернуть Nothing. Тогда, используя их напрямую, мы рискуем получить такой слабочитаемый код:
funOnMaybes x =
    case functionMayFail1 x of
        Nothing -> Nothing -- первая функция ничего нам не вернула, и мы тоже ничего не вернём
        Just x1 -> -- отлично, первая функция вернула некоторое значение, продолжим с ним работу
            case functionMayFail2 x1 of
                Nothing -> Nothing -- теперь вторая функция ничего не вернула (точнее вернула "ничего")
                Just x2 -> -- и так далее
Т.е. вместо простого последовательного вызова этих функций приходится каждый раз проверять значение.
Но ведь у нас язык позволяет передавать функции в качестве аргументов и возвращать так же, воспользуемся:
combineMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b
combineMaybe Nothing _ = Nothing
combineMaybe (Just x) f = f x
Функция combineMaybe принимает опциональное значение и функцию, а возвращает результат применения этой функции к опциональному значению, либо неудачу.
Перепишем функцию funOnMaybes:
funOnMaybes x = combineMaybe (combineMaybe x functionMayFail1) functionMayFail2 --...
Или, используя то, что функцию можно вызвать инфиксно:
funOnMaybes x = x `combineMaybe` functionMayFail1 `combineMaybe` functionMayFail2 --...

Можно заметит, что тип функции combineMaybe в точности повторяет тип bind, только вместо c стоит Maybe.
instance Computation Maybe where
    return x = Just x
    bind Nothing f = Nothing
    bind (Just x) f = f x
Именно так и определена монада Maybe, только bind там называется (>>=), плюс есть дополнительный оператор (>>), который не использует результат предыдущего вычисления.
Кроме того, для монад определён синтаксический сахар, который значительно упрощает их использование:
funOnMaybes x = do
    x1 <- functionMayFail1 x
    x2 <- functionMayFail2 x1
    if x2 == (0)
        then return (0)
        else do
            x3 <- functionMayFail3 x2
            return (x1 + x3)
Обратите внимание на дополнительный do внутри else и на его отсутствие в then. do — это всего лишь синтаксический сахар, который комбинирует несколько вычислений в одно, а так как в ветке then вычисление и так одно (return (0)), то do там не нужен; в ветке else вычисления два подряд, и чтобы их скомбинировать, надо снова использовать do.
Специальный синтаксис с обратной стрелкой (<-) преобразуется очень просто:
1. do {e} -> e
do с одним вычислением есть само это вычисление, комбинировать ничего не нужно
2. do {e; es} -> e >> do {es}
несколько подряд идущих вычислений соединяются оператором (>>)
3. do {let decls; es} -> let decls in do {es}
внутри do можно заводить дополнительные декларации наподобие let ... in
4. do {p < — e; es} -> let ok p = do {es}; ok _ = fail "..." in e >>= ok
если результат первого вычисления используется в дальнейшем, то для комбинации используется оператор (>>=)
В последнем случае такая конструкция используется потому, что в качестве p может выступать образец, который может и не совпасть.
Если он совпадёт, то будет выполнено дальшейшее вычисление, в противном случае будет возвращена ошибка со строковым описанием.
Функция fail — ещё одна дополнительная функция в монаде, которая вообще говоря к концепции монад отношения не имеет.

Какие ещё экземпляры монад есть в стандартной библиотеке?

State — вычисления с состоянием. При помощи функций get/put, которые знают о внутреннем устройстве State, можно получать и устанавливать состояние.
import Control.Monad.State

fact' :: Int -> State Int Int -- тип состояния - Int, тип результата - тоже Int
fact' 0 = do
    acc <- get -- получаем накопленный результат
    return acc -- возвращаем его
fact' n = do
    acc <- get -- получаем аккумулятор
    put (acc * n) -- домножаем его на n и сохраняем
    fact' (n - 1) -- продолжаем вычисление факториала

fact :: Int -> Int
fact n = fst $ runState (fact' n) 1 -- начальное значение состояния - 1
runState вычисляет функцию с состоянием, возвращает кортеж с результатом и изменённым состоянием. Нам нужен только результат, поэтому fst.

Список — тоже монада. Последующие вычисления применяются ко всем результатам предыдущего.
dummyFun contents = do
    l <- lines contents -- получаем все строки
    if length l < 3 then fail "" -- строки менее 3 символов игнорируем
    else do
        w <- words l -- разбиваем строку на слова
        return w -- возвращаем слово
ghci> dummyFun "line1\nword1 word2 word3\n \n \nline5"
["line1","word1","word2","word3","line5"]


Continuations (продолжения) в Хаскеле — тоже монада — Cont
Про продолжения можно почитать на русской вики и английской вики и далее по ссылкам.
Они заслуживают отдельного внимания, но всё я не умещу, да и к монадам они непосредственного отношения не имеют.
Хорошая статья про продолжения в Scheme есть у пользователя palm_mute в живом журнале.

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

(«Никогда» — это громко сказано, на самом деле есть unsafePerformIO :: IO a -> a, но на то и unsafe, чтобы использоваться только с пониманием дела и когда это крайне необходимо. Я лично ни разу не использовал).

Стоит упомянуть Monad transformers (трансформаторы монад).
Если нам нужно состояние, мы можем использовать State, если ввод-вывод — IO. А что если наша функция хочет иметь состояния и при этом осуществлять ввод-вывод?
Для этого предназначен трансформер StateT, который соединяет State и другую монаду. Для выполнения вычислений в этой другой монаде используется функция lift.
Посмотрим на примере факториала, который мы изменим так, чтобы он печатал аккумулятор
import Control.Monad.State

fact' :: Int -> StateT Int IO Int -- добавили IO
fact' 0 = do
    acc <- get
    return acc
fact' n = do
    acc <- get
    lift $ print acc -- print acc - вычисление типа IO (), поэтому его мы передаём функции lift
    put (acc * n)
    fact' (n - 1)

fact :: Int -> IO Int -- Пришлось поставить IO и здесь, никуда не деться :)
fact n = do
    (r, _) <- runStateT (fact' n) 1
    return r
ghci> fact 5
1
5
20
60
120
120

Кроме StateT есть также ListT (для списка).

Полный список монад и трансформеров монад.

Для удобства над монадами определены обобщённые функции. Их названия говорят за себя, большинство из них дублируют списочные функции, так что я просто перечислю некоторые из их и дам эту ссылку
sequence :: Monad m => [m a] -> m [a]
-- выполнить последовательно все вычисления и вернуть список результатов
mapM f = sequence.map f
forM = flip mapM
-- forM [1..10] $ \i -> print i
forever :: Monad m => m a -> m b -- думаю, пояснять не надо
filterM :: Monad m => (a -> m Bool) -> [a] -> m [a] -- filter
foldM :: Monad m => (a -> b -> m a) -> a -> [b] -> m a -- foldl
when :: Monad m => Bool -> m () -> m () -- if без ветки else

List comprehensions


(Перевести этот термин я не берусь)
List comprehensions позволяет конструировать списки в соответствии с математической нотацией:

ghci> take 5 $ [2*x | x <- [1..], x^2 > 3]
[4, 6, 8, 10, 12]
ghci> [(x, y) | x <- [1..3], y <- [1..3]]
[(1,1),(1,2),(1,3),(2,1),(2,2),(2,3),(3,1),(3,2),(3,3)]
Видно, что последний список перебирается чаще
y может зависеть от x:
ghci> [x + y | x <- [1..4], y <- [1..x], even x]
[3,4,5,6,7,8]

List comprehensions тоже синтаксический сахар и разворачивается в (последний пример):
do
    x <- [1..4]
    y <- [1..x]
    True <- return (even x)
    return (x + y)
Конечно, он разворачивается напрямую, но так видна связь между монадическими вычислениями над списками и list comprehensions.

В следующий раз я попробую ответить на все вопросы, а потом можно будет приниматься за реализацию чата (или сразу, если вопросов будет мало/не будет), так что спрашивайте, если что непонятно по всем статьям.
@VoidEx
карма
57,5
рейтинг 0,1
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (24)

  • 0
    Наверное всё-таки:
    1. instance MyEq Bool where
    2.     myEqual True True = True
    3.     myEqual False False = True
    4.     myEqual _ _ = False

    :)
    В Dive Into Python «list comprehension» перевели как «расширенная запись списков».
    • 0
      Да, спасибо, исправил.

      Насколько я узнал, название идёт из axiom of comprehension, что по-русски обычно переводят как «аксиома выделения». Но «выделение списков» как-то не звучит.
      • 0
        мне кажется, наиболее понятная аналогия — «конструкторы списков»
      • 0
        У Душкина переведено как «списочные включения».
  • 0
    Про типы и классы типов могу посоветовать соответсвующую главу «Making Our Own Types and Typeclasses» из «Learn You a Haskell for Great Good!».
  • 0
    >> foo :: (Eq a, Show a, Read a) => a -> String -> String
    Как это сделать, например, в С++/C#?


    ну, в C# можно с помощью generics и type constraints:

    public void Foo (T a) where T : IEquable, IShowable, IReadable
    • 0
      Подозревал об этом, когда писал, но не проверил.
      Буду иметь в виду.
      • 0
        с generics вообще много фана, т.к. они похожи на typeclasses

        вот, к примеру:

        var Orders = new List<Order>() 
        
        //code to populate orders omitted 
        var q = Orders  
            .Where(x => x.OrderDate < DateTime.Now)  
            .OrderBy(x => x.OrderDate)  
            .Select(x => new {ID = x.OrderID, Date = x.OrderDate})
        


        чем не монада? :)

        ( devhawk.net/2008/07/30/Monadic+Philosophy+Part+2+The+LINQ+Monad.aspx )
        • 0
          разумеется, тут ещё заслуга extension methods
          • 0
            И это только третий дотнет, увы.
  • 0
    Имхо, проводить аналогию между монадами и вычислениями — плохая идея, хоть и популярная. У меня, например, сразу возникло непонимание: вычисления — это функции, чем они не подходят? Мне было бы понятнее, если бы монады представляли как контейнеры.
    • 0
      А функции подходят, только это частный случай, когда результат предыдущего вычисления просто передаётся следующей функции.
      • 0
        Код забыл:
        runIdentity $ do
            x <- return 5
            y <- return $ x + 4
            return $ y * 2
        • 0
          Вот об этом я и говорю — своершенно неочевидно, что нам мешает написать:
          x = 5
          y = x + 4
          • 0
            Честно, не нашёлся, что ответить.
            Что мешает нам использовать напрямую класс в ООП и заставляет нас описывать какой-то интерфейс?
            Ничего не мешает, это просто удобная абстракция. В частном случае можно вызывать функции. Но это один частный случай.
            • 0
              Мешает то, что таким образом мы лишаем себя возможности легко подставлять разные имплементации. И об этом стоит четко говорить при объяснении ООП. А здесь в чем проблема?
              • 0
                Причем с функциями, возвращающими Maybe не показателен?
                • 0
                  Показателен, как и с ио, состоянием, массивами, ошибками, итд — но это все воспринимается как частные случаи. Типа — придумали библиотеку и под нее все подгоняем.

                  Нужно более общее восприятие. Например, начнем с функтора. Зачем он нам нужен? Чтобы навешивать дополнительную логику к применению функции.

                  Если x — простое значение, мы пишем «f x»; если x — функтор, мы пишем «fmap f x». Что оно делает?
                  Для x :: [a] — применяет f ко всем элементам x.
                  Для x :: Maybe a — применяет f к содержимому, если оно есть, Nothing остается Nothing.
                  Для x :: IO a — применяет f к будущему значению, которое мы получим в результате ввода.
                  Для x :: Tree a — применяет f к значениям во всех узлам дерева.
                  Для x :: (твой тип) a — сам определишь, что оно делает.

                  Однако что делать, если функция возвращает значение, которое само по себе функтор? Мы получим нагромождение функторов — [[a]], Maybe Maybe a, IO IO a, Tree Tree a. Таким образом, полезно иметь также функцию, которая схлопывает двойной функтор в одинарный — join.

                  Аналогично, нам нужна функция для помещения значения в функтор — return.

                  Вот функтор с join и return — и есть монада. А про bind можно и потом рассказать, с примерами почему он удобен.
                  • 0
                    Это ближе к теории категорий, но зато дальше от do-нотации. В итоге всё равно придётся рассказывать про вычисления, которые комбинируется bind'ом неявно внутри do и что там можно использовать любую монаду.
                    Какое объяснение проще понять — не знаю, надо опрос проводить, лично мне были наиболее понятны те, что приведены по ссылкам.
    • +2
      а мне наоборот кажется более удобным представлять себе монаду, как контейнер содержащий в себе не значение, но «замороженный процесс» его вычисления. Это во многом согласуется с тем, что теоретик-первопроходец Eugenio Moggi в далёком 89 году предложил использовать монады именно для формализации «понятия вычисления» (notion of computation).

      вот как, например, представить себе монаду Cont, как контейнер?
      • 0
        Контейнер, содержащий CPS-функцию, насколько я понимаю.
  • 0
    ну дык, следующий раз так и не настал?
    • 0
      Времени сейчас совсем немного, а дел навалом, а я хотел прежде доделать до конца, а потом последовательно расписывать. Так что всё будет, как только так сразу.
  • +1
    Сори что не по теме, перечитывал статью — наткнулся на «значниею», поправьте плиз.

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