Объяснение списков:функция сгиба

Я все больше и больше узнаю о языке Erlang и недавно столкнулся с проблемой. Я читал о функции foldl(Fun, Acc0, List) -> Acc1. Я использовал учебник Learnyousomeerlang.com, и там был пример (пример касается калькулятора обратной польской записи в Erlang):

%function that deletes all whitspaces and also execute
rpn(L) when is_list(L) ->
  [Res] = lists:foldl(fun rpn/2, [], string:tokens(L," ")),
  Res.

%function that converts string to integer or floating poitn value
read(N) ->
  case string:to_float(N) of
    %returning {error, no_float} where there is no float avaiable
    {error,no_float} -> list_to_integer(N);
    {F,_} -> F
  end.

%rpn managing all actions
rpn("+",[N1,N2|S]) -> [N2+N1|S];
rpn("-", [N1,N2|S]) -> [N2-N1|S];
rpn("*", [N1,N2|S]) -> [N2*N1|S];
rpn("/", [N1,N2|S]) -> [N2/N1|S];
rpn("^", [N1,N2|S]) -> [math:pow(N2,N1)|S];
rpn("ln", [N|S])    -> [math:log(N)|S];
rpn("log10", [N|S]) -> [math:log10(N)|S];
rpn(X, Stack) -> [read(X) | Stack].

Насколько я понимаю, lists:foldl выполняет rpn/2 для каждого элемента в списке. Но это насколько я понимаю эту функцию. Я читал документацию, но это не очень мне помогает. Может кто-нибудь объяснить мне, как работает lists:foldl?


person Marcin Majewski    schedule 10.11.2014    source источник
comment
Этот вопрос действительно более общий, чем Erlang, и затрагивает общий камень преткновения для программистов, незнакомых с функциональными парадигмами. Легкость чтения этого конкретного примера заставляет меня задаться вопросом, не будет ли вопрос полезен большему количеству людей, если его перефразировать в более общем виде.   -  person zxq9    schedule 11.11.2014


Ответы (2)


Допустим, мы хотим добавить список чисел вместе:

1 + 2 + 3 + 4.

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

3 + 7.

а потом

10.

Так в чем же дело? Проблема в том, что мы не можем понять идею суммирования из этого примера. Что, если бы вместо этого я написал: «Начните с 0, затем берите по одному элементу из списка и добавляйте его к начальному значению в виде текущей суммы»? На самом деле это то, что касается суммирования, а не произвольного решения, какие две вещи добавить первыми, пока уравнение не уменьшится.

sum(List) -> sum(List, 0).

sum([], A)    -> A;
sum([H|T], A) -> sum(T, H + A).

Если вы со мной до сих пор, то вы готовы понять фолды.

Возникла проблема с указанной выше функцией; это слишком конкретно. Он объединяет три идеи вместе, не определяя ни одну из них по отдельности:

  • итерация
  • накопление
  • добавление

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

Легко упустить независимость сложения только из-за того, как оно написано в этом примере, потому что «+» выглядит как «операция», а не как функция.

Что, если бы я сказал: «Начните с 1, затем берите по одному элементу из списка и умножайте его на текущее значение»? Мы по-прежнему будем обрабатывать список точно так же, но с двумя примерами для сравнения становится ясно, что умножение и сложение — единственная разница между ними:

prod(List) -> prod(List, 1).

prod([], A)    -> A;
prod([H|T], A) -> prod(T, H * A).

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

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

add(A, B)  -> A + B.
mult(A, B) -> A * B.

Как мы могли бы написать операцию списка самостоятельно? Нам нужно передать функцию — сложение или умножение — и заставить ее работать со значениями. Кроме того, мы должны обращать внимание на идентификацию типа и операции вещей, над которыми мы работаем, иначе мы испортим магию. это агрегация значений. "add(0, X)" всегда возвращает X, поэтому эта идея (0 + Foo) является операцией добавления идентификатора. При умножении тождественная операция заключается в умножении на 1. Поэтому мы должны начинать наш аккумулятор с 0 для сложения и 1 для умножения (и для построения списков, пустой список и т. д.). Таким образом, мы не можем написать функцию со встроенным значением аккумулятора, потому что она будет правильной только для некоторых пар тип+операция.

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

fold([], _, Accumulator) ->
    Accumulator;
fold([H|T], Operation, Accumulator) ->
    fold(T, Operation, Operation(H, Accumulator)).

С этим определением теперь мы можем написать sum/1, используя этот более общий шаблон:

fsum(List) -> fold(List, fun add/2, 0).

И prod/1 также:

fprod(List) -> fold(List, fun prod/2, 1).

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

В случае калькулятора RPN идея операций со сводным списком сочетается с концепцией выборочной отправки (выбор операции для выполнения на основе того, какой символ встречается/сопоставляется). Пример RPN относительно прост и мал (в голове сразу может уместиться весь код, это всего несколько строк), но пока вы не привыкнете к функциональным парадигмам, процесс, который он проявляет, может вызвать у вас головную боль. В функциональном программировании небольшое количество кода может создать сколь угодно сложный процесс с непредсказуемым (или даже развивающимся!) поведением, основанный только на операциях со списками и выборочной диспетчеризации; это очень отличается от методов условной проверки, проверки ввода и процедурной проверки, используемых в других парадигмах, более распространенных сегодня. Анализу такого поведения значительно помогает одиночное присваивание и рекурсивная запись, поскольку каждая итерация представляет собой концептуально независимый отрезок времени, который можно рассматривать отдельно от остальной системы. Я говорю немного раньше основного вопроса, но это основная идея, которую вы, возможно, захотите обдумать, когда будете думать, почему нам нравится использовать такие операции, как свертки и рекурсивные обозначения, вместо процедурных, множественных. петли назначения.

Надеюсь, это больше помогло, чем запутало.

person zxq9    schedule 10.11.2014

Во-первых, вы должны помнить, как работает rpn. Если вы хотите выполнить следующую операцию: 2 * (3 + 5), вы передадите функции ввод: "3 5 + 2 *". Это было полезно в то время, когда у вас было 25 шагов, чтобы войти в программу :о)

первая вызываемая функция просто разбивает этот список символов на элементы:

1> string:tokens("3 5 + 2 *"," ").
["3","5","+","2","*"]
2>

затем он обрабатывает списки:foldl/3. для каждого элемента этого списка вызывается rpn/2 с заголовком входного списка и текущим аккумулятором и возвращается новый аккумулятор. давайте шаг за шагом:

Step head  accumulator  matched rpn/2                           return value
1    "3"   []           rpn(X, Stack) -> [read(X) | Stack].    [3]
2    "5"   [3]          rpn(X, Stack) -> [read(X) | Stack].    [5,3]
3    "+"   [5,3]        rpn("+", [N1,N2|S]) -> [N2+N1|S];      [8]
4    "2"   [8]          rpn(X, Stack) -> [read(X) | Stack].    [2,8]
5    "*"   [2,8]        rpn("*",[N1,N2|S]) -> [N2*N1|S];       [16]

В конце lists:foldl/3 возвращает [16], что соответствует [R], и хотя rpn/1 возвращает R = 16

person Pascal    schedule 11.11.2014
comment
Это хорошее пошаговое описание точного случая foldl + rpn. Я надеюсь, что между двумя (мой общий, этот конкретный) ОП найдет свое Ах, ха! момент. - person zxq9; 11.11.2014