Действительно ли Haskell чист (есть ли язык, который имеет дело с вводом и выводом вне системы)? - PullRequest
33 голосов
/ 25 июня 2010

После касания Monads в отношении функционального программирования, делает ли функция на самом деле язык чистым, или это просто еще одна «карта без тюрьмы» для рассуждения компьютерных систем в реальном мире, вне математики на доске?

EDIT:

Это не огненная приманка, как кто-то сказал в этом посте, а подлинный вопрос, который, я надеюсь, кто-то может застрелить меня и сказать, доказательство, это чисто.

Также я смотрю на вопрос относительно других не очень чистых функциональных языков и некоторых ОО-языков, которые используют хороший дизайн и сравнивают чистоту. До сих пор в моем очень ограниченном мире FP я еще не испытывал чистоту Монад, вам будет приятно узнать, однако мне нравится идея неизменности, которая гораздо важнее в ставках чистоты.

Ответы [ 7 ]

71 голосов
/ 25 июня 2010

Возьмите следующий мини-язык:

data Action = Get (Char -> Action) | Put Char Action | End

Get f означает: прочитать символ c и выполнить действие f c.

Put c a означает: написатьсимвол c и выполните действие a.

Вот программа, которая печатает «xy», затем запрашивает две буквы и печатает их в обратном порядке:

Put 'x' (Put 'y' (Get (\a -> Get (\b -> Put b (Put a End)))))

Вы можетеманипулировать такими программами.Например:

conditionally p = Get (\a -> if a == 'Y' then p else End)

Это имеет тип Action -> Action - он берет программу и дает другую программу, которая сначала запрашивает подтверждение.Вот еще:

printString = foldr Put End

Это имеет тип String -> Action - он принимает строку и возвращает программу, которая записывает строку, например

Put 'h' (Put 'e' (Put 'l' (Put 'l' (Put 'o' End)))).

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

На языке, подобном C, вы можете написать функцию void execute(Action a), которая фактически выполняла программу.В Haskell вы указываете это действие, записывая main = a.Компилятор создает программу, которая выполняет действие, но у вас нет другого способа выполнить действие (кроме грязных уловок).

Очевидно, Get и Put - это не только опции, вы можете добавить множество другихВызовы API для типа данных ввода-вывода, например, работа с файлами или параллелизм.

Добавление значения результата

Теперь рассмотрим следующий тип данных.

data IO a = Get (Char -> Action) | Put Char Action | End a

Предыдущий тип Action эквивалентен IO (), т. Е. Значение IO, которое всегда возвращает «единицу», сравнимое с «void».

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

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

Get (\x -> if x == 'A' then Put 'B' (End 3) else End 4)

имеет тип IO Int и соответствует программе на C:

int f() {
  char x;
  scanf("%c", &x);
  if (x == 'A') {
    printf("B");
    return 3;
  } else return 4;
}

Оценка и выполнение

Есть разница между оценкой и выполнением.Вы можете оценить любое выражение Haskell и получить значение;например, оцените 2 + 2 :: Int в 4 :: Int.Вы можете выполнять только те выражения Haskell, которые имеют тип IO a.Это может иметь побочные эффекты;выполнение Put 'a' (End 3) выводит букву а на экран.Если вы оцените значение IO, например:

if 2+2 == 4 then Put 'A' (End 0) else Put 'B' (End 2)

, вы получите:

Put 'A' (End 0)

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

Как бы вы перевели

bool comp(char x) {
  char y;
  scanf("%c", &y);
  if (x > y) {       //Character comparison
    printf(">");
    return true;
  } else {
    printf("<");
    return false;
  }
}

в значение IO?

Исправьте какой-нибудь символ, скажем, 'v'.Теперь comp('v') - это IO-действие, которое сравнивает данный символ с 'v'.Точно так же comp('b') - это действие ввода-вывода, которое сравнивает данный символ с 'b'.В общем, comp - это функция, которая принимает символ и возвращает действие ввода-вывода.

Как программист на C, вы можете утверждать, что comp('b') является логическим значением.В C оценка и выполнение идентичны (то есть они означают одно и то же, или происходит одновременно).Не в Хаскеле.comp('b') преобразует в какое-то IO-действие, которое после выполнения 1094 * дает логическое значение(Точно, он вычисляется в блок кода, как указано выше, только с 'b', замененным на x.)

comp :: Char -> IO Bool
comp x = Get (\y -> if x > y then Put '>' (End True) else Put '<' (End False))

Теперь comp 'b' оценивается в Get (\y -> if 'b' > y then Put '>' (End True) else Put '<' (End False)).

Это также делаетсмысл математически.В C int f() является функцией.Для математика это не имеет смысла - функция без аргументов?Смысл функций в том, чтобы принимать аргументы.Функция int f() должна быть эквивалентна int f.Это не так, потому что функции в Си смешивают математические функции и действия ввода-вывода.

Первый класс

Эти значения ввода-вывода являются первоклассными.Также как вы можете иметь список списков кортежей целых чисел [[(0,2),(8,3)],[(2,8)]], вы можете создавать сложные значения с помощью ввода-вывода.

 (Get (\x -> Put (toUpper x) (End 0)), Get (\x -> Put (toLower x) (End 0)))
   :: (IO Int, IO Int)

Кортеж действий ввода-вывода: сначала читается символ и печатается в верхнем регистре, затем читаетсясимвол и возвращает его в нижнем регистре.

 Get (\x -> End (Put x (End 0))) :: IO (IO Int)

Значение IO, которое читает символ x и заканчивается, возвращая значение IO, которое записывает x на экран.

Haskell имеет специальные функции, которые позволяют легко манипулировать значениями IO.Например:

 sequence :: [IO a] -> IO [a]

, который принимает список действий ввода-вывода и возвращает действие ввода-вывода, которое выполняет их последовательно.

Монады

Монады - это некоторые комбинаторы (например, conditionally выше), которые позволяют писать программы более конструктивно.Есть функция, которая составляет тип

 IO a -> (a -> IO b) -> IO b

, которая дает IO a, и функция a -> IO b, возвращает значение типа IO b.Если вы записываете первый аргумент как функцию C a f(), а второй аргумент как b g(a x), программа возвращает g(f(x)).Учитывая приведенное выше определение Action / IO, вы можете написать эту функцию самостоятельно.

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

Purity

Важным аспектом чистоты является прозрачность по ссылкам и различие между оценкой и выполнением.

В Haskell, если у вас есть f x+f x, вы можете заменить его на 2*f x.В C f(x)+f(x) в целом отличается от 2*f(x), так как f может что-то напечатать на экране или изменить x.

Благодаря чистоте компилятор имеет гораздо больше свободыи может оптимизировать лучше.Он может переставлять вычисления, в то время как в C он должен думать, меняет ли это значение программы.

9 голосов
/ 25 июня 2010

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

6 голосов
/ 26 июня 2010

Что значит рассуждать о компьютерных системах "вне математики на доске"?Что это за рассуждение?Мертвый расчет?

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

Мы можем сделать каждую побочную функцию чистой, дав ей второй аргумент,мир, и требует, чтобы он передал нам новый мир, когда это будет сделано.Я больше не знаю C++, но говорю, что read имеет такую ​​подпись:

vector<char> read(filepath_t)

В нашем новом "чистом стиле" мы обрабатываем это так:

pair<vector<char>, world_t> read(world_t, filepath_t)

Фактически, так работает каждое действие ввода-вывода Haskell.

Итак, теперь у нас есть чистая модель ввода-вывода.Слава Богу.Если бы мы не могли этого сделать, то, возможно, лямбда-исчисление и машины Тьюринга не являются эквивалентными формализмами, и тогда у нас есть некоторые объяснения.Мы еще не закончили, но две оставленные нам проблемы просты:

  • Что входит в структуру world_t?Описание каждой песчинки, травинки, разбитого сердца и золотого заката?

  • У нас есть неформальное правило, что мы используем мир только один раз - после каждой операции ввода-вывода мывыбросить мир, который мы использовали с ним.Однако, когда все эти миры плавают вокруг, мы должны их перепутать.

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

Что касается смешивания миров, мы хотели бы скрыть мир, проходящий внутри библиотеки, чтобы не было возможности добраться до миров и, следовательно, не было бы возможности смешиватьих вверх.Оказывается, монады - отличный способ скрыть «побочный канал» в вычислениях.Введите монаду ввода / вывода.

Некоторое время назад, в вашем списке рассылки на Haskell был задан вопрос, похожий на ваш, и я попал в «побочный канал» более подробно.Вот ветка Reddit (которая ссылается на мой оригинальный адрес электронной почты):

http://www.reddit.com/r/haskell/comments/8bhir/why_the_io_monad_isnt_a_dirty_hack/

4 голосов
/ 25 июня 2010

Для расширенной версии конструкции ввода-вывода sdcwc можно взглянуть на пакет IOSpec на Hackage: http://hackage.haskell.org/package/IOSpec

4 голосов
/ 25 июня 2010

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

Чтоу нас в Haskell есть то, что вы больше подталкиваете в этом направлении написания, чтобы жестко контролировать эффекты.В ядре и во многих библиотеках огромное количество чистого кода.Хаскелл действительно все об этом.Монады в Хаскеле полезны для многих вещей.И одна вещь, для которой они использовались - это сдерживание вокруг кода, который имеет дело с примесями.

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

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

4 голосов
/ 25 июня 2010

Я очень новичок в функциональном программировании, но вот как я это понимаю:

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

В частности, есть одна функция, которая оценивается. Это постоянная функция, которая производит набор «действий». Действия включают оценку функций и выполнение ввода-вывода и других "реальных" вещей. У вас могут быть функции, которые создают и передают эти действия, и они никогда не будут выполнены, если функция не оценена с unsafePerformIO или они возвращаются основной функцией.

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

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

0 голосов
/ 12 мая 2015

Нет, это не так. Монада IO нечиста, потому что она имеет побочные эффекты и изменчивое состояние (условия гонки возможны в программах на Haskell, так что ... эх ... чистый язык FP не знает что-то вроде "условия гонки"). Действительно чистый FP - это Clean с уникальной типизацией, или Elm с FRP (функционально-реактивное программирование), а не Haskell. Хаскелл - одна большая ложь.

Доказательство:

import Control.Concurrent 
import System.IO as IO
import Data.IORef as IOR

import Control.Monad.STM
import Control.Concurrent.STM.TVar

limit = 150000
threadsCount = 50

-- Don't talk about purity in Haskell when we have race conditions 
-- in unlocked memory ... PURE language don't need LOCKING because
-- there isn't any mutable state or another side effects !!

main = do
    hSetBuffering stdout NoBuffering
    putStr "Lock counter? : "
    a <- getLine
    if a == "y" || a == "yes" || a == "Yes" || a == "Y"
    then withLocking
    else noLocking

noLocking = do
    counter <- newIORef 0
    let doWork = 
        mapM_ (\_ -> IOR.modifyIORef counter (\x -> x + 1)) [1..limit]
    threads <- mapM (\_ -> forkIO doWork) [1..threadsCount]
    -- Sorry, it's dirty but time is expensive ...
    threadDelay (15 * 1000 * 1000)
    val <- IOR.readIORef counter
    IO.putStrLn ("It may be " ++ show (threadsCount * limit) ++ 
        " but it is " ++ show val) 

withLocking = do
    counter <- atomically (newTVar 0)
    let doWork = 
        mapM_ (\_ -> atomically $ modifyTVar counter (\x -> 
            x + 1)) [1..limit]
    threads <- mapM (\_ -> forkIO doWork) [1..threadsCount]
    threadDelay (15 * 1000 * 1000)
    val <- atomically $ readTVar counter
    IO.putStrLn ("It may be " ++ show (threadsCount * limit) ++ 
        " but it is " ++ show val)
...