Programování s akumulátory

Odpřednášena v pondělí 12. 10.

Byl zde zadán #U5, #U6, #U7

Běžný rekurzivní zápis funkce pracuje s nějakým malým kouskem vstupu a přidává ho k řešení zbytku, které nechá vypočítat rekurzivním zavoláním. Například u funkce reverse:

fun reverse<T>(l :: List<T>) -> List<T>:
  cases (List) l:
    | empty => empty
    | link(first, rest) =>
      # Malý kousek
      new-last = [list: first]
      # Řešení zbytku vypočítané rekurzivním zavoláním
      new-beginning = reverse(rest)
      # Výsledek = smíchané řešení malého kousku a řešení zbytku
      new-beginning + new-last
  end
end

Tato implementace reverse je ale velmi pomalá, protože + je pro seznamy implementováno dosti nefektivně (což plyne z toho, jak jsou uloženy v paměti).

Lepší implementace používá takzvaný akumulátor — vytvoříme pomocnou funkci, která si jako jeden z parametrů bude předávat mezivýsledek. Když vyčerpáme vstupní seznam, náš mezivýsledek se stane opravdovým výsledkem a my ho vrátíme.

fun reverse<T>(l :: List<T>) -> List<T>:
  reverse-helper(l, empty)
end

fun reverse-helper<T>(l :: List<T>, acc :: List<T>) -> List<T>:
  cases (List) l:
    | empty =>
      # acc(umulator), mezivýsledek, se stává výsledkem
      acc
    | link(f, r) =>
      # link(f, acc) přidává f na začátek mezivýsledku
      new-accumulator = link(f, acc)
      # a mezivýsledek posíláme jako druhý argument do reverse-helper
      reverse-helper(r, new-accumulator)
  end
end

Některé funkce dokonce bez tohoto použití pomocné funkce a akumulátoru ani (rozumně) napsat nejdou (viz úkol 6d).

Příklady akumulátorů u běžných funkcí

Ne u všech těchto funkcí povede akumulátor k efektivnější implementaci (na rozdíl od reverse), ale u žádné nepovede k něčemu pomalejšímu. Zda použít akumulátor nebo ne je pak často otázkou stylu — nebude to s akumulátorem čitelnější?

Sum

Běžná rekurzivní implementace.

fun sum(numbers :: List<Number>) -> Number:
  cases (List) numbers:
    | empty => 0
    | link(first, rest) => first + sum(rest)
  end
end

Implementace pomocí akumulátoru.

fun sum(numbers :: List<Number>) -> Number:
  sum-helper(numbers, 0)
end

fun sum-helper(numbers :: List<Number>, acc :: Number) -> Number:
  cases (List) numbers:
    | empty => acc
    | link(first, rest) => sum-helper(rest, acc + first)
  end
end

Maximum

Běžná rekurzivní implementace.

fun maximum(numbers :: List<Number>) -> Number:
  cases (List) numbers:
    | empty => raise("List must me non-empty")
    | link(first, rest) =>
      num-max(first, maximum(rest))
  end
end

Implementace pomocí akumulátoru.

fun maximum(numbers :: List<Number>) -> Number:
  cases (List) numbers:
    | empty => raise("List must me non-empty")
    | link(first, rest) =>
      maximum-helper(rest, first)
  end
end

fun maximum-helper(numbers :: List<Number>, current-max :: Number) -> Number:
  cases (List) numbers:
    | empty => current-max
    | link(first, rest) =>
      maximum-helper(rest, num-max(current-max, first))
  end
end

Průběžné součty

Funkce, která vrátí seznam součtů prvního čísla, prvních dvou čísel, prvních třech čísel, atd.

>>> running-sum([list: 1, 2, 3, 4])
[list: 1, 3, 6, 10]
>>> running-sum([list: -1, 0, 1])
[list: -1, -1, 0]

Běžná rekurzivní implementace. Je dost neefektivní, protože v každém zavolání running-sum voláme jak sum tak seznamové +. A navíc musíme tu funkci sum sami definovat.

fun running-sum(numbers :: List<Number>) -> List<Number>:
  cases (List) numbers:
    | empty => empty
    | else =>
      dropped-last = L.take(L.length(numbers) - 1, numbers)
      running-sum(dropped-last) + [list: sum(numbers)]
  end
end

fun sum(numbers :: List<Number>) -> Number:
  cases (List) numbers:
    | empty => 0
    | link(first, rest) => first + sum(rest)
  end
end

Implementace pomocí akumulátoru. Všimněte si, že sice používáme akumulátor, ale že v něm nemáme všechno — konkrétně výsledný seznam sbíráme v running-sum-helper normálně rekurzivně.

fun running-sum(numbers :: List<Number>) -> Number:
  running-sum-helper(numbers, 0)
end

fun running-sum-helper(numbers :: List<Number>, current-sum :: number) -> List<Number>:
  cases (List) numbers:
    | empty => [list: current-sum]
    | link(first, rest) =>
      new-sum = first + current-sum
      link(current-sum, running-sum-helper(rest, new-sum))
  end
end