FoldLeft с использованием FoldRight в scala

при прохождении функциональное программирование в Scala, я наткнулся на этот вопрос:

можете ли вы правильно foldLeft с точки зрения foldRight? Как насчет другого пути? вокруг?

в решении, предоставленном авторами, они предоставили реализацию следующим образом:

def foldRightViaFoldLeft_1[A,B](l: List[A], z: B)(f: (A,B) => B): B = 
    foldLeft(l, (b:B) => b)((g,a) => b => g(f(a,b)))(z)

  def foldLeftViaFoldRight[A,B](l: List[A], z: B)(f: (B,A) => B): B = 
    foldRight(l, (b:B) => b)((a,g) => b => g(f(b,a)))(z)

может кто-нибудь помочь мне проследить через это решение и заставить меня понять, как это на самом деле получает foldl, реализованный с точки зрения foldr и наоборот?

спасибо

4 ответов


давайте посмотрим на

def foldLeftViaFoldRight[A,B](l: List[A], z: B)(f: (B,A) => B): B = 
  foldRight(l, (b:B) => b)((a,g) => b => g(f(b,a)))(z)

(другой раз похожие). Фокус в том, что во время правильной операции сгиба мы не строим конечное значение типа B. Вместо этого, мы строим функцию из B to B. Шаг сгиба принимает значение типа a: A и g: B => B и производит новую функцию (b => g(f(b,a))): B => B. Эта функция может быть выражена в виде композиции g С f(_, a):

  l.foldRight(identity _)((a,g) => g compose (b => f(b,a)))(z);

мы можем осмотреть процесс следующим образом: Для каждого элемента a of l берем частичное приложение b => f(b,a), что функция B => B. Тогда мы ... --25-->compose все эти функции таким образом, что функция, соответствующая самому правому элементу (с которого мы начинаем обход), находится далеко слева в цепочке композиции. Наконец, мы применяем большую составленную функцию на z. Это приводит к последовательности операций, которая начинается с самого левого элемента (который находится в крайнем правом углу композиции chain) и заканчивается самым правильным.

обновление: в качестве примера рассмотрим, как это определение работает в двухэлементном списке. Во-первых, мы перепишем функцию как

def foldLeftViaFoldRight[A,B](l: List[A], z: B)
                             (f: (B,A) => B): B =
{
  def h(a: A, g: B => B): (B => B) =
    g compose ((x: B) => f(x,a));
  l.foldRight(identity[B] _)(h _)(z);
}

теперь давайте вычислить, что происходит, когда мы проходим его List(1,2):

List(1,2).foldRight(identity[B] _)(h _)
  = // by the definition of the right fold
h(1, h(2, identity([B])))
  = // expand the inner `h`
h(1, identity[B] compose ((x: B) => f(x, 2)))
  =
h(1, ((x: B) => f(x, 2)))
  = // expand the other `h`
((x: B) => f(x, 2)) compose ((x: B) => f(x, 1))
  = // by the definition of function composition
(y: B) => f(f(y, 1), 2)

применение этой функции к z доходность

f(f(z, 1), 2)

по мере необходимости.


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

val f = (a: Int, b: Int) => a+b
val list = List(2,3,4)
println(list.foldLeft(1)(f))

val f1 = (b: Int) => f(b, 2)
val f2 = (b: Int) => f(b, 3)
val f3 = (b: Int) => f(b, 4)
val f4 = (b: Int) => b

val ftotal = f1 andThen f2 andThen f3 andThen f4
println(ftotal(1))

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


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

в качестве фона, давайте начнем с того, что foldLeft и foldRight do. Например, результат foldLeft в списке [1, 2, 3] с операцией * и начальное значение z значение ((z * 1) * 2) * 3

мы можем думать о foldLeft как потребляющих значения списка постепенно, слева направо. Другими словами, мы изначально начинаем со значения z (что было бы результатом, если бы список был пуст), то мы раскрываем foldLeft что наш список начинается с 1, а значение становится z * 1, потом foldLeft видит наш список следующий имеет 2 и значение становится (z * 1) * 2, и, наконец, после действия на 3, он становится значением ((z * 1) * 2) * 3.

                             1    2    3
Initially:               z
After consuming 1:      (z * 1)
After consuming 2:     ((z * 1) * 2
After consuming 3:    (((z * 1) * 2) * 3

это конечное значение-это значение, которого мы хотим достичь, кроме (как упражнение просит нас) используя . Теперь обратите внимание, что, как foldLeft потребляет значения списка слева направо,foldRight потребляет значения списка справа налево. Так в списке [1, 2, 3],

  • этот foldRight будет действовать на 3 и [что-то], давая [результат]
  • тогда он будет действовать на 2 и [результат], давая [result2]
  • наконец, он будет действовать на 1 и [result2] давая окончательное выражение
  • мы хотим, чтобы наше окончательное выражение быть (((z * 1) * 2) * 3

другими словами: использование foldRight, сначала мы приходим к тому, каким будет результат, если список будет пустым, затем результат, если список будет содержать только [3], затем результат, если список будет [2, 3], и, наконец, результат для списка будет [1, 2, 3].

то есть это те значения, к которым мы хотели бы прийти, используя foldRight:

                             1    2    3
Initially:                             z
After consuming 3:                 z * 3
After consuming 2:           (z * 2) * 3
After consuming 1:     ((z * 1) * 2) * 3

поэтому нам нужно идти от z to (z * 3) to (z * 2) * 3 для ((z * 1) * 2) * 3.

As значения, мы не можем этого сделать: нет естественного способа уйти от значения (z * 3) стоимостью (z * 2) * 3, для произвольной операции *. (Существует для умножения, поскольку оно коммутативно и ассоциативно, но мы используем только * для обозначения произвольной операции.)

, а как функции возможно, мы сможем это сделать! Нам нужно иметь функцию с "заполнителем" или "отверстием": что-то, что займет z и положите его в нужное место.

  • Е. Г. после первого шага (после О. на 3) мы имеем функцию заполнителя z => (z * 3). Или, скорее, как функция должна принимать произвольные значения, и мы использовали z для определенного значения, давайте напишем это как t => (t * 3). (Эта функция применяется на входе z дает значение (z * 3).)
  • после второго шага (после действия на 2 и результата) у нас есть функция-заполнитель t => (t * 2) * 3 может быть?

можем ли мы перейти от первой функции-заполнителя к следующей? Пусть

      f1(t) = t * 3
and   f2(t) = (t * 2) * 3

что это f2 С точки зрения f1?

f2(t) = f1(t * 2)

Да, мы можем! Итак, функцию мы хотим берет 2 и f1 и дает f2. Назовем это g. У нас есть g(2, f1) = f2 здесь f2(t) = f1(t * 2) или

g(2, f1) = 
    t => f1(t * 2)

посмотрим, сработает ли это, если мы продолжим: следующим шагом будет g(1, f2) = (t => f2(t * 1)) и RHS то же самое, что t => f1((t * 1) * 2)) или t => (((t * 1) * 2) * 3).

похоже, это работает! И, наконец, мы применяем z к этому результату.

каким должен быть начальный шаг? Мы применяем g on 3 и f0 и f1, где f1(t) = t * 3 как определено выше, а также f1(t) = f0(t * 3) из определения g. Так что, похоже, нам нужно f0 быть функцией идентификации.


давайте начнем заново.

Our foldLeft(List(1, 2, 3), z)(*) is ((z * 1) * 2) * 3
Types here: List(1, 2, 3) is type List[A]
             z is of type B
             * is of type (B, A) -> B
             Result is of type B
We want to express that in terms of foldRight
As above:
 f0 = identity. f0(t) = t.
 f1 = g(3, f0). So f1(t) = f0(t * 3) = t * 3
 f2 = g(2, f1). So f2(t) = f1(t * 2) = (t * 2) * 3
 f3 = g(1, f2). So f3(t) = f2(t * 1) = ((t * 1) * 2) * 3

и, наконец, мы применяем f3 на z и получаем выражение, которое мы хотим. Все получается. Так что

f3 = g(1, g(2, g(3, f0)))

что означает f3 = foldRight(xs, f0)(g)

давайте g на этот раз вместо x * y использование произвольной функции s(x, y):
  • первый arg к g типа A
  • второй arg к g типа таких f ' s, который B => B
  • так типа g is (A, (B=>B)) => (B=>B)
  • так g есть:

    def g(a: A, f: B=>B): B=>B = 
        (t: B) => f(s(t, a))
    

положить все это вместе

def foldLeft[A, B](xs: List[A], z: B)(s: (B, A) => B): B = {
    val f0 = (b: B) => b

    def g(a: A, f: B=>B): B=>B =
        t => f(s(t, a))

    foldRight(xs, f0)(g)(z)
}

на этом уровне работы над книгой я на самом деле предпочитаю эту форму, поскольку она более явная и более понятная. Но чтобы приблизиться к форме решения, мы можем встроить определения f0 и g (нам больше не нужно объявлять тип g как это вход в foldRight и компилятор выводит его), дает:

def foldLeft[A, B](xs: List[A], z: B)(s: (B, A) => B): B =
  foldRight(xs, (b: B) => b)((a, f) => t => f(s(t, a)))(z)

что в вопросе, только с разными символами. Аналогично для foldRight с точки зрения foldLeft.


авторы книги дают хорошее объяснение своим github / fpinscala страница.