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

Я столкнулся с этой проблемой, не разворачивая и передислоцируя войну со стеклянными рыбками. Моя структура класса была такой:

public interface A{
}

public class AImpl implements A{
}

, и она была изменена на

public abstract class A{
}

public class AImpl extends A{
}

. После остановки и перезапуска домена это получилось прекрасным. Я использовал стеклянную рыбку 3.1.43

34
задан WeNeedAnswers 5 June 2012 в 16:00
поделиться

6 ответов

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

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

This имеет тип 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 к типу данных IO, например, работу с файлами или параллелизм.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Монады

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

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

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

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

Чистота

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

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

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

74
ответ дан 27 November 2019 в 06:35
поделиться

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

4
ответ дан 27 November 2019 в 06:35
поделиться

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

9
ответ дан 27 November 2019 в 06:35
поделиться

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

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

Мы можем сделать каждую функцию побочного эффекта чистой, дав ей второй аргумент, мир, и потребовав, чтобы она передавала нам новый мир, когда это будет выполнено.Я больше не знаю 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/

6
ответ дан 27 November 2019 в 06:35
поделиться

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

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

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

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

4
ответ дан 27 November 2019 в 06:35
поделиться

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

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

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

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

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

4
ответ дан 27 November 2019 в 06:35
поделиться
Другие вопросы по тегам:

Похожие вопросы: