Есть два подхода к реализации ИО: как в большинстве мейнстримовых языков (в любой функции может быть чтение файла), или как в меньшинстве функциональных — сигнатура грязных функций "заражена" чем-то, например уникальным типом "мир" или чем-то в этом роде.
Вот возник дурацкий вопрос. Засорение стека вызовов грязных функций специальным параметром — хорошая штука или не очень? Если программа делает сплошной ввод-вывод, то это наверное не очень круто, а если ввода-вывода мало — то наоборот. Вроде как в основном в программах ввода-вывода много. Но мне все равно почему-то не нравится вариант, когда в функции вычисления суммы двух чисел может стоять форматирование жесткого диска, как-то неправильно это...
Вы за какой вариант, с явно видимым в сигнатуре функции вводом-выводом, а-ля Хаскель, или с неявным, а-ля Ц++? Почему?
Здравствуйте, AlexRK, Вы писали:
ARK>Вы за какой вариант, с явно видимым в сигнатуре функции вводом-выводом, а-ля Хаскель, или с неявным, а-ля Ц++? Почему?
В Haskell:
(1) нет стека вызовов;
(2) нет типов в рантайме.
Так что засорять нечего и нечем.
Здравствуйте, deniok, Вы писали:
ARK>>Вы за какой вариант, с явно видимым в сигнатуре функции вводом-выводом, а-ля Хаскель, или с неявным, а-ля Ц++? Почему?
D>В Haskell: D>(1) нет стека вызовов; D>(2) нет типов в рантайме.
Хорошо, пусть будет Clean, вопрос совершенно не в этом.
D>Так что засорять нечего и нечем.
The return function admits an ordinary value such as a boolean to the realm of I/O actions. What about the other direction? Can we invoke some I/O actions within an ordinary expression? For example, how can we say x + print y in an expression so that y is printed out as the expression evaluates? The answer is that we can't! It is not possible to sneak into the imperative universe while in the midst of purely functional code. Any value `infected' by the imperative world must be tagged as such. A function such as
f :: Int -> Int -> Int
absolutely cannot do any I/O since IO does not appear in the returned type.
Здравствуйте, AlexRK, Вы писали:
ARK>Вы за какой вариант, с явно видимым в сигнатуре функции вводом-выводом, а-ля Хаскель, или с неявным, а-ля Ц++? Почему?
Я за явный, причины очевидны — о чистых функциях легко рассуждать, поэтому их должно быть как можно больше. Теоретически (если всё иммутабельное) они также включают разные интересные варианты вроде автоматического распараллеливания, кеширования (мемоизации), но даже сами по себе они ценны.
Тут подумалось. В плохом Java-коде везде, где есть ввод-вывод, в сигнатурах пестрит throws IOException и он заставляет прокидывать этот throws дальше (ну или ловить, но это не интересный вариант). Может это на самом деле хороший код, который пишут хаскеллисты?
Здравствуйте, vsb, Вы писали:
ARK>>Вы за какой вариант, с явно видимым в сигнатуре функции вводом-выводом, а-ля Хаскель, или с неявным, а-ля Ц++? Почему?
vsb>Я за явный, причины очевидны — о чистых функциях легко рассуждать, поэтому их должно быть как можно больше.
Я вот тоже всегда так думал, а сейчас что-то засомневался. Очень уж много вокруг ввода-вывода. Возможно, оптимальнее вариант языка D — по умолчанию функции грязные, но можно указать модификатор Pure? А даст ли это что-то существенное?
vsb>Теоретически (если всё иммутабельное) они также включают разные интересные варианты вроде автоматического распараллеливания, кеширования (мемоизации), но даже сами по себе они ценны.
По идее, такие вещи можно отслеживать компилятором и без сигнатур (по-моему, в C# подобный анализ имеется). Сигнатуры функций — они больше для программиста, ИМХО.
vsb>Тут подумалось. В плохом Java-коде везде, где есть ввод-вывод, в сигнатурах пестрит throws IOException и он заставляет прокидывать этот throws дальше (ну или ловить, но это не интересный вариант). Может это на самом деле хороший код, который пишут хаскеллисты?
Хз, может в хаскелле это и считается хорошим кодом, но в жабе это выглядит как говно.
Здравствуйте, AlexRK, Вы писали:
ARK>Вот возник дурацкий вопрос. Засорение стека вызовов грязных функций специальным параметром — хорошая штука или не очень?
Эти типы не имеют осмысленных рантайм значений. Так что компилятор не выделяет под них память и не генерирует для них код.
Те эти типы дают проверки времени компиляции, не влияя на исполнение.
ARK>Если программа делает сплошной ввод-вывод, то это наверное не очень круто, а если ввода-вывода мало — то наоборот. Вроде как в основном в программах ввода-вывода много.
В программах, которые ты пишешь.
А в программах, которые пишу я ввод/вывод занимает ничтожную часть кода.
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Здравствуйте, WolfHound, Вы писали:
ARK>>Вот возник дурацкий вопрос. Засорение стека вызовов грязных функций специальным параметром — хорошая штука или не очень? WH>Эти типы не имеют осмысленных рантайм значений. Так что компилятор не выделяет под них память и не генерирует для них код. WH>Те эти типы дают проверки времени компиляции, не влияя на исполнение.
Это я знаю. Вопрос не в этом. Вопрос таков — засорение самого кода программы ухудшает читабельность или все-таки что-то дает?
ARK>>Если программа делает сплошной ввод-вывод, то это наверное не очень круто, а если ввода-вывода мало — то наоборот. Вроде как в основном в программах ввода-вывода много. WH>В программах, которые ты пишешь. WH>А в программах, которые пишу я ввод/вывод занимает ничтожную часть кода.
Вот меня и интересует, а каких программ больше — каких пишу я или каких пишете вы. По-моему, первое.
Здравствуйте, WolfHound, Вы писали:
ARK>>Если программа делает сплошной ввод-вывод, то это наверное не очень круто, а если ввода-вывода мало — то наоборот. Вроде как в основном в программах ввода-вывода много. WH>В программах, которые ты пишешь. WH>А в программах, которые пишу я ввод/вывод занимает ничтожную часть кода.
Тогда можно задать вопрос, а существуют ли хорошие и современные языки для программ, занимающихся в основном вводом-выводом.
The return function admits an ordinary value such as a boolean to the realm of I/O actions. What about the other direction? Can we invoke some I/O actions within an ordinary expression? For example, how can we say x + print y in an expression so that y is printed out as the expression evaluates? The answer is that we can't! It is not possible to sneak into the imperative universe while in the midst of purely functional code. Any value `infected' by the imperative world must be tagged as such. A function such as
ARK>f :: Int -> Int -> Int
ARK>absolutely cannot do any I/O since IO does not appear in the returned type.
Так это же не метка IO засоряет программу! Ее засоряет сам факт ввода-вывода. А монада IO как раз приводит хаос ввода-вывода к стандартному для Хаскеля монадическому интерфейсу работы с эффектами.
Если же просто использовать такую метку без механизма, дающего какой-то разумный набор гарантий, то толку от нее действительно мало.
Здравствуйте, deniok, Вы писали:
D>Так это же не метка IO засоряет программу! Ее засоряет сам факт ввода-вывода. А монада IO как раз приводит хаос ввода-вывода к стандартному для Хаскеля монадическому интерфейсу работы с эффектами.
Ну хорошо, пусть изначальной причиной является сам факт ввода-вывода, но, тем не менее, "метка IO" код тоже засоряет, причем всю иерархию функций, снизу доверху.
D>Если же просто использовать такую метку без механизма, дающего какой-то разумный набор гарантий, то толку от нее действительно мало.
Разумеется, речь идет о предоставлении гарантий. Только вот существует куча языков, в которых и без этой метки всё работает.
В данной теме я как раз поднял вопрос, а как "правильно"-то? С меткой или без? Кто что думает по этому поводу? То, что в природе успешно существуют оба варианта, я в курсе.
Здравствуйте, AlexRK, Вы писали:
ARK>Вы за какой вариант, с явно видимым в сигнатуре функции вводом-выводом, а-ля Хаскель, или с неявным, а-ля Ц++? Почему?
Проблема имхо не в сигнатуре функции, тут как раз все хорошо, а в самом коде где нужно явно делать лифтинг в монаду, через все эти liftM, foldM, etc. Это сильно засоряет код и мешает пониманию. Я не спец в системах типов, но возможно в будущем можно сделать что-то для автоматизации процесса. Например, если есть
foobar: IO a
pure: a -> b
можно будет просто написать "pure.foobar", как в нечистых языках или близко к тому, и компилятор сам подставит нужный lift.
N>foobar: IO a N>pure: a -> b
N>можно будет просто написать "pure.foobar", как в нечистых языках или близко к тому, и компилятор сам подставит нужный lift.
Здравствуйте, novitk, Вы писали:
N>Здравствуйте, deniok, Вы писали:
D>>
D>>pure <$> foobar
D>>
N>Я как бы в курсе, но хотелось бы избавиться, то есть чтобы компилятор это сам выводил.
Это плохо совместимо с общим механизмом вывода типов в Хаскеле. Что он должен вывести для const foobar, если не сможет различать вызовы c лифтингом и без
> {foobar::IO a; foobar = undefined}
> :t const foobar
const foobar :: b -> IO a
> :t const <$> foobar
const <$> foobar :: IO (b -> a)
Здравствуйте, deniok, Вы писали:
D>Это плохо совместимо с общим механизмом вывода типов в Хаскеле. Что он должен вывести для const foobar, если не сможет различать вызовы c лифтингом и без
Согласен, на существующей системе оно неоднозначно, но помечтать то не возбраняется. То есть я, как стратег, задачу ставлю, а вы тактики ее решайте
Если серьезно, то у меня нет идей и подготовки, что можно с этим сделать. Сам по себе <$> может и ок, но вот весь зоопарк в Functor/Applicative/Monad сильно интрузивен.
Здравствуйте, novitk, Вы писали:
ARK>>Вы за какой вариант, с явно видимым в сигнатуре функции вводом-выводом, а-ля Хаскель, или с неявным, а-ля Ц++? Почему?
N>Проблема имхо не в сигнатуре функции, тут как раз все хорошо, а в самом коде где нужно явно делать лифтинг в монаду, через все эти liftM, foldM, etc.
Ну, в языке D есть pure-функции, но при этом никаких лифтингов ни в какие монады там делать не надо.
Здравствуйте, novitk, Вы писали:
N>Проблема имхо не в сигнатуре функции, тут как раз все хорошо, а в самом коде где нужно явно делать лифтинг в монаду, через все эти liftM, foldM, etc. Это сильно засоряет код и мешает пониманию. Я не спец в системах типов, но возможно в будущем можно сделать что-то для автоматизации процесса. Например, если есть
Такое? https://koka-lang.github.io/koka/doc/kokaspec.html#sec-polymorphic-effects
Эффекты это считай обобщение монад.
Разница в том, что монада одна, а эффектов может быть много. Или вообще не быть.
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Здравствуйте, AlexRK, Вы писали:
ARK>Это я знаю. Вопрос не в этом. Вопрос таков — засорение самого кода программы ухудшает читабельность или все-таки что-то дает?
1)Ловит ошибки.
2)Улучшает читаемость кода.
3)Развязывает руки оптимизатору.
ARK>Вот меня и интересует, а каких программ больше — каких пишу я или каких пишете вы. По-моему, первое.
Есть конечно программы, которые сильно тяготеют в одну или другую сторону. Но во многих случаях зависит от того как у разработчиков программы мозги устроены.
Я в любом случае стараюсь минимизировать и изолировать ИО. Так программу легче понимать.
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
AlexRK,
ARK>>>Вы за какой вариант, с явно видимым в сигнатуре функции вводом-выводом, а-ля Хаскель, или с неявным, а-ля Ц++? Почему?
N>>Проблема имхо не в сигнатуре функции, тут как раз все хорошо, а в самом коде где нужно явно делать лифтинг в монаду, через все эти liftM, foldM, etc.
ARK>Ну, в языке D есть pure-функции, но при этом никаких лифтингов ни в какие монады там делать не надо.
Но тогда скажем функция `map` в D чистая или нет? Как D мне запретит туда передать лямбду `println(_)`? А если запрещает, как мне тогда всё же передать `prinln()`?
В хаскеле связь между монадами и грязными функциями напрямую отсутствует, непосредственно io-эффекты несёт с собой только монада IO, но при этом сам хаскель про это не знает.
Вот, например, это чистая функция, хотя я могу определить её в монаде Maybe:
fold2 :: Maybe a -> Maybe b -> (a -> b -> c) -> Maybe c
fold2 mba mbb f = do
a <- mba
b <- mbb
return $ f a b
--test it
λ> fold2 (Just 10) (Just 20) (+)
Just 30
Или, например, есть функция `mapM :: (Monad m, Traversable t) => (a -> m b) -> t a -> m (t b)`, которая внезапно одновременно и чистая, и грязная:
λ> mapM print [1,2,3]
1
2
3
[(),(),()]
λ> mapM Just [1,2,3]
Just [1,2,3]
А вот ещё пример чистой (!) функции, которая читает из и пишет в KV-сторадж и даже делает это в транзакции:
type Ops = Free OpsF
foo :: Ops (Map Key Val)
foo = do
let k1 = "foo1"let k2 = "foo2"
put k1 "value1"
put k2 "value2"
transact $ do
(vt1, vt2) <- transact $ do
v1 <- get k1
v2 <- get k2
return (v1, v2)
transact $ do
put k1 vt2
put k2 vt1
-- get one by one, not in transaction
vn1 <- get k1
vn2 <- get k2
return $
insert k1 vn1 $
insert k2 vn2 empty