Ввод / вывод в Haskell является функциональным?

Я только начинаю смотреть на Haskell (мой предыдущий опыт работы с FP в Scheme), и я наткнулся на этот код:

do { putStrLn "ABCDE" ; putStrLn "12345" }

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

Кто-нибудь, пожалуйста, объясните, как этот код "функционален" в любом отношении?

5 ответов

Решение

Хотя это и выглядит процедурной программой, приведенный выше синтаксис переводится в функциональную программу, например так:

   do { putStrLn "ABCDE" ; putStrLn "12345" }
=>
   IO (\ s -> case (putStrLn "ABCDE" s) of
                  ( new_s, _ ) -> case (putStrLn "12345" new_s) of
                                      ( new_new_s, _) -> ((), new_new_s))

То есть ряд вложенных функций, через которые проходит уникальный параметр world, упорядочивая вызовы примитивных функций "процедурно". Этот дизайн поддерживает кодирование императивного программирования в функциональный язык.

Лучшим введением в семантические решения, лежащие в основе этого дизайна, является статья "Неудобный отряд",

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

Подумайте об этом таким образом. На самом деле он не "выполняет" инструкции ввода-вывода. Монада IO - это чистое значение, заключающее в себе "императивное вычисление", которое должно быть сделано (но на самом деле оно не выполняется). Вы можете объединить монады (вычисления) в большее "вычисление", используя операторы и конструкции типа "do". Тем не менее, ничто не "выполнено" само по себе. Фактически, в целом цель программы на Haskell состоит в том, чтобы собрать большое "вычисление", которое является ее main значение (которое имеет тип IO a). И когда вы запускаете программу, запускается именно это "вычисление".

Это монада. Прочитайте о нотации для объяснения того, что происходит за обложками.

Кто-нибудь, пожалуйста, объясните, как этот код

do { putStrLn "ABCDE" ; putStrLn "12345" }

"функционально" в каком-то отношении?

Вот как я вижу текущую ситуацию с вводом-выводом в Haskell; применяются обычные заявления об отказе от ответственности>_<

Если под "функциональным" вы подразумеваете "чисто функциональный" - прямо сейчас (июнь 2020 г.), это зависит от вашей реализации Haskell. Но так было не всегда - на самом деле исходная модель ввода-вывода языка Haskell действительно была чисто функциональной!

Пришло время вернуться к ранним дням существования Haskell, чему способствует книга Филипа Вадлера Как объявить императив:

import Prelude hiding (IO)
import qualified Prelude (IO)

import Control.Concurrent.Chan(newChan, getChanContents, writeChan) 
import Control.Monad((<=<))


 -- pared-back emulation of retro-Haskell I/O
 --
runDialogue :: Dialogue -> Prelude.IO ()
runDialogue d =
  do ch <- newChan
     l <- getChanContents ch
     mapM_ (writeChan ch <=< respond) (d l)

respond :: Request -> Prelude.IO Response
respond Getq     = fmap Getp getChar
respond (Putq c) = putChar c >> return Putp

main = runDialogue (retro_main :: Dialogue)

{-
          implementation side
  -----------------------------------
  ========== retro-Haskell ==========
  -----------------------------------
             language side
-}

 -- pared-back definitions for retro-Haskell I/O
 -- from page 14 of Wadler's paper
 --
data Request = Getq | Putq Char
data Response = Getp Char | Putp

type Dialogue = [Response] -> [Request]

(Распространение его на весь ввод-вывод ретро-Haskell оставлено в качестве упражнения для очень увлеченных читателей;-)

Готово: чисто функциональный ввод-вывод! Ответы отправляются наmain retro_main, который затем передает запросы обратно:

программа ретро-Haskell, взаимодействующая со своим окружением

Со всей этой элегантной чистотой вы с радостью могли бы определить:

 -- from page 15 of Wadler's paper
echoD :: Dialogue
echoD p =
  Getq :
    case p of
      Getp c : p' ->
        if (c == '\n') then
          []
        else
          Putq c :
            case p' of
              Putp : p'' -> echoD p''

Вы смущены - все в порядке; Вы научитесь:-D

Вот более сложный пример со страницы 24 книги A History of Haskell:

{-

main ~(Успех: ~((Str userInput): ~(Успех: ~(r4: _))))
  = [AppendChan stdout "введите имя файла \n",
      ReadChan stdin,
      AppendChan имя стандартного вывода,
      ReadFile имя,
      AppendChan стандартный вывод
          (случай r4 из
              Str contents -> contents
              Ошибка ioerr -> "невозможно открыть файл")
    ] где (имя: _) = строки userInput

-}

Ты еще там?

Это мусорное ведро рядом с вами? А? Вы заболели? Штопать.

Хорошо - возможно, вам будет немного проще с более узнаваемым интерфейсом:

 -- from page 12 of Wadler's paper
 --
echo  :: IO ()
echo  =  getc >>= \ c ->
         if (c == '\n') then
           done
         else
           putc c >>
           echo


 -- from pages 3 and 7
 --
puts  :: String -> IO ()
puts []    = done
puts (c:s) = putc c >> puts s

done :: IO ()
done = return ()


 -- based on pages 16-17
 --
newtype IO a = MkIO { enact :: Reality -> (Reality, a) }
type Reality = ([Response], [Request])

bindIO    :: IO a -> (a -> IO b) -> IO b
bindIO m k =  MkIO $ \ (p0, q2) -> let ((p1, q0), x) = enact m     (p0, q1)
                                       ((p2, q1), y) = enact (k x) (p1, q2)
                                   in
                                       ((p2, q0), y)


unitIO :: a -> IO a
unitIO x = MkIO $ \ w -> (w, x)

putc :: Char -> IO ()
putc c  = MkIO $ \ (p0, q1) -> let q0        = Putq c : q1
                                   Putp : p1 = p0
                               in
                                   ((p1, q0), ())

getc :: IO Char
getc    = MkIO $ \ (p0, q1) -> let q0          = Getq : q1
                                   Getp c : p1 = p0
                               in
                                   ((p1, q0), c)

mainD :: IO a -> Dialogue
mainD main = \ p0 -> let ((p1, q0), x) = enact main (p0, q1)

                         q1            = []
                     in
                         q0

 -- making it work
instance Monad IO where
    return = unitIO
    (>>=)  = bindIO

Я также включил ваш пример кода; может это поможет:

 -- local version of putStrLn
putsl :: String -> IO ()
putsl s = puts s >> putc '\n'

 -- bringing it all together
retro_main :: Dialogue
retro_main = mainD $ do { putsl "ABCDE" ; putsl "12345" }

Да: это все еще чисто функциональный ввод-вывод; проверить типretro_main.

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

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

Но некоторых беспокоила эта новая абстрактная модель ввода-вывода - был ли Haskell по-прежнему полностью функциональным?

Хорошо, если модель основана на абстракции, в этом случае:

  • абстрактный тип действий ввода-вывода: IO
  • абстрактная функция для построения простых действий ввода-вывода: return
  • абстрактные функции для объединения действий ввода / вывода: (>>=), catch, так далее
  • абстрактные функции для конкретных действий ввода / вывода: getArgs, getEnv, так далее

тогда то, как эти сущности на самом деле определены, будет зависеть от каждой реализации Haskell.

Вместо того, чтобы спрашивать:

  • является ли ввод-вывод в Haskell чисто функциональным?

теперь следует спросить:

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

Итак, ответ на ваш вопрос:


Кто-нибудь, пожалуйста, объясните, как этот код

do { putStrLn "ABCDE" ; putStrLn "12345" }

"функционально" в каком-то отношении?


теперь зависит от того, какую реализацию Haskell вы используете.

Не тот ответ, который вы хотели? Щас либо это, либо скунс...

Это не функциональный код. С чего бы это?

Другие вопросы по тегам