"Foldp" нарушает FP без изменчивого государственного принципа?

Я узнаю о Elm из Семь других языков за семь недель. Следующий пример меня смущает:

import Keyboard
main = lift asText (foldp (\dir presses -> presses + dir.x) 0 Keyboard.arrows)

foldp определяется как:

Signal.foldp : (a -> b -> b) -> b -> Signal a -> Signal b

Мне кажется, что

  • начальное значение аккумулятора presses составляет только 0 при первой оценке main
  • после первой оценки main кажется, что начальное значение presses является тем, что результат функции (a -> b -> b) или (\dir presses -> presses + dir.x) в этом примере был на предыдущей оценке.

Если это действительно так, то разве это не является нарушением принципов функционального программирования, так как в настоящее время поддерживается внутреннее состояние (или, по крайней мере, foldp)?

Как это работает, когда я использую foldp в нескольких местах в моем коде? Поддерживает ли он несколько внутренних состояний, по одному на каждый раз, когда я его использую?

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

Я что-то пропустил?

Ответы

Ответ 1

Как это работает

Да, foldp поддерживает некоторое внутреннее состояние. Сохранение всей истории было бы расточительным и не было сделано.

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

import Keyboard

plus  = (foldp (\dir presses -> presses + dir.x) 0 Keyboard.arrows)
minus = (foldp (\dir presses -> presses - dir.x) 0 Keyboard.arrows)
showThem p m = flow down (map asText [p, m])
main  = lift2 showThem plus minus

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

import Keyboard

plus  = (foldp (\dir presses -> presses + dir.x) 0 Keyboard.arrows)
showThem p m = flow down (map asText [p, m])
main  = lift2 showThem plus plus

Основной вопрос

Если это действительно так, то разве это не является нарушением принципов функционального программирования, так как в настоящее время поддерживается внутреннее состояние (или, по крайней мере, foldp)?

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

Но что такое изменяемое состояние? Что такое изменчивое значение? Это значение внутри программы, которое является изменяемым. То есть, это может измениться. Это могут быть разные вещи в разное время. Ах, но мы знаем, как Элм называет ценности со временем меняющимися изменениями! Это a Signal.
Так что действительно, Signal в Elm - это значение, которое может меняться со временем и поэтому может рассматриваться как переменная, изменяемое значение или изменяемое состояние. Просто мы справляемся с этим значением очень строго, разрешая только несколько хорошо подобранных манипуляций на Signal s. Такой Signal может быть основан на других Signal в вашей программе, или поступать из библиотеки или поступать из внешнего мира (подумайте о входах, таких как Mouse.position). И кто знает, как внешний мир придумал этот сигнал! Таким образом, чтобы ваш собственный Signal был основан на прошлом значении Signal, это действительно нормально.

Заключение /TL; DR

Вы можете видеть Signal как защитную оболочку вокруг изменяемого состояния. Мы предполагаем, что сигналы, поступающие из внешнего мира (как входные данные в вашу программу), не предсказуемы, но поскольку у нас есть эта защитная оболочка, которая позволяет только лифтинг/образец/фильтр/foldp, программа, которую вы пишете, в противном случае полностью предсказуема. Побочные эффекты содержатся и управляются, поэтому я думаю, что это все еще "функциональное программирование".

Ответ 2

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

Не думайте о main как о неоднократной оценке, каждый раз возвращая разные результаты. A Signal концептуально представляет собой бесконечный список значений. main принимает бесконечный список клавиатурных стрелок в качестве входных данных и преобразует их в бесконечный список элементов. Учитывая тот же список стрелок, он всегда будет возвращать тот же список элементов без побочных эффектов. На этом уровне абстракции это, следовательно, чистая функция.

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

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

Ответ 3

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

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