LINUX.ORG.RU

Зачем в Haskell IO?

 ,


0

2

Haskell язык ленивый. Для интерактивных программ достаточно вообще обычных строк:

main = interact realMain

realMain :: String -> String
realMain = unlines . f . lines
  where
    f x = "Введите своё имя: ":g x
    g (имя:числа) = ["Привет, " ++ имя, "Вводите числа, q для выхода"] ++ корни числа
    корни ("q":_) = []
    корни (x:xs) = (show . sqrt) (read x :: Double):корни xs

Вот программа, которая запрашивает имя пользователя, затем выводит корни введённых чисел пока не получит q. Никакого IO внутри realMain не требуется.

Если брать произвольное взаимодействие с окружающим миром, то достаточно main :: [inEvent] -> [outEvent].

Зачем нужен «магический» тип IO, делящий Haskell на фактически два разных языка?

★★★★★

Последнее исправление: monk (всего исправлений: 1)

Чтобы на нём можно было написать что-то реальное. Взаимодействие с RealWorld - это принципиально последовательная процедура, а значит д.б. завернута в монаду

Crocodoom ★★★★★
()

Никакого IO внутри realMain не требуется.

Правильно, потому что всю работу последовательную работу делает interact

Crocodoom ★★★★★
()

Если брать произвольное взаимодействие с окружающим миром, то достаточно main :: [inEvent] -> [outEvent].

А раньше так и было лол!

https://raw.githubusercontent.com/stasoid/Gofer/master/haskell-report-1.2.pdf

Смотри тут. Глава 7, «Input/Output».

Зачем нужен «магический» тип IO, делящий Haskell на фактически два разных языка?

Нет там двух языков. IO – это просто ST s, где s ~ RealWorld.

Haskell язык ленивый.

Ленивость тут вообще не при делах.

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 2)
Ответ на: комментарий от hateyoufeel

Нет там двух языков. IO – это просто ST s, где s ~ RealWorld.

Хорошее замечание. Также добавлю, что монады composable, но вот поскольку IO — это просто ST Realworld, то IO монаду не накрутишь сверху, она всегда будет в самой глубине составных типов. Это такая абстракция максимально неизвестного нам побочного эффекта

Crocodoom ★★★★★
()
Ответ на: комментарий от hateyoufeel

Окей, я был не точен.

Вообще, имел в виду вот эту штуку

https://ru.wikibooks.org/wiki/Haskell/Monad_transformers

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

Crocodoom ★★★★★
()
Последнее исправление: Crocodoom (всего исправлений: 2)
Ответ на: комментарий от Crocodoom

Трансформеры не являются монадами сами по себе. Это отдельный конструкт со своими свойствами.

Сейчас для описания подобного набирают популярность библиотеки с эффектами на основе free monads с блекджеком и шлюхами.

hateyoufeel ★★★★★
()
Ответ на: комментарий от Crocodoom

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

Зачем? Можно ведь просто работать со списком.

monk ★★★★★
() автор топика
Ответ на: комментарий от Crocodoom

Правильно, потому что всю работу последовательную работу делает interact

А ещё выше всю работу делает виртуальная машина Haskell (исполнитель IO). Чем плохо было бы, если бы main имел типом обработку списка?

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

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

Aswed ★★★★★
()
Ответ на: комментарий от hateyoufeel

А раньше так и было лол!

Как интересно. А почему отказались?

Ведь «With this view of I/O, there is no need for any special-purpose syntax or constructs for I/O; the I/O system is defined entirely in terms of how the operating system responds to a program with the above type|i.e. what response it issues for each request.»

Ленивость тут вообще не при делах.

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

monk ★★★★★
() автор топика
Ответ на: комментарий от Aswed

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

В чистых вычислениях есть seq, если очень надо прочитать и ничего не делать.

IO действия, которые нужно выполнять строго так, как написано

Обработка списка тоже идёт всегда строго так, как написано.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

seq

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

Обработка списка тоже идёт всегда строго так, как написано.

Тут согласен, но конструктор (:) и оператор применения функции ( ) это в общем-то, похоже, эквивалентные штуки. Думаю, и на списках можно сделать «монаду», лисп же сделали

Crocodoom ★★★★★
()
Последнее исправление: Crocodoom (всего исправлений: 1)
main :: [inEvent] -> [outEvent]
Зачем нужен «магический» тип IO

IO является «обычным» библиотечным типом и примерно так и определен: https://github.com/ghc/ghc/blob/master/libraries/ghc-prim/GHC/Types.hs#L250

newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))

Только вместо списков значений ([inEvent]) State# RealWorld является генератором значений: RealWorld -> (RealWorld, a): https://github.com/ghc/ghc/blob/master/libraries/base/GHC/Base.hs#L1591

instance  Monad IO  where
    -- ...
    (>>=)     = bindIO

-- ...

bindIO :: IO a -> (a -> IO b) -> IO b
bindIO (IO m) k = IO (\ s -> case m s of (# new_s, a #) -> unIO (k a) new_s)

Проблема [inEvent] -> [outEvent] только синтаксическая: нужно всё явно передавать.

Зачем нужен «магический» тип IO, делящий Haskell на фактически два разных языка?

Если речь о do синтаксисе, то от не IO-специфичен и является чистым синтаксичеким сахаром поверх лямбда функций: https://en.wikibooks.org/wiki/Haskell/do_notation. Любое do-выражение можно развернуть в вызов функции.

Для IO do-синтаксис используется, чтобы не заставлять пользователя явно передавать новую версию RealWorld на каждом шаге:

main = do c <- readFile "a"
          writeFile "b" c
main world = case readFile world "a" in
                 (world', c) -> case writeFile world' "b" c in
                     (world'', _) -> world''

Добавился world / world' / world'' boilerplate (и возможность случайно 2 раза использовать один из world).

sf ★★★
()
Ответ на: комментарий от Crocodoom

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

Это для производительности (чтобы память не жрало на ленивый хвост). На результат вычисления не влияет.

Думаю, и на списках можно сделать «монаду»

Список является монадой:

> do { x <- [1,2,3]; return $ x+1 }
[2,3,4]
monk ★★★★★
() автор топика
Ответ на: комментарий от monk

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

λ> do { x <- [1,2,3]; y <- [10,20,30]; return $ x+y }
[11,21,31,12,22,32,13,23,33]
Crocodoom ★★★★★
()
Последнее исправление: Crocodoom (всего исправлений: 2)
Ответ на: комментарий от sf

Если речь о do синтаксисе, то от не IO-специфичен и является чистым синтаксичеким сахаром поверх лямбда функций

Нет. Я про несовместимость Int и IO Int.

Добавился world / world’ / world'' boilerplate (и возможность случайно 2 раза использовать один из world).

Это именно из-за генератора вместо списка.

main events = (readFile "a"):g events
  where
    g (inString с:_) = [(writeFile "b" с)]

никаких world не требует.

monk ★★★★★
() автор топика
Ответ на: комментарий от Crocodoom

Моя ЛОР-аватарка, которую у меня недавно бесстыже отобрали, родилась как раз для того хаскель-чата... Ну вот зачем ты напомнил 😔

Crocodoom ★★★★★
()
Ответ на: комментарий от Crocodoom

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

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

monk ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

Как Int и Maybe Int. Или как Int и Either String Int.

Да. Но для Maybe или Either вытащить Int можно, а из IO никак.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Да. Но для Maybe или Either вытащить Int можно, а из IO никак.

Окей, вытащи мне Int из Proxy Int. IO тут вообще никак не уникален, если не считать некоторых хаков внутри компилятора.

hateyoufeel ★★★★★
()
Ответ на: комментарий от monk

Во-первых seq не работает как надо. Что бы прям целиком структуру просчитать нужен deepSeq. Во-вторых с IO и do синтаксисом код выглядит красиво и структурированно, а через seq это будет нечитаемая мешанина. Не забывай, что haskell позиционируется все же как язык общего назначения, а не просто игрушка математиков.

Aswed ★★★★★
()

почитал ответы хаскеллистов, ничего не понял. лан, лишь бы они сами понимали

Virtuos86 ★★★★★
()
Последнее исправление: Virtuos86 (всего исправлений: 1)
Ответ на: комментарий от hateyoufeel

IO тут вообще никак не уникален, если не считать некоторых хаков внутри компилятора.

На событиях можно было бы тестировать функции IO как чистые.

Типа

assertEqual "Должно работать" 
    (main ["Ваня","9","4"]) 
    ["Введите своё имя: ", "Привет, Ваня", "Вводите числа, q для выхода", "3", "2"]
monk ★★★★★
() автор топика
Ответ на: комментарий от monk

На событиях можно было бы тестировать функции IO как чистые.

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

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

https://hackage.haskell.org/package/polysemy

hateyoufeel ★★★★★
()
Ответ на: комментарий от token_polyak

В ту тему писать не могу, и я её в первый раз вижу (спасибо за ссылку!), поэтому отвечу здесь

Для меня это просто буква Λ (= Л = Лябмда), обведённая в кружок. Нашёл её очень давно в гугле по запросу типа «lambda stylized» и поставил на аватар в хаскель-чате в честь лямбда-исчисления (+ это единственная согласная буква моего имени). В телеграме вообще хорошо смотрятся круглые аватарки: ничего не страдает при вырезании миниатюры.

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

В связи с последними событями перевернул аватарку, так во-первых я уже не тот, что был 5-7 лет назад; и буква V стала культурно куда более актуальной (хайповой), чем буква Λ. Вроде такой символ никем не занят, или его тоже уже кто-то застолбил?

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

P.S. Сейчас перечитал статью на вики ещё раз. Мои взгляды точно не сводятся к этой статье, хотя некоторые из тамошних мыслей мне понравились

P.P.S. Я всю жизнь прожил в России, сейчас тоже живу там и имею о Европе лишь диванное представление, за исключением нескольких отдельных городов

Crocodoom ★★★★★
()
Ответ на: комментарий от hateyoufeel

В IO Int тоже нет значения.

Но это генератор значений. Проблема даже не в том, что оттуда вытащить, а в том, как туда затащить.

Если на событиях список событий формируется легко,

main events = (readFile "a"):g events
  where
    g (inString с:_) = [(writeFile "b" с)]

assertEqual "Однострочный файл"
  (main [inString "строка файла"])
  [(readFile "a", writeFile "b" "строка файла")]

Как сделать тест для

main = do c <- readFile "a"
          writeFile "b" c

?

monk ★★★★★
() автор топика
Ответ на: комментарий от Crocodoom

Но если вас оскорбляют такие значки

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

token_polyak ★★★★★
()
Последнее исправление: token_polyak (всего исправлений: 1)
Ответ на: комментарий от monk

Ну я тебе выше про Polysemy написал. Оно именно это и позволяет.

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

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Ну я тебе выше про Polysemy написал. Оно именно это и позволяет.

Да. Вместо interact будет interpret и то же самое. Вопрос был, зачем вообще нужен IO, если всё можно сделать на Polysemy.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Вопрос был, зачем вообще нужен IO, если всё можно сделать на Polysemy.

Потому что Polysemy реализован в терминах IO, а не наоборот. Твоя проблема в том, что ты считаешь, что IO Int обязательно вернёт Int. Но оттуда так же может вылезти IOException, и с этим тоже надо что-то делать.

Если ты дальше спросишь, почему системы эффектов нет прямо в GHC, то потому что 25 лет назад до неё не додумались. Это сравнительно новая штука. Из коробки она есть в PureScript, например. Ну и это сравнительно небыстрая штука всё таки. Код тупо на IO быстрее кода на эффектах примерно в два-три раза в бенчмарках. В реальности всё не так страшно, но тем не менее.

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 2)
Ответ на: комментарий от hateyoufeel

Твоя проблема в том, что ты считаешь, что IO Int обязательно вернёт Int.

В списке тоже можно [1,2,error IOException] делать.

Код тупо на IO быстрее кода на эффектах примерно в два-три раза в бенчмарках.

В смысле, с Polysemy сравнивали? А с interact?

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

В списке тоже можно [1,2,error IOException] делать.

error – это не исключение, это другая штука совершенно. Их важно различать. И как именно ты перехватывать IOException собрался?

В смысле, с Polysemy сравнивали? А с interact?

interact – это тупо \f -> putStrLn . f =<< getLine, в этом нет ничего особенного. Даже сигнатуры одинаковые.

Prelude> :t \f -> putStrLn . f =<< getLine
\f -> putStrLn . f =<< getLine :: (String -> String) -> IO ()
Prelude> :t interact
interact :: (String -> String) -> IO ()

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

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 2)
Ответ на: комментарий от hateyoufeel

error – это не исключение, это другая штука совершенно. Их важно различать.

error превращается в исключение, если в IO.

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

С interact единственное ограничение — на входе только строка. Список событий вполне работает и с потоками и с синхронизацией.

main events = (spawn func):g events
  where
    g (inString x:xs) = processKbd x:xs
    g (inResult x:xs) = processFuncResult x:xs
monk ★★★★★
() автор топика
Ответ на: комментарий от monk

error превращается в исключение, если в IO.

Нет, не превращается. Error можно перехватить, но так делать не надо за очень редким исключением. В Haskell слегка бардак с термином «исключение» и под ним часто понимаются совершенно разные вещи. Если вкратце, error вызовет у тебя асинхронное исключение, как и, например, деление на 0 или завершение треда извне. Исключения типа IOException – синхронные, и обрабатываются иначе.

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

Prelude> let error :: String -> a; error s = error s
Prelude> :t error
error :: String -> a

Список событий вполне работает и с потоками и с синхронизацией.

Я не понимаю, как в твоём представлении это работает. Как именно ты spawn реализуешь и какой тип будет иметь эта функция?

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от hateyoufeel

Исключения типа IOException – синхронные, и обрабатываются иначе.

Только внутри IO и только если их бросать через throwIO. Опять же, ещё одна «магия».

деление на 0

Это уже просто Infinity

Как именно ты spawn реализуешь и какой тип будет иметь эта функция?

spawn :: ([inEvent] -> [outEvent]) -> outEvent

Передаёт функцию в виде события для исполнителя (для выполнения в отдельном потоке).

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Только внутри IO и только если их бросать через throwIO. Опять же, ещё одна «магия».

У тебя вся программа внутри IO. Никакой магии в этом случае нет.

Это уже просто Infinity

Нет.

Prelude> 1 `div` 0
*** Exception: divide by zero

Передаёт функцию в виде события для исполнителя (для выполнения в отдельном потоке).

А синхронизация как будет происходить? И откуда InEvent будет приходить?

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

У тебя вся программа внутри IO. Никакой магии в этом случае нет.

Тогда объясни без магии чем отличаются throw и throwIO.

А синхронизация как будет происходить?

Синхронизация по событию наружу типа outResult и ловли снаружи inResult.

И откуда InEvent будет приходить?

Откуда угодно. События ввода/вывода, работы с внешним миром, передача чего-нибудь от породившего потока.

Как в Эрланге.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Тогда объясни без магии чем отличаются throw и throwIO.

throw e   `seq` x  ===> throw e
throwIO e `seq` x  ===> x

Вот этим. throwIO гарантирует порядок операций, throw – нет. Ну и, ИМХО, throw использовать вообще нигде и никогда не надо. Это чудовищных размеров костыль из древности, от которого надо избавиться. Языку больше 30 лет уже, там такого много.

Синхронизация по событию наружу типа outResult и ловли снаружи inResult.

Ловли чем? Ты упускаешь самое интересное, на самом деле, сосредотачиваясь только на одном аспекте.

Как в Эрланге.

Если ты хочешь как в Erlang, у тебя все функции должны возвращать IO, потому что чистых функций в Erlang нет. Как и статической типизации.

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Нет, не превращается. Error можно перехватить, но так делать не надо за очень редким исключением.

catch (... error ...) (\e ... (e :: SomeException) ...) 

ловит исключение. Значит error является исключением.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

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

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Вот этим. throwIO гарантирует порядок операций, throw – нет.

С seq как раз и есть магия. Так как по определению seq вычисляет первый аргумент и возвращает второй, если первый не ошибка. То есть throwIO e не является ошибкой. А становится ею только когда её результат вычисляется внутри монады IO.

Ловли чем?

Я же написал псевдокод. Если первый элемент списка событий является событием от второго потока, его обрабатываем. Фактически, полный аналог receive/end из Эрланга.

Если ты хочешь как в Erlang, у тебя все функции должны возвращать IO, потому что чистых функций в Erlang нет. Как и статической типизации.

В моём варианте IO вообще нет и все функции чистые. Как и в раннем Хаскеле. Только ленивый список событий на входе и возврат другого ленивого списка событий на выходе.

monk ★★★★★
() автор топика
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от monk

С seq как раз и есть магия.

Это не магия, это ленивость. seq вычисляет WHNF первого аргумента, и всё. Например, из этого следует что

Prelude> undefined `seq` 1
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
  undefined, called at <interactive>:1:1 in interactive:Ghci1
Prelude> Just undefined `seq` 1
1

С IO в данном случае примерно так же: seq вычисляет конструктор, но не его аргументы. Вот тебе примерное определение seq, чтобы было понятнее:

seq :: a -> b -> b
seq a b =
  case a of
    _ -> b

Как видишь, к IO это не имеет вообще никакого отношения, это ортогональные вещи.

Если первый элемент списка событий является событием от второго потока, его обрабатываем. Фактически, полный аналог receive/end из Эрланга.

Что будет с твоим списком, если сделать deepseq и вычислить его полностью? Всё повиснет? Согласись, так себе вариант.

В моём варианте IO вообще нет и все функции чистые. Как и в раннем Хаскеле. Только ленивый список событий на входе и возврат другого ленивого списка событий на выходе.

Ранний хацкелл не очень работал, и поэтому придумали IO. Где-то есть даже статья об этом, можешь её почитать.

hateyoufeel ★★★★★
()
Ответ на: комментарий от monk

Ты блин почитай, на что сам ссылаешься :)

But this catchAny function isn’t quite correct, due to asynchronous exceptions

Там дальше следует объяснение, почему не надо catch прямо брать и использовать, что такое асинхронные исключения, и почему их ловить не надо.

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Что будет с твоим списком, если сделать deepseq и вычислить его полностью? Всё повиснет? Согласись, так себе вариант.

Его вычисление примерно эквивалентно

main = sequence $ repeat getLine

Тоже всё повиснет.

Где-то есть даже статья об этом, можешь её почитать.

Самое близкое нашёл: https://stackoverflow.com/questions/13536761/what-other-ways-can-state-be-handled-in-a-pure-functional-language-besides-with/13538351#13538351

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

monk ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.