Даже unsafePerformIO не выполнит IO-действие, пока его обратно не обернут так или иначе в IO в main. Потому что реальные вычисления только там (и оттуда же форсируются чистые).
Т.е.
Ну на самом деле конечно и переменные есть и всё прочее, но на Haskell так писать не принято. Само разделение на IO и не-IO уже провоцирует код разделять на логику и ввод-вывод.
И в этом смысле очевидный способ реагировать на события «грязными» IO-функция сработает, конечно, и в Haskell, но это не идиоматично.
Сразу возникает вопрос, а зачем надрываться? Поначалу это кажется пустой тратой времени, но потом выясняется, что работать разделенной логикой и вводом-выводом намного проще. Divide et impera.
Хм. Не понятно. Почему иметь три разные конструкции и выстраивать между ними мозговыворачивательные переходники лучше, чем иметь универсальный foldr, который легко подстраивать под конкретную ситуацию?
Затем же, зачем делают обёртки над одной универсальной функцией.
Затем же, зачем в CAD'ах есть кнопка «прямоугольник», хотя это просто полилиния замктуная. И даже для Arc делают аж 3-4 кнопки, а не одну, которой можно создать любой Arc.
Затем же, зачем пишут (пример условный) square(point(0, 0), 20);
вместо polyline(point(0,0), point(20,0), point(20,20), point(0, 20));
Чтобы было проще читать код. По имени map/filter и прочим сразу ясно, что делается. По for/foldr — набившие руку в этом деле, конечно, достаточно быстро разбирают, но тем не менее напрямую использовать foldr — дурной тон. Зачем лишние усложнения?
Если все перейдут на Haskell, то и на нём навертят такого, что без слёз не взглянешь, это понятно. И что инструмент лучше попроще, чтоб не надо было на него умника искать, а взять десяток средних.
Я лишь говорил о том, что для программиста на Haskell, написать свою монаду даже проще, чем написать класс. Чем ближе к матану, тем мощнее абстракции. Порог, конечно, значительно повышается, но если его преодолеть, это с лихвой окупится.
Если не бояться слова «монада», то это равносильно написанию одного класса. Специализированные функции для Parser monad, плюс instance Monad Parser в 2 тривиальных функции. Это правда сложно?
Краткость измеряется не в символах. Конкретно этот код никак не короче приведённого в статье на 4 строки (алгоритм разный, я понимаю, я просто о том, что хоть там и 4 строки, но он всё же короче)
Я привёл несколько неудачный пример. Вот пример поудачнее:
do
send int 1
x <- receive int
send string ("hello" ++ show x)
y <- receive string
return (show x ++ y)
send — отправляет сообщение клиенту
receive — получает сообщение
Однако receive не блокирует, так как на деле он не ждёт сообщения, а регистрирует callback. Т.е. всё, что ниже receive — автоматом callback.
Другой пример:
do
line <- lines contentOfFile
word <- words line
ch <- word
if ch == 'x' return True else return False
Аналогичен:
foreach (line in lines(contentOfFile))
foreach (word in words(line))
foreach (ch in word)
if (ch == 'x') { yield return true; } else { yield return false; }
Однако тут нет вложенных foreach, это делает монада.
Контракт может быть полезным, может не быть.
Однако если у нас нет возможности его наложить, сложно обсуждать его полезность.
В Agda можно статически гарантировать, что функция, принимающая число, вернёт список с длиной, равной этому числу.
В Haskell и C++ этого гарантировать нельзя, и в этом смысле Agda более мощный язык.
Тот факт, что в язык вводят nothrow и pure, говорит о полезности таких контрактов.
А как в D дела с STM (Software Transactional Memory)? В Haskell это органично вписалось без каких-либо проблем на существующую систему типов. Как это сделать в D так же органично?
У Haskell выбраны другие умолчания: чистый язык и навешиваемые расширения (STM, IO, State, Cont, list-monad...). На мой взгляд этот путь себя оправдывает.
А то чувство, когда горы кода после первой же удачной компиляции сразу работают как надо, незабываемо, и про него говорит почти каждый новоявленный Haskell'ист :)
Именованный тип от кортежа ничем структурно не отличается, т.е. в данном случае достаточно взять 4 фигурки подтипов и соединить их как удобнее.
Вообще говоря если брать мат.запись, то имеются кортеж (product) (A x B), т.е. оба значения, и coproduct (A + B), т.е. одно из значений (Either). Для типов с фиксированным кол-вом значений используют число. Т.е. например Bool — это 2.
Пример:
Список — голова или хвост, содержащий элемент и список
list(A) = 1 + A x list(A)
Maybe:
maybe(A) = 1 + A
Изобразить кортеж, в общем-то, несложно. А вот по поводу coproduct и аналогов Bool надо думать.
Визуализация штука сложная, конечно.
Функция — это преобразователь из одного типа данных в другой, и тогда значения одного типа должны выглядеть одинаково (но разные типы — по-разному).
А guard — это фильтр значений, т.е. пропускает определённые значения и не пропускает другие, тогда его форма должна зависеть от самого значения.
Ну, например.
length — тоннель с треугольным входом и квадратным выходом. Входит любой длины треугольная призма, выходит параллепипед. Причём в поперечной оси отражается тип (треугольник — список, квадрат — число) а в продольной — значение.
length xs == 0 — guard, который сначала пропускает значение вперёд через length, меняя его тип, а затем поперёк через тоннель, способный пропустить только 0.
Список, правда, продвинутый пример, так как он полиморфный.
Интересно было бы подумать, не пригодится ли при визуализации математическая запись типов:
list(A) = 1 + A * list(A)
bool = 2
nat = 1 + nat
В общем, полезного я мало сказал, но может хоть идей на размышление подкинул.
Haskell, конечно, далёк от идеала. Например, там есть две функции: map :: (a -> b) -> [a] -> [b]
mapM :: Monad m => (a -> m b) -> [a] -> m [b]
Хотя вообще говоря достаточно лишь одной реализации (первой), а во вторую она превращается автоматически переносом на стрелки Клейсли.
Т.е. map для списка может быть генерализован до: map :: (Arrow a, ArrowChoice a) => a b c -> a [b] [c]
с одной единственной имплементацией, подходящей как для функций с эффектами, так и для чистых.
Т.е. любая лямбда-функция типа \f x -> f x + 2 * f x может иметь сразу обобщённый тип. Но в Haskell этого нет, и пока вроде нигде.
Этот вопрос не понял, мне незнаком термин «плоское написание асинхронного кода».
Например как тут.
Т.е. вместо явной передачи callback мы пишем, к примеру x <- download "foo";
lift $ print "foo is downloading"
y <- download "bar";
lift $ print "bar is downloading"
x' <- await x
y' <- await y
return (x' + y')
Это как пример, нафантазировать можно то, что удобнее.
Я это к тому, что цель языка программирования — быть удобным и практичным, а не предоставить строгую красивую модель.
Основной момент в «мне бы не хватало». Поверьте, мне очень многого не хватало из C++ поначалу, но потом оказалось, что это от лукавого :)
Строгая красивая модель делает язык удобным для чтения, ибо мало частных случаев и каких-то ситуаций, где только знание каких-то нюансов стандарта помогает понять, что происходит. Всё декларируется достаточно явно.
Как насчёт вставки в ваш мутабельный пример inline assembler'a?
Ну ассемблер напрямую не вставить, ибо Haskell куда выше уровнем, да и компилироваться может через LLVM. Но звать сишные функции никто не запрещает.
Кстати, никто не мешает и в чистой функции пользоваться state'ом через ST монаду, она позволяет использовать переменные только внутри, поэтому результат гарантированно чистый (причём опять же, никаких хаков). Это похоже на ограничения pure в D.
Вы исходите из предпосылки, что императивный код «грязнее» и хуже только потому, что он императивный.
Вовсе нет. Я исхожу из предпосылки, что чем меньше мы можем наложить контракта на функцию, тем больше вероятность ошибок.
В динамике никаких ограничений не наложить, в статике дела получше — можно задать типы, но как правило не запретить ввод-вывод и прочее. В Haskell благодаря чистой функциональности и это выводится на уровень системы типов — можем по типу задать эффекты функции. В языках с зависимыми типами (Agda, Coq) можно статически гарантировать практически всё, что угодно. Например, что функция именно конкатенирует списки, а не делает что-либо ещё.
Т.е.
И в этом смысле очевидный способ реагировать на события «грязными» IO-функция сработает, конечно, и в Haskell, но это не идиоматично.
Сразу возникает вопрос, а зачем надрываться? Поначалу это кажется пустой тратой времени, но потом выясняется, что работать разделенной логикой и вводом-выводом намного проще. Divide et impera.
Затем же, зачем делают обёртки над одной универсальной функцией.
Затем же, зачем в CAD'ах есть кнопка «прямоугольник», хотя это просто полилиния замктуная. И даже для Arc делают аж 3-4 кнопки, а не одну, которой можно создать любой Arc.
Затем же, зачем пишут (пример условный)
square(point(0, 0), 20);вместо
polyline(point(0,0), point(20,0), point(20,20), point(0, 20));Чтобы было проще читать код. По имени map/filter и прочим сразу ясно, что делается. По for/foldr — набившие руку в этом деле, конечно, достаточно быстро разбирают, но тем не менее напрямую использовать foldr — дурной тон. Зачем лишние усложнения?
collect :: [Int] -> [String] -> [String]
collect is ss = [s | (i, s) <- zip [0..] ss, i `elem` is]
Я лишь говорил о том, что для программиста на Haskell, написать свою монаду даже проще, чем написать класс. Чем ближе к матану, тем мощнее абстракции. Порог, конечно, значительно повышается, но если его преодолеть, это с лихвой окупится.
Обобщённость и могущественность — это математика.
task!readText?Я привёл несколько неудачный пример. Вот пример поудачнее:
do send int 1 x <- receive int send string ("hello" ++ show x) y <- receive string return (show x ++ y)send — отправляет сообщение клиенту
receive — получает сообщение
Однако receive не блокирует, так как на деле он не ждёт сообщения, а регистрирует callback. Т.е. всё, что ниже receive — автоматом callback.
Другой пример:
Аналогичен:
foreach (line in lines(contentOfFile)) foreach (word in words(line)) foreach (ch in word) if (ch == 'x') { yield return true; } else { yield return false; }Однако тут нет вложенных foreach, это делает монада.
Однако если у нас нет возможности его наложить, сложно обсуждать его полезность.
В Agda можно статически гарантировать, что функция, принимающая число, вернёт список с длиной, равной этому числу.
В Haskell и C++ этого гарантировать нельзя, и в этом смысле Agda более мощный язык.
Тот факт, что в язык вводят nothrow и pure, говорит о полезности таких контрактов.
А как в D дела с STM (Software Transactional Memory)? В Haskell это органично вписалось без каких-либо проблем на существующую систему типов. Как это сделать в D так же органично?
У Haskell выбраны другие умолчания: чистый язык и навешиваемые расширения (STM, IO, State, Cont, list-monad...). На мой взгляд этот путь себя оправдывает.
А то чувство, когда горы кода после первой же удачной компиляции сразу работают как надо, незабываемо, и про него говорит почти каждый новоявленный Haskell'ист :)
Вообще говоря если брать мат.запись, то имеются кортеж (product) (A x B), т.е. оба значения, и coproduct (A + B), т.е. одно из значений (Either). Для типов с фиксированным кол-вом значений используют число. Т.е. например Bool — это 2.
Пример:
Список — голова или хвост, содержащий элемент и список
list(A) = 1 + A x list(A)
Maybe:
maybe(A) = 1 + A
Изобразить кортеж, в общем-то, несложно. А вот по поводу coproduct и аналогов Bool надо думать.
Функция — это преобразователь из одного типа данных в другой, и тогда значения одного типа должны выглядеть одинаково (но разные типы — по-разному).
А guard — это фильтр значений, т.е. пропускает определённые значения и не пропускает другие, тогда его форма должна зависеть от самого значения.
Ну, например.
length — тоннель с треугольным входом и квадратным выходом. Входит любой длины треугольная призма, выходит параллепипед. Причём в поперечной оси отражается тип (треугольник — список, квадрат — число) а в продольной — значение.
length xs == 0 — guard, который сначала пропускает значение вперёд через length, меняя его тип, а затем поперёк через тоннель, способный пропустить только 0.
Список, правда, продвинутый пример, так как он полиморфный.
Интересно было бы подумать, не пригодится ли при визуализации математическая запись типов:
list(A) = 1 + A * list(A)
bool = 2
nat = 1 + nat
В общем, полезного я мало сказал, но может хоть идей на размышление подкинул.
map :: (a -> b) -> [a] -> [b]
mapM :: Monad m => (a -> m b) -> [a] -> m [b]
Хотя вообще говоря достаточно лишь одной реализации (первой), а во вторую она превращается автоматически переносом на стрелки Клейсли.
Т.е. map для списка может быть генерализован до:
map :: (Arrow a, ArrowChoice a) => a b c -> a [b] [c]
с одной единственной имплементацией, подходящей как для функций с эффектами, так и для чистых.
Т.е. любая лямбда-функция типа
\f x -> f x + 2 * f xможет иметь сразу обобщённый тип. Но в Haskell этого нет, и пока вроде нигде.Например как тут.
Т.е. вместо явной передачи callback мы пишем, к примеру
x <- download "foo";
lift $ print "foo is downloading"
y <- download "bar";
lift $ print "bar is downloading"
x' <- await x
y' <- await y
return (x' + y')
Это как пример, нафантазировать можно то, что удобнее.
Основной момент в «мне бы не хватало». Поверьте, мне очень многого не хватало из C++ поначалу, но потом оказалось, что это от лукавого :)
Строгая красивая модель делает язык удобным для чтения, ибо мало частных случаев и каких-то ситуаций, где только знание каких-то нюансов стандарта помогает понять, что происходит. Всё декларируется достаточно явно.
Ну ассемблер напрямую не вставить, ибо Haskell куда выше уровнем, да и компилироваться может через LLVM. Но звать сишные функции никто не запрещает.
Кстати, никто не мешает и в чистой функции пользоваться state'ом через ST монаду, она позволяет использовать переменные только внутри, поэтому результат гарантированно чистый (причём опять же, никаких хаков). Это похоже на ограничения pure в D.
Вовсе нет. Я исхожу из предпосылки, что чем меньше мы можем наложить контракта на функцию, тем больше вероятность ошибок.
В динамике никаких ограничений не наложить, в статике дела получше — можно задать типы, но как правило не запретить ввод-вывод и прочее. В Haskell благодаря чистой функциональности и это выводится на уровень системы типов — можем по типу задать эффекты функции. В языках с зависимыми типами (Agda, Coq) можно статически гарантировать практически всё, что угодно. Например, что функция именно конкатенирует списки, а не делает что-либо ещё.