Подтвердить что ты не робот

Почему foldl определенно задан в Racket?

В Haskell, как и во многих других функциональных языках, функция foldl определена так, что, например, foldl (-) 0 [1,2,3,4] = -10.

Это нормально, потому что foldl (-) 0 [1, 2,3,4] по определению ((((0 - 1) - 2) - 3) - 4).

Но в Racket (foldl - 0 '(1 2 3 4)) равно 2, потому что Racket "интеллектуально" вычисляет так: (4 - (3 - (2 - (1 - 0)))), который действительно равен 2.

Конечно, если мы определим вспомогательную функцию flip, например:

(define (flip bin-fn)
  (lambda (x y)
    (bin-fn y x)))

тогда мы могли бы в Racket достичь такого же поведения, как в Haskell: вместо (foldl - 0 '(1 2 3 4)) мы можем написать: (foldl (flip -) 0 '(1 2 3 4))

Возникает вопрос: почему foldl в racket, определенном в таком нечетном (нестандартном и неинтуитивном) способе, иначе, чем на любом другом языке?

4b9b3361

Ответ 1

  • Определение Haskell не является однородным. В Racket функция для обеих складок имеет одинаковый порядок входов, поэтому вы можете просто заменить foldl на foldr и получить тот же результат. Если вы сделаете это с помощью версии Haskell, вы получите другой результат (обычно) - и вы увидите это в разных типах двух.

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

  • У этого есть хороший побочный продукт, где вам предлагается выбрать foldl или foldr в соответствии с их семантическими различиями. Я предполагаю, что с ордером Haskell вы, вероятно, будете выбирать в соответствии с операцией. У вас есть хороший пример для этого: вы использовали foldl, потому что хотите вычесть каждое число, и что такой "очевидный" выбор позволяет легко упустить тот факт, что foldl обычно является плохим выбором в ленивом язык.

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

  • Наконец, неправильно предположить, что Racket отклонился от "многих других функциональных языков", поскольку сложение далеко не новое, а у Racket есть корни, которые намного старше, чем Haskell (или эти другие языки). Таким образом, вопрос может идти по-другому: почему Haskell foldl определен странным образом? (И нет, (-) не является хорошим оправданием.)

Историческое обновление:

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

  • Сначала было Lisp, и не упоминалось о "сворачивании" любого рода. Вместо этого Lisp имеет reduce, который очень неравномерен, особенно если вы рассматриваете его тип. Например, :from-end - это аргумент ключевого слова, который определяет, будет ли это левое или правое сканирование, и использует различные функции аккумулятора, что означает, что тип аккумулятора зависит от этого ключевого слова. Это в дополнение к другим хакам: обычно первое значение берется из списка (если вы не укажете :initial-value). Наконец, если вы не укажете :initial-value, и список пуст, он фактически применит функцию к нулевым аргументам, чтобы получить результат.

    Все это означает, что reduce обычно используется для того, что предлагает его название: сокращение списка значений в одно значение, где оба типа обычно одинаковы. Вывод здесь состоит в том, что он служит своего рода аналогичной целью для свертывания, но он не так полезен, как общая конструкция итераций списка, которую вы получаете при складывании. Я предполагаю, что это означает, что нет сильной связи между reduce и более поздними операциями fold.

  • Первым соответствующим языком, который следует за Lisp и имеет правильную сгиб, является ML. Выбор, который был сделан там, как отмечено в нижеприведенном ответе ниже, состоял в том, чтобы перейти к версии унифицированных типов (то есть, что использует Racket).

  • Следующая ссылка - Bird and Wadler ItFP (1988), которая использует различные типы (как в Haskell). Однако они отмечают в приложении, что Миранда имеет тот же тип (как в Racket).

  • Миранда позже переключила порядок аргументов (т.е. переместилась из ордера Racket в Haskell). В частности, в этом тексте говорится:

    ПРЕДУПРЕЖДЕНИЕ - это определение foldl отличается от определения в более ранних версиях Miranda. Здесь тот же, что и у Bird and Wadler (1988). У старого определения были два аргумента "op". Обратный.

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

  • OCaml пошел с направлением Haskell и использовал разные типы

  • Я предполагаю, что "Как создавать программы" (aka HtDP) был написан примерно в тот же период, и они выбрали тот же тип. Однако нет мотивации или объяснения - и на самом деле после этого упражнения он просто упоминается как одна из встроенных функций.

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

  • Затем появился SRFI-1, и выбор заключался в использовании версии того же типа (как Racket). Это решение было вопросом Джона Дэвида Стоуна, который указывает на комментарий в SRFI, в котором говорится

    Примечание: MIT Scheme и Haskell переверните F arg для своих функций reduce и fold.

    Olin позже обратился к этому: все, что он сказал, было:

    Хорошая точка, но я хочу согласованность между двумя функциями.     state-value first: srfi-1, SML     state-value last: Haskell

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

Ответ 2

"иначе, чем на любом другом языке"

Как встречный пример, стандартный ML (ML - очень старый и влиятельный функциональный язык) foldl также работает следующим образом: http://www.standardml.org/Basis/list.html#SIG:LIST.foldl:VAL

Ответ 3

Racket foldl и foldr (а также SRFI-1 fold и fold-right) обладают тем свойством, что

(foldr cons null lst) = lst
(foldl cons null lst) = (reverse lst)

Я предполагаю, что порядок аргументов был выбран по этой причине.

Ответ 4

В документации Racket описание foldl:

(foldl proc init lst ...+) → any/c

Упомянуты две точки интереса для вашего вопроса:

входные lsts перемещаются слева направо

и

foldl обрабатывает lsts в постоянном пространстве

Я собираюсь рассказать о том, как может выглядеть реализация для этого, с одним списком для простоты:

(define (my-foldl proc init lst)
  (define (iter lst acc)
    (if (null? lst)
        acc
        (iter (cdr lst) (proc (car lst) acc))))
  (iter lst init))

Как вы можете видеть, выполняются требования прохождения слева и справа и постоянного пространства (обратите внимание на хвостовую рекурсию в iter), но порядок аргументов для proc никогда не указывался в описании. Следовательно, результатом вызова вышеуказанного кода будет:

(my-foldl - 0 '(1 2 3 4))
> 2

Если бы мы задали порядок аргументов для proc следующим образом:

(proc acc (car lst))

Тогда результатом будет:

(my-foldl - 0 '(1 2 3 4))
> -10

Моя точка зрения заключается в том, что документация для foldl не делает никаких предположений относительно порядка оценки аргументов для proc, она должна только гарантировать, что используется постоянное пространство и что элементы в списке оцениваются слева направо.

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

(- 0 1 2 3 4)
> -10