# dp23a 1. FP-előadás, 2023-09-05
## Két lista rekurzív összefűzése (app)
A bevezető DP-előadáson a deklaratív stílus érzékeltetésére két lista összefűzését mutattuk be egy Elixir-függvényel és egy Prolog-eljárással. Most, az első FP-előadáson az app/2
Elixir-függvény kapcsán fogok sok részletet elmondani, bemutatni a Livebook használatáról, az Elixir nyelvről, a rekurzióról, a rekurzió hatékonyságáról.
```elixir
# app(l1, l2): l1 és l2 listák összefűzöttje (l1⊕l2)
# [] ⊕ b = b
def app([], b) do
b
end
# [x|a] ⊕ b = [x|a⊕b]
def app([x | a], b) do
[x | app(a, b)]
end
```
```elixir
defmodule App do
# app(xs, ys): xs és ys listák összefűzöttje zs == (xs⊕ys)
# [] ⊕ ys == ys
def app([], ys) do
ys
end
# [x|xs] ⊕ ys = [x|xs⊕ys]
def app([x | xs], ys) do
[x | app(xs, ys)]
end
end
```
```elixir
defmodule App2 do
@spec app(xs :: [integer()], ys :: [integer()]) :: zs :: [integer()]
# xs és ys listák összefűzöttje zs == (xs⊕ys)
# [] ⊕ ys == ys
def app([], ys) do
ys
end
# [x|xs] ⊕ ys = [x|xs⊕ys]
def app([x | xs], ys) do
[x | app(xs, ys)]
end
end
```
```elixir
defmodule App3 do
@spec app(xs :: [integer()], ys :: [integer()]) :: zs :: [integer()]
# xs és ys listák összefűzöttje zs == (xs⊕ys)
# [] ⊕ ys == ys
def app([], ys) do
ys
end
# [x|xs] ⊕ ys = [x|xs⊕ys]
def app([x | xs], ys) do
[x | app(xs, ys)]
end
end
App3.app([], [])
```
```elixir
App3.app([], [])
App3.app([], [1, 2, 3])
```
```elixir
IO.inspect(App3.app([], []))
IO.inspect(App3.app([], [1, 2, 3]))
```
```elixir
IO.inspect(App3.app([], []))
IO.inspect(App3.app([5, 6, 7], [1, 2, 3]))
```
```elixir
IO.inspect(App3.app([], []))
IO.inspect(App3.app([5, 6, 7], [1, 2, 3]))
IO.inspect(App3.app([7, 10, 12], [97, 98, 99]))
```
```elixir
IO.inspect(App3.app([], []))
IO.inspect(App3.app([5, 6, 7], [1, 2, 3]))
IO.inspect(App3.app([7, 10, 12], [97, 98, 99]), charlists: :as_lists)
```
```elixir
App3.app([7, 10, 12], [97, 98, 99]) |> IO.inspect(charlists: :as_lists)
```
```elixir
App3.app([7, 10, 12], [97, 98, 99]) |> inspect(charlists: :as_lists) |> IO.puts()
```
```elixir
App3.app([7, 10, 12], [97, 98, 99]) |> inspect(charlists: :as_charlists) |> IO.puts()
```
```elixir
1.0 === 1
```
```elixir
~c"\a\n\fabc" == ~c"\a\n\fabc"
```
```elixir
1..13_200_000 |> Range.to_list() |> App3.app([]) |> length() |> IO.puts()
```
```elixir
1..13_200_000 |> Range.to_list() |> Enum.reverse() |> length() |> IO.puts()
```
```elixir
((1..13_200_000 |> Range.to_list()) ++ []) |> length() |> IO.puts()
```
```elixir
for(i <- 1..13_200, do: i) |> App3.app([]) |> length() |> IO.puts()
```
^^^^^ for
-komprehenzió, ~-jelölés: nagyon hasznos! Még találkozunk vele.
## Faktoriális (jobbrekurzív is)
Ugyancsak a bevezető DP-előadáson szerepelt az $n!$ kiszámítása C nyelven, a rekurzív fact
függvénnyel. „Igen, de ez lasssúúú! $\leadsto$ Ún. jobbrekurzív (farokrekurzív, tail
recursive) változata a ciklussal azonos hatékonyságú kóddá fordul.” - olvashatják az előadásdián. Most megírjuk a függvényt Elixir nyelven.
Az első változat a matematikai definíciót másoló, rekurzív függvény. Az ilyen rekurziót angolul *body recursion*-nek, magyarul talán törzsrekurziónak szokták nevezni, ha hangsúlyozni akarják, hogy a rekurzív hívás eredményével még el kell végezni valamilyen műveletet a függvény törzsében.
A második, jobbrekurzív, angolul: *tail recursive* változat kevésbé szigorúan követi a matematikai definíciót, emiatt nehezebb is megérteni, nehezebb hozzá kifejező, pontos fejkommentet írni. A jobbrekurziót magyarul szokták még terminális (ritkábban farok-) rekurziónak is nevezni, mert a rekurzív hívás az adott függvénytörzsben az utolsó – befejező, lezáró – hívás, az eredményét egy-az-egyben vissza kell adni, már semmiféle műveletet nem végzünk vele.
fac(100_000)
```elixir
defmodule Fac do
# Típusspecifikáció
@spec fac(n :: integer()) :: f :: integer()
# f = n! (azaz f az n faktoriálisa) # Fejkomment
# ha az n=0 mintaillesztés sikeres
def fac(0), do: 1
# ha az n=0 mintaillesztés sikertelen
def fac(n), do: n * fac(n - 1)
end
Fac.fac(5) |> IO.puts()
Fac.fac(0) |> IO.puts()
Fac.fac(1) |> IO.puts()
Fac.fac(100_000) |> IO.puts()
```
```elixir
defmodule Fac1 do
# Típusspecifikáció
@spec fac(n :: integer()) :: f :: integer()
# f = n! (azaz f az n faktoriálisa)
def fac(n), do: fac(n, 1)
@spec fac(n :: integer(), a :: integer()) :: f :: integer()
defp fac(0, a), do: a
defp fac(n, a), do: fac(n - 1, n * a)
end
Fac1.fac(5) |> IO.puts()
Fac1.fac(0) |> IO.puts()
Fac1.fac(1) |> IO.puts()
# Fac1.fac(45000) |> IO.puts()
Fac1.fac(100_000)
```
Tapasztalt-e különbséget a törzsrekurzív és a jobbrekurzív függvény futási ideje között?
## Két lista jobbrekurzív összefűzése
Most a listák összefűzésére jobbrekurzív függvényt írunk. Ha az eredeti sorrendet meg akarjuk tartani, akkor az első listát az összefűzés előtt meg kellene fordítanunk (miért is? magyarázza el!). Most megelégszünk azzal a verzióval, amelyik az első lista megfordítottját fűzi a második lista elé: ezt csinálja a revapp/2
függvény.
```elixir
defmodule App4 do
@spec revapp(xs :: [integer()], ys :: [integer()]) :: zs :: [integer()]
# megfordított xs és ys listák összefűzöttje zs == (rev(xs)⊕ys)
# [] ⊕ ys == ys
def revapp([], ys) do
ys
end
# [x|xs] ⊕ ys = xs⊕[x|ys]
def revapp([x | xs], ys) do
revapp(xs, [x | ys])
end
end
App4.revapp([], []) |> length()
1..13_600_000 |> Range.to_list() |> App4.revapp([]) |> length()
```
A tanulság:
Nowadays compilers and virtual machines are too smart to predict their behavior.
This is to ensure that no system resources, for example, call stack, are consumed.
When the call is not tail-recursive, the stack must be preserved across calls. Consider the following example.
```elixir
defmodule NTC do
def inf do
inf()
IO.puts(".")
DateTime.utc_now()
end
end
```
Here we need to preserve stack to make it possible to continue execution of the caller when the recursion would return. It won't because this recursion is infinite. The compiler is unable to optimize it and here is what we get:
```elixir
NTC.inf
[1] 351729 killed iex
```
Please note, that no output has happened, which means that the function was recursively calling itself until the stack blew up.
Please not too, that with Tail-Call Optimization, infinite recursion is possible – and it is used widely in message handling.
Source: https://stackoverflow.com/questions/61626928/elixir-why-tail-recursive-use-more-memory-than-a-body-recursive-function
See also: https://www.erlang.org/doc/efficiency_guide/myths.html, 2.1 Myth: Tail-Recursive Functions are Much Faster Than Recursive Functions
```elixir
defmodule NTC do
def inf do
# inf()
IO.puts(".")
DateTime.utc_now()
end
end
NTC.inf()
```
Az inf/0
függvény rekurzívan hívja saját magát, méghozzá úgy, hogy előtte semmilyen más műveletet nem végez: az ilyen rekurzív hívást balrekurzívnak nevezzük. Ez a rekurzió ráadásul végtelen rekurzió, nincs leállási feltétele. Ha futtatni akarja az Elixir-cella tartalmát, törölje a kommentjelet (#), majd pár másodperc futás után kattintson a megjelenő *Stop* gombra.
A végtelen **jobb**rekurziónak (de nem a balrekurziónak) pl. olyan esetekben van jogosultsága, amikor két vagy több processz üzenetet küld egymásnak, és végtelen rekurzív hívásban várnak a válaszüzetenetre.
## Egyszerű műveletek listákon
Zárásul nézzünk néhány egyszerű műveletet listákon.
```elixir
xs = [10, 20, 30]
x = hd(xs)
rs = tl(xs)
{zs, xs} = {xs, [5, 6]}
xs = [5, 7]
IO.inspect(tl(xs), charlists: :as_lists)
xs = [7, 8, 9]
hd(tl(xs))
```
```elixir
for i <- 7..13, do: [i]
```
```elixir
for i <- 7..13, do: [i] |> inspect(charlists: :as_lists)
```
```elixir
for(i <- 7..13, do: [i]) |> inspect(charlists: :as_lists)
```
```elixir
for i <- 7..13, do: [i] |> IO.inspect(charlists: :as_lists)
```
```elixir
for(i <- 7..13, do: [i]) |> IO.inspect(charlists: :as_lists)
```
```elixir
for(i <- 30..36, do: [i]) |> IO.inspect(charlists: :as_lists)
```
```elixir
for(i <- 32..95, do: [i]) |> inspect(charlists: :as_lists)
```
```elixir
for(i <- 32..95, do: [i]) |> inspect(charlists: :as_lists, limit: :infinity)
```
```elixir
for(i <- 32..95, do: [i]) |> inspect(limit: :infinity)
```