Является ли Haskell действительно чистым (является ли любой язык, который имеет дело с вводом и выводом вне системы)?

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

EDIT:

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

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

7 ответов


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

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 вы указываете, что действия писать!--25-->. Компилятор создает программу, которая выполняет действие, но у вас нет другого способа выполнить действие (кроме грязных трюков).

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

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

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

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

предыдущей Action тип эквивалентно 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) помещает букву a на экран. Если вы оцениваете значение ввода-вывода, например:

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;
  }
}

в стоимость ИО?

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

как программист в C, вы можете утверждать, что comp('b') - это логическое. В C оценка и выполнение идентичны (i.e они означают одно и то же, или происходит одновременно). Не в Хаскелле. comp('b') оценивает в некоторое действие ввода-вывода, которое послевыполнена дает логическое. (Именно, он оценивает в блок кода, как указано выше, только с "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. Это не так, потому что функции в C смешивают математические функции и IO действия.

первый класс

эти значения ввода-вывода являются первоклассными. Так же, как у вас может быть список списков кортежей целых чисел [[(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)

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

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

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

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

монады

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

 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, вы можете написать эту функцию самостоятельно.

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

чистота

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

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

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


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


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

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

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

vector<char> read(filepath_t)

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

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

это на самом деле, как работает каждое действие Haskell IO.

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

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

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

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

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

некоторое время назад такой вопрос, как ваш, был задан в списке рассылки Haskell, и там я вошел в "боковой канал" Более подробно. Вот поток Reddit (который ссылается на мою оригинальную электронную почту):

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


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

в haskell вы определяете кучу функций. Эти функции не выполняются. Их могут оценить.

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

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

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


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

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

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

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


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


нет, это не так. Монада IO нечиста, потому что она имеет побочные эффекты и изменчивое состояние (условия гонки возможны в программах Haskell так ? а. .. чистый язык FP не знает что-то вроде "состояния расы"). Действительно чистый FP чист с типом уникальности, или вяз с 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)