Вот, что смог. Сумбурно.
----
Здесь я попытаюсь рассмотреть проблему ввода-вывода и механизм монад применительно к ленивому языку без побочных эффектов с достаточно мощной для их реализации системой типов. Такому как Haskell. Монады как таковые вовсе не являются специфичными для Haskell, вся их поддержка в нем заключена лишь в минимальной и необязательной доработке кодогенератора и парсера, а сами они могут быть реализованы на любом языке. Реализация некоторых монад так же входит в поставку языка Clean
Осознавая, что читать это будут люди с разной подготовкой, я постараюсь избежать лишних подробностей и аналогий непонятных никому кроме специалистов в какой-то области. Заранее прошу прощения за возможные ошибки и неточности.
Введение в проблему.
Имея в качестве данности чисто функциональный, а точнее нужно сказать – язык без возможности иметь какие-либо побочные эффекты вопрос об организации ввода-вывода стоит особенно остро. Что вообще такое отсутствие побочных эффектов?
Это требование, которое происходит из предположения о том, что если у нас есть функция f(x) = y, то для любого x она всегда и при любых обстоятельствах вернет один и тот же y. Если f(2) = 4, то всегда f(2) будет оставаться =4. Очевидно, что для полной уверенности в этом мы должны полностью исключить возможность функции влиять на окружение и, соответственно, на результаты других функций. Следовательно, исчезает любая возможность чтения чего-то извне или сохранения результатов в переменные для дальнейшего чтения.
При кажущейся бесполезности, как требования, так и следствия, в результате оно дает нам гораздо больше, чем требует, но об этом позже.
А как вообще тогда...?
Понятно, что в общем это ограничение запросто может не касаться основной функции программы, которая может получать необходимые данные при вызове и возвращать нужный результат. Например, так:
-- ПСЕВДОХАСКЕЛЬ
main input = calcSomethingStupid (read input :: Int)
Уже что-то, но мало пригодно для чего-то, кроме вычислительных задач.
Следующей идеей, которая может прийти в голову, будет внешняя программа написанная на менее привередливом языке, которая читает с STDIN какой-то заранее определенный набор команд, выполняет их и направляет их результат туда, куда скажут. Так как это делает UNIX shell:
dd bs=10 count=1 if=/dev/random > random.value
# Но можно и:
dd bs=10 count=1 if=/dev/random | ./myprogram
То есть если мы запустим нашу программу как:
`./myprogram start`
то она сможет давать шеллу команду сделать что-то полезное вместе с указанием вызвать себя же с этим полезным в качестве входа.
после чего вернуть следующую команду и по новой. Еще лучше, если мы напишем свою обертку, которая сможет избежать ненужного оверхеда и будет уметь вызывать не просто нашу программу, а конкретные ее функции. Например так:
-- ПСЕВДОХАСКЕЛЬ
-- функции main просто возвращает пару значений,
-- где первый - это что сделать, а второй - куда его подать
main _ = (readLine "Enter your name", hello)
hello name = (writeLine name, ())
-- функция hello принимает на вход строку и возвращает команду напечатать
-- и пустое значение (), означающее, что пора заканчивать.
-- readLine и writeLine просто представляют из себя функции,
-- возвращающие закодированное сообщение в нужном виде для внешнего цикла.
Уже гораздо лучше, хотя до идеального удобства далеко, да и выглядит это не очень все элегантно (не теряйте терпения, до финиша нам остался один шаг.).
Тем не менее, даже в таком варианте у нас есть фактически полноценный ввод-вывод и интуитивно можно прийти к очень важным выводам вообще для программирования:
1. Мы получили очень надежный ввод-вывод, который никак не задевает нашу программу
2. Мы уверены, что сохранили ссылочную прозрачность, при этом если вернуться и посмотреть на код, то можно заметить, что он визуально похож на императивный, хотя таковым не является.
3. Мы пришли к пониманию, что сама семантика ввода-вывода необязательно должна быть императивной, в вполне может определяться как соответствие между _значением_ входа и выхода. То есть мы вполне можем читать программу вслух не как: <прочитать байт, прибавить единицу, записать байт>, а {функция для прочитанного байта будет равна записи этого байта плюс единица}. Причем, интуитивно нет оснований полагать, что такая интерпретация выглядит менее естественной, чем императивная.
Еще один пример на всякий случай:
-- ПСЕВДОХАСКЕЛЬ
main _ = (readByte, plus 1)
plus x y = (writeLine result, readName)
where result = "result: " ++ show add
add = x + y
readName = (readString "enter your name", processName)
processName name =
let out = "Hello, " ++ name
in (writeLine out, ())
А теперь, пристегните ремни, пришло время для самого главного, того чего все больше всего боятся, но чего бояться совершенно не стоит, а наоборот, стоит любить.
Монады.
Несмотря на то, что в 92 году решения подобные вышеописанному кое как работали (чуть сложнее конечно, я упростил некоторые не самые важные для понимания идеи подробности), научный поиск (как свой так и чужой) привел гениального Фила Уодлера к публикации документа о монадах (их придумал не он, они уже существовали в математике — в теории категорий).
Что такое Монады вообще такое?
Это объект, точнее сказать, в хаскеле — это тип высокого порядка, параметризующийся другим типом (кто еще не понял как устроена система типов в хаскеле, могут вспомнить шаблоны в С++ или generics в Java).
У этого типа есть две определяющие его операции — unit и bind
unit принимает на вход значение произвольного типа |a| и возвращает значение типа |m a|, где m — это конкретная реализация какой-нибудь монады. А конкретнее: unit :: a -> m a
То есть unit — это конструктор.
bind принимает на вход 2 параметра, первое — это собственно запакованная монада и второе — это _функция_ которая принимает на вход значение того типа, которым параметризовано данное монадическое значение и должна вернуть опять значение монадического типа.
конкретнее:
bind :: m a -> (a -> m b) -> m b
то есть функция должна явным образом вызывать конструктор unit.
Непосредственно в хаскеле unit и bind называются немного по другому. unit — это return, а bind — это инфиксный оператор (>>=)
И все это определено как обычный класс типов:
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b -- специальный bind для пустого типа: ()
-- И IO определяется как:
instance Monad IO where
-- здесь нечто, что скрывает в себе непосредственно ввод-вывод
Но что нам это все дает?
Давайте посмотрим сначала как этим можно пользоваться
-- настоящий хаскель, можно компилировать и выполнять
module Main where
import IO
main :: IO ()
main = process
-- здесь первый оператор >> принимает на вход значение putStr (IO ())
-- и функцию hFlush, которая возвращает тот же IO ()
-- которому через >> мы скармливаем функцию readName
where process :: IO ()
process = putStr "Enter your name: " >> hFlush stdout >> readName
-- тут мы к getLine, который является значением IO String
-- байндим анонимную функцию с параметром name,
-- значение функции putStrLn, которое всегда будет IO ()
readName :: IO ()
readName = getLine >>= (\name -> putStrLn ("Hello, " ++ name))
То есть, поскольку значение монадического типа мы можем увидеть лишь забайндив к нему через (>>=) другую функцию, которой система типов не дает вернуть ничего, кроме другого монадического значения, мы получаем с одной стороны мощный механизм для описания логики последовательных действий, а с другой — полностью соблюденную прозрачность, т.к в той же функции получаем еще одну, всегда одинаковую монаду, из которой ничего не можем извлечь, а с другой стороны функция, которую мы к монаде байндим и которая оказывается внутри нее не видит самой монады, а видит значение внутри монады. И не может понять изменилось вообще что-то или нет.
Осталась одна маленькая деталь — это все синтаксически не очень красиво выглядит и не всегда хорошо читается. Для этого в хаскель добавили так называемую do-нотацию. В ней перенос строки — это неявный bind и она превращает выражения вроде (x <- mvalue) в (mvalue >>= \x ->) — пример:
main :: IO ()
main = do
putStr "Enter your name: " -- в конце строки стоит неявное: >> \_ ->
hFlush stdout
name <- getLine -- превращается в getLine >>= \name ->
putStrLn ("Hello, " ++ name)
Вот и вся магия. Под конец пример с минимальной полезной работой:
module Main where
import IO
fact :: Integer -> Integer
fact 0 = 1
fact n = n * fact (n - 1)
main :: IO ()
main = do
putStr "Enter a number: " >> hFlush stdout
str <- getLine
let num = read str
resnum = fact num
putStrLn $ "result: " ++ show resnum
Честно говоря получилось рассказать раз в 5 меньше, чем я думал и получилось довольно скомкано, но, надеюсь, хоть что-то будет понятно. Но даже если вы ничего не поняли, запомните главное – все "операции" ввода-вывода — это не операции, а _значение_ этих операций (это особенно верно, что в ленивом языке реальный ввод может произойти в будущем), никакая функция не может вернуть в разное время разное значение и вся программа представляет из себя композицию из функций получающих эти значения ввода-вывода. Как в UNIX shell
А еще монады прекрасный инструмент для очень многих вещей, а не только ввода-вывода.
Про не менее замечательные монады Maybe, State и List как-нибудь потом или можно прочитать здесь (я на самом деле очень рекомендую этот текст тому, кому интересно):
Здравствуйте, reductor, Вы писали:
R>Вот, что смог. Сумбурно.
[]
Хочется кое-что добавить насчет ввода/вывода. Вот сигнатура главной функции Хаскель-программы:
main :: IO()
Для меня самым большим потрясением в Хаскеле было осознание того, что "значение" типа IO(), возвращаемое этой функцией, и является наблюдаемым поведением программы. Т.е. самой целью существования программы. Тем, что в других языках полуформально определяется как последовательность вызовов внешних функций (и обращений к volatile-переменным, там где они есть).
Здравствуйте, Костя Ещенко, Вы писали:
КЕ>Для меня самым большим потрясением в Хаскеле было осознание того, что "значение" типа IO(), возвращаемое этой функцией, и является наблюдаемым поведением программы. Т.е. самой целью существования программы. Тем, что в других языках полуформально определяется как последовательность вызовов внешних функций (и обращений к volatile-переменным, там где они есть).
Ну, на самом деле не так все просто. Это значение _наблюдаемым_ как раз не является
Как и любое другое IO-значение, оно не является определенным. Да, формально функция main возвращает программу. Можно сказать систему одновременно со всеми возможными значениями. Но при попытке наблюдения _та_ программа перестает существовать и сводится к единственному значению.
Причем, это далеко не единственная из возможных интерпретаций для такокой мощной абстракции как IO Monad.
Но вообще цель была помочь понять сам принцип работы монад. Я постарался изложить как можно проще. Надеюсь, успешно.
Сигнатуру я там кстати указал.
Здравствуйте, reductor, Вы писали:
R>Здравствуйте, Костя Ещенко, Вы писали:
КЕ>>Для меня самым большим потрясением в Хаскеле было осознание того, что "значение" типа IO(), возвращаемое этой функцией, и является наблюдаемым поведением программы. Т.е. самой целью существования программы. Тем, что в других языках полуформально определяется как последовательность вызовов внешних функций (и обращений к volatile-переменным, там где они есть).
R>Ну, на самом деле не так все просто. Это значение _наблюдаемым_ как раз не является R>Как и любое другое IO-значение, оно не является определенным. Да, формально функция main возвращает программу. Можно сказать систему одновременно со всеми возможными значениями. Но при попытке наблюдения _та_ программа перестает существовать и сводится к единственному значению.
Как это, при попытке наблюдения программа перестает существовать? Это что-то на тему принципа непределенности?
Наблюдаемое поведение — это устоявшийся перевод устоявшегося английского термина "observable behavior". Этот термин используется по крайней мере в стандартах C, C++, C#.
Программы запускаются на выполнение именно ради их наблюдаемого поведения — производимых ими побочных эффектов — например, запросить ввод и сохранить результат расчета в файл, отправить пакет в сеть, отобразить и позволить отредактировать текстовый документ. Две программы с одинаковым наблюдаемым поведением эквивалентны для пользователя — они выполняют одну и ту же функцию. Естественно, наблюдаемое поведение программы как правило зависит от состояния внешнего мира, и не обязано повторяться от запуска к запуску.
R>Причем, это далеко не единственная из возможных интерпретаций для такокой мощной абстракции как IO Monad. R>Но вообще цель была помочь понять сам принцип работы монад. Я постарался изложить как можно проще. Надеюсь, успешно.
Ясно, я просто дополнил. Мне в понимании IO Monad как раз помогла концепция наблюдаемого поведения.
R>Сигнатуру я там кстати указал.
Здравствуйте, Костя Ещенко, Вы писали:
R>>Ну, на самом деле не так все просто. Это значение _наблюдаемым_ как раз не является R>>Как и любое другое IO-значение, оно не является определенным. Да, формально функция main возвращает программу. Можно сказать систему одновременно со всеми возможными значениями. Но при попытке наблюдения _та_ программа перестает существовать и сводится к единственному значению.
КЕ>Как это, при попытке наблюдения программа перестает существовать? Это что-то на тему принципа непределенности?
Precisely. Причем, эта связь с квантовым миром не совсем случайна.
КЕ>Наблюдаемое поведение — это устоявшийся перевод устоявшегося английского термина "observable behavior". Этот термин используется по крайней мере в стандартах C, C++, C#.
Я не думаю, что здесь уместна аналогия с этим. В первую очередь потому что любое наблюдение подразумевает изменения во времени, чего здесь категорически нет. В то же время, есть модели и абстракции более точно описывающие то, о чем вы говорите. Те же комонады как монады наоборот, стрелки (arrows) как реакции. Вообще есть целая парадигма под названием Reactive Functional Programming с центром в йейльском университете. Они там роботов программируют.
Ее двигает небезызвестный Paul Hudak. кстати в его The Haskell School of Expression есть порядком про это (даже слишком, я бы сказал).
КЕ>Программы запускаются на выполнение именно ради их наблюдаемого поведения — производимых ими побочных эффектов — например, запросить ввод и сохранить результат расчета в файл, отправить пакет в сеть, отобразить и позволить отредактировать текстовый документ. Две программы с одинаковым наблюдаемым поведением эквивалентны для пользователя — они выполняют одну и ту же функцию. Естественно, наблюдаемое поведение программы как правило зависит от состояния внешнего мира, и не обязано повторяться от запуска к запуску.
Вот вы делаете здесь ту ошибку (чисто формальную, впрочем), от которой я в тексте два раза предостерег — не совсем правильно рассматривать программу на Haskell как *поведение*. И хотя денотационно ее невозможно полностью описать, поскольку очевидно, что у некоторых функций запросто может быть значение _|_, хотя при этом польза в них есть. У Simon PJ была где-то статья, где он формально определял операционную семантику монад сохраняя при этом полную декларативность. В любом случае, рассматривать программы на хаскеле как автомат будет мало осмысленно. Хотя бы потому что это нам ничего не дает полезного (за что боролись).
Вся соль в том, что монады позволяют нам вообще не рассматривать программу на хаскеле как что-то единое — их эффекты расслаивают код на множество независимых компонентов каждый из которых является самостоятельной единицей, что само по себе является очень большим достоинством как для формализации, так и для code reuse
КЕ>Ясно, я просто дополнил. Мне в понимании IO Monad как раз помогла концепция наблюдаемого поведения.
И спешу сообщить, что пониманий у монад может быть очень много. Главное, что это понимание в каждом конкретном случае можно менять в зависимости от ситуации. В чем и вся соль.
Ну вот, хотел помочь, а в результате опять спор начинается. В целом пост слишком теоретический для меня, но на высказывания "по делу" постараюсь ответить. Чисто конкретно.
[]
КЕ>>Наблюдаемое поведение — это устоявшийся перевод устоявшегося английского термина "observable behavior". Этот термин используется по крайней мере в стандартах C, C++, C#.
R>Я не думаю, что здесь уместна аналогия с этим. В первую очередь потому что любое наблюдение подразумевает изменения во времени, чего здесь категорически нет.
Уместна и есть, ИМО. В IO существенным является последовательность операций, для формирования которой и используются монады.
R>В то же время, есть модели и абстракции более точно описывающие то, о чем вы говорите. Те же комонады как монады наоборот, стрелки (arrows) как реакции. Вообще есть целая парадигма под названием Reactive Functional Programming с центром в йейльском университете. Они там роботов программируют. R>Ее двигает небезызвестный Paul Hudak. кстати в его The Haskell School of Expression есть порядком про это (даже слишком, я бы сказал).
Если есть ссылка "популярно для чайников", то с удовольствием просмотрел был.
КЕ>>Программы запускаются на выполнение именно ради их наблюдаемого поведения — производимых ими побочных эффектов — например, запросить ввод и сохранить результат расчета в файл, отправить пакет в сеть, отобразить и позволить отредактировать текстовый документ. Две программы с одинаковым наблюдаемым поведением эквивалентны для пользователя — они выполняют одну и ту же функцию. Естественно, наблюдаемое поведение программы как правило зависит от состояния внешнего мира, и не обязано повторяться от запуска к запуску.
R>Вот вы делаете здесь ту ошибку (чисто формальную, впрочем), от которой я в тексте два раза предостерег — не совсем правильно рассматривать программу на Haskell как *поведение*.
Ее результатом является описание поведения. Если нет, то что по-Вашему является результатом программы?
R>И хотя денотационно ее невозможно полностью описать, поскольку очевидно, что у некоторых функций запросто может быть значение _|_, хотя при этом польза в них есть. У Simon PJ была где-то статья, где он формально определял операционную семантику монад сохраняя при этом полную декларативность. В любом случае, рассматривать программы на хаскеле как автомат будет мало осмысленно. Хотя бы потому что это нам ничего не дает полезного (за что боролись).
Кажется, я читал эту статью, по крайней мере пытался. Предлагаю перевести разговор в более конкретное русло, меня скорее интересует не сама мат. абстракция, а ее интерпретация в область программирования.
R>Вся соль в том, что монады позволяют нам вообще не рассматривать программу на хаскеле как что-то единое — их эффекты расслаивают код на множество независимых компонентов каждый из которых является самостоятельной единицей, что само по себе является очень большим достоинством как для формализации, так и для code reuse
R>И спешу сообщить, что пониманий у монад может быть очень много. Главное, что это понимание в каждом конкретном случае можно менять в зависимости от ситуации. В чем и вся соль.
Сложно понимать такие расплывчатые фразы. Но на всякий случай согласшусь.
Это как раз стандартный хаскелловский сахар, называемый layout, и он эквивалентен тексту do { xxx ; yyy ; zzz } точно так же, как и let-in, where, case и т.п.
А вот над do { xxx; yyy; zzz } уже проводятся преобразования...
do {} -- запрещённая конструкция.
-- одиночное действие (монадическая функция)
do action = action
do { action } = action
-- последовательность - ассоциативна
do { statements1 ; statements2 } = do { statements1 } >>= do { statements2 }
-- вводим определение - оно применяется в контексте всех следующий за ним действий
do { let definition ; statements } = (let definition in do { statements })
-- переменная получает значение из действия:
-- нужно не только отдать это значение по конвейеру, но и ввести определение переменной
-- поэтому вводим функцию-обёртку, где var - это имя параметра
do { var <- action ; statements } = action >>= (\var -> do { statements })
-- это то, о чём ты писал: var <- action [crlf] заменили на action >>= \var -> [crlf]
-- если результат действия сопоставляем с образцом, то функция-обёртка усложняется
do { pattern <- action ; statements }
let justmatch pattern = do {statements} -- сопоставили и попутно определили переменные образца
justmatch _ = fail "сопоставление обломилось" -- вернули исключительное значение
in action >>= justmatch
-- кстати, это значит, что ни let, ни something<- не может быть последним элементом do-списка
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, reductor, Вы писали:
К><>
К>Кстати, о монадах можно почитать в документе Yet Another Haskell Tutorial (http://www.isi.edu/~hdaume/htut/, http://www.haskell.org/learning.html) К>И кстати, do-нотация является не только синтаксическим сахаром, но и имеет формализм.
Да понятно, просто это вообще ответ на другое сообщение был.
не знаю как это сюда попало сейчас — здесь, думаю, и так все знают что такое монады и do вместе с list comprehensions и тп, так что получается диковато, действительно.
Я просто попытался объяснить максимально просто в дискуссии там.
Здравствуйте, reductor, Вы писали:
R>Да понятно, просто это вообще ответ на другое сообщение был. R>не знаю как это сюда попало сейчас — здесь, думаю, и так все знают что такое монады и do вместе с list comprehensions и тп, так что получается диковато, действительно. R>Я просто попытался объяснить максимально просто в дискуссии там.
Модераторы постарались: IT отцепил, а я перенёс сюда.
Потому что:
— Эта информация может быть интересна многим, а не только тем, кто закопался в недра мега-ветки.
— Она представляет самостоятельный интерес и может быть вытащена в изолированный контекст.
— К общефилософии (на мой модераторский взгляд) твой текст имеет меньшее отношение, чем к вопросам подробностей ФЯ (и даже конкретно Haskell).
— Если в философии или ещё где возникнет вопрос про монады — ссылку-то на сообщение всегда можно дать, заодно человек копнёт тематический форум.
— Наконец, не стоит завышать мнение о здешних завсегдатаях (не говоря уже о новичках).
Например, для меня недавно было открытием, что list comprehensions в Haskell является монадическим действием (вплоть до синтаксиса). Как дела с этим обстоят в Python-е — не знаю, нужно подумать, почитать документацию.
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, reductor, Вы писали:
R>>Да понятно, просто это вообще ответ на другое сообщение был. R>>не знаю как это сюда попало сейчас — здесь, думаю, и так все знают что такое монады и do вместе с list comprehensions и тп, так что получается диковато, действительно. R>>Я просто попытался объяснить максимально просто в дискуссии там.
К>Модераторы постарались: IT отцепил, а я перенёс сюда. К>Например, для меня недавно было открытием, что list comprehensions в Haskell является монадическим действием (вплоть до синтаксиса).
да, это то же самое, что и do, просто для списков, что в общем понятно из-за (<-)
Причем, специальная нотация для списков лично меня нервирует (раньше, кстати, вроде, они были равнозначны с do)
К>Как дела с этим обстоят в Python-е — не знаю, нужно подумать, почитать документацию.
в питоне совершенно ужасно
то есть сначала это был просто наглый хак для парсера и рантайма, потом как-то генерализовали через generator expressions и операцию yield. И это при всем при том, что GvR выкидывает оттуда лямбду и отказался когда-то поддерживать тот же stackless и нормальную раскрутку tail calls (через которые все эти вещи яйца выеденого не стоят).
Здравствуйте, Кодт, Вы писали:
К>- Наконец, не стоит завышать мнение о здешних завсегдатаях (не говоря уже о новичках).
Вот-вот. В остальных форумах оживление, новички спрашивают, бывалые отвечают, и иногда из вопроса "а что такое указатели" вполне может вырасти интересное обсуждение. А зачем еще форумы?
А здесь — как на совете старейшин индейского племени, собрались гуры и глубокомысленно молчат , всем все ясно. Если новички вроде меня не будут нарушать тишину, форум пропадет (очень ИМХО, своих заслуг на преувеличиваю).
Здравствуйте, reductor, Вы писали:
К>>Например, для меня недавно было открытием, что list comprehensions в Haskell является монадическим действием (вплоть до синтаксиса).
R>да, это то же самое, что и do, просто для списков, что в общем понятно из-за (<-) R>Причем, специальная нотация для списков лично меня нервирует (раньше, кстати, вроде, они были равнозначны с do)
Не могу сказать, что это "в общем понятно" — раз в одном месте синтаксический сахар насыпали, то и в другом могли.
Например, let-in и let-do — и синтаксически, и по смыслу отличаются, хотя кейворд один и тот же.
Опять же, '|' в кейсах обозначает сопоставления с шаблонами, в функциях — проверки условий, в l.c. — отделяет результат от do-формы. В let/where и do-формах для разделения стейтментов используются ';' а в l.c. — ','
Почему бы не решить, что '<-' значит нечто другое?
Вот это, кстати, меня в Хаскелле и напрягает, и радует одновременно: количество синтаксического сахара.
Радует — потому что язык не страдает пуризмом (как, например, лисп — lots of idiotic silly parenthesis) и поворачивается лицом к кодеру.
Здравствуйте, reductor, Вы писали:
К>>Например, для меня недавно было открытием, что list comprehensions в Haskell является монадическим действием (вплоть до синтаксиса). R>да, это то же самое, что и do, просто для списков, что в общем понятно из-за (<-)
Кстати, а как отобразить l.c. в do? Там нужно присобачивать запаковку/распаковку монады, плюс создать цикл (концевую рекурсию)...
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, reductor, Вы писали:
К>>>Например, для меня недавно было открытием, что list comprehensions в Haskell является монадическим действием (вплоть до синтаксиса). R>>да, это то же самое, что и do, просто для списков, что в общем понятно из-за (<-)
К>Кстати, а как отобразить l.c. в do? Там нужно присобачивать запаковку/распаковку монады, плюс создать цикл (концевую рекурсию)...
Ага!
Вот почему меня l.c и нервируют так в смысле селективности.
here comes magic
Prelude> do { x <- [1,2,3,4]; return (x + 5) }
[6,7,8,9]
Можно дополнительно посмотреть как определена монада List
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, reductor, Вы писали:
К>>>Например, для меня недавно было открытием, что list comprehensions в Haskell является монадическим действием (вплоть до синтаксиса).
R>>да, это то же самое, что и do, просто для списков, что в общем понятно из-за (<-) R>>Причем, специальная нотация для списков лично меня нервирует (раньше, кстати, вроде, они были равнозначны с do)
К>Не могу сказать, что это "в общем понятно" — раз в одном месте синтаксический сахар насыпали, то и в другом могли. К>Например, let-in и let-do — и синтаксически, и по смыслу отличаются, хотя кейворд один и тот же. К>Опять же, '|' в кейсах обозначает сопоставления с шаблонами, в функциях — проверки условий, в l.c. — отделяет результат от do-формы. В let/where и do-формах для разделения стейтментов используются ';' а в l.c. — ',' К>Почему бы не решить, что '<-' значит нечто другое?
К>Вот это, кстати, меня в Хаскелле и напрягает, и радует одновременно: количество синтаксического сахара. К>Радует — потому что язык не страдает пуризмом (как, например, лисп — lots of idiotic silly parenthesis) и поворачивается лицом к кодеру.
Да нормально у лиспа все с синтаксисом причем в его случае там все еще и меняется очень легко.
Лисп гораздо менее склонен к пуризму, чем хаскель
Но у лиспа и хаскеля совершенно различается семантика, потому отобразить один подход в другой будет очень непросто.
а так вся ж программа на хаскеле — это исключительно \x -> \y -> lots of idiotic silly arrows
И нет ничего через лямбду невыразимого.
ну немножко с сахаром, в лиспе он тоже есть — то же loop macro, например.
не считая огромного количества символьных макросов типа того же '() — (quote)
Здравствуйте, reductor, Вы писали:
R>here comes magic
Белый шаман!
Я так понимаю, List специально сделали монадой (да ещё и монадой+), чтобы можно было в l.c. играть?
Не, ну до чего затейливо и сумрачно!
Особенно прикольно вместо return напрямую конструировать список.
A> do { x<-[1..3]; [x*2,x*3] }
[2,3,4,6,6,9]
-- а может быть, я хотел вот это, да попал в затмение
do { x<-[1..3]; return [x*2,x*3] }
[[2,3],[4,6],[6,9]]
-- ну допустим, типизация нас спасла бы выше... а вот здесь
do { x<-[1..3]; return [] }
[[],[],[]]
do { x<-[1..3]; [] } -- признак 'fail'
[]
Вообще-то, это бардак — когда конструкторы и операторы монад (return, fail, mplus и т.п.) выражаются одним термом (да ещё и встроенным в синтаксис): [x], [], ++
Такое раздолье для ошибок.
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, reductor, Вы писали:
R>>here comes magic
К>Белый шаман!
К>Я так понимаю, List специально сделали монадой (да ещё и монадой+), чтобы можно было в l.c. играть? К>Не, ну до чего затейливо и сумрачно!
Да нет, просто монады хорошо формализуют это дело
К>Особенно прикольно вместо return напрямую конструировать список. К>
К>-- ну допустим, типизация нас спасла бы выше... а вот здесь
К>do { x<-[1..3]; return [] }
К>[[],[],[]]
здесь кстати типизация тоже спасет — не в пустоту же пойдет [a]] вместо [Integer]
No instance for (Num [a])
К>do { x<-[1..3]; [] } -- признак 'fail'
К>[]
А вот и разгадка операции [] :)
К>Вообще-то, это бардак — когда конструкторы и операторы монад (return, fail, mplus и т.п.) выражаются одним термом (да ещё и встроенным в синтаксис): [x], [], ++ К>Такое раздолье для ошибок.
Да нет, как раз ошибки-то в хаскеле сложно совершать с такой формализируемостью.
Но лично мне не нравится, что я должен выделять те же списки из остальных монад.
А вот операторы там и синонимы смущают не очень сильно — как-то двусмысленностей на самом деле нет, иначе бы формально это все не прошло.
Здравствуйте, reductor, Вы писали:
К>>Особенно прикольно вместо return напрямую конструировать список. К>>
К>>-- ну допустим, типизация нас спасла бы выше... а вот здесь
К>>do { x<-[1..3]; return [] }
К>>[[],[],[]]
R>
R>здесь кстати типизация тоже спасет — не в пустоту же пойдет [ [a] ] вместо [Integer]
А кто сказал, что я там хочу [Integer]. Я, может быть, как раз хотел получить список из трёх пустых списков типа... ну мало ли какого типа. Пусть будет [(Integer,Integer)]].
Потом забыл записать return — и получилось, что я записал fail!
[] :: [a], где a может быть равен [(Integer,Integer)], правда же?
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, reductor, Вы писали:
К>>>Особенно прикольно вместо return напрямую конструировать список. К>>>
К>>>-- ну допустим, типизация нас спасла бы выше... а вот здесь
К>>>do { x<-[1..3]; return [] }
К>>>[[],[],[]]
R>>
R>>здесь кстати типизация тоже спасет — не в пустоту же пойдет [ [a] ] вместо [Integer]
К>А кто сказал, что я там хочу [Integer]. Я, может быть, как раз хотел получить список из трёх пустых списков типа... ну мало ли какого типа. Пусть будет [(Integer,Integer)]].
Так просто ни формализм хаскеля ни его систему типов не обмануть. То есть случайным образом не обмануть точно :)
К>Потом забыл записать return — и получилось, что я записал fail!
так это определено:
instance Monad [] where
m >>= k = concat (map k m)
return x = [x]
fail s = []
К>[] :: [a], где a может быть равен [(Integer,Integer)], правда же?
Да, [a] матчится на [(Integer,Integer)], но [[],[],[]] уже не матчится на [()].
в случае же с [] без return — это явно прописаный fail иначе бы это в свою очередь не совпало с определением списка как
data [a] = [] | a : [a]
где пустая клетка — конец списка
точно так же список записывается просто так:
1 : 2 : 3 : []
Где без явного fail в конце система типов нас грязно обругает
так же:
Вообще такие вещи через пару недель плотного программирования на хаскеле просто ложатся в подкорку и выводятся потом автоматически
А пропустить это невозможно как и в случае с явным определением fail pattern в рекурсивных функция для списков: