Возьмите следующий мини-язык:
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 он должен думать, меняет ли это значение программы.