haskel
Добавлено: Пн, 30 марта 2015, 12:36:49
https://lurkmore.co/Haskell
http://habrahabr.ru/post/152889/
http://learnyouahaskell.com/
http://www.realworldhaskell.org/
Установка haskell
http://eax.me/haskell-install/
Зачем нужны функторы и монады
http://habrahabr.ru/post/212955/
http://habrahabr.ru/post/152889/
http://learnyouahaskell.com/
http://www.realworldhaskell.org/
Установка haskell
http://eax.me/haskell-install/
Зачем нужны функторы и монады
http://habrahabr.ru/post/212955/
В Хаскеле все данные неизменяемые, поэтому функции инкремента, равно как и циклов (и много чего ещё) нет.
- Спойлер
- ачем нужны все эти функторы и монады?
Функциональное программирование*, Haskell*, Программирование*
Очень часто в статьях про Хаскель сплошь и рядом встречаются функторы и особенно монады.
Так часто, что порой не реже встречаются комментарии «сколько можно про какие-то новые монады» и «пишите о чём-либо полезном».
На мой взгляд это свидетельствует о том, что люди порой не понимают зачем же нужны все эти функторы и монады.
Это статья попытка показать, что сила функциональных языков и в первую очередь Хаскеля — это в том числе и силе функторов и монад.
Чистые данные
Попытаюсь показать это на примере достаточно искусственном и наверняка бесполезном, однако акцент будет поставлен на важности использования общего кода и переиспользования.
Термин «чистый» перенагружено в программировании.
Например, фразу «Руби — чисто объектный язык» мы понимаем как «Руби — язык, где всё — объекты».
А вот фразу «Хаскель — это чистый функциональный язык» следует понимать как «Хаскель — функциональный язык без побочных эффектов».
В этой статье мы будем использовать термин «чистый» ещё в одном контексте.
«Чистые данные» — это данные, которые я хочу получить.
В основном примитивные типы — это числа, строки, иногда более сложные, например — картинка или несколько значений.
Соответственно, «грязные данные» — это данные, которые содержат, помимо того что я хочу, дополнительную информацию.
Вот сочиняем программку:
module Main where
foo = undefined -- функция неопределена
main :: IO ()
main = do
putStrLn "Input a: "
a <- getLine -- получаем 1 линию на вход
putStrLn "Input b: "
b <- getLine -- получаем 2 линию на вход
print (foo a b) -- печатаем вычисленное значение
Программа проста до безобразия — мы просим ввести пользователю 2 строчки, а после выводим результат вычисления.
Видим, что наша функция foo ещё не определена (она всегда вызывает падение программы), хотя Хаскель уже может откомпилировать наш код.
Теперь перепишем более детально нашу функцию, используя только «чистые» данные:
pure2args :: Int -> Int -> Int
pure2args = (+) -- функция сложения
pure1arg :: Int -> Int
pure1arg = (+ 1) -- функция добавления единицы
unsafe2args :: Int -> Int -> Int
unsafe2args = div -- фунция целочисленного деления
foo :: String -> String -> Int
foo a b = unsafe2args extraUnsafeE unsafeC -- фарш из сложения и деления
where
unsafeA :: Int
unsafeA = read a -- функция перевода строк в числа
unsafeB :: Int
unsafeB = read b -- функция перевода строк в числа
unsafeC :: Int
unsafeC = pure1arg unsafeB
reallyUnsafeD :: Int
reallyUnsafeD = pure2args unsafeA unsafeC
extraUnsafeE :: Int
extraUnsafeE = unsafe2args unsafeA reallyUnsafeD
Как видим, тут тоже понятно, функция foo по сути является [неважно какой] смесью целочисленного деления и сумм.
Большинство функциональных языков программирования позволяют легко и просто создавать функции, основанные на чистых данных.
Казалось бы всё замечательно — простая и элегантная программка. Но нетушки!
Результат функции намного сложнее, чем нам того хотелось бы.
Как мы понимаем, на 0 делить нельзя, да и пользователь может ввести не числа, а левые строки, и при преобразовании строк в числа может выкинуть ошибку. Наш код получился небезопасным.
Императивный подход к разрешения подобных проблем делится на 2 группы: или использовать ветвления, или использовать исключения. Зачастую оба подхода комбинируется.
Эти подходы настолько эффективны, что в основном используются и в функциональных языках.
Скажем прямо — в Хаскеле присутствуют исключения, однако они недоразвиты, нуждаются в реформировании, не лучшим образом отлавливаются. Да и самое важное — в большинстве случаев они просто не нужны.
Но нем не менее — можно.
Поэтому попытаемся переписать наш код используя ветвления и исключения.
module Main where
import Control.Exception (IOException, catch)
printError :: IOException -> IO ()
printError = print
pure2args :: Int -> Int -> Int
pure2args = (+)
pure1arg :: Int -> Int
pure1arg = (+ 1)
unsafe2args :: Int -> Int -> Int
unsafe2args a b = if b == 0
then error "Error 'unsafe2args' : wrong 2nd argument = 0" --unsafe source
else div a b
foo :: String -> String -> Int
foo a b = unsafe2args extraUnsafeE unsafeC
where
unsafeA :: Int
unsafeA = read a --unsafe source
unsafeB :: Int
unsafeB = read b --unsafe source
unsafeC :: Int
unsafeC = pure1arg unsafeB
reallyUnsafeD :: Int
reallyUnsafeD = pure2args unsafeA unsafeC
extraUnsafeE :: Int
extraUnsafeE = unsafe2args unsafeA reallyUnsafeD
main :: IO ()
main = do
putStrLn "Input a: "
a <- getLine
putStrLn "Input b: "
b <- getLine
catch (print (foo a b)) printError --пробуем вычислить значение или напечатать исключение
Грязные данные
В Хаскеле (да и многих функциональных языках) есть достойный ответ на подобные задачи.
Основная сила заключена в Алгебраических Типах Данных.
Если мы рассматриваем вышеприведённый пример, видно, что наши функции могут падать.
Решение — пользоваться нулабельными типами данных.
В ML языках и Scala такой тип называется Option, в Хаскеле он называется Maybe a.
import Prelude hiding (Maybe) -- этот тип уже описан в стандартной библиотеке. Мы попробуем создать его с нуля
data Maybe a = Nothing | Just a
deriving Show
Мы не обращаем внимание на deriving часть, мы тут просто говорим, что просим компилятор самостоятельно уметь переводить в строку наш тип данных.
А именно,
show Nothing == "Nothing"
show (Just 3) == "Just 3"
Тип данных принимает значение Nothing если у нас нет данных, и Just a, если есть.
Как видим, тип данных — «грязный», так как содержит лишнюю информацию.
Давайте перепишем наши функции более правильно, более безопасно и без исключений.
Прежде всего заменим функции, которые вызывали падение на безопасные аналоги:
maybeResult2args :: Int -> Int -> Maybe Int
maybeResult2args a b = if b == 0
then Nothing --safe
else Just (div a b)
...
maybeA :: Maybe Int
maybeA = readMaybe a --safe
maybeB :: Maybe Int
maybeB = readMaybe b --safe
Теперь эти функции вместо падения дают результат Nothing, если всё в порядке — то Just результат.
Но весь остальной код у нас зависит от этих функций. Нам придётся изменить почти все функции, в том числе и те, которые много раз тестировались.
pure2args :: Int -> Int -> Int
pure2args = (+)
safePure2args :: Maybe Int -> Maybe Int -> Maybe Int
safePure2args a b = case a of
Nothing -> Nothing
Just a' -> case b of
Nothing -> Nothing
Just b' -> Just (pure2args a' b')
pure1arg :: Int -> Int
pure1arg = (+ 1)
safePure1arg :: Maybe Int -> Maybe Int
safePure1arg a = case a of
Nothing -> Nothing
Just a' -> Just (pure1arg a')
maybeResult2args :: Int -> Int -> Maybe Int
maybeResult2args a b = if b == 0
then Nothing
else Just (div a b)
foo :: String -> String -> Maybe Int
foo a b = case maybeE of
Nothing -> Nothing
Just e -> case maybeC of
Nothing -> Nothing
Just c -> maybeResult2args e c
where
maybeA :: Maybe Int
maybeA = readMaybe a
maybeB :: Maybe Int
maybeB = readMaybe b
maybeC :: Maybe Int
maybeC = safePure1arg maybeB
maybeD :: Maybe Int
maybeD = safePure2args maybeA maybeC
maybeE = case maybeA of
Nothing -> Nothing
Just a1 -> case maybeD of
Nothing -> Nothing
Just d -> maybeResult2args a1 d
printMaybe :: Show a => Maybe a -> IO ()
printMaybe Nothing = print "Something Wrong"
printMaybe (Just a) = print a
main :: IO ()
main = do
putStrLn "Input a: "
a <- getLine
putStrLn "Input b: "
b <- getLine
printMaybe (foo a b)
Как видим простая программа превратилась в достаточно монстро-образный код.
Много обёрточных функций, много избыточного кода, много изменено.
Но именно на этом останавливаются многие функциональные языки программирования.
Теперь можно понять почему в тех языках, несмотря на возможность создания множества АТД, АТД не так уж часто используются в коде.
Можно жить с АТД, но без подобной вакханалии? Оказывается можно.
Функторы
На помощь нам в начале приходят функторы.
Функторы — это такие типы данных, для которых существует функция fmap
class Functor f where
fmap :: (a -> b) -> f a -> f b
а так же его инфиксный синоним:
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap
такая что для всех значений типа данных всегда выполняются следующие условия:
Условие идентичности:
fmap id == id
Условие композиции:
fmap (f . g) == fmap f . fmap g
Где id — функция идентичности
id :: a -> a
id x = x
И (.) — функциональная композиция
(.) :: (b -> c) -> (a -> b) -> a -> c
f . g = \x -> f (g x)
Функтор — это класс типов, где мы создали специальную функцию fmap. Посмотрим на её аргументы — она берёт одну «чистую» функцию a -> b, берём «грязное» функторное значение f a и получаем на выходе функторное значение f b.
Тип данных Maybe является функтором. Создадим инстанс (экземпляр) для типа Maybe, так чтобы не нарушались законы функторов:
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
Как нам использовать чистую функцию с функтором Maybe? Очень просто:
safePure1arg :: Maybe Int -> Maybe Int
safePure1arg = fmap pure1arg
Мы тут видим главное — мы не переписывали нашу функцию pure1arg, а значит нам не надо её ещё раз тестировать на баги и ко всему она осталась универсальной и чистой, зато мы с лёгкостью создали её безопасную версию, которая на вход принимает не числа, а нулабельные числа.
Однако, если мы захотим применить функтор, пытаясь переписать safePure2args, мы потерпим фиаско.
Функторы работают только с функциями с единственным функторно-«грязным» аргументом.
Что же делать для функций с несколькими параметрами?
Аппликативные функторы
Тут нам на помощь приходят аппликативные функторы:
Аппликативные функторы — такие функторы, для которых определены 2 функции: pure и (<*>)
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
Такие, что для них для любых значений одного типа данных всегда выполняются следующие правила:
Условие идентичности:
pure id <*> v == v
Условие композиции:
pure (.) <*> u <*> v <*> w == u <*> (v <*> w)
Условие гомоморфизма:
pure f <*> pure x == pure (f x)
Условие обмена:
u <*> pure y == pure ($ y) <*> u
Основное отличение фунтора от аппликативного фунтора состоит в том, что фунтор протаскивает сквозь функторное значение чистую функцию, в то время как аппликативный фукнтор позволяет нам протаскивать сквозь функторное значение функторную функцию f (a -> b).
Maybe является аппликативным функтором и определяется следующим образом:
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
_ <*> Nothing = Nothing
(Just f) <*> (Just a) = Just (f a)
Самое время переписать safePure2args.
В основном функцию переписывают, совмещая функторый fmap для первого аргумента, и аппликативное нанизывание остальных аргументов:
safePure2args :: Maybe Int -> Maybe Int -> Maybe Int
safePure2args a b = pure2args <$> a <*> b
Но можно переписать функцию, пользуясь исключительно аппликативными функциями (монадный стиль) — вначале «чистую» функцию делаем чисто-аппликативной, и аппликативно нанизываем аргументы:
safePure2args :: Maybe Int -> Maybe Int -> Maybe Int
safePure2args a b = (pure pure2args) <*> a <*> b
Замечательно!
Может можно заодно переписать функцию maybeE с помощью аппликативных функторов? Увы.
Монады
Давайте обратим внимание на подпись функции maybeResult2args:
maybeResult2args :: Int -> Int -> Maybe Int
Функция берёт на вход «чистые» аргументы, и выдаёт на выходе «грязный» результат.
Так вот, в большинстве своём в реальном программировании, именно такие функции встречаются чаще всего — берут на вход «чистые» аргументы, и на выходе — «грязный» результат.
И когда у нас есть несколько таких функций, вместе совместить их помогают монады.
Монады — это такие типы данных, для которых существует функции return и (>>=)
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
такие, что выполняются правила для любых значений типа:
Левой идентичности:
return a >>= k == k a
Правой идентичности:
m >>= return == m
Ассоциативности:
m >>= (\x -> k x >>= h) == (m >>= k) >>= h
Для удобства, есть дополнительная функция с обратным порядком аргументов:
(=<<) :: Monad m => (a -> m b) -> m a -> m b
(=<<) = flip (>>=)
Где
flip :: (a -> b -> c) -> b -> a -> c
flip f a b = f b a
Мы понимаем, что тип Maybe является монадой, а значит можно определить его инстанс (экземпляр):
instance Monad Maybe where
return = Just
(Just x) >>= k = k x
Nothing >>= _ = Nothing
Кстати, если мы присмотримся внимательнее к внутреннему содержанию, и подписям, увидим, что:
pure == return
fmap f xs == xs >>= return . f
Пришло время переписать функцию maybeE
maybeE = maybeA >>= (\a1 -> maybeD >>= (maybeResult2args a1))
Да уж, вышло не намного красивее. Это связано с тем, что монады красиво пишутся для одной переменной. К счастью существуют много дополнительных функций.
Можно написать функцию bind2
bind2 :: Monad m => (a -> b -> m c) -> m a -> m b -> m c
bind2 mf mx my = do
x <- mx
y <- my
mf x y
maybeE = bind2 maybeResult2args maybeA maybeD
Или использовать функцию liftM2 и join
liftM2 :: Monad m => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r
join :: Monad m => m (m a) -> m a
maybeE = join $ liftM2 maybeResult2args maybeA maybeD
На крайний случай, можно воспользоваться синтаксическим сахаром для монад, используя do нотацию:
maybeE = do
a1 <- maybeA
d <- maybeD
maybeResult2args a1 d
Различие в применении фунторов и монад
Если мы сведём основные функции к одному виду, то увидим:
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
(=<<) :: Monad f => (a -> f b) -> f a -> f b
Все используются для того, чтобы передавать функциям «грязные» значения, тогда как функции ожидают «чистые» значения на входе.
Фунторы используют «чистую» функцию.
Аппликативные функторы — «чистую» функцию внутри «загрязнения».
Монады используют функции, которые на выходе имеют «грязное» значение.
Программа без рутины
Что ж, наконец, можно полностью и аккуратно переписать всю программу:
module Main where
import Control.Monad
import Control.Applicative
import Text.Read (readMaybe)
bind2 :: Monad m => (a -> b -> m c) -> m a -> m b -> m c
bind2 mf mx my = do
x <- mx
y <- my
mf x y
pure2args :: Int -> Int -> Int
pure2args = (+)
pure1arg :: Int -> Int
pure1arg = (+ 1)
maybeResult2args :: Int -> Int -> Maybe Int
maybeResult2args a b = if b == 0
then Nothing --safe
else Just (div a b)
foo :: String -> String -> Maybe Int
foo a b = bind2 maybeResult2args maybeE maybeC
where
maybeA :: Maybe Int
maybeA = readMaybe a --safe
maybeB :: Maybe Int
maybeB = readMaybe b --safe
maybeC :: Maybe Int
maybeC = fmap pure1arg maybeB
maybeD :: Maybe Int
maybeD = pure2args <$> maybeA <*> maybeC
maybeE :: Maybe Int
maybeE = bind2 maybeResult2args maybeA maybeD
printMaybe :: Show a => Maybe a -> IO ()
printMaybe Nothing = print "Something Wrong"
printMaybe (Just a) = print a
main :: IO ()
main = do
putStrLn "Input a: "
a <- getLine
putStrLn "Input b: "
b <- getLine
printMaybe (foo a b)
Код снова стал прост и понятен!
При этом мы не поступились ни пядью безопасности!
При этом мы почти не изменили код!
При этом чистые функции остались чистыми!
При этом избежали рутины!
Вывод
Можно ли жить в функциональном мире без функторов и монад? Можно.
Но, если мы хотим вовсю использовать всю силу Алгебраических Типов Данных, нам для удобной функциональной композиции различных функций придётся использовать функторы и монады.
Ибо это отличное средство от рутины и путь к краткому, понятному и часто пере-используемому коду!
P.S. Следует понимать, что для различных типов данных, аналогия с «чистыми» и «грязными» типами данных не совсем уместна.
Например, для списков
fmap = map
А монада:
a = do
c <- cs
d <- ds
return (zet c d)
на самом деле является
a = [zet c d | c <- cs, d <- ds]
Что не всегда очевидно с первого взгляда.
монады, функторы, аппликативные функторы, haskell
+36 19277
169Vitter 0,0
Похожие публикации
Очисти код свободными монадами (7)
Пример решения типичной ООП задачи на языке Haskell (7)
Слово на букву «М», или Монады уже здесь (33)
Функторы, аппликативные функторы и монады в картинках (60)
Монада ContT в картинках (Haskell) (10)
Комментарии (44) отслеживать новые: в почте в трекере
+14 NeoCode18 февраля 2014 в 12:07#
Спасибо за статью!
И как всегда объяснения на Хаскеле:) Думаю, для людей знакомых с Хаскелем, думаю это все и так понятно, а для незнакомых… сложно просто ориентироваться в коде, в синтаксисе языка, даже чтобы «распарсить» программу в уме.
Я время от времени все пытаюсь понять, что же такое эти функторы и монады, чего-то даже иногда понимаю, но единой картины все-же нет.
Начать надо с вопроса — что такое алгебраический тип данных и в чем его отличие от неалгебраических? Если пользоваться терминологией императивных языков, то например переменная типа int — АТД? Массив чисел? Структура? Класс?
В вашей предыдущей статье вы даете определение АТД как «АТД называются алгебраическими, потому что их можно представить как некую алгебраическую композицию типов его составляющих», но это все-же несколько не то, что могло бы облегчить понимание)…
+3 dotneter18 февраля 2014 в 12:54#↵↑
Попробуйте прочитать вариацию на тему с С# habrahabr.ru/post/151703/
+2 NeoCode18 февраля 2014 в 13:06 (комментарий был изменён)#↵↑
Я очень хочу разобраться в этом вопросе и помочь разобраться другим. Статью ту я с интересом читал как и многие другие статьи по теме здесь (но все равно спасибо, освежу в памяти).
Просто если есть люди, которые безусловно понимают тему на глубоком уровне (как автор топика), то почему не воспользоваться их присутствием на Хабре? Серией наводящих вопросов (возможно даже глупых) можно уточнить некоторые моменты, попытаться совместными усилиями переформулировать результаты обсуждения несколько раз до тех пор, пока не будет достигнуто полное понимание со стороны незнающих и согласие в корректности и полноте формулировок со стороны знающих.
Комментарии к статье ИМХО идеальный вариант для этой цели.
+1 Vitter18 февраля 2014 в 13:24#↵↑
Можно представить, что Int определён так:
data Int = -33554432 | -33554431 | ... |-2 | -1 | 0 | 1 | 2 | 3 | ... | 33554431 | 33554432
Да, Int — АТД, как и любые типы в Хаскеле.
Другое дело, что под капотом они реализованы по другому.
Списки — тоже АТД — рекурсивные
data List a = Nil | Cons a (List a)
Структура — это обычный тупл(кортеж)
Тупл — это АТД
data Struct = Struct Int (String->Int) String
Класс — это структура со спец-способностями (инкапсуляция, полиморфизм, наследование)
Классов и объектов в Хаскеле нет.
+1 NeoCode18 февраля 2014 в 13:48#↵↑
Какие типы не АТД? Что нужно «сломать» в АТД, чтобы он перестал быть АТД?
0 mibori18 февраля 2014 в 14:00#↵↑
например, тип Collection n a — «коллекция из n элементов типа a», кажется, не может быть АДТ. Т. е. — это тип от терма.
да поправят меня математики, если это не так.
0 NeoCode18 февраля 2014 в 14:01#↵↑
Поясните плиз. Для меня «коллекция n элементив типа a» это массив a[n]
0 mibori18 февраля 2014 в 14:07#↵↑
да, вы почти правильно написали на си:
a collection[n]; // так лучше
n — тут это число. Не тип. Т. е. — это терм. Т. е. — это значение некого типа, а не сам тип.
0 NeoCode18 февраля 2014 в 14:34#↵↑
То есть получается, что для списка Maybe будет работать, а для массива фиксированной длины — нет? Почему?
0 mibori18 февраля 2014 в 15:39 (комментарий был изменён)#↵↑
Потому что вам придется число(терм) выразить с помощью типа. Но в конечном итоге это уже не будет честный Algebraic Data Type (это будет называться Generalized Algebraic Data Type)
Определим стандартный (нефиксированный длины) список с указанием типа его конструкторов:
data List a where
Nil :: List a
Cons :: a -> List a -> List a
видите, оба конструктора имеют тип результата List a
теперь определим список с фиксированным количеством элементов.
сначала термы поднимем в типы. Вот аналог нуля и единицы:
data Zero
data Succ n
Вот как будут выглядеть алиасы для остальных натуральных чисел:
type One = Succ Zero
type Two = Succ One
type Three = Succ Two
type Four = Succ Three
Теперь определим сам список ListN:
data ListN n a where
Nil :: ListN Zero a
Cons :: a -> ListN n a -> ListN (Succ n) a
После этого тип списка из четырех картинок будет выглядеть так ListN Four Image
Как видите, в отличии от List a, типы результата двух конструкторов отличаются — один ListN Zero a, а второй ListN (Succ n) a.
Поэтому это не совсем ADT. Хотя в хаскелле его можно определить.
0 NeoCode18 февраля 2014 в 15:58#↵↑
Ну я совсем не это хотел получить в ответ:) Меня интересует не реализация на хаскеле, а общие принципы.
Все что я хотел узнать — что такое АДТ с точки зрения классических императивных языков, а не Хаскеля.
0 mibori18 февраля 2014 в 16:09#↵↑
АДТ, с императивной точки зрения, это продвинутый enum
Color = enum {Red, Green, Yellow, RGB(Byte, Byte, Byte) }
Эти enum еще могут иметь параметры (дженерики)
Так понятнее?
0 NeoCode18 февраля 2014 в 16:46 (комментарий был изменён)#↵↑
Это я знаю, когда-то на rsdn читал статьи про язык nemerle. Это все определения той или иной степени формальности: от совсем неформальных («продвинутый enum») до строгих математических.
Но что это дает в реальном кодинге?
В чем киллер-фича ADT?
0 mibori18 февраля 2014 в 17:30#↵↑
В мире со строгой типизацией кратко, лаконично и как можно точнее описать тип того, с чем ты работаешь.
Вот, например, описан тип выражающий грамматику javascript hackage.haskell.org/package/hjs-0.2.1/docs/HJS-Parser-JavaScript.html (тип JSProgram вконце странички).
+2 Si1en7ium18 февраля 2014 в 22:54#↵↑
Киллер-фича ADT, на мой взгляд — это простота в сочетании с гибкостью.
Классы смешивают воедино идентичность, состояние и поведение. Это сложно (не тяжело для восприятия, а просто объективно сложно: много сущностей, с которыми нельзя работать по отдельности)
Структуры недостаточно мощны: тип классических структур не может быть параметризован, и структуры представляют из себя произведение типов, но никогда — сумму (то есть если взять значение типа A И значение типа B, то можно получить значение структуры, содержащей A и B, но нельзя создать структуру, которая бы хранила значение типа A ИЛИ значение типа B)
ADT же просты, так как являются просто данными, без всяких смешений с какими-то идентичностями да поведениями, и при этом достаточно гибки, так как позволяют создавать сумму/произведение (алгебраическую композицию) других типов, да ещё и с параметрическим полиморфизмом (как в дженериках .net или java).
Благодаря этим двум свойствам ADT позволяют выразить очень много полезных типов данных и при этом достаточно удобно с ними работать посредством сопоставления с шаблоном, без написания горы методов для доступа к ним.
Например, в статье выше, определив в одну строку простейший тип данных Maybe, мы получили:
1) Параметризованный «контейнер» для 0 либо 1 элемента
2) Два конструктора: Just :: a -> Maybe a и Nothing :: Maybe a
3) Два «деконструктора» с аналогичными именами, которые используются при сопоставлении по шаблону и позволяют избежать условных операторов/операторов ветвлений, а также сравнений.
И всё это совершенно без дополнительных трудозатрат со стороны разработчика, то есть задаром.
К тому же можно было заметить строку deriving (Show), которая автоматически определила экземпляр класса типа Show для Maybe. Компилятор легко может создать этот и некоторые другие (например, Read, Eq, Ord) экземпляры автоматически, потому что ADT очень просты по своей сути.
Получается, что ADT — это удобный фреймворк для создания новых типов данных на основе композиции существующих.
0 Vitter18 февраля 2014 в 20:42#↵↑
Не совсем. Вообще в языках с зависимыми типами(типа Coq, Agda) тоже есть АТД.
Объекты — не АТД, поскольку они обладают свойствами.
АТД — чистый тип, он не может обладать свойствами.
0 mibori18 февраля 2014 в 13:54#↵↑
Кажется, вы немного неправы на счет Int. hackage.haskell.org/package/base-4.2.0.0/docs/Data-Int.html#t%3AInt
> A fixed-precision integer type with at least the range [-2^29… 2^29-1].
0 Vitter18 февраля 2014 в 20:44#↵↑
На самом деле числа Int на 32-битных машинах как правило (на GHC) 32-битные, на 64-х битных машинах — 64-битные
0 nickolaym19 февраля 2014 в 15:30#↵↑
Полиморфизм в хаскелле есть в ассортименте.
Инкапсуляция есть: модули и GADT.
Далеко ходить не надо: IO a (без инкапсуляции любой дурак смог бы создать поддельный real world object и поломать порядок вычислений).
Наследование — с этим сложнее.
Интерфейс-реализация — это классы типов и их инстансы.
0 mibori18 февраля 2014 в 13:48#↵↑
> то например переменная типа int — АТД? Массив чисел? Структура? Класс?
Переменная типа Int — это АДТ. Вот псевдо-определение этого типа:
data Int = -536870912 | -536870911 |… | -1 | 0 | 1 |… | 536870910 | 536870911
+27 eschava18 февраля 2014 в 12:24#
Вот как обычно, статья с таким названием, будто я ее сейчас прочту и пойму зачем нужна вся эта функциональщина с хаскелями и прочими скалами
Но нет, после второго же абзаца все как всегда скатывается в дебри странных слов и непонятного кода
Или это я такой тупой?
+3 steck18 февраля 2014 в 12:36#↵↑
Пост просто назван неправильно. Было бы коррейтней его назвать «пример использования Maybe как функтора и как монады».
А Ваш вопрос можно разделить на два:
1) Зачем вообще нужны эти языки
2) Как понимать и использовать хаскель (скалу, что угодно ещё)
И если на первый вопрос ответить можно достаточно просто, то второй потребует практики и набивания руки.
+2 Vitter18 февраля 2014 в 12:37#↵↑
чесно, а что сложного в функциях?
bar = (+)
baz = (+ 1)
foo = div
Это функции сложения, добавление единицы, целочисленное деление.
0 yomayo19 февраля 2014 в 14:25#↵↑
Если смотреть на эти функции глазами математика, то в них нет ничего сложного. Вопросы возникают, когда на них смотрят глазами программиста. Какими типами данных оперируют эти функции? А что будет, если я сложу 32-битное знаковое с 16-битовым беззнаковым и какой тип будет иметь результат? А какова будет реакция на переполнение? Описана функция целочисленного деления, а для деления чисел с плавающей запятой используется одноимённая функция или другая? Есть функция «+ 1», а что – там нет обычного инкремента? Он даже в ассемблере есть.
Как-то многовато информации остаётся за кадром – для тех, кто не знает Хаскель. Чтобы понять функциональное программирование, нужно читать учебники. Но и в них многое остаётся за кадром. Есть люди, которым материал остаётся непонятным до тех пор, пока они не поймут, как это устроено изнутри. Си и Паскаль понятны, потому что известно, какой ассемблерный код они генерируют. А что генерирует Хаскель – увы, пока сие неведомо, хотя хотелось бы разобраться.
+1 Vitter19 февраля 2014 в 16:20#↵↑
Спасибо за ответ!
В Хаскеле с переполнением чисел не всё опрятно.
А вот сложить можно однотиповые числа:
(+) :: Num a => a -> a -> a
то есть или только 32-битно-знаковые, или только 16-битовые беззнаковые.
Что касается целочисленного деления и нецелого деления, есть 2 функции:
div :: Integral a => a -> a -> a
(/) :: Fractional a => a -> a -> a
Есть функция «+ 1», а что – там нет обычного инкремента?
Тут сложнее ответ. Ответ двояк — есть и нету одновременно.
Есть функция «следующее» для любых перечислимых типов данных:
succ :: Enum a => a -> a
А теперь что касается «обычного» инкремента, он подразумевает изменение переменной.
В Хаскеле все данные неизменяемые, поэтому функции инкремента, равно как и циклов (и много чего ещё) нет.
0 HaruAtari18 февраля 2014 в 13:23#↵↑
Вы хотите понять, как и где применять ФП, но при этом не имеете желания разбираться в нем. Не удивительно, что вы не понимаете.
0 ImLiar18 февраля 2014 в 15:16 (комментарий был изменён)#↵↑
Скалка раскрывает себя полноценно в многоядерных(процессорных), а так же кластерных системах. Akka вместе с иммутабельными структурами данных и функциональный подход в их обработке сокращают количество потенциальных ошибок до минимума (дедлоки например), при этом количество написанных строк гораздо меньше тех же реализаций каких-нибудь семафоров с тредами на джавасишках, да и система становится прозрачнее и понятнее, т.к. никакой черт из коробки дебрей кода внезапно не выскочит и не изменит нашу переменную просто потому, что ему так захотелось.
офк это все справедливо для более менее средних и крупных проектов. гостевые книги на этом писать имхо излишне…
+1 burjui19 февраля 2014 в 22:29#↵↑
Наверное, дело в примерах. В этой статье из простой задачи делают сложную, пытаясь таким образом продемонстрировать мощь языка. Эффект получается противоположный, потому что делать надо наоборот: решать относительно сложную задачу так, чтобы решение получалось простым. Это по-настоящему интригует и мотивирует, особенно, если решение получается проще, чем на большинстве других популярных ЯП.
+6 Vanger1318 февраля 2014 в 13:53#
Не понятно зачем эта статья.
Те, кто не понимали\не знали азов монадного исчисления — не поймут. А те кто знали — зачем им еще одно, не самое очевидное, объяснение?
Нельзя использовать в качестве примера однобуквенные переменные, функции с именем foo, baz, <*> и >>=. Людей не знакомых с синтаксисом хаскеля и без этого много что смущать будет в примерах.
Ну а даже если и знаком с хаскелем — это все равно страшно:
safebaz a b = baz <$> a <*> b
...
(>>=) :: m a -> (a -> m b) -> m b
+1 I_Am_Hated18 февраля 2014 в 14:43#
Чтобы быстро получить представление о монадах и функторах, достаточно посмотреть вот этот очень толковый пост. Поясняется синтаксис конструкций и становится понятной общая идея. Все вполне понятно без знания синтакисиса.
0 NeoCode18 февраля 2014 в 15:05#↵↑
Не совсем.
Вот допустим, я понял следующее.
Есть функция, принимающая на вход допустим тип T1 и возвращающая тип T2. Простая функция, ведать не ведающая ни о каких монадах.
Все эти конструкции (функторы, аппликативные функторы и монады) позволяют подавать на вход функции и получать на выходе этой функции разные другие типы, порожденные на основе T1 и T2 соответственно (например, Maybe, List, Future и т.д.). Что важно — без переписывания кода функции. Функция как не знала о монадах, так и дальше не знает, но мы получаем принципиально новые возможности в программе.
То есть если у нас есть функция увеличения числа на 1, мы можем ее совершенно прозрачно применить к целому списку чисел, получив на выходе другой список.
Я прав?
0 I_Am_Hated18 февраля 2014 в 15:31#↵↑
То есть если у нас есть функция увеличения числа на 1, мы можем ее совершенно прозрачно применить к целому списку чисел, получив на выходе другой список.
Я прав?
Да, все верно. Список в haskell — это тоже функтор и для списка определена композиция функции и значений списка.
0 RomanYankovsky18 февраля 2014 в 18:42#↵↑
Воспринимайте монады как паттерн программирования. Это всего-лишь способ связывать функции в цепочки. Монада — это абстрактный интерфейс, а Maybe, List и т.д. — его реализации.
+3 grey_kristy18 февраля 2014 в 14:58#
Не покидает ощущение, что люди сначала создают себе проблемы (АДТ) а потом изобретают остроумные способы их решения (Монады и Функторы)
+1 corristo18 февраля 2014 в 16:17#↵↑
Ну да, лучше по старинке с null сравнивать.
0 qehgt18 февраля 2014 в 20:33 (комментарий был изменён)#↵↑
Нет, ADT — это очень удобно. Это как правильно сделанный union из C/C++. Вместе с (встроенным в язык) pattern matching — сильно упрощает код, и уменьшает количество мест, где можно ошибиться. К монадам и функторам отношения не имеет.
0 ilammy18 февраля 2014 в 23:01#↵↑
s/АДТ/отсутствие побочных эффектов/
+6 Si1en7ium18 февраля 2014 в 23:24#↵↑
ADT — это просто. Серьёзно. Не обязательно легко доступно для понимания, но очень просто по сути.
Есть примитивные типы, которые имеют только один экземпляр. Есть их суммы (объединение множеств значений) и произведения (декартово произведение множеств значений). Есть параметрический полиморфизм: Bool не паметризован ничем и живёт сам по себе, Maybe a параметризован типом элемента. Всё. Больше в ADT нет ничего. Это намного меньше, чем то, что есть в системах типов классических ООП-языков, и при этом позволяет добиться схожей (а зачастую даже большей) гибкости. Где здесь создание проблем? Налицо устранение некоторых проблем сложности, присущих другим системам типов.
Монады и функторы пришли в программирование из теории категорий, и, действительно, монады изначально были призваны решить проблему языка Хаскель, хотя и никак не связанную с алгебраическими типами данных: проблему работы с IO в чистом функциональном языке. Только спустя некоторое время нашлись многие другие применения этого полезного в хозяйстве, хм, паттерна. Оказалось, что монады — это остроумный способ решения многих других проблем, не связанных с IO. Просто ещё один способ композиции вычислений, как и функтор, и аппликативный функтор, и всё в таком духе. Больше способов композиции — больше возможностей писать композабельные системы — налицо loose coupling.
Разумеется, это всё, вероятно, не попадёт в мэйнстрим ещё очень долго. Отчасти из-за высокого порога вхождения, отчасти из-за того, что бизнес предъявляет несколько отличные от внутренней простоты требования. Бизнесу нужна лёгкость. Лёгкость замены и обучения разработчиков, доступность инструментов и так далее. Остроумных решений этой проблемы теория категорий нам, к сожалению, пока не предложила.
0 worldmind19 февраля 2014 в 09:56#
Благодарю, очень в тему — вчера вечером пролистал последние главы книги «Изучай haskell во имя добра» ибо начиная с функторов только ничего не понял, статья помогла немного прояснить картину
0 uvelichitel20 февраля 2014 в 00:12#
В GHC 7.8 решились наконец сделать аппликативный функтор суперклассом монады. Решение логически очевидное но архитектурно радикальное и разрушающее обратную совместимость.
0 shock_one28 октября 2014 в 23:02#↵↑
Это нововведение Haskell 2014. www.haskell.org/haskellwiki/Functor-Applicative-Monad_Proposal
+1 shock_one28 октября 2014 в 22:59 (комментарий был изменён)#
let foo = "oчень"
foo' = "названы"
baz = "беспорядочно"
bar = "функции"
quux = "сложно"
bar1 = "потому"
Было foo quux следить за кодом bar1, что bar foo' baz.
+1 Vitter30 октября 2014 в 02:49#↵↑
А вот подумал, чем чёрт не шутит, и поменял имена всем переменным для более осознанного чтения ))
Спасибо за отзыв
0 shock_one30 октября 2014 в 08:55 (комментарий был изменён)#↵↑
Спасибо большое, не ожидал. Ваши труды не пропадут зря: как минимум они улучшат вашу карму.