Подтвердить что ты не робот

Является ли монадическая конструкция ввода в Haskell просто конвенцией?

Является ли монадическая конструкция ввода-вывода в Haskell просто конвенцией или есть причина для ее реализации?

Не могли бы вы просто FFI в libc.so вместо этого сделать IO и пропустить IO Monad?

Будет ли это работать в любом случае, или результат является недетерминированным из-за того, что Haskell оценивает ленивый или что-то еще, например, GHC является сопоставлением шаблонов для IO Monad, а затем обрабатывает его особым образом или что-то еще.

Какова настоящая причина? В итоге вы оказываетесь в побочном эффекте. Так почему бы не сделать это простым способом?

4b9b3361

Ответ 1

Да, монадический ввод-вывод является следствием того, что Хаскелл ленив. В частности, хотя монадический ввод-вывод является следствием того, что Haskell является чистым, что фактически необходимо для ленивого языка быть предсказуемым.

Это легко проиллюстрировать на примере. Представьте себе, что Хаскелл не был чист, но он все еще ленился. Вместо putStrLn, имеющего тип String -> IO (), он просто будет иметь тип String -> (), и он будет печатать строку в stdout как побочный эффект. Проблема в том, что это произойдет только тогда, когда putStrLn действительно вызван, а на ленивом языке функции вызываются только тогда, когда их результаты необходимы.

Существует проблема: putStrLn производит (). Глядя на значение типа () бесполезно, потому что () означает "скучно" . Это означает, что эта программа будет делать то, что вы ожидаете:

main :: ()
main =
  case putStr "Hello, " of
    () -> putStrLn " world!"

-- prints "Hello, world!\n"

Но я думаю, вы можете согласиться с тем, что стиль программирования довольно странный. case ... of необходимо, однако, потому что он заставляет оценку вызова putStr сопоставлять с (). Если вы немного измените программу:

main :: ()
main =
  case putStr "Hello, " of
    _ -> putStrLn " world!"

... теперь он печатает только world!\n, и первый вызов вообще не оценивается.

Это на самом деле становится еще хуже, потому что становится еще труднее прогнозировать, как только вы начнете пытаться выполнять какое-либо фактическое программирование. Рассмотрим эту программу:

printAndAdd :: String -> Integer -> Integer -> Integer
printAndAdd msg x y = putStrLn msg `seq` (x + y)

main :: ()
main =
  let x = printAndAdd "first" 1 2
      y = printAndAdd "second" 3 4
  in (y + x) `seq` ()

Выдает ли эта программа first\nsecond\n или second\nfirst\n? Не зная порядка, в котором (+) оценивает свои аргументы, мы не знаем. И в Haskell оценочный порядок даже не всегда четко определен, поэтому вполне возможно, что порядок, в котором выполняются два эффекта, на самом деле совершенно невозможно определить!

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

Почему? И как это возможно? Ну, монадический интерфейс обеспечивает понятие зависимости данных в сигнатуре для >>=, что обеспечивает четко определенный порядок оценки. Реализация Haskells IO является "волшебной", в том смысле, что ее реализовано во время выполнения, но выбор монадического интерфейса далеко не произволен. Это, по-видимому, довольно хороший способ кодировать понятие последовательных действий на чистом языке, и это позволяет Haskell быть ленивым и ссылочно прозрачным, не жертвуя предсказуемым секвенированием эффектов.

Стоит отметить, что монады - это не единственный способ кодировать побочные эффекты чистым способом - фактически, исторически theyre даже не единственный способ, которым Haskell обрабатывал побочные эффекты. Не следует вводить в заблуждение, думая, что монады предназначены только для ввода-вывода (theyre not), только полезны на ленивом языке (они очень полезны для поддержания чистоты даже на строгом языке), полезны только на чистом языке (многие вещи являются полезными монадами это arent только для обеспечения чистоты), или что вам нужны монады для ввода/вывода (вы не делаете). Тем не менее, они, похоже, хорошо справились в Haskell для этих целей.


† В связи с этим Саймон Пейтон Джонс однажды заметил, что "Лень держит вас честно" в отношении чистоты.

Ответ 2

Не могли бы вы просто FFI в libc.so вместо этого выполнить IO и пропустить IO Monad?

Вывод из https://en.wikibooks.org/wiki/Haskell/FFI#Impure_C_Functions, если вы объявляете функцию FFI чистой (так, без ссылки на IO), тогда

GHC не видит смысла в вычислении дважды результата чистой функции

что означает, что результат вызова функции эффективно кэшируется. Например, программа, в которой объявлен генератор псевдослучайных чисел с иностранными нечистотами, возвращает CUInt

{-# LANGUAGE ForeignFunctionInterface #-}

import Foreign
import Foreign.C.Types

foreign import ccall unsafe "stdlib.h rand"
  c_rand :: CUInt

main = putStrLn (show c_rand) >> putStrLn (show c_rand)

возвращает то же самое при каждом вызове, по крайней мере, в моем компиляторе/системе:

16807
16807

Если мы изменим объявление, чтобы вернуть a IO CUInt

{-# LANGUAGE ForeignFunctionInterface #-}

import Foreign
import Foreign.C.Types

foreign import ccall unsafe "stdlib.h rand"
  c_rand :: IO CUInt

main = c_rand >>= putStrLn . show >> c_rand >>= putStrLn . show

то это приводит к (вероятно) другому номеру, возвращаемому каждому вызову, поскольку компилятор знает, что он нечист:

16807
282475249

Итак, вы все равно должны использовать IO для вызовов стандартных библиотек.

Ответ 3

Скажем, используя FFI, мы определили функцию

c_write :: String -> ()

который лежит о своей чистоте, в том, что всякий раз, когда его результат принудительно, он печатает строку. Чтобы мы не сталкивались с проблемами кэширования в ответе Михала, мы можем определить эти функции для принятия дополнительного аргумента ().

c_write :: String -> () -> ()
c_rand :: () -> CUInt

На уровне реализации это будет работать до тех пор, пока CSE не слишком агрессивна (что не в GHC, потому что это может привести к неожиданным утечкам памяти, оказывается). Теперь, когда у нас есть вещи, определенные таким образом, есть много неудобных вопросов использования, которые Алексис указывает, но мы можем решить их с помощью монады:

newtype IO a = IO { runIO :: () -> a }

instance Monad IO where
    return = IO . const
    m >>= f = IO $ \() -> let x = runIO m () in x `seq` f x

rand :: IO CUInt
rand = IO c_rand

В принципе, мы просто используем все нелепые вопросы использования Алексиса в монаде, и до тех пор, пока мы используем монадический интерфейс, все остается предсказуемым. В этом смысле IO - это просто соглашение, потому что мы можем реализовать его в Haskell, в этом нет ничего принципиального.

Это с точки зрения эффективности.

С другой стороны, семантика Haskell в отчете определяется с помощью денотационной семантики. И, на мой взгляд, тот факт, что Haskell имеет точную денотационную семантику, является одним из самых красивых и полезных качеств языка, что позволяет мне четко охарактеризовать абстракции и, таким образом, управлять сложностью с точностью. И хотя обычная абстрактная монада IO не имеет принятой денотационной семантики (к плачу некоторых из нас), по крайней мере возможно, что мы могли бы создать для него денотационную модель, сохранив при этом некоторые преимущества денотационной модели Хаскелла. Однако форма ввода-вывода, которую мы только что дали, полностью несовместима с денотационной семантикой Хаскелла.

Проще говоря, предполагается только два отличимых значения (по модулю фатальных сообщений об ошибках) типа (): () и & perp;. Если мы рассматриваем FFI как основы ввода-вывода и используем только монаду IO только как "соглашение", то мы фактически добавляем значения jillion для каждого типа. Чтобы продолжить иметь денотационную семантику, каждое значение должно быть сопряжено с возможность выполнения ввода-вывода до его оценки, а также с дополнительной сложностью, которую это вводит, мы по существу теряем всякую способность рассматривать любые две различные эквивалентные программы, за исключением самых тривиальных случаев, т.е. мы теряем способность рефакторировать.

Конечно, из-за unsafePerformIO это уже так технически, и продвинутые программисты Haskell также должны думать о эксплуатационной семантике. Но большую часть времени, в том числе при работе с I/O, мы можем забыть обо всем этом и реорганизовать с уверенностью, именно потому, что мы узнали, что, когда мы используем unsafePerformIO, мы должны быть очень осторожны, чтобы гарантировать, что он играет хорошо, что он по-прежнему дает нам как можно больше денотационных рассуждений. Если функция имеет unsafePerformIO, я автоматически даю ей в 5 или 10 раз больше внимания, чем обычные функции, потому что мне нужно понять правильные шаблоны использования (обычно подпись типа говорит мне все, что мне нужно знать), мне нужно думать о кешировании и условиях гонки, мне нужно подумать о том, насколько глубоко мне нужно, чтобы заставить его результаты и т.д. Это ужасно [1]. Такая же осторожность будет необходима для ввода/вывода FFI.

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

[1] Ну, на самом деле я думаю, что это довольно весело, но, конечно, не всегда нужно думать обо всех этих сложностях все время.

Ответ 4

Это зависит от того, что означает значение "есть", или, по крайней мере, что означает "конвенция" .

Если "конвенция" означает "как обычно делаются вещи" или "соглашение между сторонами, охватывающими конкретный вопрос", тогда легко дать скучный ответ: да, монада IO является конвенцией. Это то, как разработчики языка согласились обрабатывать операции ввода-вывода и то, как пользователи языка обычно выполняют операции ввода-вывода.

Если нам разрешено выбирать более интересное определение "соглашение", тогда мы можем получить более интересный ответ. Если "конвенция" - это дисциплина, налагаемая на язык ее пользователями, чтобы достичь определенной цели без помощи самого языка, тогда ответ не будет: монада IO - это напротив конвенции. Это дисциплина, наложенная на язык, который помогает его пользователям создавать и анализировать программы.

Цель типа IO - создать четкое различие между типами "чистых" значений и типами значений, которые требуют выполнения системой времени выполнения для создания значимого результата. Система типа Haskell обеспечивает такое строгое разделение, не позволяя пользователю (скажем) создать значение типа Int, которое запускает пресловутые ракеты. Это не соглашение во втором смысле: его цель состоит в том, чтобы переместить дисциплину, необходимую для выполнения побочных эффектов безопасным и последовательным образом от пользователя и на язык и его компилятор.

Не могли бы вы просто FFI в libc.so вместо этого выполнить IO и пропустить IO Monad?

Конечно, можно сделать IO без монады IO: см. почти все остальные существующие языки программирования.

Будет ли он работать в любом случае или результат неопределенен из-за того, что Haskell оценивает ленивый или что-то еще, например, GHC является сопоставлением шаблонов для IO Monad, а затем обрабатывает его особым образом или что-то еще.

Нет такой вещи, как бесплатный обед. Если Haskell допускает любое значение, требующее выполнения с использованием IO, тогда ему придется потерять другие вещи, которые мы ценим. Наиболее важным из них является, вероятно, ссылочная прозрачность: если myInt иногда может быть 1 и иногда быть 5 в зависимости от внешних факторов, тогда мы потеряем большую часть нашей способности рассуждать о наших программах строго (так называемый эквациональное рассуждение).

Лень упоминалась в других ответах, но проблема с лени конкретно заключалась в том, что совместное использование больше не будет безопасным. Если x в let x = someExpensiveComputationOf y in x * x не было ссылочно прозрачным, GHC не смог бы поделиться работой и должен был бы вычислить его дважды.

Какова реальная причина?

Без строгого разделения эффективных значений от неэффективных значений, предоставляемых IO и принудительно исполняемых компилятором, Haskell фактически перестанет быть Haskell. Существует множество языков, которые не применяют эту дисциплину. Было бы неплохо иметь по крайней мере один из них.

В конце концов вы заканчиваете свое действие в побочном эффекте. Так почему бы не сделать это простым способом?

Да, в конце ваша программа представлена ​​значением main с типом ввода-вывода. Но вопрос заключается не в том, где вы в конечном итоге, там, где вы начинаете: если вы начинаете с того, что можете строго различать эффективные и неэффективные значения, тогда вы получаете много преимуществ, когда построив эту программу.