Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bevezetés

A tantárgy a deklaratív programozási nyelvekkel foglalkozik, gyakorlati megközelítésben. Két fő irányát tanuljuk:

  • a funkcionális programozást az Elixir nyelven,
  • a logikai programozást a Prolog nyelven.

A jegyzet első része a deklaratív szemlélet áttekintése után az Elixir nyelvet és a funkcionális programozás eszközeit tárgyalja.

Forrás: dp26a-fp1ea.pdf (3. dia)

Deklaratív programozás

Kijelentő és felszólító nyelvek

A Wikipédia meghatározása szerint a deklaratív programozás olyan programozási paradigma, amely kifejezi a számítás logikáját anélkül, hogy leírná a vezérlési folyamatát (declarative programming is a programming paradigm that expresses the logic of a computation without describing its control flow).

A „deklaratív” jelző a nyelvészetből származik: jelentése kijelentő, kinyilatkoztató, ellentmondást nem tűrő, mint a „kijelentő mondat”. A mondatfajták között van kérdő és felszólító (imperatív) is. A számítógépek belső nyelve, a gépi kód alapvetően felszólító jellegű: add hozzá, szorozd meg, ugorj. A magas szintű programozási nyelvek többsége is imperatív: while ... do ..., goto ..., értékadás (írd felül a változó értékét).

C-ben is lehet deklaratívan programozni, például ciklus helyett rekurzióval:

int fact(int n) {if (n > 0) return n * fact(n-1);
                  else return 1;
                 }

Ez a változat lassú, mert minden hívás a verembe kerül. Az ún. jobbrekurzív (farokrekurzív, tail recursive) változata azonban a ciklussal azonos hatékonyságú kóddá fordul (lásd Rekurzió).

A deklaratív szemlélet előnye, hogy a programkód sokkal közelebb áll a specifikációhoz, ezért a helyességéről sokkal könnyebb meggyőződni. A megközelítés jelmondata:

MIT és nem HOGYAN

vagy kicsit enyhítve: inkább MIT, mint HOGYAN (WHAT rather than HOW).

A deklaratív nyelvekben a változó a matematika változófogalmának felel meg: egyetlen, esetleg még ismeretlen értéket jelöl, és nem írható felül.

Funkcionális és logikai programozás

A deklaratív programozás két fő ága egy-egy alapvető matematikai fogalomhoz kapcsolódik:

  • a funkcionális programozás (FP) a függvényekhez,
  • a logikai programozás (LP) a relációkhoz.
graph TD
  P["Programozási paradigmák – programozási nyelvek"] --> I["Imperatív<br/><i>Fortran, Algol, C, Java, Python, ...</i>"]
  P --> D["Deklaratív"]
  D --> F["Funkcionális<br/><i>LISP, ML, Haskell, Erlang, <b>Elixir</b>, ...</i>"]
  D --> L["Logikai<br/><i>SQL, <b>Prolog</b>, Constraint Prog., ...</i>"]

A kurzus tárgya az Elixir funkcionális és a Prolog logikai programozási nyelv.

Példa: listák összefűzése Elixirben és Prologban

Az Elixir és a Prolog közös szintaxist használ a láncolt listák jelölésére:

  • [] az üres lista,
  • [Head|Tail] olyan lista, amelynek feje (első eleme) Head, farka (a fej utáni része) pedig a Tail lista.

Az 1, 2, 3 számokból álló lista így [1|[2|[3|[]]]], vagy tömörebben [1,2,3].

Írjunk egy app nevű kétargumentumú Elixir-függvényt (app/2), amely két listát összefűz! Két esetet kell megkülönböztetni: ha az első lista üres, az eredmény a második lista; ha nem üres, akkor a feje lesz az eredmény feje, a farka pedig a farok és a második lista összefűzöttje:

#   app(l1,   l2): l1 és l2 listák összefűzöttje (l1⊕l2)
def app([],    b) do            b end       # [] ⊕b = b
def app([x|a], b) do [x|app(a,b)] end       # [x|a] ⊕b = [x|a⊕b]

Az app függvénynek egy háromargumentumú Prolog-eljárás (másnéven predikátum) felel meg (app/3); a harmadik argumentum az Elixir-függvény eredménye:

%   app(L1, L2, L12): L1 és L2 listák összefűzöttje L12 (L1⊕L2 = L12)
    app([],    B,               B).         % [] ⊕B = B
    app([X|A], B,    [X|       C]) :-       % [X|A] ⊕B = [X|C] ha
           app(A, B, C).                    %      A ⊕B = C

Az eljáráshívások a függvényhívásokkal ellentétben nem ágyazhatók egymásba, ezért kell a C segédváltozó. De ennek köszönhetően az app/3 Prolog-eljárás jobbrekurzív, azaz ciklussá fordul.

Prologban az is megengedett, hogy egy adatstruktúrában behelyettesítetlen változó szerepeljen. Az eljárás így is írható:

app([],    B, B).
app([X|A], B, L) :- L = [X|C], app(A, B, C).

A Prolog-változó pointerként is felfogható: app először felépíti az eredménylista első láncszemét ([X|C]), majd a jobbrekurzív hívással kitölti az eredménylista C által mutatott farkát, például:

app([1], [2], L) ⇒ L = [1|C], app([], [2], C) ⇒ L = [1|[2]] = [1,2]

Az app/3 eljárás nemcsak összefűzésre használható. Mivel relációt ír le, bármelyik argumentuma lehet ismeretlen. Bal oldalt a kérdések, a ⟹ után a Prolog válaszai (a ; újabb megoldást kér, a no jelzi, hogy nincs több):

| ?- app([1,2], [3,4], L).        ⟹   L = [1,2,3,4] ? ; no
| ?- app([1,2], B, [1,2,3,4]).    ⟹   B = [3,4] ? ; no
| ?- app([1,2], B, [1,3,4,5]).    ⟹   no
| ?- app(A, B, [1,2]).            ⟹   A = [], B = [1,2] ? ;
                                       A = [1],  B = [2] ? ;
                                       A = [1,2], B = [] ? ; no

Az utolsó kérdés az [1,2] lista összes lehetséges kettévágását sorolja fel.

Forrás: dp26a-fp1ea.pdf (14–18. dia)

Az Elixir nyelv

Fő jellemzők

Az Elixir

  • funkcionális nyelv;
  • a nyelvben minden kifejezés: nincs utasítás (statement) és kifejezés (expression) megkülönböztetés;
  • a különféle esetek felismerésére és szétválasztására mintaillesztést használ (Mintaillesztés);
  • ciklusok helyett rekurziót és magasabb rendű függvényeket használ (Rekurzió, Függvény);
  • dinamikusan típusos (Típusok);
  • nincs semmi megosztva: a processzek üzenetekkel kommunikálnak;
  • Erlang-függvények hívhatók Elixirből, Elixir-függvények Erlangból.

Erlang, BEAM és Elixir

Az Erlang Open Telecom Platform (OTP) az Ericsson által fejlesztett nyílt forráskódú rendszer masszívan párhuzamos, elosztott, megbízható alkalmazások fejlesztésére. Kihasználja az üzenetküldést és azt, hogy nincs megosztott memória és nincs változómódosítás; akár függvények is átküldhetők a szerverek között.

  • Erlang: az OTP platform eredeti funkcionális programozási nyelve, a Prologhoz hasonló szintaxissal.
  • BEAM: az Erlang/OTP virtuális gépe.
  • Elixir: funkcionális programozási nyelv a BEAM platformra, modern (a Rubyra hasonlító) szintaxissal.

A viszonyuk olyan, mint a Java-világban: BEAM : Erlang : Elixir ≈ JVM : Java : Kotlin. A Livebook pedig az Elixir számára az, ami a Jupyter a Pythonnak: notebook.

Folyamatok üzenetekkel kommunikálnak

A folyamatok (processzek) között nincs megosztott memória; egymással üzenetküldéssel kommunikálnak (aktor modell). Az alábbi példában a spawn/1 új folyamatot indít, amely a receive blokkban üzenetre vár. A főfolyamat a send/2-vel küld neki egy üzenetet, benne a saját azonosítójával (self()) mint válaszcímmel, majd maga is a válaszra vár; a beérkező üzenetet mintaillesztéssel fogadja:

worker = spawn(fn -> # Új folyamat indítása
  receive do # Várakozás üzenetre
    {:hello, from} -> # Válasz küldése from felé
      send(from, {:reply, "Hello from the worker process!"})
  end
end)
send(worker, {:hello, self()}) # Üzenet küldése válaszcímmel
receive do # Várakozás a worker folyamat válaszára
  {:reply, message} -> # Üzenet fogadása mintaillesztéssel
    IO.puts(message)
end

Az egymás utáni üzenetekre rekurzív függvényekkel lehet válaszolni: a függvény fogad egy üzenetet, válaszol rá, majd jobbrekurzívan újra meghívja önmagát, és várja a következőt.

Forrás: dp26a-fp1ea.pdf (21., 26–27. dia)

Interaktív használat: IEx és Livebook

Az IEx

Az Elixir interaktív héja, az iex egy REPL (read-eval-print loop): beolvas egy kifejezést, kiértékeli, és kiírja az értékét. A promptban zárójelben a kifejezés sorszáma áll.

$ iex
Erlang/OTP ...
Interactive Elixir ...
  press Ctrl+C to exit
  (type h() ENTER for help)

iex> 3.2 + 2.1 * 2
7.4

iex> :atom
:atom

iex> Atom
Atom

iex> "string"
"string"

iex> {:ennes,:%,A,:':',9.8}
{:ennes, :%, A, :":", 9.8}

iex> [:lista,:%,A,:':',9.8]
[:lista, :%, A, :":", 9.8]

iex> i :':'
...Data type
    Atom...

Az i segédfüggvény információt ír ki a kapott termről, például a típusát.

Az IEx-ből a Ctrl+C lenyomásával léphetünk ki. Ekkor a BEAM megszakítási menüje jelenik meg; egy újabb Ctrl+C kilép a héjból:

iex> Ctrl+C
BREAK: (a)bort (A)bort with dump (c)ontinue
   (p)roc info (i)nfo (l)oaded (v)ersion
   (k)ill (D)b-tables (d)istribution
Ctrl+C
$

A Ctrl+G a felhasználói parancsmódot (User switch command) nyitja meg; a h kiírja a parancsait, a q kilép:

iex> Ctrl+G
User switch command
--> h
  c [nn]            - connect to job
  i [nn]            - interrupt job
  k [nn]            - kill job
  j                 - list all jobs
  s [shell]         - start local shell
  r [node [shell]]  - start remote shell
  q                 - quit erlang
  ? | h             - this message
  --> q
$

IEx-parancsok

Az IEx segédfüggvényeit (helpers) a h() listázza:

iex> h().
Welcome to Interactive Elixir.
...
c/1            - compiles a file
c/2            - compiles a file and writes bytecode to the given path
cd/1           - changes the current directory
clear/0        - clears the screen
exports/1      - shows all exports (functions + macros) in a module
h/1            - prints help for the given module, function or macro
i/0            - prints information about the last value
i/1            - prints information about the given term
ls/0           - lists the contents of the current directory
ls/1           - lists the contents of the specified directory
pwd/0          - prints the current working directory
r/1            - recompiles the given module's source file
v/0            - retrieves the last value from the history
v/1            - retrieves the nth value from the history
...
To learn more about IEx as a whole, type h(IEx).

Az IEx-ben érdemes kipróbálni a Kernel Elixir-modul és a :math Erlang-könyvtár ismert függvényeit, és figyelni a tabulátorral kérhető szövegkiegészítést (completion).

Saját program fordítása, futtatása

Függvényt csak modulban lehet definiálni. Az alábbi fpea.ex fájl az Fpea modulban definiálja a faktoriális függvényt. A fájlnév konvenció szerint csupa kisbetűs, a szavak között aláhúzással (snake_case); a modulnév egybeírt, a szavak nagy kezdőbetűvel (BumpyCase, CamelCase).

defmodule Fpea do
# Fájlnév csupa kisbetűvel, szavak között aláhúzás (snake_case)
# Modulnév egybe, szavak nagy kezdőbetűvel (BumpyCase, CamelCase)
  @spec fac(n::integer) :: f::integer # Típusspecifikáció
  # f = n! (azaz f az n faktoriálisa) # Fejkomment
  def fac(0), do: 1            # ha az n=0 mintaillesztés sikeres
  def fac(n), do: n * fac(n-1) # ha az n=0 mintaillesztés sikertelen
end

A fájlt az IEx-ben a c paranccsal fordítjuk le; a függvényt a modulnévvel minősítve hívjuk:

iex> c "fpea.ex"  # fordítás
[Fpea]

iex> Fpea.fac(5)  # futtatás
120

iex> fac(5)         # a modulnevet ki kell írni
** (CompileError) iex:3: undefined function fac/1
iex> Fpea.fac 5   # argumentum körül a zárójel sokszor elhagyható
120

Egy már betöltött modul exportált függvényeit az exports Fpea írja ki (itt fac/1), a módosított forrásfájlt pedig az r Fpea fordítja újra.

Modulok és függvények azonosítása

Az Elixirben egy függvényt három dolog azonosít: a neve, az aritása (a paramétereinek száma) és annak a modulnak a neve, amelyben definiálva van. Például a String.slice/2 és a String.slice/3 a String modul két azonos nevű függvénye: az egyiknek két, a másiknak három paramétere van. Ha ugyanabban a modulban hivatkozunk egy függvényre, amelyben definiálva van, a modulnév elmaradhat.

A def exportált (a modulon kívülről is hívható), a defp privát függvényt definiál:

defmodule Dummy do

  def dummy0, do: 2024             # exportált, paraméter nélkül

  def dummy1(p), do: dummy2(p)     # exportált fv.

  defp dummy2(p), do: IO.puts (p)  # privát fv.

end

Dummy.dummy0

Dummy.dummy0 |> IO.inspect()

Dummy.dummy1('karakterlánc')    # karakterkódokból álló lácolt lista, nem sztring!

Dummy.dummy1(~c"karakterlista") # a ~c egy ún. szigil, speciális jelölés az Elixirben

Dummy.dummy1("sztring")

# Dummy.dummy2("sztring")

Az IO.puts/1 mindhárom paramétert szövegként írja ki (karakterlánc, karakterlista, sztring). A kikommentezett Dummy.dummy2("sztring") hívás UndefinedFunctionError hibát adna, mert a privát függvény a modulon kívülről nem látszik. Modulon kívül függvény nem definiálható: a def dummy, do: "halihó" önmagában a cannot invoke def/2 outside module hibát adja.

Livebook

A Livebook az Elixir notebookja, a Python Jupyteréhez hasonló. A gyakorlatok feladatsorai Livebook-formátumban (.livemd) jelennek meg. Az IEx-szel szemben a Livebookban modul is definiálható egy cellában.

A Livebook Elixir-cellái különálló modulokként fordulnak le. Ha egy cellában egy modulnak nevet adtunk, ugyanazt a nevet egy másik cellában már nem használhatjuk: ha az azonos nevű modult tartalmazó második cellát is kiértékeljük, az Elixir hibát jelez. Ennek előnye, hogy a korábbi cellákban definiált függvényeket a modulnév megadásával a későbbi cellákból is hívhatjuk.

Kiírás: IO.inspect, inspect, IO.puts

Egy kifejezéssorozat kiértékelésekor az Elixir (a Livebook is) csak az utolsó kifejezés értékét írja ki. A korábbi kifejezések értékét az IO.inspect/1-gyel íratjuk ki:

[1|[2|[3|[]]]] |> IO.inspect()
[1,2|[3]] # |> IO.inspect()
[1,2,3]

Az alábbi cellában az első kifejezés értékével nem kezdünk semmit, ezért nem is jelenik meg sehol; csak a második, utolsó kifejezés értéke ([1, 2, 3]) látszik:

App3.app([], [])        # E kifejezés értékével nem kezdünk semmit, nem is jelenik meg sehol.
App3.app([], [1, 2, 3])

Az IO.inspect/1-gyel mindkettő kiíratható (az App3.app/2 két listát fűz össze, lásd Típusok):

IO.inspect(App3.app([], []))        # Itt sem kezdünk vele semmit, de kiírjuk a már látott
IO.inspect(App3.app([], [1, 2, 3])) #  IO.inspect/1 függvénnyel, direkt paraméterátadással.

A |> az ún. pipe operátor (mint a Linuxban a |): a bal oldalán álló kifejezés eredményét a jobb oldalán álló függvénynek adja át, mégpedig az első paramétereként, ha több paramétere is van.

Az IO.inspect/1 nemcsak kiírja a kapott kifejezés értékét, hanem eredményként változatlanul vissza is adja, ezért a többi inspect függvénnyel együtt nagyon hasznos hibakeresési eszköz: bárhová beszúrható egy kifejezésbe.

[1 |> IO.inspect(), 2 |> IO.inspect(), 3 |> IO.inspect()] |> IO.inspect()

Ez sorban kiírja az 1, 2, 3 értéket, majd az [1, 2, 3] listát.

Az IO.inspect/2 és a Kernel modulban definiált inspect/2 (a Kernel modulnév elhagyható) között az a különbség, hogy az előbbi kiírja a kapott kifejezést, és az értékét változatlan formában továbbadja, az utóbbi pedig sztringgé konvertálva adja eredményül (a Livebook ezt a sztringet a cella eredményeként jeleníti meg). Az IO.puts/1 a kapott kifejezést sztringgé alakítva írja ki, és az :ok atomot adja vissza. Az inspect függvények működése opciókkal befolyásolható; a listák kiírását szabályozó charlists: opciót a Lista és karakterlánc fejezet mutatja be.

Forrás: dp26a-fp1ea.pdf (35–37. dia), dp26a-fp1gyfel.livemd, dp26a-fp1gy-megoldasok.livemd

Projektek, mérés, típusellenőrzés: mix, benchee, dialyzer

Miért kell projektszervezés?

  • Az Elixirhez sokféle modul van. A gyakran használtak (pl. Kernel, Enum, List, String) az Elixir-alapcsomag részei, a többit (pl. Benchee) utólag kell telepíteni, ha és amikor szükség van rájuk.
  • Magának a fordítónak (elixir, elixirc, iex) is, a moduloknak is több verziója van, és az újabb verziók nem mindig kompatibilisek a korábbiakkal: a függőségeket kezelni kell.
  • Egy saját projekt általában több modulból áll, plusz a teszteléshez használt adatokból és segédprogramokból; ezeket célszerű áttekinthetően, rendben tartani.
  • Az Elixirhez kidolgozott segédeszközök csak akkor használhatók, ha betartjuk a konvenciókat, nemcsak a névadásra, hanem például a fájlokat tároló mappák szerkezetére vonatkozóakat is.

Elixir-projektek kezelésére készült a mix, amely az Elixir-csomag része (https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html). A legfontosabb hívási formái a mix --help szerint:

mix             - Invokes the default task (mix run) in a project
mix new PATH    - Creates a new Elixir project at the given path
mix help        - Lists all available tasks
mix help TASK   - Prints documentation for a given task

Új projekt

Hozzunk létre egy fp nevű projektet egy új, ugyancsak fp nevű mappában! Az fp/lib mappában létrejön az fp.ex fájl, benne az Fp modul sablonjával; ez lenne a modul neve a --module opció nélkül is.

# mix new fp --module Fp
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/fp.ex
* creating test
* creating test/test_helper.exs
* creating test/fp_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
    cd fp
    mix test
Run "mix help" for more commands.

# ls -F fp
lib/  mix.exs  README.md  test/

A projekt leírása a mix.exs fájlban van, és ez – mi más is lehetne – Elixir-kód:

defmodule Fp.MixProject do
  use Mix.Project
  def project do
    [
      app: :fp,
      version: "0.1.0",
      elixir: "~> 1.18",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end
  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
   [
    # {:dep_from_hexpm, "~> 0.3.0"},
    # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: ...}
   ]
  end
end

A mix.exs két publikus (def) és egy privát (defp) függvényt definiál. A project a projekt konfigurációjáról tárol adatokat, az application-nel egy applikációs fájlt lehet generálni; ezek részleteibe nem megyünk bele. A deps privát függvény törzsében kell leírni a függőségeket: a kívánt modulok nevét és paramétereit.

Függőség felvétele: benchee

Új függőségként a Benchee modult vesszük fel, amely – ahogy a neve is sugallja – benchmarkingra, futási idők mérésére használható (https://github.com/bencheeorg/benchee). A mix.exs vége ezzel így néz ki:

 # Run "mix help deps" to learn about dependencies.
 defp deps do
  [
   {:benchee, "~> 1.0", only: :dev},
   # {:dep_from_hexpm, "~> 0.3.0"},
   # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: ...}
  ]
 end

Ezután letöltjük és lefordítjuk az új modult és a függőségeit. Az új modulok az adott projekt részei lesznek: lokálisak, nem globálisak.

~/tmp/fp$ mix do deps.get + deps.compile
Resolving Hex dependencies...
Resolution completed in 0.051s
New:
  benchee 1.4.0
  deep_merge 1.0.0
  statistex 1.1.0
...
Compiling ...

Fordítás és futtatás mix-szel

Tegyük a lib/sum.ex fájlba egy egészlista összegét kiszámoló függvény három változatát (a különbségüket a Rekurzió fejezet tárgyalja):

defmodule Sum do
  def sum1([]), do: 0
  def sum1([x|xs]), do: x + sum1(xs)

  def sum2([x|xs]), do: x + sum2(xs)
  def sum2([]), do: 0

  def sum3(xs), do: sumi(xs, 0)

  defp sumi([x|xs], sum), do: sumi(xs, sum+x)
  defp sumi([], sum), do: sum
end

# A fájl végére írt kifejezéseket az iex automatikusan ki fogja értékelni
1..1000 |> Range.to_list() |> Sum.sum1() |> IO.inspect()
1..1000 |> Range.to_list() |> Sum.sum2() |> IO.inspect()
1..1000 |> Range.to_list() |> Sum.sum3() |> IO.inspect()
  • Fordítani a mix compile-lal lehet. A lefordított fájl a _build/dev/lib/fp/ebin/ mappába kerül, a sum.ex esetében Elixir.Sum.beam néven.

  • Az iex-et a mix-projekt konfigurációjával és függőségeivel így indítjuk (lásd mix help, elixir --help):

    iex -S mix # Starts IEx and runs the default task
    
  • A programot az r paranccsal (pontosabban az r segédfüggvénnyel) lehet betölteni, újratölteni:

    iex> r Sum
    
  • A Sum modulban definiált függvények ezután hívhatók:

    iex> Sum.sum1 [1,2,3,4,5]
    15
    

A modult záró end után álló függvényhívásokat az iex betöltéskor kiértékeli; az IO.inspect ezek eredményét ki is írja. Ha újratöltjük a programot az r segédfüggvénnyel, az eredmény megjelenik a képernyőn:

iex> r Sum
...
500500
{:reloaded, [Sum]}

Mérés és profilozás: benchee

A méréshez a Benchee.run/1 függvényt kell meghívni egy fájlban, például a benchee_sum.exs-ben. A paramétere egy szótár: a kulcsok a mérések nevei, az értékek névtelen függvények, amelyek törzsében a mérendő hívás áll.

Benchee.run(%{"sum1" => fn -> 1..10_000 |> Enum.to_list() |> Sum.sum1() end,
              ...
              }
           )

Az elemzést a mix run lib/benchee_sum.exs paranccsal indítjuk. Ha azt is tudni szeretnénk, hogy a függvényeink által meghívott függvények milyen gyakran és mennyi ideig futnak, a profile_after opciót is meg kell adni:

Benchee.run(%{"sum1" => fn -> 1..10_000 |> Enum.to_list() |> Sum.sum1() end,
              ...
             },
             profile_after: true
           )

A Benchee először kiírja a gép és a futtatás adatait (operációs rendszer, processzor, Elixir- és Erlang-verzió, a bemelegítés és a mérés ideje), majd egy táblázatot. Az ips a másodpercenkénti végrehajtások száma (iterations per second, K = ezer), az average, a median és a 99th % a futási idő átlaga, mediánja és 99. percentilise, a deviation a szórás az átlag százalékában. A Comparison rész a leggyorsabbhoz viszonyítja a többit (1.83x slower +51.30 µs). A három sum változat eredményei a Rekurzió fejezetben láthatók.

Livebookban a függőségeket a notebook első cellájában a Mix.install/1 tölti be, például Mix.install([{:benchee, "~> 1.3"}]). Livebook-cellából futtatott mérésnél a Benchee figyelmeztet, hogy a mért függvények kiértékelt (evaluated) és nem lefordított függvények, ezért lassabbak; pontosabb méréshez a hívást modulbeli függvénybe vagy mix run-nal futtatott .exs fájlba érdemes tenni.

Típusellenőrzés: dialyzer

A dialyzer az Erlang/Elixir programok statikus elemzője: a lefordított kódból kikövetkezteti a függvények típusát (success typing), és összeveti a típusspecifikációkkal (@spec, lásd Típusok). Jelzi például, ha egy specifikáció nem felel meg a függvénynek. Elixir-projektben a dialyxir modullal használjuk, amelyet a benchee-hez hasonlóan a függőségek közé kell felvenni:

 # Run "mix help deps" to learn about dependencies.
 defp deps do
  [
   {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
   # {:dep_from_hexpm, "~> 0.3.0"},
   # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: ...}
  ]
 end
~/tmp/fp$ mix do deps.get, deps.compile
Resolving Hex dependencies...
Resolution completed in 0.051s
New:
  dialyxir 1.4.6
  erlex 0.2.7
...
Compiling ...

A projekt forrásfájljainak a lib mappában kell lenniük. Rakjunk ide egy Elixir-programot, például az egyik kisházit, rontsunk el egy-két specifikációt, és dializáljuk!

  • A dializálás a lib mappában lévő összes .ex fájlt vizsgálja.
  • Az első futtatás sokáig tart, mert a dialyzer rengeteg ún. PLT-fájlt (Persistent Lookup Table) telepít a modulokhoz tartozó típusszignatúrákkal.
  • A dializálás a .beam fájlokat elemzi, ezért ha valamelyik forrásfájl megváltozott, az elemzés előtt lefordítja.

Az alábbi sum/1 specifikációja szerint a függvény egészlistát adna vissza, pedig egy számot ad. A dialyzer ezt invalid_contract hibaként jelzi, és megmutatja a kikövetkeztetett típust (success typing) a specifikáció mellett:

@spec sum(xs::[integer()]) :: s::[integer()]
# Az xs számlista összege s
def sum([x|xs]), do: x + sum(xs)
def sum([]), do: 0
~/tmp/fp$ mix dialyzer
lib/sum.ex:2:invalid_contract
The @spec for the function does not match the success typing ...
Function: Sum.sum/1
Success typing: ([number()]) -> number()
But the spec is: (xs::[integer()]) -> s::[integer()]

A dialyzert Livebook-cellában nem lehet futtatni, mert a dialyzer a lefordított BEAM-kódot elemzi, a Livebook pedig nem menti el a BEAM-kódot a háttértárba. Ezért hozzunk létre egy mix-projektet, másoljuk ki az elemzendő programrészeket a Livebook-cellá(k)ból, és mentsük el a projekt lib mappájába egy .ex kiterjesztésű fájlba. Ezután, a dialyxir függőség letöltése és a modulok lefordítása után, futtatható a mix dialyzer (lásd még https://hexdocs.pm/dialyxir/readme.html). A mix parancssoros használatához az Elixirt telepíteni kell a saját gépre, vagy Dockerből kell tudni futtatni.

Forrás: dp26a-fp1ea.pdf (41–49. dia), dp26a-fp3ea.pdf (29–30. dia), dp26a-fp1ea-sum-benchee.livemd, dp26a-fp3gy.livemd, dp26a-fp2gy-megoldasok.livemd

Típusok

Az Elixir erősen típusos nyelv, dinamikus típusellenőrzéssel:

  • erősen típusos: minden értéknek pontosan egy futásidejű típusa van;
  • dinamikus típusellenőrzésű: a típusokat nem kötelező megadni a kódban, és egy változóhoz nem feltétlenül csak egy típus tartozik.

A fontosabb típusok (a felsorolás nem teljes; a dőlt betűs típusok más alaptípusokra épülnek):

ÉrtéktípusokValue types
AtomAtom
Tetszőleges hosszú egész számArbitrary-sized integer (integer)
Lebegőpontos számFloating-point number (float)
FüggvényFunction
TartományRange
Reguláris kifejezésRegular expression (regex)
SztringString
Kollekció-típusokCollection types
EnnesTuple
ListaList
BinárisBinary
SzótárMap
StruktúraStruct

A következő alfejezetek sorra veszik őket.

Típusspecifikáció

Bár a típusokat nem kötelező megadni, dokumentációs céllal típusspecifikáció írható a függvényekhez a @spec attribútummal. Ez javítja a függvény dokumentáltságát és ezáltal az olvashatóságát, továbbá lehetővé teszi, hogy a dialyzer segédprogrammal ellenőrizzük a függvény típushelyességét (lásd Projektek, mérés, típusellenőrzés). Két példa:

  • @spec tl(xs :: [any()]) :: ts :: [any()] | nil: „A tl függvény argumentuma egy xs tetszőleges elemű lista, visszatérési értéke (ts) egy szintén tetszőleges elemű lista vagy nil.”
  • @spec nth(xs :: [any()], n :: integer()) :: r :: any() | nil: „Az nth függvény argumentumai egy xs tetszőleges elemű lista és egy n egész szám, visszatérési értéke (r) vagy egy tetszőleges típusú érték vagy nil.”

A paramétereknek és az eredménynek nevet is adhatunk (xs ::, ts ::); a | két típus unióját jelöli.

A specifikáció mellé a kurzus a #-tel kezdődő deklaratív fejkommentet is elvárja. A fejkomment a bemenő paraméter(ek) és a függvény visszatérési értéke közötti kapcsolatot fejezi ki deklaratív módon, azaz lehetőleg a mi-re, és nem a hogyan-ra ad választ. A specifikációban adott nevekre hivatkozik:

defmodule App3 do
  @spec app(xs :: [integer()], ys :: [integer()]) :: zs :: [integer()]
  # xs és ys listák összefűzöttje zs
  def app([x|xs], ys), do: [x|app(xs, ys)]
  def app([], ys),     do: ys
end

Forrás: dp26a-fp2ea.pdf (4. dia), dp26a-fp1ea.pdf (25. dia), dp26a-fp1gyfel.livemd

Atom, szám, igazságérték

Atom

Az atom olyan konstans, amelynek az értéke maga a neve.

  • Kettősponttal (:) kezdődik.
  • Kezdődhet az angol ábécé nagybetűjével is, kettőspont nélkül, de ez konvenció szerint a modulnevekre van fenntartva.
  • A : után UTF-8 kódolású karaktersorozat, Elixir-operátor vagy sztring állhat.
  • Az UTF-8 kódolású karaktersorozatban betűk, számjegyek és kétféle írásjel (_, @) lehetnek; a karaktersorozat végén általában kérdőjel (?) vagy felkiáltójel (!) is állhat.
  • Saját magát jelöli, nem sztring: egy atom értéke maga a neve.
  • Két azonos nevű atom mindig egyenlő, akárhol is vannak definiálva.
  • Hasonló a Prolog névkonstanshoz (atomhoz).

Példák: :jános, :is_bin?, :vált@2, :<>, :"fun/3", :"éljen soká!", :Éljen_soká!, :"Őrült Űrőr tűrjön", Dp, Gy1.

Szám

Egész (integer):

  • decimális, pl. 1234;
  • hexadecimális, pl. 0xcafe;
  • oktális, pl. 0o765;
  • bináris, pl. 0b1010;
  • tagolható, pl. 123_456_789;
  • korlátlan pontosságú, pl. 123456789012345678901234567890;
  • karakterkód (Unicode codepoint): ha nyomtatható, ?z, ha vezérlő, ?\n. A ?z tehát egész szám, a z karakter kódja (122).

Lebegőpontos (float):

  • pl. 3.14159;
  • vezető nullával, pl. 0.14159 (a tizedespont előtt mindig kell számjegy);
  • exponenssel, pl. 0.2e-22;
  • IEEE 754 szerinti, dupla pontosságú (64 bit, kb. 16 számjegy, max. exponens kb. ).

Igazságérték

Igazságérték, másnéven logikai érték (boolean):

  • Három atomot tekintünk igazságértéknek: :true, :false, :nil.
  • Mindhárom írható kettőspont nélkül is: true, false, nil.
  • A false és a nil hamis, minden más érték (nemcsak a true) igaz.
  • Angolul szokás megkülönböztetni a true-t a truthy-tól (igaznak számító érték), a false-t a falsy-tól (hamisnak számító érték), pl. a JavaScriptben, a Javában, az Elixirben.

A logikai műveleteket a Műveletek és beépített függvények fejezet tárgyalja.

Forrás: dp26a-fp2ea.pdf (5–6., 25. dia)

Függvény

A függvény is érték

A függvény is érték: változóhoz köthető, adatstruktúra eleme lehet, függvény eredménye lehet, paraméterként átadható stb. Azaz a függvény is ún. first class citizen, teljes jogú polgár.

Egy modulban definiált függvényt a & operátorral (capture operator) tehetünk értékké: &Modul.név/aritás. Az így kapott függvényértéket ponttal és zárójelpárral hívjuk. Az infix operátorok is függvények, és prefix helyzetben is alkalmazhatók; az :math Erlang-modul függvényei ugyanígy elérhetők:

iex> fac = &Fpea.fac/1 # &: capture operator
&Fpea.fac/1

iex> fac.(5) # pont és zárójelpár kell, szóközökkel tagolható
120

iex> Kernel.+(3,2) # infix operátor alkalmazása prefix helyzetben
5

iex> fs = [&Kernel.+/2, &*/2, &:math.sin/1] # :math Erlang modul!
[&:erlang.+/2, &:erlang.*/2, &:math.sin/1]

iex> (hd fs).(3,2)
5

iex> (hd tl fs).(3,2)
6

iex> (hd tl tl fs).(:math.pi * 90 / 180)
1.0

A fs lista elemei függvények; a hd fs a lista feje (az összeadás), a hd tl fs a második eleme (a szorzás), a hd tl tl fs a harmadik (a szinusz). Az eredmény kiírásából látszik, hogy az Elixir Kernel.+/2 és */2 operátora valójában az :erlang modul függvénye.

Névtelen függvény

Névtelen (anonim) függvényt az fn paraméterek -> törzs end kifejezéssel definiálunk. Közvetlenül is meghívható, és névhez is köthető; a hívásnál itt is kell a pont:

iex> fn ki -> "Szia, " <> ki <> "!" end # <>: konkatenálás
#Function<44.40011524/1 in :erl_eval.expr/5>

iex> fn ki -> "Szia, "<>ki<>"!" end.("Péter") # pont, zárójel!
"Szia, Péter!"

iex> szia = fn ki -> "Szia, " <> ki <> "!" end
#Function<44.40011524/1 in :erl_eval.expr/5>

iex> szia
#Function<44.40011524/1 in :erl_eval.expr/5>

iex> szia.("Bea")
"Szia, Bea!"

A #Function<...> a függvényérték kiírt alakja; a számok verziónként és futásonként mások.

Függvénydefiníció modulban

Modulban a def publikus, a defp privát, azaz a modulon belül lokális függvényt definiál. A modulban definiált függvény hívásakor a pont nem kell, és az argumentumok körüli zárójel sokszor elhagyható:

def sum_of_squares(a,b), do: sqr(a) + sqr(b)
defp sqr(a), do: a*a # p[rivát], azaz lokális a modulon belül
iex> Fpea.sum_of_squares 3, 4.5
29.25

A függvény típusa: (arg1 típusa, arg2 típusa, ...) :: eredmény típusa. Például a sum_of_squares/2 függvényé: (number, number) :: number.

Paraméter alapértelmezett értéke

Egy függvény egy vagy több paraméterének a \\ jelöléssel alapértelmezett (default) értéket adhatunk. Az ilyen paraméter opcionális, a többi elvárt. Ha egy függvényt

  • a kötelezően (default argumentumok nélkül) elvártnál kevesebb paraméterrel hívunk meg, a hívás meghiúsul;
  • az elvárt számú paraméterrel hívunk meg, az összes opcionális paraméter az alapértelmezett értékét veszi fel;
  • az elvártnál több paraméterrel hívunk meg, az aktuális paraméterek értékét balról jobbra haladva veszik fel az opcionális paraméterek.
def sum_of_sqrs_b5(a, b \\ 5), do: sqr(a) + sqr(b)
iex> Fpea.sum_of_sqrs_b5 3, 4.5
29.25

iex> Fpea.sum_of_sqrs_b5 3
34
def sum_of_sqrs_a6b5(a \\ 6, b \\ 5), do: sqr(a) + sqr(b)
iex> Fpea.sum_of_sqrs_a6b5 3
34

iex> Fpea.sum_of_sqrs_a6b5
61

A sum_of_sqrs_a6b5 3 hívásban az egyetlen aktuális paraméter az első opcionális paraméterhez, a-hoz kerül, b az alapértelmezett 5 lesz: . Paraméter nélkül mindkettő az alapértelmezett: .

Magasabb rendű függvények

Mivel a függvény is érték, átadható egy másik függvénynek. Azt a függvényt, amelynek paramétere vagy eredménye függvény, magasabb rendű függvénynek nevezzük. A ciklusok helyett ezekkel szétválaszthatjuk az adatszerkezet rekurzív bejárását az elemeken elvégzendő műveletektől. Az alábbi L.map/2 egy egyszeresen láncolt listát jár be, a T.map/2 egy bináris fát; mindkettő az f paraméterként kapott függvényt alkalmazza minden elemre:

defmodule L do # Egyszeresen láncolt lista bejárás
  def map([], _f), do: []
  def map([hd|tl], f), do: [f.(hd)|map(tl, f)]
end
defmodule T do # Bináris fa bejárás
  def map(nil, _f), do: nil
  def map({x, left, right}, f), do: {f.(x), map(left, f), map(right, f)}
end

A fát itt {x, left, right} hármasok ábrázolják, az üres fát a nil. Egy alkalmazás:

iex> L.map [1, 2, 3], fn(x) -> 2 * x end
[2, 4, 6]

A magasabb rendű függvények egyre gyakoribbak az objektumorientált nyelvekben is:

  • Java: List.of(1, 2, 3).stream().map(x -> 2 * x).toList()
  • C#: new List<int>{1, 2, 3}.Select(x => 2 * x)

Forrás: dp26a-fp2ea.pdf (7–9. dia), dp26a-fp1ea.pdf (24. dia)

Ennes és tartomány

Ennes (Tuple)

Az ennes rögzített számú, tetszőleges kifejezésből álló, fix sorrendű kollekció. Jelölése kapcsos zárójel, az elemek vesszővel elválasztva. Ennesként tudunk két vagy több értéket paraméterként átadni vagy eredményként visszakapni.

iex> {0x1ff, :erlang, Armstrong, 'Joe'++[0], [], {}}
{511, :erlang, Armstrong, [74, 111, 101, 0], [], {}}

iex> {plus, per, sin} = # mintaillesztések kötésekkel
  {&Kernel.+/2, &//2, &:math.sin/1}
{&:erlang.+/2, &:erlang.//2, &:math.sin/1}

iex> {plus.(3,4), per.(3,4)} # infix volt, prefix lett
{7, 0.75}

iex> sin.(90*:math.pi/180)
1.0

Az első példában a 0x1ff értéke 511, a 'Joe'++[0] pedig a 'Joe' karakterkód-lista és a [0] összefűzése; mivel a 0 nem nyomtatható karakter kódja, a lista számokként jelenik meg (lásd Lista és karakterlánc). A második példa mintaillesztéssel köti a három függvényt a plus, per és sin változóhoz (lásd Mintaillesztés).

Tartomány (Range)

A tartomány egész számok sorozata a [start, end] tartományban, start..end vagy lépésközzel start..end//lépés alakban:

iex> {18..23, 18..10}
{18..23, 18..10//-1}

iex> for i <- 18..10 // -3, do: i
[18, 15, 12]

A 18..10 csökkenő tartomány, az Elixir ki is írja a -1 lépésközt. A második példa a for-jelöléssel sorolja fel a 18..10//-3 tartomány elemeit.

Forrás: dp26a-fp2ea.pdf (10. dia)

Lista és karakterlánc

Lista (List)

A lista korlátlan számú, tetszőleges kifejezésből álló, egyszeresen láncolt sorozat, a deklaratív nyelvek talán legalapvetőbb adatstruktúrája. Lineáris rekurzív adatstruktúra:

  • vagy üres (jele []),
  • vagy egy elemből áll, amelyet egy lista követ: [x|xs].

A lista első eleme, ha van, a lista feje; az első eleme utáni, esetleg üres része a lista farka. Mivel a lista láncolt, csak az első elemét érjük el közvetlenül; az utolsó és bármely közbülső elemét csak úgy, ha előbb az összes előtte álló elemet eltávolítjuk.

Ugyanaz a lista többféleképpen is leírható; a tömörebb változatok könnyebben írhatók és olvashatók. Az 1, 2, 3 egészekből álló lista például [1|[2|[3|[]]]], [1,2|[3]] vagy [1,2,3]. A | előtt több elem is állhat, vesszővel elválasztva:

iex> [:elem] # egyelemű lista
[:elem]

iex> [:elem|[]] # fejből és üres farokból létrehozott lista
[:elem]

iex> [:elem1|[:elem2]] # fejből-farokból létrehozott lista
[:elem1, :elem2]

iex> [:elem,123,3.14,'elem'] # több elemű listák
[:elem, 123, 3.14, ~c"elem"]

iex> [:elem,123|[3.14,'elem']]
[:elem, 123, 3.14, ~c"elem"]

iex> [:egy|[:két]] ++ [:elem,123|[3.14,'elem']] # ++: konkatenáció
[:egy, :két, :elem, 123, 3.14, ~c"elem"]

A listaműveleteket a Műveletek listákon fejezet tárgyalja.

Karakterlánc (single-quoted)

Az aposztrófok közé írt karakterlánc (karakterlista) rövidítés: karakterkódok listája.

'erl' ≡ [?e,?r,?l] ≡ [101,114,108]

Ugyanez a ~c szigillel is írható: ~c"erl". A szigil speciális, „bűvös” jelölés az Elixirben. Az újabb Elixir-verziók a '...' jelölésre figyelmeztetnek, és a karakterlistát maguk is ~c"..." alakban írják ki.

Az Elixir/Erlang-héj a nyomtatható karakterkódokból (7..13, 27, 32..126) álló listát karakterláncként írja ki. Ha ezektől különböző érték is van a listában, listaként, számokkal írja ki:

iex> [101,114,108]
~c"erl"

iex> [31,101,114,108]
[31, 101, 114, 108]

iex> 'erl' ++ 'ang' # konkatenálható
~c"erlang"

Important

A karakterlánc NEM sztring! A 'abc' egy lista, a "abc" sztring (bináris); a lista- és a sztringműveletek különbözőek (lásd Sztring és bináris).

A kiírás tehát csak megjelenítés: a lista mindkét esetben számokból áll. Például a [0 | ~c"abc"] kifejezés eredménye [0, 97, 98, 99], mert a 0 nem nyomtatható karakter kódja, ezért az egész lista számokként jelenik meg. Egy mintaillesztés után a farka, ~c"bc", már megint karakterláncként látszik:

[_,_|xs] = [0 | ~c"abc"] |> IO.inspect
xs
[0, 97, 98, 99]
~c"bc"

Az Á kódja 193, ami kívül esik a 32..126 tartományon, ezért a ~c"Ábc" is számokkal jelenik meg ([193, 98, 99]), a farka (~c"bc") viszont nem.

A karakterlisták kiírása

Az inspect függvények kiírása a charlists: opcióval szabályozható: a charlists: :as_lists a lista elemeit számként, a charlists: :as_charlists karakterként jeleníti meg. Az alábbi példák az App3.app/2 listaösszefűző függvényt használják (Típusok):

IO.inspect(App3.app([], []))
IO.inspect(App3.app([5, 6, 7], [1, 2, 3]))
IO.inspect(App3.app([7, 10, 12], [97, 98, 99])) # Ha kiírható a karakterkód, akkor úgy is látjuk
[]
[5, 6, 7, 1, 2, 3]
~c"\a\n\fabc"
IO.inspect(App3.app([7, 10, 12], [97, 98, 99]), charlists: :as_lists) # Ha számként szeretnénk látni
[7, 10, 12, 97, 98, 99]

Az IO.inspect/2 kiírja és változatlanul továbbadja az értéket, az inspect/2 sztringgé alakítja, az IO.puts/1 pedig a sztringet idézőjelek nélkül írja ki:

App3.app([7, 10, 12], [97, 98, 99]) |> IO.inspect(charlists: :as_lists)
App3.app([7, 10, 12], [97, 98, 99]) |> inspect(charlists: :as_lists)
App3.app([7, 10, 12], [97, 98, 99]) |> inspect(charlists: :as_charlists)
App3.app([7, 10, 12], [97, 98, 99]) |> inspect(charlists: :as_charlists) |> IO.puts()

A négy kifejezés eredménye rendre a [7, 10, 12, 97, 98, 99] lista, a "[7, 10, 12, 97, 98, 99]" sztring, a "~c\"\\a\\n\\fabc\"" sztring, végül az IO.puts/1 kiírja a ~c"\a\n\fabc" szöveget, és :ok-t ad vissza.

A 0..127 tartományba eső ASCII-kódú karakterek megjelenési formáját így nézhetjük meg:

(for code <- 0..127, do: code) |> IO.inspect(charlists: :as_charlists)
[127] |> IO.inspect(charlists: :as_charlists)
~c"\0\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\e\x1C\x1D\x1E\x1F !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\d"
~c"\d"

A 7..13, 27 és 32..126 tartományon kívül eső kódú karakterek hexadecimális kódjukkal (\x01) vagy saját escape-szekvenciájukkal (\0, \d) jelennek meg. A nyomtatható karakterek teljes listáját a Műveletek listákon fejezet is bemutatja.

Forrás: dp26a-fp2ea.pdf (11–12. dia), dp26a-fp1gyfel.livemd

Sztring és bináris

Sztring (String, double quoted)

Az idézőjelek közé írt sztring UTF-8 kódolású karakterek ábrázolása bájtok sorozataként, azaz bináris típusú érték. Ennek két következménye van:

  • az UTF-8 kódolás miatt a sztring (karakterekben mért) hossza kisebb lehet az őt ábrázoló bináris (bájtokban mért) hosszánál;
  • a lista- és a sztringműveletek különbözőek.
iex> dxdy = "δx/δy"
"δx/δy"

iex> {String.length(dxdy), byte_size(dxdy)}
{5, 7}

iex> {String.at(dxdy,0), String.codepoints(dxdy)}
{"δ", ["δ", "x", "/", "δ", "y"]}

iex> [dx, dy] = String.split(dxdy, "/")
["δx", "δy"]

iex> dx <> "/" <> dy # <>: konkatenálás
"δx/δy"

A δ két bájton tárolódik, ezért az öt karakterből álló sztring hét bájt. A String modul függvényeit a Műveletek sztringeken fejezet tárgyalja.

Ami közös a karakterláncban és a sztringben

A karakterlánc és a sztring is UTF-8 kódolású karakterekből áll, és mindkettőben lehetnek ún. escape-szekvenciák:

\aBEL (0x07)\bBS (0x08)\dDEL (0x7f)
\eESC (0x1b)\fFF (0x0c)\nNL (0x0a)
\rCR (0x0d)\sSP (0x20)\tTAB (0x09)
\vVT (0x0b)\uhhhhUnicode codepoint in hexadecimal
\xhhsingle byte in hexadecimal

Néhány karakter speciális jelentését az elé írt \ megszünteti, pl. \\ maga a visszaper-jel.

Mindkettő megengedi az ún. interpolációt, azaz egy kifejezés helyettesítését az értékével: "...#{<expr>}...".

iex> name = "dávid" # Sztring
"dávid"

iex> "Helló, #{String.capitalize name}!"
"Helló, Dávid!"

iex> bubo = 'Bubo' # Karakterlánc
~c"Bubo"

iex> "Helló, #{List.to_string [bubo, ? , "Réka"]}!"
"Helló, Bubo Réka!"

Az utolsó példában a ? (kérdőjel és szóköz) a szóköz karakter kódja; a List.to_string/1 a karakterláncból, a kódból és a sztringből álló listát egyetlen sztringgé alakítja.

Bináris (Binary)

A bináris típusba tartozó értékek bitsorozatok. Egy bináris érték jelölése << kif, ... >> alakú. A legegyszerűbb kif a [0,255] tartományba eső egész szám; a számokat bájtként tároljuk a binárisban:

iex> b = << 1, 2, 3 >>
<<1, 2, 3>>

iex> {byte_size(b), bit_size b}
{3, 24}

A tárolásra használt bitek száma a ::size(n) módosítóval megszabható. Az alábbi példában az 1 két biten (01), a második 1 három biten (001) tárolódik, együtt az öt bites 01001, ami decimálisan 9:

iex> b = << 1::size(2), 1::size(3) >> # 01 001
<<9::size(5)>> # = 9 (decimálisként)

iex> {byte_size(b), bit_size b}
{1, 5}

Egész és lebegőpontos számok és más értékek is tárolhatók binárisan. Itt a <<2.5>> a 2.5 lebegőpontos szám 64 bites ábrázolása:

iex> << <<1>> :: binary, <<2.5>> :: binary >>
<<1, 64, 4, 0, 0, 0, 0, 0, 0>>

A bináris tárolás hasznos médiafájlok és UTF-8 karakterek tárolására, processzek közötti kommunikációban stb.

Forrás: dp26a-fp2ea.pdf (13–15. dia)

Kulcs-érték lista és szótár

Kulcs-érték lista (Keyword list)

Egy kulcs-érték pár kételemű ennesként írható le: {:key, value}, ahol a kulcs csak atom, az érték tetszőleges típusú lehet. Az ilyen párokból álló listára gyakran van szükség, ezért az Elixir többféle jelölést, rövidítést, bizonyos esetekben zárójelelhagyást is megenged: [{:név, "Szöszi"}] helyett írható [név: "Szöszi"], és ha a kulcs-érték lista egy függvényhívás vagy egy ennes utolsó eleme, a szögletes zárójel is elhagyható.

iex> [{:név,"Szöszi"},{:szerelme,"jazz-zongorista"},{:város,"Prága"}]
[név: "Szöszi", szerelme: "jazz-zongorista", város: "Prága"]

iex> [név: "Szöszi", szerelme: "jazz-zongorista", város: "Prága"]
[név: "Szöszi", szerelme: "jazz-zongorista", város: "Prága"]

iex> inspect név: "Szöszi", szerelme: "jazz-zongorista", város: "Prága"
"[név: \"Szöszi\", szerelme: \"jazz-zongorista\", város: \"Prága\"]"

iex> [:cseh_film, név: "Szöszi", város: "Prága", szerelme: "zongorista"]
[:cseh_film, {:név, "Szöszi"}, {:város, "Prága"}, {:szerelme, "zongorista"}]

iex> {:cseh_film, név: "Szöszi", szerelme: "zongorista", város: "Prága"}
{:cseh_film, [név: "Szöszi", szerelme: "zongorista", város: "Prága"]}

A negyedik példában a lista első eleme atom, nem pár, ezért ez már nem kulcs-érték lista, és az Elixir a párokat ennesként írja ki. Az ötödikben a zárójel nélküli kulcs-érték lista az ennes második eleme lesz.

A kulcs-érték párokat leginkább függvényopciók megadására használjuk, pl. limit: :infinity, charlists: :as_lists.

Szótár (Map)

A szótár kulcs-érték párok rendezett kollekciója.

  • Jelölése (map literal): %{ key1 => value1, key2 => value2, ...}.
  • Ha a kulcs atom, alternatív jelölés: %{atom1: value1, atom2: value2}.
  • A kulcsok és az értékek típusa tetszőleges, lehet kifejezés is; egy szótáron belül a kulcsok különböző típusúak lehetnek.
iex> states = %{"UA"=>"Ukraine", "SK"=>"Slovakia", "AT"=>"Austria"}
%{"AT" => "Austria", "SK" => "Slovakia", "UA" => "Ukraine"}

iex> msgs = %{{:error,:enoent} => :fatal, {:error,:busy} => :retry}
%{{:error, :busy} => :retry, {:error, :enoent} => :fatal}

iex> colors = %{:red=>0xff0000, :green=>0x00ff00, :blue=>0x0000ff}
%{green: 65280, red: 16711680, blue: 255}

iex> colors = %{red: 0xff0000, green: 0x00ff00, blue: 0x0000ff}
%{green: 65280, red: 16711680, blue: 255}

iex> mix = %{(&+/2).(3,2) => "három+kettő", fütty: "dal"<>"olka"}
%{5 => "három+kettő", :fütty => "dalolka"}

Az utolsó példában a (&+/2).(3,2) kulcskifejezés értéke 5. Ha a kulcsok vegyesen atomok és más értékek, az atom kulcsú párokat is ki lehet írni rövid alakban, de csak a lista végén (fütty: ...).

A szótárt elsősorban asszociatív tömbként szokás használni. Értéket a kulccsal, szögletes zárójeles jelöléssel nyerünk ki belőle; ha nincs ilyen kulcs, az eredmény nil. Ha a kulcs atom, a rövidebb pontos jelölés is használható:

iex> states["UA"]
"Ukraine"

iex> states["HU"]
nil

iex> msgs[{:error, :busy}]
:retry

iex> colors[:green]
65280

iex> colors.red
16711680

iex> mix[(&Kernel.*/2).(1,5)]
"három+kettő"

iex> mix.fütty
"dalolka"

További részletek a Map modul dokumentációjában találhatók.

Forrás: dp26a-fp2ea.pdf (16–18. dia)

Reguláris kifejezés

Az Elixirben a reguláris kifejezés (Regex) is önálló típus. Jelölése ~r{regexp} vagy ~r{regexp}options. A ~r{...} jelölés is szigil, azaz bűvös jelölés; a szigilekről részletek a Kernel dokumentációjában találhatók. A reguláris kifejezés szintaxisa a PCRE (Perl Compatible Regular Expressions, http://www.pcre.org) szerinti.

A Regex.run/2 az első illeszkedést adja vissza, a Regex.scan/2 az összeset, a Regex.split/2 az illeszkedések mentén darabolja a sztringet, a Regex.replace/3 pedig lecseréli az illeszkedő részeket:

iex> Regex.run ~r{[cdr]}, "madárcsicsergés"
["d"]

iex> Regex.scan ~r{[cdr]}, "madárcsicsergés"
[["d"], ["r"], ["c"], ["c"], ["r"]]

iex> Regex.split ~r{[cdr]}, "madárcsicsergés"
["ma", "á", "", "si", "se", "gés"]

iex> Regex.replace ~r{[cdr]}, "madárcsicsergés", "."
"ma.á..si.se.gés"

A regexp után egy vagy több egykarakteres opció állhat:

JelJelentés
fTöbbsoros sztring első sorában kezdődjön az illesztés
iAz illesztés ne különböztesse meg a kis- és nagybetűket
mTöbbsoros sztring esetén a ^ és a $ az egyes sorok elejét és végét jelentse (a \A és \z jelentése változatlanul a sztring eleje és vége)
sA . illeszkedjen az újsor-karakterekre is
UAz egyébként mohó * és + módosítók legyenek lusták, azaz a minta a lehető leghosszabb karaktersorozat helyett a lehető legrövidebbre illeszkedjen
uEngedje meg Unicode-specifikus minták, pl. \p használatát
xEngedje meg a bővített mód használatát: ignorálja a szóköz-jellegű (ún. whitespace) karaktereket és a kommenteket (a # jeltől a sor végéig)

Az alábbi példákban a cs.*s minta cs-vel kezdődő, s-re végződő részt keres. Opció nélkül csak a kisbetűs cs-re illeszkedik, és a mohó .* a lehető leghosszabb részt veszi; az i opcióval a nagybetűs Cs is illeszkedik; az U opcióval a .* lusta lesz, így a legrövidebb illeszkedést kapjuk:

iex> Regex.run ~r{cs.*s}, "Madarak Csicsergése"
["csergés"]

iex> Regex.run ~r{cs.*s}i, "Madarak Csicsergése"
["Csicsergés"]

iex> Regex.run ~r{cs.*s}iU, "Madarak Csicsergése"
["Csics"]

További részletek a Regex modul dokumentációjában találhatók.

Forrás: dp26a-fp2ea.pdf (19–20. dia)

Termek, azonosítók, változók

Term

A term tetszőleges adatstruktúra. Minden termnek van értéke és típusa, és a term maga is kifejezés. Közelítő rekurzív definíciója: szám-, atom-, függvény- és más értékekből, illetve termekből konstruktorokkal felépített, tovább nem egyszerűsíthető kifejezés.

A term tehát tovább nem egyszerűsíthető és tömör: kiértékelhető, azaz nincs benne szabad változó. Ha a kötött változó értéke már kötött = 2021, akkor az alábbiak termek:

123456789
{'Diák Detti', [{:khf, [:prolog, :elixir, :prolog]}]}
[&:erlang.+/2, kötött, fn(x,y) -> x*y end]

Nem termek viszont azok a kifejezések, amelyek tovább egyszerűsíthetők vagy nem tömörek:

5+6                   # műveletet tartalmaz
(&:erlang.+/2).(5,6) # függvényalkalmazást tartalmaz
szabad                # szabad változó

Azonosító (identifier)

Az azonosító kisbetűvel vagy aláhúzásjellel (_) kezdődő, betűket, számjegyeket és aláhúzásjeleket tartalmazó, opcionálisan kérdő- vagy felkiáltójellel végződő karaktersorozat. A betű és a számjegy UTF-8 kódolású betű, illetve decimális számjegy lehet (lásd https://hexdocs.pm/elixir/unicode-syntax.html). Az azonosító változót vagy függvénynevet jelöl.

Konvenciók:

  • a ?-lel végződő azonosító kiértékelése igazságértéket ad eredményül;
  • a !-lel végződő azonosító kiértékelése kivételt dob, ha meghiúsul;
  • az azonosító részeit aláhúzásjellel tagoljuk (ún. megengedő snake_case; vö. az atom szintaxisával).
what_s_in_a_name   name?   exec!
_unused   rómeó_és_Júlia   year_2021

Változó

Egy változó lehet szabad vagy kötött. A szabad változónak nincs értéke és típusa; a kötött változó valamely konkrét term szinonimája.

A változóhoz új érték köthető, de ez a korábbi felhasználását nem módosítja: a korábban kiértékelt kifejezések a régi értéket használták. A ^ (pin) operátor a kötött változó értékét fixálja: így a változó nem köthető új értékhez, hanem a mintaillesztésben a meglévő értékével vesz részt.

iex> x = fn(x) -> 2*x end # a külső és a belső x nem ugyanaz!
#Function<44.40011524/1 in :erl_eval.expr/5>

iex> y = x
#Function<44.40011524/1 in :erl_eval.expr/5>

iex> ^x = y.(2)
** (MatchError) no match of right hand side value: 4
iex> x = y.(2)
4

iex> y
#Function<44.40011524/1 in :erl_eval.expr/5>

A ^x = y.(2) meghiúsul, mert x értéke itt a függvény, nem a 4. Az x = y.(2) után x új értéke 4, de y továbbra is a függvényt jelöli, mert a kötésekor x még a függvény volt.

Változó hatásköre

A változók hatásköre lexikális:

  • A függvény törzsében és fejében definiált változók (az utóbbiak másnéven a formális paraméterek) lokálisak a függvényre nézve.
  • Modulban is lehet változót definiálni, de az csak modulszinten látható, a modulban definiált függvényekből nem.
  • A with kifejezéssel is definiálhatunk lokális változót: a with után kötött változók csak a do: utáni kifejezésben látszanak.
iex> with a = 5, b = 7, do: a*a + 2*a*b + b*b
144

iex> a = 11; with a = 5, b = 7, do: a*a + 2*a*b + b*b; a
11

A második példában a with-en belüli a = 5 nem változtatja meg a külső a értékét, amely 11 marad. A ; egy sorban választ el kifejezéseket.

Komment

A komment # jellel kezdődik, és a sor végéig tart.

Forrás: dp26a-fp2ea.pdf (22–25. dia)

Műveletek és beépített függvények

Aritmetikai és bitműveletek

Aritmetikai műveletek (a Kernel modulban), a precedencia szerint csoportosítva (kisebb szám: erősebb kötés):

  • előjel: +, - (precedencia: 1);
  • multiplikatív műveletek: *, /, div, rem (precedencia: 2);
  • additív műveletek: +, - (precedencia: 3).

Bitműveletek (a Bitwise modulban):

  • bnot vagy ~~~, band vagy &&& (precedencia: 2);
  • bor vagy |||, bxor, bsl vagy <<<, bsr vagy >>> (precedencia: 3).

Szabályok:

  • +, -, * és / egész és lebegőpontos operandusokra is alkalmazhatók;
  • +, - és * eredménye egész, ha mindkét operandusuk egész, egyébként lebegőpontos;
  • / eredménye mindig lebegőpontos;
  • div (egészosztás) és rem (maradék) prefix helyzetűek, például div(7, 2); eredményük egész;
  • div, rem és a bitműveletek operandusai csak egészek lehetnek;
  • ~~~, &&&, |||, <<<, >>> infix, a többi bitművelet prefix helyzetű.

A bitműveleteket használat előtt importálni kell:

import Bitwise

Ekkor például 5 &&& 3 értéke 1, bnot(5) értéke -6, 1 <<< 4 értéke 16. A régebbi anyagokban szereplő use Bitwise[, (only_operators | skip_operators): true] alak az újabb Elixir-verziókban elavult, figyelmeztetést ad.

Összehasonlító műveletek (relációk)

Egy reláció (összehasonlítás) eredménye a true vagy a false atom.

  • Kisebb, kisebb-egyenlő, nagyobb-egyenlő, nagyobb: <, <=, >=, >.
  • Érték szerinti egyenlőség (integer és float lehet egyenlő): ==, !=.
  • Szigorú egyenlőség (integer és float nem lehet egyenlő): ===, !==.

Például 5.0 == 5 értéke true, 5.0 === 5 értéke false.

Különböző típusú termek is összehasonlíthatók. A típusok sorrendje (vö. Típusok):

number < atom < reference < function < port < pid < tuple < map < list < binary

Lebegőpontos értékek összehasonlítása

Elrettentő példák: a lebegőpontos számok kettes számrendszerben csak közelítőleg ábrázolhatók, ezért az egyenlőségvizsgálat meglepő eredményt adhat.

iex> 10.1 - 9.9 == 0.2
false

iex> (10.1 - 9.9) * 10
1.999999999999993

iex> 0.000000000000001 + 1 == 1
false

iex> 0.0000000000000001 + 1 == 1
true

Warning

Lebegőpontos értékek egyenlőségének vizsgálata helyett vizsgáljuk a különbségüket a <= vagy >= relációval: ε-nál kisebb-e a különbségük?

Logikai műveletek

  • Prefix helyzetű operátor: not és !. A not operandusa csak boolean lehet, a !-é tetszőleges típusú kifejezés.
  • Infix helyzetű operátorok: and és &&, or és ||.
    • not, and és or használható őrkifejezésben (lásd Mintaillesztés), !, && és || nem.
    • and és or első operandusa csak boolean lehet, && és || első operandusa tetszőleges típusú kifejezés.
    • A false és nil értékű kifejezéseket kivéve minden más érték true-nak számít.
    • A második operandusuk tetszőleges típusú kifejezés lehet.
    • Eredményük típusa a két operandus típusának uniója.
    • Lusta kiértékelésű, ún. short-circuit műveletek: ha az első operandus kiértékelése eldönti az eredményt, a másodikra nem kerül sor.
iex> !:atom && div(3,0) === 2
false

iex> :atom && div(3,0) === 2
** ...bad argument in ...: div(3, 0)
iex> :atom and rem(3,2) === 1
** ...expected a boolean on left-side of "and"
iex> true and rem(3,2)
1

iex> false and rem(3,2)
false

iex> nil && rem(3,2)
nil

Az első példában a !:atom értéke false, ezért a && a második operandust, a nullával való osztást ki sem értékeli. A másodikban :atom igaznak számít, így az osztásra sor kerül, és hibát ad. A harmadikban az and első operandusa nem boolean, ez hiba. A true and rem(3,2) eredménye a második operandus értéke, 1, tehát nem feltétlenül boolean.

Beépített függvények (Built-In Functions, BIFs)

A BIF-ek a BEAM-be beépített, rendszerint C-ben írt függvények. Többségük az erts Erlang-könyvtár erlang moduljának része; az Elixir-specifikációjuk az Elixir Kernel moduljában található. A csak az Erlang erlang moduljában definiált BIF-ek az :erlang modulnévvel hívhatók.

Az alaptípusokon alkalmazható leggyakoribb BIF-ek:

  • Számok: abs(num), trunc(num), ceil(num), floor(num), round(num), :erlang.float(num). Az :erlang.float helyett 1-gyel is oszthatjuk az egész számot, pl. 5/1.
  • Sztring, bináris: bit_size(string), byte_size(string).
  • Szótár: map_size(map).
  • Ennes: tuple_size(tuple), elem(tuple, index), put_elem(tuple, index, value), ahol 0 ≤ index ≤ tuple_size(tuple)-1.
  • Lista: length(list), hd(list), tl(list).

Az operátorok is BIF-ek a Kernel-ben, például Kernel.*(3,4).

Típusvizsgálat és típuskonverzió

Típusvizsgálat (BIF-ek a Kernel-ben):

  • is_integer(term), is_float(term), is_number(term),
  • is_atom(term), is_boolean(term), is_nil(term),
  • is_binary(term), is_bitstring(term),
  • is_tuple(term), is_list(term), is_map(term),
  • is_function(term), is_function(term, arity).

Típuskonverzió (az egyes típusokhoz tartozó modulokban, pl. Atom.to_string/1):

  • Atom: to_charlist(atom), to_string(atom);
  • Float: to_charlist(float), to_string(float);
  • Integer: to_charlist(integer), to_string(integer);
  • List: to_atom(list), to_charlist(list), to_float(list), to_integer(list), to_integer(list, base), to_string(list), to_tuple(list);
  • String: to_atom(string), to_charlist(string), to_float(string), to_integer(string), to_integer(string, base);
  • Tuple: to_list(tuple);
  • Map: to_list(map).

Forrás: dp26a-fp3ea.pdf (17–21. dia)

Mintaillesztés

Az Elixir – más funkcionális, illetve deklaratív nyelvekhez hasonlóan – mintaillesztést (pattern matching) használ a különféle esetek felismerésére és szétválasztására, valamint változók értékhez kötésére.

Minta és mintaillesztés

A mintakifejezés, röviden minta, termhez hasonló olyan kifejezés, amelyben nincs függvénykifejezés, de lehet benne szabad változó (a term és a szabad változó fogalmát lásd: Termek, azonosítók, változók).

  • A konstansok csak velük azonos értékre illeszkednek, pl. 123, [], [1,2,3], ~c"abc", "abc", :eof, nil, true.
  • A kötött, azaz értékkel rendelkező változók csak velük azonos értékre illeszkednek.
  • Egy szabad (kötetlen), azaz értékkel nem rendelkező változó mindenre illeszkedik, és sikeres illesztéskor az illeszkedő kifejezés megfelelő részéhez kötjük; ezután hivatkozhatunk rá.
  • Az aláhúzásjel (_, a névtelen változó) és az aláhúzásjellel kezdődő változónév is mindenre illeszkedik. Az előbbire nem lehet, az utóbbira nem szokás hivatkozni.
  • Egy mintában ugyanaz a változó többször is előfordulhat, ha mindenütt azonos értékre kell illeszkednie.
  • Kifejezés nem lehet minta, pl. 1+x, 1+2, round(x), round(5.3).

A mintaillesztés műveleti jele az =, a bal oldalán a mintával, a jobb oldalán egy tömör kifejezéssel: a mintaillesztés egyirányú. A mintaillesztés a minta nem fixált (vö. ^ operátor) változóit értékhez köti.

Important

A kötés nem értékadás! Az = nem írja felül a változó értékét egy tárolóban, hanem a bal oldali mintát illeszti a jobb oldali értékre, és a minta szabad változóit a megfelelő részekhez köti.

Változóhoz kétféleképpen köthetünk értéket:

  • az = mintaillesztés- vagy kötésoperátorral,
  • paraméterátadással függvényhíváskor: az aktuális paramétereket illesztjük a formális paraméterekre az egyes klózokban.

Ha egy változót értékhez kötünk, de nem használjuk, az Elixir figyelmeztet rá, kivéve akkor, ha a változónév _-sal kezdődik.

A Prologban – a funkcionális nyelvekkel ellentétben – kétirányú mintaillesztés van, ennek egyesítés a neve.

Néhány egyszerű eset:

  • x = 5 az x mintát illeszti az 5 kifejezésre, x értéke 5 lesz.
  • [hd|tl] = valami() a hd és tl változókat a valami() által visszaadott lista fejéhez és farkához köti. Ha a valami() üres listával (vagy bármi mással) tér vissza, a kiértékelés hibával leáll: (MatchError) no match of right hand side value.
  • Az [x | xs] minta a legalább egyelemű listákra illeszkedik: x a lista fejének, xs a farkának az értékét veszi fel; az utóbbi lehet üres is.
  • Az [x1, x2 | xs] minta a legalább kételemű listákra illeszkedik: x1 a lista első, x2 a második elemének, xs a farkának az értékét veszi fel.
  • Az [_|xs] minta is a legalább egyelemű listákra illeszkedik, de a lista fejét a névtelen változóhoz köti, azaz eldobja.
  • Az [_,_|_] minta a legalább kételemű listákra illeszkedik, de az elemek értékével nem kezd semmit.
iex> 123 = 123
123

iex> 123 = 321
** (MatchError) no match of right hand side value: 321
iex> ~c"abc" = [97, 98, 99]
~c"abc"

iex> :eof = :eof
:eof

iex> nil = nil
nil

Listamintákkal:

[x|xs] = ~c"abc" |> IO.inspect
{x, xs}
~c"abc"
{97, ~c"bc"}
[_,_|xs] = ~c"abc" |> IO.inspect
xs
~c"abc"
~c"c"
[_|xs] = ~c"Ábc" |> IO.inspect
xs
[193, 98, 99]
~c"bc"

(Hogy miért jelenik meg az egyik lista karakterekkel, a másik számokkal, azt a Lista és karakterlánc fejezet magyarázza.)

Kifejezést tartalmazó minta fordítási hibát ad:

defmodule BadPattern do
  def fun(x < 0), do: x
end
defmodule BadPattern do
  def fun(round(x)), do: x
end
1+2 = 1+2

Mindhárom esetben a hibaüzenet: cannot invoke remote function ... inside a match (az :erlang.</2, az :erlang.round/1, illetve az :erlang.+/2 függvényre).

Példák mintaillesztésre

Függvényérték nem lehet minta, és a & sem állhat mintában; szabad változó viszont illeszkedhet függvényértékre. Szabad változó a jobb oldalon nem állhat:

iex> [x, &+/2] = [5, &+/2]
** (CompileError) ... & is not allowed in matches
iex> [x, f] = [5, &+/2]
[5, &:erlang.+/2]

iex> [x, f] = [5, f]
[5, &:erlang.+/2]

iex> a = fn(x) -> x+1 end
#Function<44.40011524/1 in :erl_eval.expr/5>

iex> {a, b} = {fn(x) -> x+1 end, 23}
{#Function<44.40011524/1 in :erl_eval.expr/5>, 23}

iex> fn(x) -> x+1 end = a
** (CompileError) ... fn is not allowed in matches
iex> 3 = szabad
** (CompileError) iex:505: undefined function szabad/0

A [x, f] = [5, f] sikerül, mert a jobb oldalon f már kötött, az értéke a függvény. Listamintákkal:

iex> [z | zs] = [0,1,2,3]
[0, 1, 2, 3]

iex> [z1 | [z2 | [z3 | [z4 | zs]]]] = [0,1,2,3]
[0, 1, 2, 3]

iex> [z1, z2, z3 | [3]] = [0,1,2,3]
[0, 1, 2, 3]

iex> [z1, z2, z3 | 3] = [0,1,2,3]
** (MatchError) no match of right hand side value: [0, 1, 2, 3]
iex> [z1, z2 | [3]] = [0,1,2,3]
** (MatchError) no match of right hand side value: [0, 1, 2, 3]

A [z1, z2, z3 | 3] minta azért nem illeszkedik, mert a negyedik elem utáni farok nem a 3 szám, hanem a [3] lista; a [z1, z2 | [3]] pedig csak háromelemű listára illeszkedhetne.

Ennesmintákkal; ugyanaz a változó többször is előfordulhat a mintában:

iex> {{a, b}, {a, b}} = {{:a, :b}, {:a, :b}}
{{:a, :b}, {:a, :b}}

iex> {{a, b, a}} = {{:a, :b, :b}}
** (MatchError) no match of right hand side value: {{:a, :b, :b}}
iex> {a, b, _b, _} = {:a, :b, :b, :a}
{:a, :b, :b, :a}

iex> {a, b}
{:a, :b}

iex> _b
warning: the underscored variable "_b" is used after being set...
please rename the variable to remove the underscore
:b

Az _b kötött ugyan, de mivel aláhúzásjellel kezdődik, a rá való hivatkozásra az Elixir figyelmeztet.

Szótármintákkal. A szótárminta részleges: elég, ha a mintában szereplő kulcsok megvannak a szótárban. Kulcs helyén szabad változó nem állhat. A ^ fixálja a változó értékét:

iex> x = %{b: "barna", z: "zöld"}
%{b: "barna", z: "zöld"}

iex> %{k1: v1, k2: v2} = x
** (MatchError) no match of right hand side value: %{b: "barna", z: "zöld"}
iex> %{k1 => v1, k2 => v2} = x
** (CompileError) iex:3: cannot use variable k1 as map key inside a pattern...
iex> %{b: v1, z: v2} = x
%{b: "barna", z: "zöld"}

iex> {v1, v2}
{"barna", "zöld"}

iex> %{z: ^v1, b: v2} = x
** (MatchError) no match of right hand side value: %{b: "barna", z: "zöld"}
iex> %{z: v1, b: v2} = x
%{b: "barna", z: "zöld"}

iex> {v1, v2}
{"zöld", "barna"}

iex> %{b: v} = x # részleges mintaillesztés
%{b: "barna", z: "zöld"}

iex> v
"barna"

A %{z: ^v1, b: v2} = x azért hiúsul meg, mert v1 értéke ekkor "barna", a :z kulcshoz tartozó érték pedig "zöld".

Réteges minta

A réteges minta (layered pattern) minta = változó alakú: a változó a teljes illesztett értékhez kötődik, a minta pedig a részeihez. Így egyszerre hivatkozhatunk az egészre és a komponenseire:

iex> ([y|ys] = yss) = [1, 2, 3] # réteges minta (layered pattern)
[1, 2, 3]

iex> {y, ys, yss}
{1, [2, 3], [1, 2, 3]}

Függvény paraméterében is használható. Az alábbi [z | zs] = zzs mintával a klóz törzsében a teljes listára zzs-sel, a fejére z-vel, a farkára zs-sel hivatkozhatunk:

  def app([z | zs] = zzs, ys) do
    [z | app(zs, ys)]
  end

A réteges minta további haszna a Klózok szakaszban látható.

Mintaillesztés case kifejezéssel

A case kifejezés a kapott értéket sorban illeszti az ágak mintáira, és az első illeszkedő ág kifejezését értékeli ki:

case valami() do
  [hd|tl] -> ... # Ha nem üres lista
  [] -> ... # Ha üres lista
  _ -> ... # Bármi más
end

Őr

A mintában csak tömör, azaz kiértékelhető kifejezés lehet, változót tartalmazó kifejezés nem. Ilyesmit tehát nem írhatunk le:

def fac(n >= 0), do: ...

Az ilyen esetek gyakoriak, ezért a minta ún. őrrel (guard) egészíthető ki. Az őrt a függvényfejben a paraméter(eke)t követő when kulcsszó vezeti be, utána őrkifejezés áll:

  def fac(n) when n >= 0, do: ...

Az őrkifejezésben csak őrként (guard) definiált, garantáltan mellékhatás nélküli könyvtári függvényeket hívhatunk, más függvényeket, így saját függvényeket sem. Ilyenek például a típusvizsgáló is_... függvények és a not, and, or műveletek (lásd Műveletek és beépített függvények).

Őr a case ágaiban is állhat. Az alábbi case kifejezés egészekkel kezdődő nem üres listára a fej kétszeresét, üres listára 0-t, bármi másra nil-t (hibajelzést) ad:

case valami() do
  [hd|tl] when is_integer(hd) -> hd * 2 # Ha nem üres lista
  [] -> 0 # Ha üres lista
  _ -> nil # Bármi más: hibajelzés
end

Klózok

Egy függvényt több klózzal definiálhatunk: minden előforduló esetre egy-egy klózt írunk, és a függvény hívásakor az első olyan klóz törzse értékelődik ki, amelynek mintájára (és őrére) az aktuális paraméterek illeszkednek. Minden klóz független a többitől: a paramétereik neve lehet azonos, de különböző is; egy klóz törzsében csak a saját paramétereire lehet hivatkozni.

Ha egyik klóz sem illeszkedik, a hívás FunctionClauseError hibával leáll. Például ha az összefűzés első változata csak az üres listát kezeli:

defmodule App0 do
  # app(xs, ys): xs és ys listák összefűzöttje zs == (xs ⊕ ys)
  # [] ⊕ ys == ys
  def app([], ys), do: ys
end

akkor az App0.app([], ~c"abc") eredménye ~c"abc", az App0.app([1,2,3], ~c"abc") viszont FunctionClauseError hibát ad. A hiányzó eset tehát nem fordítási hiba, hanem futás közben derül ki. A dialyzer (Típusellenőrzés: dialyzer) sem jelzi a hiányzó eseteket; csak akkor figyelmeztet, ha be tudja bizonyítani, hogy egy hívás soha nem lehet sikeres.

Egymást kölcsönösen kizáró minták

Egy függvény klózainak mintái lehetőleg zárják ki kölcsönösen egymást. Tegyük fel például, hogy egy függvénynek – az üres listát nem kezelve – azt kell megkülönböztetnie, amikor a listának pontosan egy eleme van, attól, amikor legalább egy eleme van:

def fun([x|xs])...
def fun([x])...

Ezzel az a gond, hogy az első klóz minden olyan listára illeszkedik, amelynek van feje, a farka pedig tetszőleges elemszámú, azaz üres is lehet. Ezért az első klóz az egyelemű listára is illeszkedik, a második klózra soha nem kerül sor: a minták nem zárják ki kölcsönösen egymást (erre az Elixir-fordító figyelmeztet is). A két klóz sorrendjének megfordításával a mintaillesztés már meg tudja különböztetni a két esetet, ám ennek hatékonyságromlás az ára:

def fun([x])...
def fun([x|xs])...

Írhatunk azonban olyan mintát is, amely a legalább kételemű listákra illeszkedik, és így kölcsönösen kizárja a pontosan egyelemű listára illeszkedő mintát:

def fun([x1,x2|xs])...
def fun([x])...

Az első klóz törzsében a lista fejére az x1 változóval, a farkára az [x2|xs] kifejezéssel hivatkozhatunk. Ez utóbbi hivatkozást egyszerűbbé (és olcsóbbá) teszi a réteges minta; a lista farkára ekkor az xxs változóval hivatkozunk:

def fun([x1 | xxs = [x2|xs]])...
def fun([x])...

Ha az x2 és xs változókat nem használjuk a klóz törzsében, az Elixir-fordító figyelmeztet. A figyelmeztetést úgy kerülhetjük el, hogy a változó nevét aláhúzásjellel kezdjük, vagy elég csak aláhúzásjelet írni:

def fun([x1 | xxs = [_x2|_xs]])...
def fun([x1 | xxs = [_|_]])...

A beszédes nevek azonban segítik a megértést, utalnak a változó szerepére. Ha a lista második elemére vagy a harmadik elemtől kezdődő farkára hivatkozni akarunk a klóz törzsében, akkor ne aláhúzásjellel kezdődő változóneveket használjunk.

Forrás: dp26a-fp1ea.pdf (22. dia), dp26a-fp3ea.pdf (23–26. dia), dp26a-fp1gyfel.livemd, dp26a-fp2gy-megoldasok.livemd, dp26a-fp1gy-megoldasok.livemd

Rekurzió

Lineáris és elágazó rekurzió

A deklaratív programozás alappillére a rekurzió. A deklaratív nyelvekben nincs ciklus, ezért ismétlést, iterációt is rekurzív algoritmussal valósítunk meg. A rekurzió kétféle lehet: lineáris és elágazó (angolul linear recursion és tree recursion).

  • Lineáris rekurzió: egy rekurzív hívás van a függvényben.

    def fac(0), do: 1 # 1. klóz: Alapeset
    def fac(n), do: n * fac(n - 1) # 2. klóz: Rekurzív eset
    
  • Jobbrekurzió (farokrekurzió, tail recursion): a rekurzív hívás visszatérési pozícióban van, ezért hatékonyabb gépi kód készül belőle, nem kell hozzá verem.

    def fac(n), do: fac(n, 1) # Segédfüggvény meghívása akkumulátor paraméterrel
    def fac(0, a), do: a
    def fac(n, a), do: fac(n - 1, n * a)
    
  • Elágazó rekurzió: több rekurzív hívás van a függvényben; sokszor nem hatékony (lásd Dinamikus programozás).

    def fib(n) when n <= 1, do: n # 1. klóz őrfeltétellel: Alapeset
    def fib(n), do: fib(n - 1) + fib(n - 2) # 2. klóz: Rekurzív eset
    

Az adatszerkezetek is lehetnek rekurzívak:

  • Lineárisan rekurzív adatszerkezet pl. az egyszeresen láncolt lista ([...], nem egydimenziós tömb!). Alapesete az [] üres lista, rekurzív esete a legalább egyelemű [H|T], ahol T is egy lista ([] vagy legalább egyelemű).
  • Elágazóan rekurzív adatszerkezet pl. a (bináris vagy többágú) fa.

Rekurzív adatszerkezetek feldolgozásának természetes módja a rekurzív algoritmus. Lineáris adatszerkezetek, pl. listák feldolgozására imperatív nyelveken még lehet ciklust írni, de elágazóan rekurzív adatszerkezeteket, pl. fákat ciklusokkal bejárni már nagy kihívás. Ez az algebrai módszer: a rekurzív adatszerkezeteket mintaillesztéssel dolgozzuk fel rekurzív függvényekkel, minden esetre egy-egy klózt írva.

Rekurzív függvény írása: két lista összefűzése

A deklaratív stílus érzékeltetésére írjuk meg lépésről lépésre két lista összefűzését app/2 néven! Az xs és ys listák összefűzése azt jelenti, hogy az xs lista összes elemét az ys elé fűzzük az elemek eredeti sorrendjének megőrzésével: xs⊕ys.

Mivel a lista láncolt adatszerkezet, csak az első elemét érjük el közvetlenül. Egyszerű a dolgunk, ha az első lista, xs, üres: ekkor a második listát, ys-t kell változtatás nélkül visszaadnunk. Ezt az esetet már láttuk (App0, Mintaillesztés).

Ugyanez a rövidített (, do:) függvénydefiníció helyett a teljes, do ... end alakú definícióval:

defmodule App0 do
  # app(xs, ys): xs és ys listák összefűzöttje zs == (xs ⊕ ys)
  # [] ⊕ ys == ys
  def app([], ys) do
    ys
  end
end

Ha xs nem üres, akkor ahhoz, hogy az ys elé fűzzük, rendre le kell emelni és félre kell rakni az elemeit, amíg csak üressé nem válik. Hová tegyük ezeket az elemeket? Ha a függvény rekurzív módon hívja meg saját magát, akkor átmenetileg automatikusan a hívási verembe kerülnek.

Amikor rekurzív függvényt írunk, abból indulunk ki, hogy a függvény valamilyen egyszerűbb adatszerkezetre – pl. egy paraméterként kapott lista farkára – elvégzi, amit elvárunk tőle, és ezután már csak a lista fejével kell valamit kezdenie, pl. a rekurzív hívás eredményeként kapott lista elé fűznie. A mintaillesztéssel az előforduló eseteket világosan elkülöníthetjük. Két lista összefűzésénél az első paraméter, xs, értéke szerint két esetet érdemes megkülönböztetni (lehetne többet is, de felesleges lenne): 1. xs nem üres, 2. xs üres.

defmodule App1 do
  # app(xs, ys): xs és ys listák összefűzöttje zs == (xs ⊕ ys)

  # [z | zs] ⊕ ys = [z | zs ⊕ ys]
  def app([z | zs] = zzs, ys) do
    [z | app(zs, ys)]
  end

  # [] ⊕ ys == ys
  def app([], ys) do
    ys
  end

end

A [z | zs] = zzs réteges minta (lásd Mintaillesztés) csak a magyarázat kedvéért szerepel, hogy a teljes paraméterre is utalni tudjunk, ne csak a komponenseire. Mivel a törzsben nem használjuk, a fordító figyelmeztetésének elkerülésére aláhúzásjellel kezdjük:

defmodule App2 do
  # app(xs, ys): xs és ys listák összefűzöttje zs == (xs ⊕ ys)
  def app([z | zs] = _zzs, ys) do
    [z | app(zs, ys)]
  end

  def app([], ys) do
    ys
  end
end

Ha az app/2 hívásakor az első paraméter üres lista, az Elixir a második klózt értékeli ki, ha nem üres, az elsőt. Az első klóz rekurzív hívásában az app/2-t a kapott _zzs lista farkára, zs-re alkalmazzuk, feltételezve, hogy képes a zs listát az ys elé fűzni. Amikor ebből a hívásból visszatér, már csak a verembe félretett z-t kell a kapott lista elé fűznie ([z | app(zs, ys)]).

Két dolgot kell még belátnunk.

  • A rekurzió nem végtelen. A listák véges hosszúságúak, és a rekurzív hívás az app/2-t mindig a kapott _zzs-nél eggyel rövidebb zs-re alkalmazza. A lista tehát egyre rövidül, és amikor üressé válik, a második klóz kiértékelésére kerül sor: ennek törzsében nincs rekurzív hívás, a függvény a rekurzív hívások során változás nélkül továbbadott ys-sel tér vissza.
  • A függvény azt csinálja, amit elvárunk tőle. Amikor a lista már üressé vált, a rekurzív hívás az ys listával tér vissza; ez elé fűzi az első klóz az eredeti lista utolsó elemét, amelyet a veremből vesz elő. A következő visszatéréskor az így kibővített lista elé fűzi az utolsó előtti elemet, majd a hátulról harmadikat, és így tovább, amíg ki nem ürül a verem. A végeredmény valóban az, amit várunk.
iex> App2.app([1,2,3], [4,5])
[1, 2, 3, 4, 5]

Ha a klóz törzse egyetlen kifejezésből áll, célszerű a rövidített , do: függvényjelölést alkalmazni; a típusspecifikációval és a fejkommenttel kiegészített tömör változat az App3 (Típusok). Ellenőrizzük, hogy lefedtünk-e minden lehetséges esetet! Az első paraméter a két klózban kétféle mintára illeszkedhet, []-ra vagy [x|xs]-re, azaz üres vagy legalább egyelemű listára: ez lefedi az első paraméter összes lehetséges (lista) értékét. A második paraméter mindkét klózban az ys kötetlen változó, ami mindenre illeszkedik. A két klózzal tehát valóban minden esetet lefedtünk.

Törzsrekurzió és jobbrekurzió

A rekurzió szokásos iskolapéldája az kiszámítása. Matematikai definíciója:

Az első változat a matematikai definíciót másoló rekurzív függvény:

defmodule Fac do
  @spec fac(n :: integer()) :: f :: integer() # Típusspecifikáció
  # 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

A második klózban alkalmazott rekurziót angolul body recursion-nek, magyarul törzsrekurziónak mondhatjuk, ha hangsúlyozni akarjuk, hogy a rekurzív hívás eredményével a függvény törzsében még további műveletet (itt szorzást) kell végezni.

A jobbrekurzív (tail recursive) változat kevésbé szigorúan követi a matematikai definíciót, ezért nehezebb megérteni, és nehezebb hozzá kifejező, pontos fejkommentet írni. A jobbrekurziót magyarul terminális rekurziónak, ritkábban farokrekurziónak is nevezik, mert a rekurzív hívás az adott klózban az utolsó – befejező, lezáró – hívás: az eredményét változatlanul vissza kell adni, már semmilyen műveletet nem végzünk vele.

A jobbrekurzív változathoz egy plusz paraméterre van szükség. Ezt akkumulátornak szokták nevezni, mert a részeredményeket gyűjtjük benne – ahelyett, hogy a még elvégzendő műveleteket az argumentumaikkal együtt a verembe tennénk. Itt a részletszorzatokat adjuk át benne a rekurzív hívásban.

Az akkumulátornak az első híváskor adunk értéket: a fac/1 hívja meg az 1 kezdőértékkel a fac/2-t. Ha a fac/1-et -val hívjuk, az eredménynek -nek kell lennie, ezért a fac/2 első klózának esetén az akkumulátort kell visszaadnia, rekurzív hívás nélkül. Ha az első paraméter, , nem , a második klózra kerül sor: a rekurzív hívásban az első paramétert eggyel csökkentjük (), a másodikban pedig -nel megszorozzuk az eddig összegyűjtött részletszorzatot (). Így alakul ki az eredmény.

defmodule FacJobbrek 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

Mindkét változatot kipróbálhatjuk ugyanazokkal a hívásokkal:

Fac.fac(5)
Fac.fac(0)
Fac.fac(1)
Fac.fac(100_000)
FacJobbrek.fac(5)
FacJobbrek.fac(0)
FacJobbrek.fac(1)
FacJobbrek.fac(100_000)

Az eredmény mindkét függvénnyel 120, 1, 1, illetve egy több mint 450 000 jegyű szám (az egész számok pontossága korlátlan). Érdemes kipróbálni, tapasztalható-e lényeges különbség a két függvény futási ideje között.

A jobbrekurzív kódot a modern értelmező- és fordítóprogramok nagyon hatékonyan, iteratív processzként valósítják meg: a hívás nem foglal újabb veremhelyet. A jobbrekurzió manapság főleg olyan esetekben indokolt, amikor két vagy több processz üzenetet küld egymásnak, és végtelen jobbrekurzív hívásban várnak a válaszüzenetre (lásd Az Elixir nyelv).

Balrekurzió

Balrekurziónak (fejrekurziónak, angolul head recursion) nevezzük, ha a rekurzív hívás egy klóz első és egyetlen rekurzív hívása, azaz a rekurzív hívás előtt nem végzünk semmilyen műveletet. A különbség jól látszik, ha egy függvény a rekurzív hívás előtt vagy után ír ki valamit: a jobbrekurzív változat a hívás előtt írja ki a soron következő számot, a balrekurzív a hívás után, a visszatérés során. A két változatot a fejezet végi upto_by_3 feladat mutatja be.

Klózsorrend és hatékonyság

Egy rekurzív adatszerkezet feldolgozására legalább két, esetleg több klózt írunk. Közöttük vannak olyanok, amelyekre az adatszerkezet jellegzetessége miatt csak egyszer vagy csak nagyon ritkán kerül sor, másokra gyakrabban. Ilyen például az üres és a nem üres lista esete: az üres listát feldolgozó klóz kiértékelésére csak egyszer kerül sor, a nem üres listát feldolgozó klózt a lista hosszától függően akár nagyon sokszor hívjuk.

A kurzus ajánlása: az algoritmus hatékonyságát javítja, ha

  • egy függvény klózai kölcsönösen kizárják egymást, és
  • közülük a gyakrabban hívott(ak) megelőzi(k) a ritkábban hívott(ak)at:
def fun([x|xs])...
def fun([])...

Az App3.app/2 második klóza illeszkedik az üres listára; a rekurzió során erre csak egyszer kerül sor, ezért ez észszerű döntésnek tűnik. Az App4.app/2 a két klózt fordított sorrendben tartalmazza. Érdemes megmérni, van-e észrevehető különbség a futási időben egy 25 millió elemű listán:

defmodule App4 do
  @spec app(xs :: [integer()], ys :: [integer()]) :: zs :: [integer()]
  # xs és ys listák összefűzöttje zs
  def app([], ys),     do: ys
  def app([x|xs], ys), do: [x|app(xs, ys)]
end
App4.app(Range.to_list(1..25_000_000), [])
:ok
App3.app(Range.to_list(1..25_000_000), [])
:ok

Mérések: egészlista összege háromféleképpen

Az egészlista összegét kiszámoló Sum modulnak (lásd Projektek, mérés, típusellenőrzés) három változata van: a sum1 első, a sum2 második klóza illeszkedik az üres listára, a sum3 pedig egy jobbrekurzív segédfüggvényt hív meg. Van-e különbség a hatékonyságukban?

defmodule Sum do

  @spec sum1(xs::[integer()]) :: sum::integer()
  # xs elemeinek összege sum
  # üres listára az első klóz illeszkedik
  def sum1([]), do: 0
  def sum1([x|xs]), do: x + sum1(xs)

  # üres listára a második klóz illeszkedik
  def sum2([x|xs]), do: x + sum2(xs)
  def sum2([]), do: 0

  # jobbrekurzív
  def sum3(xs), do: sumi(xs, 0)

  defp sumi([x|xs], sum), do: sumi(xs, sum+x)
  defp sumi([], sum), do: sum

end

1..1000 |> Range.to_list() |> Sum.sum1() |> IO.inspect()
1..1000 |> Range.to_list() |> Sum.sum2() |> IO.inspect()
1..1000 |> Range.to_list() |> Sum.sum3() |> IO.inspect()

Mindhárom 500500-at ír ki. A gyakorló notebook mindhármat egy 90 millió elemű listára is lefuttatja:

1..90_000_000 |> Range.to_list() |> Sum.sum1() |> IO.inspect()
1..90_000_000 |> Range.to_list() |> Sum.sum2() |> IO.inspect()
1..90_000_000 |> Range.to_list() |> Sum.sum3() |> IO.inspect()

A Benchee-mérés egy 10 000 elemű listán (1..10_000 |> Enum.to_list |> Sum.sum1() stb.):

Benchee.run(
  %{
    "sum1 ([] az 1. klozban)"  => fn -> 1..10_000 |> Enum.to_list |> Sum.sum1() end,
    "sum2 ([] a 2. klozban)"  => fn -> 1..10_000 |> Enum.to_list |> Sum.sum2() end,
    "sum3 (iterativ, [] a 2. klozban)"  => fn -> 1..10_000 |> Enum.to_list |> Sum.sum3() end
  }# , profile_after: true
)
:ok

Az előadás diáján közölt eredmény (mix run-nal, Elixir 1.18.4, Erlang 28.0.1, Intel i5-8365U; a zárójeles megjegyzések az előadó kiegészítései):

Name           ips        average  deviation         median         99th %
sum3       16.26 K       61.50 µs    ±16.25%       63.65 µs       79.93 µs
sum2        8.87 K      112.80 µs    ±18.45%       97.88 µs      175.92 µs
sum1        8.26 K      121.01 µs    ±18.40%      105.16 µs      187.74 µs

Comparison:
sum3       16.26 K (2. klóz illeszkedik az üres listára a jobbrekurzív segédfüggvényben)
sum2        8.87 K - 1.83x slower +51.30 µs (2. klóz illeszkedik az üres listára)
sum1        8.26 K - 1.97x slower +59.52 µs (1. klóz illeszkedik az üres listára)

Az első előadás segédanyagában (Livebookban, ugyanazon a gépen) mért eredmény:

Name                                       ips        average  deviation         median         99th %
sum3 (iterativ, [] a 2. klozban)       11.60 K       86.22 μs    ±16.11%       82.37 μs      150.53 μs
sum1 ([] az 1. klozban)                 7.54 K      132.56 μs    ±22.29%      126.42 μs      230.17 μs
sum2 ([] a 2. klozban)                  6.73 K      148.58 μs    ±15.46%      136.23 μs      204.25 μs

Comparison: 
sum3 (iterativ, [] a 2. klozban)       11.60 K
sum1 ([] az 1. klozban)                 7.54 K - 1.54x slower +46.33 μs
sum2 ([] a 2. klozban)                  6.73 K - 1.72x slower +62.36 μs

Az első gyakorlat nth feladatánál (lásd lent) egy 100 001 elemű lista első, középső és utolsó elemét kérjük el, egyszer az üres listát utolsóként (EmptyLast), egyszer elsőként (EmptyFirst) kezelő klózsorrenddel:

Name                           ips        average  deviation         median         99th %
firstElemEmptyFirst      1745.97 K        0.57 μs    ±98.93%        1.02 μs        2.05 μs
firstElemEmptyLast       1724.38 K        0.58 μs   ±100.87%        1.02 μs        2.05 μs
middleElemEmptyLast         6.32 K      158.12 μs    ±41.63%      102.40 μs      307.20 μs
middleElemEmptyFirst        6.28 K      159.34 μs    ±50.58%      102.40 μs      307.20 μs
lastElemEmptyLast           3.16 K      316.78 μs    ±30.90%      307.20 μs      614.40 μs
lastElemEmptyFirst          3.08 K      324.93 μs    ±31.75%      307.20 μs      614.40 μs

A két klózsorrend között mért különbségek kicsik és nem következetesek: a diáin a sum2 a gyorsabb, a segédanyagban a sum1, az nth-nél pedig 1–3% az eltérés. Egyértelmű nyereséget a jobbrekurzió hoz: a sum3 mindkét mérésben másfél-kétszer gyorsabb. A futási idő a lista hosszával arányosan nő: az utolsó elem elérése kétszer annyi ideig tart, mint a középsőé.

Hibajelzés: Erlang- és Elixir-stílus

Gyakran előfordul, hogy bizonyos listákon bizonyos műveleteket nem lehet elvégezni. Egy üres listának például egyetlen eleme sincs, bármelyik elemét is kérjük, nincs mit eredményül adni.

Ilyenkor dönthetünk úgy, hogy az adott műveletet üres listára nem értelmezzük, és a hiba jelzését a rendszerre bízzuk (a hívás például FunctionClauseError hibával leáll). Ha úgy döntünk, hogy a helyes eredmény mellett a hibát is jelezzük, ezt hagyományosan kétféle stílusban tehetjük meg:

  • Erlang-stílusban a visszatérési érték típusa {:ok, any()} | :error: siker esetén a visszatérési érték egy {:ok, value} pár, ahol value a visszaadott, any() típusú érték, meghiúsulás esetén pedig az :error atom.
  • Elixir-stílusban a visszatérési érték típusa any() | nil: siker esetén maga az any() típusú value, meghiúsulás esetén a nil atom.

Mindhárom változatra példa a lenti 5. feladat (Last, LastEx, LastEr).

Gyakorló feladatok

Az alábbi feladatok megoldására ne használja az azonos vagy hasonló feladatokat megoldó könyvtári függvényeket a Kernel, List, Enum és más modulokból, pl. hd, tl, first, last, at, length, split, slice! Gyakorlásképpen saját, ahol kell, rekurzív függvénydefiníciókat írjon. A specifikációs kódrészletek a megírandó függvény keretét (modul, típusspecifikáció, fejkomment) és néhány teszthívást tartalmaznak; a ... helyére kell a megoldást írni.

1. Lista feje

Írjon függvényt egy lista fejének (első elemének) visszaadására! Ha a lista üres, Elixir-stílusban jelezze, azaz a nil atomot adja eredményül. Tipp: az első klóz a legalább egyelemű listára, a második az üres listára illeszkedjen.

defmodule Head do
  @spec hd(xs :: [any()]) :: r :: any() | nil
  # Ha xs nem üres, x az xs lista feje, egyébként nil
  def hd(...), do:
  ...
end
IO.puts(Head.hd([]) == nil)
IO.puts(Head.hd(Range.to_list(1..5)) == 1)
IO.puts(Head.hd(~c"almárium") == ?a)
Megoldás
defmodule Head do
  @spec hd(xs :: [any()]) :: r :: any() | nil
  # Ha xs nem üres, x az xs lista feje, egyébként nil
  def hd([x|_]), do: x
  def hd([]), do: nil
end

Mindhárom teszt true-t ír ki.

2. Lista farka

Írjon függvényt egy lista farkának (az első eleme utáni részlistájának) visszaadására! Ha a lista üres, Elixir-stílusban jelezze, azaz a nil atomot adja eredményül. Tipp: az első klóz a legalább egyelemű listára, a második az üres listára illeszkedjen.

defmodule Tail do
  @spec tl(xs: [any()]) :: ts :: [any()] | nil
  # Az xs lista farka ts
  def tl(...), do:
  ...
end
IO.puts(Tail.tl([]) == nil)
IO.puts(Tail.tl(Range.to_list(1..5)) == [2,3,4,5])
IO.puts(Tail.tl(~c"almárium") == ~c"lmárium")
Megoldás
defmodule Tail do
  @spec tl(xs: [any()]) :: ts :: [any()] | nil
  # Az xs lista farka ts
  def tl([_|xs]), do: xs
  def tl([]), do: nil
end

Mindhárom teszt true-t ír ki.

3. Lista n-edik eleme

Írjon rekurzív függvényt egy lista n-edik elemének visszaadására! (A lista indexelése 0-tól indul.) Ha a lista n-nél rövidebb, Elixir-stílusban jelezze, azaz a nil atomot adja eredményül. Tipp: három klózt kell írnia: egyet az üres listára, egyet a megtalált elem visszaadására, egyet pedig arra, hogy rekurzív hívással folytassa a keresést.

defmodule Nth do
  @spec nth(xs :: [any()], n :: integer()) :: r :: any() | nil
  # Ha xs elég hosszú, r az xs n-edik eleme; egyébként nil (indexelés 0-tól)
  def nth(..., n), do:
  ...
end
IO.puts(Nth.nth([], 5) == nil)
IO.puts(Nth.nth(Range.to_list(1..5), 4) == 5)
IO.puts(Nth.nth(Range.to_list(1..5), 5) == nil)
IO.puts(Nth.nth(~c"almárium", 3) == ?á)
IO.puts(Nth.nth(~c"almárium", -3) == nil)

A benchee modul segítségével hasonlítsa össze a futási időket egy hosszú lista első, középső és utolsó elemének elérése esetén, olyan klózsorrendekkel, amikor az első, illetve amikor az utolsó klóz illeszkedik az üres listára!

Megoldás
defmodule Nth do
  @spec nth(xs :: [any()], n :: integer()) :: r :: any() | nil
  # Ha xs elég hosszú, r az xs n-edik eleme; egyébként nil (indexelés 0-tól)
  def nth([x|_], 0), do: x
  def nth([_|xs], n), do: nth(xs, n-1)
  def nth([], _), do: nil
end

Mind az öt teszt true-t ír ki. A mérés:

defmodule NthEmptyFirst do
  @spec nth(xs :: [any()], n :: integer()) :: r :: any() | nil
  # Ha xs elég hosszú, r az xs n-edik eleme; egyébként nil (indexelés 0-tól)
  def nth([], _), do: nil
  def nth([x|_], 0), do: x
  def nth([_|xs], n), do: nth(xs, n-1)
end
IO.puts(NthEmptyFirst.nth([], 5) == nil)
IO.puts(NthEmptyFirst.nth(Range.to_list(1..5), 4) == 5)
IO.puts(NthEmptyFirst.nth(Range.to_list(1..5), 5) == nil)
IO.puts(NthEmptyFirst.nth(~c"almárium", 3) == ?á)
IO.puts(NthEmptyFirst.nth(~c"almárium", -3) == nil)

l = Range.to_list(0..100_000)
Benchee.run(%{
        "firstElemEmptyLast" => fn -> Nth.nth(l, 0) end,
        "middleElemEmptyLast" => fn -> Nth.nth(l, 50_000) end,
        "lastElemEmptyLast" => fn -> Nth.nth(l, 100_000) end,
        "firstElemEmptyFirst" => fn -> NthEmptyFirst.nth(l, 0) end,
        "middleElemEmptyFirst" => fn -> NthEmptyFirst.nth(l, 50_000) end,
        "lastElemEmptyFirst" => fn -> NthEmptyFirst.nth(l, 100_000) end
    }
)

Az eredmény a Mérések szakaszban látható. Ha egy mérendő függvény nagyon gyors (itt az első elem elérése), a Benchee figyelmeztet, hogy a mérés megbízhatatlanabb.

4. Lista hossza

Írjon rekurzív függvényt egy lista hosszának meghatározására! Ne használjon segédfüggvényt! Tipp: az első klóz a legalább egyelemű listákra, a második az üres listára illeszkedjen.

defmodule Length do
  @spec len(xs :: [any()]) :: n :: integer()
  # Az xs lista hossza n
  def len(...), do:
  ...
end
IO.puts(Length.len([]) == 0)
IO.puts(Length.len(Range.to_list(1..5)) == 5)
IO.puts(Length.len(~c"kőszerű") == 7)
Megoldás
defmodule Length do
  @spec len(xs :: [any()]) :: n :: integer()
  # Az xs lista hossza n
  def len([_|xs]), do: 1 + len(xs)
  def len([]), do: 0
end

Mindhárom teszt true-t ír ki.

Most akkumulátort és segédfüggvényt használó jobbrekurzív függvényt írjon a lista hosszának megállapítására! Tipp: az akkumulátorban gyűjtse a listaelemek számát: amikor leszedi a soron következő elemet a lista elejéről, a rekurzív hívásban az akkumulátor korábbi értékéhez adjon 1-et.

defmodule Length2 do
  @spec len(xs :: [any()]) :: n :: integer()
  # Az xs lista hossza n
  def len(....), do: ...

  @spec len(xs :: [any()], count :: integer()) :: n :: integer()
  # Az xs lista hossza és count összege n
  def len(..., count), do:
    ...
end

IO.puts(Length2.len([]) == 0)
IO.puts(Length2.len(Range.to_list(1..5)) == 5)
IO.puts(Length2.len(~c"kőszerű") == 7)
Megoldás
defmodule Length2 do
  @spec len(xs :: [any()]) :: n :: integer()
  # Az xs lista hossza n
  def len(xs), do: len(xs, 0)

  @spec len(xs :: [any()], count :: integer()) :: n :: integer()
  # Az xs lista hossza és count összege n
  defp len([_|xs], count), do: len(xs, count + 1)
  defp len([], count), do: count
end

Mindhárom teszt true-t ír ki.

5. Lista utolsó eleme

Írjon rekurzív függvényt egy lista utolsó elemének visszaadására! Ne használjon segédfüggvényt! (A klózok mintáinak megválasztásához lásd Egymást kölcsönösen kizáró minták.)

Először olyan függvényt írjon, amely visszaadja egy lista utolsó elemét, de a hibajelzést a rendszerre bízza.

defmodule Last do
  @spec last(xs :: [any()]) :: x :: any()
  # Ha xs nem üres, az utolsó eleme x
  def last(...), do:
  ...
end
IO.puts(Last.last(~c"Itt vagy?") == ??)
IO.puts(Last.last([]))
Megoldás
defmodule Last do
  @spec last(xs :: [any()]) :: x :: any()
  # Ha xs nem üres, az utolsó eleme x
  def last([_a | xs=[_b|_cs]]), do: last(xs)
  def last([x]), do: x
end

Az első teszt true-t ír ki, a második hibával leáll:

** (FunctionClauseError) no function clause matching in Last.last/1

Most írja meg a függvényt Elixir-stílusú hibakezeléssel!

defmodule LastEx do
  @spec last(xs :: [any()]) :: r :: (x :: any()) | nil
  # Ha xs nem üres, r == x, ahol az xs utolsó eleme x, egyébként r == nil
  def last(...), do:
  ...
end
IO.puts(LastEx.last(~c"Itt vagy?") == ??)
IO.puts(LastEx.last([]) == nil)
Megoldás
defmodule LastEx do
  @spec last(xs :: [any()]) :: r :: (x :: any()) | nil
  # Ha xs nem üres, r == x, ahol az xs utolsó eleme x, egyébként r == nil
  def last([_a | xs=[_b|_cs]]), do: last(xs)
  def last([x]), do: x
  def last([]), do: nil
end

Mindkét teszt true-t ír ki.

Végül írja meg újra a függvényt Erlang-stílusú hibakezeléssel!

defmodule LastEr do
  @spec last(xs :: [any()]) :: r :: {:ok, x :: any()} | :error
  # Ha xs nem üres, r == {:ok, x}, ahol az xs utolsó eleme x, egyébként r == :error
  def last(...), do:
  ...
end
IO.puts(LastEr.last(~c"Itt vagy?") == {:ok, ??})
IO.puts(LastEr.last([]) == :error)
Megoldás
defmodule LastEr do
  @spec last(xs :: [any()]) :: r :: {:ok, x :: any()} | :error
  # Ha xs nem üres, r == {:ok, x}, ahol az xs utolsó eleme x, egyébként r == :error
  def last([_a | xs=[_b|_cs]]), do: last(xs)
  def last([x]), do: {:ok, x}
  def last([]), do: :error
end

Mindkét teszt true-t ír ki.

6. Lista k-adik elemétől induló, n hosszú részlistája

Írjon rekurzív függvényt egy lista olyan hosszú részlistájának visszaadására, amely a -adik elemtől kezdődik (a lista indexelése 0-tól indul)! Ha nincs ilyen hosszú részlistája, Elixir-stílusú hibajelzéssel térjen vissza. Ne használjon segédfüggvényt! A klózok sorrendjének megválasztásával törekedjen hatékony megoldásra. Gondolja át, hányféle esetet kell megkülönböztetnie!

Tipp, a megkülönböztetendő esetek:

  1. Kiszedtük a lista -adik elemével kezdődő, hosszú részlistát.
  2. Elhagytuk a lista első elemét, kigyűjthetjük a következő darab elemet.
  3. Elhagyjuk a lista első elemét.
  4. Elfogytak az elemek a listából, mielőtt kiszedtük volna az hosszú részlistát.
defmodule Slice do
  @spec slice(xs :: [any()], k :: integer(), n :: integer()) :: r :: [any()] | nil
  # Ha xs elég hosszú, r az xs k-tól induló, n hosszú részlistája; egyébként nil
  # Indexelés 0-tól
  def slice(..., k, n), do:
  ...
end
IO.puts(Slice.slice([], 0, 5) == nil)
IO.puts(Slice.slice(Range.to_list(1..5), 1, 3) == [2,3,4])
IO.puts(Slice.slice(Range.to_list(1..5), 4, 1) == [5])
IO.puts(Slice.slice(~c"almárium", 3, 3) == ~c"ári")
IO.puts(Slice.slice(~c"almárium", -3, 3) == nil)

A benchee segítségével mérje meg a futási időket különféle paraméterezések mellett, továbbá profilozással nézze meg, melyik klóz, illetve hívott függvény használja el a legtöbb futási időt! Hasonlítsa össze a saját Slice.slice/3 függvénye futási idejét az Enum.slice/3 függvényével különféle szélsőséges paraméterezések mellett!

Megoldás
defmodule Slice do
  @spec slice(xs :: [any()], k :: integer(), n :: integer()) :: r :: [any()] | nil
  # Ha xs elég hosszú, r az xs k-tól induló, n hosszú részlistája; egyébként nil
  # Indexelés 0-tól
  def slice(_xs, 0, 0), do: []
  def slice([x|xs], 0, n) do
    case slice(xs, 0, n-1) do
      nil -> nil
      t -> [x|t]
    end
  end
  def slice([_x|xs], k, n), do: slice(xs, k-1, n)
  def slice([], _, _), do: nil
end

A tesztek mellé a megoldás egy hatodikat is ad, amely szintén true-t ír ki:

IO.puts(Slice.slice(~c"almárium", 3, 9) == nil)

A mérés:

l = Range.to_list(0..100_000)
Benchee.run(%{
        "begin" => fn -> Slice.slice(l, 0, 1_000) end,
        "middle" => fn -> Slice.slice(l, 50_000, 1_000) end,
        "end" => fn -> Slice.slice(l, 99_000, 1_000) end,
        "all" => fn -> Slice.slice(l, 0, 100_000) end,
        "beginEnum" => fn -> Enum.slice(l, 0, 1_000) end,
        "middleEnum" => fn -> Enum.slice(l, 50_000, 1_000) end,
        "endEnum" => fn -> Enum.slice(l, 99_000, 1_000) end,
        "allEnum" => fn -> Enum.slice(l, 0, 100_000) end
    },
    profile_after: true
)

A profilozás szerint a teljes lista kivágásakor (all) a futási idő 99,72%-át maga a Slice.slice/3 használja el, 100 001 hívással.

7. Tagsági vizsgálat

Írjon rekurzív függvényt annak eldöntésére, hogy egy érték benne van-e egy listában! Ne használjon segédfüggvényt, ügyeljen a hatékonyságra.

  defmodule Member do
    @spec member?(xs :: [any()], e :: any()) :: b :: boolean()
    # b == true, ha e benne van xs-ben, egyénként false
    def member?(..., e), do:
    ...
  end
  (Member.member?(~c"A szó elszáll", ?ó) == true) |> IO.inspect()
  (Member.member?([~c"A szó", ~c"elszáll", ~c"az írás", ~c"megmarad."], ~c"elszáll")
    == true) |> IO.inspect()
  (Member.member?([1.2, ?v, "str", false], false) == true) |> IO.inspect()
  (Member.member?([1.2, ?v, "str", false], "str") == true) |> IO.inspect()
  (Member.member?([1.2, ?v, "str", false], ~c"str") == false) |> IO.inspect()
  (Member.member?([], []) == false) |> IO.inspect()
Megoldás

Az első klóz mintájában az e változó kétszer szerepel, így csak akkor illeszkedik, ha a lista feje egyenlő a keresett értékkel:

defmodule Member do
  @spec member?(xs :: [any()], e :: any()) :: b :: boolean()
  # b == true, ha e benne van xs-ben, egyénként false
  def member?([e|_], e), do: true
  def member?([_|xs], e), do: member?(xs, e)
  def member?([], _), do: false
end

Mind a hat teszt true-t ír ki.

8. Prímvizsgálat

Írjon rekurzív programot annak eldöntésére, hogy egy egész szám prím-e! Feltételezheti, hogy a függvényt egészszám-paraméterrel hívjuk. Segédfüggvényt használhat. Ügyeljen a hatékonyságra.

  defmodule Prime do
    @spec prime?(x :: integer()) :: b :: boolean()
    # b == true, ha x prím
    def prime?(x), do:
    ...
  end
  (Prime.prime?(17) == true) |> IO.inspect()
  (Prime.prime?(18) == false) |> IO.inspect()
  (Prime.prime?(197_628) == false) |> IO.inspect()
  (Prime.prime?(1_000_003) == true) |> IO.inspect()
  (Prime.prime?(1_213_457) == false) |> IO.inspect()
  (Prime.prime?(179_424_691) == true) |> IO.inspect()
Megoldás

A segédfüggvény -től lefelé haladva próbálja az osztókat; ha 1-ig nem talált osztót, a szám prím. Az őrök a mintaillesztést egészítik ki az oszthatóság vizsgálatával:

defmodule Prime do
  @spec prime?(x :: integer()) :: b :: boolean()
  # b == true, ha x prím
  def prime?(x), do: prime?(x, floor(:math.sqrt(x)))
  defp prime?(_x, 1), do: true
  defp prime?(x, y) when rem(x, y) == 0, do: false
  defp prime?(x, y) when rem(x, y) != 0, do: prime?(x, y-1)
end

Mind a hat teszt true-t ír ki. A megoldás esetén helyes: Prime.prime?(1) is true-t ad, Prime.prime?(0) pedig FunctionClauseError hibát.

További gyakorló feladatok

defmodule Fp1Gy do
  @spec revapp(xs :: [integer()], ys :: [integer()]) :: zs :: [integer()]
  # zs == xs fordítottja ys elé fűzve
  def revapp(...) do
    ...
  end

  @spec rev(xs :: [integer()]) :: zs :: [integer()]
  # zs == xs fordítottja
  def rev(...) do
    ...
  end

  @spec diff(xs :: [integer()], ys :: [integer()]) :: zs :: [integer()]
  # zs == xs és ys különbsége, azaz xs azon elemei, melyek nincsenek benne ys-ben
  # a listában az azonos értékű elemeket külön elemeknek tekintjük,
  # azaz az ilyen lista zsák (bag), nem halmaz (set)
  def diff(...) do
    ...
  end
end

Kiírás a rekurzív hívás előtt és után

Írjon lineárisan rekurzív függvényeket az alábbi feladatok megoldására direkt rekurzióval! Törekedjen elegáns, tömör, érthető és hatékony függvények írására.

Kiírás a rekurzív hívás előtt. Írjon olyan rekurzív függvényt upto_by_3 néven, amely növekvő sorrendben kiírja az és közé eső, -nél nem nagyobb, 3-mal osztható természetes számokat! Az -et paraméterként adja át a függvénynek. A rekurzív hívás az adott klóz utolsó hívása, eredménye az adott klóz eredménye legyen, azaz a rekurzív hívás eredményével már ne végezzen semmilyen műveletet: a soron következő számot tehát a rekurzív hívás előtt írja ki. Segédfüggvényt definiálhat. Használjon őrt a minta szerinti feltétel kiegészítésére.

defmodule UptoBy3TailR do
  @spec upto_by_3(n :: integer()) :: :ok
  def upto_by_3(n) do
    IO.puts(i)
    ...
  end
end
UptoBy3TailR.upto_by_3(20)
Megoldás
defmodule UptoBy3TailR do
  @spec upto_by_3(n :: integer()) :: :ok
  def upto_by_3(n), do: upto_by_3(3, n)
  defp upto_by_3(i, n) when i <= n do
    IO.puts(i)
    upto_by_3(i+3, n)
  end
  defp upto_by_3(i, n) when i > n, do: :ok
end
UptoBy3TailR.upto_by_3(20)

Kiírás a rekurzív hívás után. Írja át előző megoldását úgy, hogy a rekurzív hívás az adott klóz első hívása legyen, azaz a rekurzív hívás előtt ne végezzen semmilyen műveletet: a soron következő számot tehát a rekurzív hívás után írja ki. Az eredményt továbbra is növekvő sorrendben írja ki. Segédfüggvényt definiálhat.

defmodule UptoBy3HeadR do
  @spec upto_by_3(n :: integer()) :: :ok
  def upto_by_3(n) do
    ...
    IO.puts(i)
  end
end
UptoBy3HeadR.upto_by_3(20)

Vesse össze a két függvényalkalmazás által kiírt számsorozatot! Miben különbözik a kétféle megoldás veremhasználata?

Tipp: előfordulhat, hogy a második változata nem teljesíti a specifikációt, hogy ti. növekvő sorrendben kell kiírni a számokat. Ezen úgy segíthet, hogy nem 1-től felfelé halad a generáláskor, hanem -től lefelé. Ennek az a járulékos előnye itt és hasonló esetekben, hogy a végállomás a 0 (esetleg más, előre tudható konstans) lesz, így elég a mintaillesztés, ami hatékonyabb, mintha őrt is használnánk. Ha tehát most is őrt használt volna, cserélje le pusztán mintaillesztésre. Használjon segédfüggvényt.

Megoldás
defmodule UptoBy3HeadR do
  @spec upto_by_3(n :: integer()) :: :ok
  def upto_by_3(n), do: downto_by_3(n - rem(n, 3))
  defp downto_by_3(0), do: :ok
  defp downto_by_3(i) do
    downto_by_3(i - 3)
    IO.puts(i)
  end
end
UptoBy3HeadR.upto_by_3(20)

Mindkét változat a 3, 6, 9, 12, 15, 18 számokat írja ki, soronként. A balrekurzív változat előbb lemegy a 0-ig, és a számokat a visszatérés során írja ki; ehhez minden szám a verembe kerül.

Forrás: dp26a-fp1ea.pdf (23., 47., 50. dia), dp26a-fp1ea-sum-benchee.livemd, dp26a-fp1gyfel.livemd, dp26a-fp1gy-megoldasok.livemd, dp26a-fp2gy-megoldasok.livemd

Műveletek listákon

A lista költségei

A lista láncolt lineáris adatstruktúra, ezért olcsó az első elemét (a fejét) és az összes többi elemét (a farkát) megkapni, de drága az utolsó elemét elérni, mert végig kell gyalogolni a listán.

A funkcionális nyelvekben – a többi adatstruktúrához hasonlóan – a lista nem frissíthető, az Elixirben sem. Ha a lista egy elemét le akarjuk cserélni, másolatot kell készítenünk a lecserélendő elem előtti részlistáról. A másolás során a lecserélendő elem előtti összes elemet félre kell raknunk, majd a lecserélendő elem utáni farokrész elé be kell fűznünk az új elemet, ezt követően pedig a félrerakott elemeket egyesével be kell fűznünk az új elemet már tartalmazó listarész elé. A lista adott elem utáni farkáról viszont nem készül másolat: az Elixir megosztja a lista farkát a régi és az új elemet tartalmazó lista között.

Alapműveletek

A Kernel modulban definiált alapműveletek:

  • Lista feje, farka, hossza: hd(xs), tl(xs), length(xs).
  • Két lista összefűzése (konkatenációja): xs ++ ys; eredménye xs összes eleme ys elé fűzve, az eredeti sorrendben.
  • Két lista különbsége: xs -- ys; eredménye xs azon elemeinek listája az eredeti sorrendben, amelyek nincsenek benne ys-ben. Az ys minden eleme xs-ből legfeljebb egy előfordulást töröl, balról az elsőt.
  • Tagsági vizsgálat: x in xs eredménye true, ha x eleme xs-nek.
iex> [:a, 'a', [65]] ++ [1+2, 2/1, 'a'] # 65 == ?A
[:a, ~c"a", ~c"A", 3, 2.0, ~c"a"]

iex> Enum.to_list(1..100000) ++ [100001] # rossz hatékonyságú!
[1, 2, 3, 4, 5, 6, 7, 8, ...100001]

iex> [:a, 'a', [65], 'a'] -- ["A", 2/1, 'a']
[:a, ~c"A", ~c"a"]

iex> [:a, 'a', [65], 'a'] -- ["A", 2/1, 'a', :a, :a, :a]
[~c"A", ~c"a"]

iex> [1, 2, 3] -- [1.0, 2] # szigorú egyenlőség: 1 !== 1.0
[1, 3]

iex> "A" in ["A", 2/1, 'a', :a, :a, :a]
true

A ++ az első listát lemásolja, ezért egy hosszú lista végére egyetlen elemet fűzni rossz hatékonyságú. A -- szigorú egyenlőséggel hasonlít, ezért az 1.0 nem törli az 1-et.

A List modul függvényei

  • Lista első / utolsó eleme; ha nincs, default vagy nil: first(list, default \\ nil), last(list, default \\ nil).
  • Egy elem első előfordulásának törlésével kapott lista: delete(list, elem).
  • Adott pozíciójú elem törlésével / beszúrásával / cseréjével kapott lista: delete_at(list, index), insert_at(list, index, value), replace_at(list, index, value), update_at(list, index, fun). Az indexelés 0-tól indul, a negatív index a lista végéről. Az update_at/3 a fun függvényt alkalmazza az adott pozíciójú elemre.
  • Lista kilapításával / kilapítása után a tail elé fűzésével kapott lista: flatten(list), flatten(list, tail).
  • Elem többszörözésével kapott lista: duplicate(elem, n).
  • Listák listájából ennesek listája: zip(list_of_lists); a hosszabb listák végét levágja.
  • Konverziós függvények, pl. List.to_string, Tuple.to_list.
  • Három tesztelő függvény:
    • improper?(list) igaz, ha list nem valódi lista, azaz egy listakonstruktorban a farok nem lista, pl. [1,2|3], [:a,:b|nil];
    • starts_with?(list, prefix) igaz, ha list prefix-szel kezdődik;
    • ascii_printable?(list, n \\ :infinity) igaz, ha list első n karaktere 7 bites ASCII-kódolású és nyomtatható, beleértve a vezérlő karaktereket is (\a, \b, \t, \n, \v, \f, \r, \e).
iex> List.zip([[:a, :b, :c], [:d, :e, :f, :g, :h], [:i, :j, :k, :l]])
[{:a, :d, :i}, {:b, :e, :j}, {:c, :f, :k}] # Listák vége levágva

iex> List.flatten(['abc', [['defgh']], ['ijkl']], 'zzz')
~c"abcdefghijklzzz"

iex> List.starts_with? 'almafa', [?a, ?l]
true

Az Enum modul függvényei

Az Enum modul függvényei is alkalmazhatók listákra:

  • Lista megfordításával / megfordítása után a tail elé fűzésével kapott lista: reverse(list), reverse(list, tail).
  • Lista adott indexű eleme, ha nincs ilyen, default vagy nil: at(list, index, default \\ nil).
  • Lista legkisebb / legnagyobb eleme: min(list, sorter \\ &<=/2, empty_fallback \\ fn -> raise(Enum.EmptyError) end). A max/3 paraméterezése hasonló, &<=/2 helyett &>=/2-vel. Ha list üres, a harmadik paraméterként átadott függvény aktivizálódik.
  • Lista n elemű eleje, n elem utáni farka: take(list, n), drop(list, n). Ha n negatív, az elemeket a lista végéről kezdve emeli le / dobja el.
  • Lista részlistája: slice(list, range) a range tartományba eső indexű elemek listája, slice(list, start, n) a start indextől kezdődő n elemű részlista. Ha range, illetve start negatív, az indexelés a lista végéről indul.
  • Lista kettévágva: split(list, n), ugyanaz, csak rövidebben, mint {(take list, n), (drop list, n)}.
  • Lista rendezve alapértelmezés / fun függvény szerint: sort(list), sort(list, fun).
  • Lista többszörös értékek nélkül: uniq(list).
iex> Enum.reverse 'almafa'
~c"afamla"

iex> Enum.at 'almafa', 2
109

iex> {(Enum.max 'mióta') === ?ó, (Enum.min [], fn -> 0 end)}
{true, 0}

iex> xs='indulakutyasatyukaludni'; {(Enum.take xs,-5), (Enum.drop xs,5)}
{~c"ludni", ~c"akutyasatyukaludni"}

iex> {(Enum.slice xs, 6..10), (Enum.slice xs, -10..-7)}
{~c"kutya", ~c"tyuk"}

iex> xs='indulakutyasatyukaludni'; [(Enum.split xs,5),(Enum.split xs,-5)]
[{~c"indul", ~c"akutyasatyukaludni"}, {~c"indulakutyasatyuka", ~c"ludni"}]

iex> Enum.sort xs
~c"aaaaddiikkllnnsttuuuuyy"

iex> Enum.sort xs, &>=/2
~c"yyuuuuttsnnllkkiiddaaaa"

iex> Enum.uniq xs
~c"indulaktys"

Az Enum.at 'almafa', 2 eredménye a ?m karakterkód, azaz 109.

Az Enum modul függvényei – mind mohó kiértékelésűek – egyéb korlátos, felsorolható (enumerable) adatstruktúrákra is alkalmazhatók. Nem korlátos adatstruktúrákra a Stream modul lusta kiértékelésű függvényeit lehet használni.

Rövid példák

Mintaillesztés, változók kötése és újrakötése listákkal:

iex> xs = [10,20,30] # mintaillesztés és változó kötése értékhez
[10, 20, 30]

iex> x = hd xs       # hd: lista feje
10

iex> rs = tl xs      # tl: lista farka
[20, 30]

iex> {zs,xs} = {xs,[5,6]} # mintaillesztés és változó újrakötése
{[10, 20, 30], [5, 6]}

iex> xs              # xs-hez új értéket kötöttünk
[5, 6]

iex> zs              # xs változott, zs nem!
[10, 20, 30]

iex> ^xs = [7,8,9]   # ^: változó 'fixálása', csak mintaillesztés, kötés nélkül
** (MatchError) no match of right hand side value: ~c"\a\b\t"
iex> hd tl xs       # összetett kifejezés is kiértékelhető
6

iex> tl []           # mi az üres lista farka?
** (ArgumentError) errors were found at the given arguments:
  * 1st argument: not a nonempty list
    :erlang.tl([])

A [7,8,9] lista vezérlő karakterek kódjaiból áll, ezért a hibaüzenet karakterláncként írja ki.

A nyomtatható karakterkódok (7..13, 27, 32..126) egyelemű listái:

iex> for i <- 7..13, do: [i]
[~c"\a", ~c"\b", ~c"\t", ~c"\n", ~c"\v", ~c"\f", ~c"\r"]

iex> for i <- 27..27, do: [i]
[~c"\e"]

iex> for i <- 32..126, do: [i]
[~c" ", ~c"!", ~c"\"", ~c"#", ~c"$", ~c"%", ~c"&", ~c"'", ~c"(", ~c")", ~c"*",
 ~c"+", ~c",", ~c"-", ~c".", ~c"/", ~c"0", ~c"1", ~c"2", ~c"3", ~c"4", ~c"5",
 ~c"6", ~c"7", ~c"8", ~c"9", ~c":", ~c";", ~c"<", ~c"=", ~c">", ~c"?", ~c"@",
 ~c"A", ~c"B", ~c"C", ~c"D", ~c"E", ~c"F", ~c"G", ~c"H", ~c"I", ~c"J", ~c"K",
 ~c"L", ~c"M", ~c"N", ~c"O", ~c"P", ~c"Q", ...]

iex> IO.puts (for i <- 35..126, do: [i])
#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
:ok

iex> IO.inspect (for i <- 32..126, do: [i]), limit: :infinity
[~c" ", ~c"!", ~c"\"", ~c"#", ~c"$", ~c"%", ~c"&", ~c"'", ~c"(", ~c")", ~c"*",
 ~c"+", ~c",", ~c"-", ~c".", ~c"/", ~c"0", ~c"1", ~c"2", ~c"3", ~c"4", ~c"5",
 ~c"6", ~c"7", ~c"8", ~c"9", ~c":", ~c";", ~c"<", ~c"=", ~c">", ~c"?", ~c"@",
 ~c"A", ~c"B", ~c"C", ~c"D", ~c"E", ~c"F", ~c"G", ~c"H", ~c"I", ~c"J", ~c"K",
 ~c"L", ~c"M", ~c"N", ~c"O", ~c"P", ~c"Q", ~c"R", ~c"S", ~c"T", ~c"U", ~c"V",
 ~c"W", ~c"X", ~c"Y", ~c"Z", ~c"[", ~c"\\", ~c"]", ~c"^", ~c"_", ~c"`", ~c"a",
 ~c"b", ~c"c", ~c"d", ~c"e", ~c"f", ~c"g", ~c"h", ~c"i", ~c"j", ~c"k", ~c"l",
 ~c"m", ~c"n", ~c"o", ~c"p", ~c"q", ~c"r", ~c"s", ~c"t", ~c"u", ~c"v", ~c"w",
 ~c"x", ~c"y", ~c"z", ~c"{", ~c"|", ~c"}", ~c"~"]
[~c" ", ~c"!", ~c"\"", ~c"#", ~c"$", ~c"%", ~c"&", ~c"'", ~c"(", ~c")", ~c"*",
 ~c"+", ~c",", ~c"-", ~c".", ~c"/", ~c"0", ~c"1", ~c"2", ~c"3", ~c"4", ~c"5",
 ~c"6", ~c"7", ~c"8", ~c"9", ~c":", ~c";", ~c"<", ~c"=", ~c">", ~c"?", ~c"@",
 ~c"A", ~c"B", ~c"C", ~c"D", ~c"E", ~c"F", ~c"G", ~c"H", ~c"I", ~c"J", ~c"K",
 ~c"L", ~c"M", ~c"N", ~c"O", ~c"P", ~c"Q", ...]

Az alapértelmezett kiírás az 50. elem után levágja a listát (...); a limit: :infinity opcióval az IO.inspect a teljes listát kiírja, majd az IEx a visszaadott értéket ismét, alapértelmezett módon.

Számlista összege

Az fpea.ex fájlba írt sum/1 a lista fejét és farkát a hd és tl függvénnyel kéri el. A , do: jelölés többsoros változata a do ... end; egy sorban a ; választja el a kifejezéseket:

@spec sum(xs::[integer]) :: s::integer
# Az xs számlista összege s
def sum([]), do: 0 # a ", do:" jelölés többsoros változata a "do ... end"
def sum(xs)  do x = hd xs; rs = tl xs; x + sum rs end # újsor helyett ;
iex> c "fpea.ex"
[Fpea]

iex> xs = [10, 20.5, 30.5]
[10, 20.5, 30.5]

iex> Fpea.sum xs
61.0

iex> Fpea.sum tl xs
51.0

iex> Fpea.sum(tl(tl(tl xs)))
0

iex> Fpea.sum "abc" # "abc" !== [97, 98, 99]: "abc" sztring, nem lista
** (ArgumentError) errors were found at the given arguments:
  * 1st argument: not a nonempty list
    :erlang.hd("abc")
    fpea.ex:13: Fpea.sum/1
iex> Fpea.sum 'abc' # 'abc' === [97, 98, 99]: 'abc' karakterkódok listája
294

Két lista összefűzése, megfordítva összefűzése

Az append/2 két listát fűz össze, a revapp/2 az első listát megfordítva fűzi a második elé. Az utóbbi jobbrekurzív: a fejet az akkumulátorként használt ys elé teszi.

@spec append(xs::[any], ys::[any]) :: rs::[any]
# rs az xs lista ys elé fűzésével kapott lista
def append([], ys), do: ys
def append(xs, ys), do: [(hd xs) | (append (tl xs), ys)]
@spec revapp(xs::[any], ys::[any]) :: rs::[any]
# rs a megfordított xs lista ys elé fűzésével kapott lista
def revapp([], ys), do: ys
def revapp(xs, ys), do: revapp (tl xs), [(hd xs) | ys]
iex> c "fpea.ex"
[Fpea]

iex> xs
[10, 20.5, 30.5]

iex> Fpea.append(xs, [:a,:b,:c,:d])
[10, 20.5, 30.5, :a, :b, :c, :d]

iex> Fpea.revapp xs, [:a,:b,:c,:d]
[30.5, 20.5, 10, :a, :b, :c, :d]

Gyakorló feladatok

Írjon többféle megoldást a feladatokra: saját rekurzív függvényekkel, különféle könyvtári függvények – minél több magasabb rendű függvény (Enum.map/2, Enum.filter/2, Enum.reduce/3, List.foldr/3, List.foldl/3 stb.) – felhasználásával, valamint for-komprehenzióval (For-jelölés). Hasonlítsa össze a megoldások futási idejét a benchee-vel, próbáljon hatékonyabb kódot írni, pl. jobbrekurzióval. A specifikációk típus- és függvényspecifikációinak helyességét a dialyzerrel ellenőrizheti (Típusellenőrzés: dialyzer).

Lista kettévágása

Írjon függvényt egy lista kettévágására! Írhat segédfüggvényt, használhat akkumulátort és jobbrekurziót, használhatja a for-jelölést. Ne használja az Enum.split*, Enum.take* és Enum.drop* függvények semelyik változatát a split/2 függvény megvalósítására (de bármilyen könyvtári függvényt használhat az eredmény ellenőrzésére)!

defmodule Split do
  @spec split(xs :: [any()], n :: integer()) :: {ps :: [any()], ss :: [any()]}
  # Az xs lista n hosszú prefixuma (első n eleme) ps, length(xs)-n
  # hosszú szuffixuma (első n eleme utáni része) pedig ss
  def split(xs, n) do
  ...
  end
end
IO.puts(Split.split([10, 20, 30, 40, 50], 3) === {[10, 20, 30], [40, 50]})
IO.puts(IO.inspect(Split.split(~c"egyedem-begyedem", 8)) === Enum.split(~c"egyedem-begyedem", 8))
IO.puts(IO.inspect(Split.split(~c"papás-mamás", 6)) === Enum.split(~c"papás-mamás", 6))
IO.puts(Split.split(~c"nem_vágom", 0) === Enum.split(~c"nem_vágom", 0))
IO.puts(Split.split(~c"", 10) === Enum.split(~c"", 10))
IO.puts(Split.split(~c"", 0) === Enum.split(~c"", 0))
Megoldás

Törzsrekurzív változat: a rekurzív hívás eredményét mintaillesztéssel bontja szét, és a fejet az első rész elé teszi.

defmodule Split1 do
  @spec split(xs :: [any()], n :: integer()) :: {ps :: [any()], ss :: [any()]}
  # Az xs lista n hosszú prefixuma (első n eleme) ps, length(xs)-n
  # hosszú szuffixuma (első n eleme utáni része) pedig ss
  def split(xs, 0), do: { [], xs }
  def split([x|xs], n) do
    { ps, ss } = split(xs, n-1)
    { [x|ps], ss }
  end
  def split([], _), do: { [], [] }
end

Jobbrekurzív változat akkumulátorral; az akkumulátorban fordított sorrendben gyűlnek az elemek, ezért a végén meg kell fordítani:

defmodule Split2 do
  @spec split(xs :: [any()], n :: integer()) :: {ps :: [any()], ss :: [any()]}
  # Az xs lista n hosszú prefixuma (első n eleme) ps, length(xs)-n
  # hosszú szuffixuma (első n eleme utáni része) pedig ss
  def split(xs, n), do: split(xs, n, [])
  defp split([x|xs], n, ps) when n > 0, do: split(xs, n-1, [x|ps])
  defp split(xs, 0, ps), do: { Enum.reverse(ps), xs }
  defp split([], _, ps), do: { Enum.reverse(ps), [] }
end

Mindkét változattal minden teszt true-t ír ki. Az IO.inspect a {~c"egyedem-", ~c"begyedem"} és a {[112, 97, 112, 225, 115, 45], [109, 97, 109, 225, 115]} párt is kiírja (az á kódja, 225, nem nyomtatható ASCII-kód, ezért a második pár számokkal jelenik meg). A mérés:

ls = Range.to_list(0..100_000)
n = 70_000
Benchee.run(%{
    "split1" => fn -> Split1.split(ls, n) end,
    "split2" => fn -> Split2.split(ls, n) end,
    "enumSplit" => fn -> Enum.split(ls, n) end
})

Lista adott feltételt kielégítő elemeiből álló prefixuma

Írjon függvényt egy lista adott feltételt kielégítő prefixumának előállítására! Írhat segédfüggvényt, használhat akkumulátort és jobbrekurziót, használhatja a for-jelölést. Ne használja az Enum.split*, Enum.take* és Enum.drop* függvények semelyik változatát a takewhile/2 függvény megvalósítására (de bármilyen könyvtári függvényt használhat az eredmény ellenőrzésére)!

defmodule Take do
  @spec takewhile(xs :: [any()], f :: (any() -> boolean())) :: rs :: [any()]
  def takewhile(xs, f) do
  ...
  end
end
IO.puts(Take.takewhile(~c"álom12" ++ [:a] ++ ~c"34brigád", &is_integer/1) === ~c"álom12")
IO.puts(Take.takewhile(~c"abcdefghijkl", fn x -> x < ?f end) === ~c"abcde")
Megoldás
defmodule Take1 do
  @spec takewhile(xs :: [any()], f :: (any() -> boolean())) :: rs :: [any()]
  def takewhile([x|xs], f) do
    if f.(x) do
      [x|takewhile(xs, f)]
    else
      []
    end
  end
  def takewhile([], _f), do: []
end
defmodule Take2 do
  @spec takewhile(xs :: [any()], f :: (any() -> boolean())) :: rs :: [any()]
  def takewhile(xs, f), do: takewhile(xs, f, [])
  defp takewhile([x|xs], f, acc) do
    if f.(x) do
      takewhile(xs, f, [x|acc])
    else
      Enum.reverse(acc)
    end
  end
  defp takewhile([], _f, acc), do: Enum.reverse(acc)
end

Mindkét változattal mindkét teszt true-t ír ki. A mérés:

ls = Range.to_list(0..100_000)
f = fn x -> x < 70_000 end
Benchee.run(%{
    "take1" => fn -> Take1.takewhile(ls, f) end,
    "take2" => fn -> Take2.takewhile(ls, f) end,
    "enumTake" => fn -> Enum.take_while(ls, f) end
})

Lista minden n-edik elemének kihagyásával létrejövő lista

Írjon függvényt egy olyan lista létrehozására, amelyből a paraméterként átadott lista minden n-edik eleme, a nulladiktól kezdve, ki van hagyva! (A listák indexelése 0-val kezdődik.) Ne használja az Enum.split*, Enum.take* és Enum.drop* függvények semelyik változatát a dropevery/2 függvény megvalósítására (de bármilyen könyvtári függvényt használhat az eredmény ellenőrzésére)! Tipp: ha nem ír segédfüggvényt, és nincs más ötlete, használhatja a for-jelölést, generátoraként a ../2, szűrőjeként a rem/2 függvényt, a listaelemek elérésére pedig az Enum.at/2 függvényt.

defmodule Drop do
  @spec dropevery(xs :: [any()], n :: integer()) :: rs :: [any()]
  def dropevery(xs, n) do
    ...
  end
end
ls = ~c"álom" ++ [:a] ++ ~c"egybrigád"
IO.inspect(Drop.dropevery(ls, 4) === ~c"lomegyrigd")
ls = ~c"abcdefghijkl"
IO.inspect(Drop.dropevery(ls, 5) === ~c"bcdeghijl")
ls = ~c"1234567"
IO.inspect(Drop.dropevery(ls, 2) === ~c"246")
ls = []
IO.inspect(Drop.dropevery(ls, 3) === [])
ls = [:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m]
IO.inspect(Drop.dropevery(ls, 3) === [:b, :c, :e, :f, :h, :i, :k, :l])

Lista egyre rövidülő szuffixumainak listája

Írjon olyan függvényt, amely egy xs lista elemeiből álló részlistákat ad eredményül: az első részlista maga az xs legyen, a második az xs második, azaz 1 indexű elemétől a végéig tartson, a harmadik az xs harmadik, azaz 2 indexű elemétől a végéig, és így tovább; az utolsó részlista az üres lista legyen. Tipp: a tails függvény eredménye listák listája, így üres listára alkalmazva olyan lista a visszatérési értéke, amelynek egyetlen eleme van, az üres lista.

defmodule Tails do
  @spec tails(xs :: [any()]) :: zss :: [[any()]]
  # Az xs lista egyre rövidülő szuffixumainak listája zss
  def tails(xs) do
  ...
  end
end
IO.puts(Tails.tails([1, 4, 2]) === [[1, 4, 2], [4, 2], [2], []])
IO.puts(Tails.tails([:a, :b, :c, :d]) === [[:a, :b, :c, :d], [:b, :c, :d], [:c, :d], [:d], []])
IO.puts(Tails.tails([:z]) === [[:z], []])
IO.puts(Tails.tails([]) === [[]])

Lista egymást követő két-két eleméből képzett párok listája

Írjon olyan rekurzív függvényt, amely egy lista 1. és 2., 3. és 4., 5. és 6. stb. elemeiből képzett párok listáját adja eredményül! Ha a listának kettőnél kevesebb eleme van, az eredmény az üres lista legyen. Ha a listának páratlan számú eleme van, az utolsót dobja el.

defmodule Pairs do
  @spec pairs(xs::[any()]) :: zs :: [any()]
  def pairs(xs), do: ...
end
zs = [{1,2}, {3,4}, {5,6}, {7,8}, {9,10}, {11,12}, {13,14}, {15,16}, {17,18}, {19,20}]
(1..20 |> Range.to_list() |> Pairs.pairs() == zs) |> IO.puts
zs = [{1,2}, {3,4}, {5,6}, {7,8}, {9,10}]
(1..11 |> Range.to_list() |> Pairs.pairs() == zs) |> IO.puts
([1] |> Pairs.pairs() == []) |> IO.puts

Listában párosával előforduló elemek listája

Írjon olyan rekurzív függvényt, amely egy lista elemei közül az összes olyat visszaadja az eredménylistában, amelyet vele azonos értékű elem követ: például két egymást követő, azonos értékű elemből egyet, három egymást követőből kettőt stb. Írhat

  1. segédfüggvényt és akkumulátort nem használó, valamint
  2. akkumulátoros segédfüggvényt használó változatot.

Próbáljon meg egyéb változatokat is írni, pl.

  1. a for-jelöléssel és az Enum.zip/1 függvény alkalmazásával.
defmodule Parosan do
  @spec parosan(xs :: [any()]) :: rs :: [any()]
  # Az xs lista összes olyan elemének listája rs, amely
  # után vele azonos értékű elem áll
  def parosan xs do
  ...
  end
end
IO.puts(Parosan.parosan([:a, :a, :a, 2, 3, 3, :a, 2, :b, :b, 4, 4]) === [:a, :a, 3, :b, 4])
IO.puts(Parosan.parosan([:a, 2, 3, :a, 2, :b, 4]) === [])
IO.puts(Parosan.parosan([:a]) === [])
IO.puts(Parosan.parosan([]) === [])

Lista elején azonos értékű elemekből álló részlisták listája

Írjon függvényt olyan nem üres, folytonos részlisták előállítására, amelyek egy lista elejétől indulnak, és velük azonos értékű és elemszámú részlisták követik őket! Példák:

  • [1,1] ⟶ [[1]]: a lista elején kezdődő [1] részlistát az [1] részlista követi.
  • [1,1,1] ⟶ [[1]]: a lista elején kezdődő [1] részlistát az [1] részlista követi, de a lista elején kezdődő [1,1] részlistát már nem követi azonos értékű részlista.
  • [1,1,0,0] ⟶ [[1]]: a lista elején kezdődő [1] részlistát az [1] részlista követi, de a [0] részlista már nem a lista elején kezdődik.
  • [1,1,1,1] ⟶ [[1], [1, 1]]: a lista elején kezdődő [1] és [1, 1] részlistákat azonos értékű részlisták követik.
  • [1,1,1,1,1] ⟶ [[1], [1, 1]]: a lista elején kezdődő [1] és [1, 1] részlistákat azonos értékű részlisták követik, de a lista elején kezdődő [1, 1, 1] részlistát már nem követi azonos értékű részlista.
  • [1,1,0,1,1] ⟶ [[1]]: a lista elején kezdődő [1] részlistát az [1] részlista követi, de nincs több olyan ismétlődő részlista, amely a lista elején kezdődne.
  • [1,1,1,1,1,1] ⟶ [[1], [1, 1], [1, 1, 1]]: a lista elején kezdődő [1], [1, 1] és [1, 1, 1] részlistákat azonos értékű részlisták követik.

Lehetőleg írjon többféle változatot, pl. akkumulátort használó és nem használó, könyvtári függvényeket alkalmazó és nem alkalmazó változatot. Tipp: ha nincs jobb ötlete, használja az Enum.take/2 és Enum.drop/2 függvényt egy segédfüggvényben.

defmodule Repeated1 do
  @spec repeated(xs :: [any()]) :: rs :: [any()]
  def repeated(xs) do
  ...
  end
end
(Repeated1.repeated([1,1]) == [[1]]) |> IO.inspect
(Repeated1.repeated([1,1,1]) == [[1]]) |> IO.inspect
(Repeated1.repeated([1,1,0,0]) == [[1]]) |> IO.inspect
(Repeated1.repeated([1,1,1,1]) == [[1], [1, 1]]) |> IO.inspect
(Repeated1.repeated([1,1,1,1,1]) == [[1], [1, 1]]) |> IO.inspect
(Repeated1.repeated([1,1,0,1,1]) == [[1]]) |> IO.inspect
(Repeated1.repeated([1,1,1,1,1,1]) == [[1], [1, 1], [1, 1, 1]]) |> IO.inspect

(Repeated1.repeated([:a, :a, :a, 2, 3, 3, :a, :b, :b, :b, :b]) === [[:a]]) |> IO.inspect
(Repeated1.repeated([:a, :b, :b, :b, :b]) === []) |> IO.inspect
(Repeated1.repeated([:b, :b, :b, :b]) === [[:b], [:b, :b]]) |> IO.inspect
(Repeated1.repeated([]) === []) |> IO.inspect

Listában párosával előforduló részlisták listája

Írjon függvényt egy lista összes olyan nem üres, folytonos részlistájának előállítására, amelyet vele azonos értékű részlista követ! Lehetőleg írjon többféle változatot. Tipp: használja a Tails.tails/1 függvényt; felhasználhatja a Repeated1.repeated/1 vagy egy Repeated2.repeated/1 függvényt is.

defmodule Stammering do
  @spec stammering(xs :: [any()]) :: zss :: [[any()]]
  # zss az xs lista összes olyan nemüres, folytonos részlistájából
  # álló lista, amelyet vele azonos értékű részlista követ
  def stammering(xs) do
    ...
  end
end
(Stammering.stammering([:a, :a, :a, 2, 3, 3, :a, :b, :b, :b, :b]) ===
  [[:a], [:a], [3], [:b], [:b, :b], [:b], [:b]]) |> IO.puts()
IO.puts(Stammering.stammering([]) === [])
IO.puts(Stammering.stammering([:a]) === [])
IO.puts(Stammering.stammering([:a, :a]) === [[:a]])
IO.puts(Stammering.stammering([:a, :b]) === [])

Forrás: dp26a-fp2ea.pdf (28–38. dia), dp26a-fp2gy-megoldasok.livemd, dp26a-fp3gy.livemd

Műveletek sztringeken

A sztringek nem listák az Elixirben – még csak nem is kollekciók –, de mivel kényelmes listaszerűen kezelni őket, a String modulban vannak ezt lehetővé tevő függvények.

Kódpont és graféma

Két fogalmat kell megkülönböztetnünk:

  • A kódpont (code point) egyetlen Unicode-karakter, amelyet egy vagy több bájt ábrázol.

    iex> {byte_size("á"), String.length("á")}
    {2, 1}
    
  • A graféma (grapheme cluster, röviden grapheme) egy vagy több kódpont, amely egyetlen karakternek látszik. Az alábbi str két kódpontból áll: az e betűből és az utána álló, az előző betűre ráíródó kalapból (U+0302, Combining Circumflex Accent); együtt egyetlen grafémát, az ê-t alkotják.

    iex> str = "\u0065\u0302"; {byte_size(str), String.length(str)}
    {3, 1}
    
    iex> "u\u0302"# U+0302 Combining Circumflex Accent
    "û"
    
    iex> String.codepoints(str)
    ["e", "̂"] # Két egykarakteres sztring van a listában.
    
    iex> String.graphemes(str)
    ["ê"] # Egyetlen egykarakteres sztring van a listában.
    

A String.length/1 a grafémákat számolja, a byte_size/1 a bájtokat.

A String modul függvényei

  • <> a konkatenálás jele: "ál"<>"om".
  • string első / utolsó grafémája, grafémáinak száma: first(string), last(string), length(string).
  • Graféma a string pos pozíciójában: at(string, pos).
  • Tartalmazza-e string a patts legalább egy elemét: contains?(string, patts).
  • string elejéről / végéről / mindkettőről levágja a szóköz-jellegű (whitespace) UTF-8 karaktereket: trim_leading(string), trim_trailing(string), trim(string).
iex> str = " "<>" "<>"kutyafüle"<>" "; String.at(str, 8)
"ü"

iex> String.contains?(str, "ü")
true

iex> String.contains?(str, ["ü","ty","n"])
true

iex> String.contains?(str, ["n"])
false

iex> {String.trim_leading(str), String.trim(str)}
{"kutyafüle ", "kutyafüle"}

Mint a listánál, csak graféma-elemekkel: slice(string, range), slice(string, start, n), duplicate(string, n), reverse(string), starts_with?(string, prefix).

A sztring két darabra vágva adott pozícióban: split_at(string, pos) (párt ad eredményül); több darabra szabdalva az UTF-8 whitespace-ek mentén: split(string) (listát ad eredményül). A split/1 a vezető és záró UTF-8 whitespace-eket figyelmen kívül hagyja.

iex> str = "indulakutyasatyukaludni"; String.reverse(str)
"indulakuytasaytukaludni"

iex> String.starts_with?(str, "indula")
true

iex> {String.slice(str, 6..10), String.slice(str, -10..-7)}
{"kutya", "tyuk"}

iex> String.duplicate("indul ", 3)
"indul indul indul "

iex> String.split_at(" "<>" "<>"kutya füle macska farka"<>" ", 12)
{"  kutya füle", " macska farka "} # párt ad eredményül

iex> String.split(" "<>" "<>"kutya füle macska farka"<>" ")
["kutya", "füle", "macska", "farka"] # listát ad eredményül

Forrás: dp26a-fp2ea.pdf (41–43. dia)

For-jelölés

Gyűjtemények (kollekciók) kezelésére a for-jelölést használjuk; az angol elnevezést teljesen átvéve for-komprehenziónak (for-comprehension) is nevezik. A komprehenzió ma már sokféle programozási nyelvben megtalálható (https://en.wikipedia.org/wiki/List_comprehension); a for-jelölés részletes összefoglalója: https://www.mitchellhanberg.com/the-comprehensive-guide-to-elixirs-for-comprehension/.

Alakja és jelentése

Az alábbiakban a szögletes zárójelek jelentése: opcionális.

for q₁[, q₂, …, qₙ][, into: coll], do: exp

ahol

  • a qᵢ
    1. pattern <- list alakú generátor, vagy
    2. predikátum (igazságérték-eredményű függvény, feltétel);
  • legalább egy qᵢ-nek generátornak kell lennie;
  • a pattern mintának illeszkednie kell a list lista kiválasztandó elemeire, és ki kell elégítenie az adott pattern <- list generátortól jobbra álló összes qᵢ predikátumot;
  • az exp tetszőleges, a pattern mintától függő vagy nem függő kifejezés;
  • a generátorban a minta előállítására lista helyett más felsorolható kollekciót, leggyakrabban tartomány típusú értéket is megadhatunk;
  • az opcionális into: után álló coll-lal megadhatjuk, hogy milyen típusú felsorolható kollekciót hozzon létre a for-jelölés; ha elhagyjuk, alapértelmezés szerint lista jön létre. coll-ként üres kollekciót kell megadni: "" (sztring), %{} (szótár), [] (lista), <<>> (bináris);
  • a for-jelölésben definiált változók lokálisak.

A for-jelölés értéke az összes olyan exp kifejezés kollekciója, amelyre a mintaillesztés sikerült és a predikátumok teljesültek. Az eddig bemutatott változat tehát egy vagy több kollekció elemein műveletek elvégzésére és/vagy bizonyos elemek szűrésére használható (vö. Enum.map/2, Enum.filter/2).

Ha a generátor mintája egy elemre nem illeszkedik, az az elem egyszerűen kimarad az eredményből, és a kiértékelés a következő elemmel folytatódik; a mintaillesztés így külön szűrőfeltétel nélkül is szűr.

Több generátor esetén a jobbra álló generátor fut gyorsabban: a bal oldali generátor minden eleméhez végigmegy a jobb oldali összes elemén (keresztszorzat).

A uniq: opció

for q₁[, q₂, …, qₙ][, into: coll], uniq: true|false, do: exp

A uniq: true opció hatására az előállított gyűjteménybe csak egymástól különböző értékek kerülnek be. Csak lista, sztring és bináris esetén van értelme használni, hiszen a szótárban a kulcsok sohasem ismétlődhetnek.

A reduce: opció

for q₁[, q₂, …, qₙ], reduce: acc₀ do acc -> fun(pat, acc) end

  • A reduce: opcióval a for-jelölés nem az Enum.map/2-t, hanem az Enum.reduce/3-at váltja ki.
  • Az acc₀ az eredményt gyűjtő akkumulátor kezdőértéke.
  • A do ... end között egy névtelen függvényt kell megadni (az fn és a hozzá tartozó end nélkül!), amelynek egyetlen argumentuma az acc akkumulátor (a neve bármi lehet). A törzsében olyan kétargumentumú függvényt vagy operátort kell használni, amelynek egyik argumentuma a generátorban használt pat minta, a másik pedig ugyancsak az acc akkumulátor. Az argumentumok sorrendjének hatása lehet az eredményre!

Kis példák

A kommentek az egyes for-kifejezéseknek megfelelő matematikai halmazjelölést mutatják:

iex> for x <- 1..6 // 2, do: x     # { x | x ∈{1, 3, 5} }
[1, 3, 5]

iex> for x <- [1,2,3], do: 2*x+1   # { 2 · x + 1 | x ∈{1, 2, 3} }
[3, 5, 7]

iex> for x <- 1..9, rem(x, 2) === 0, x > 2, do: 2*x
[8, 12, 16]

iex> for {k,v} <- [egy: 1, két: 2, há: 3], into: %{}, do: {k,v}
%{egy: 1, két: 2, há: 3}

iex> for {k,v} <- %{egy: 1, két: 2, há: 3}, into: [], do: {k,v}
[egy: 1, két: 2, há: 3]

iex> for c <- [?c, ?s, ?ó, ?k, ?a], into: "", do: <<c>>
<<99, 115, 243, 107, 97>>

iex> for c <- [?c, ?s, ?o, ?k, ?a], into: "", do: <<c>>
"csoka"

iex> for x <- 0..-2 // -2, y <- 1..x, do: {x,y}
[{0, 1}, {0, 0}, {-2, 1}, {-2, 0}, {-2, -1}, {-2, -2}]

iex> for x <- 0..-2 // -2, y <- 1..x, xy = {x,y}, do: xy
[{0, 1}, {0, 0}, {-2, 1}, {-2, 0}, {-2, -1}, {-2, -2}]

iex> for x <- 2..4, rem(x,3) !== 0, y <- 1..3, x > y, do: {x,y}
[{2, 1}, {4, 1}, {4, 2}, {4, 3}]

Néhány megjegyzés a példákhoz:

  • A <<c>> egyetlen bájtot állít elő. Az ó kódja (243) egy bájton nem érvényes UTF-8 karakter, ezért az eredmény nem írható ki sztringként, csak bájtokként; ékezet nélkül "csoka" lesz az eredmény.
  • A 0..-2 // -2 tartomány elemei 0 és -2; az 1..x tartomány a függő generátorban x értékétől függ (1..0, illetve 1..-2, csökkenő tartományok).
  • Az xy = {x,y} qᵢ-ként mintaillesztéssel köt változót, amely a do: utáni kifejezésben használható.
  • Az utolsó példában a rem(x,3) !== 0 szűrő az első generátorhoz, az x > y a másodikhoz tartozik.

További példák:

iex> for i <- 1..3, j <- 2..1//-1, do: {i,j} # keresztszorzatok listája
[{1, 2}, {1, 1}, {2, 2}, {2, 1}, {3, 2}, {3, 1}]

iex> for i <- 1..3, do: (for j <- 2..1//-1, do: {i,j}) # listák listája
[[{1, 2}, {1, 1}], [{2, 2}, {2, 1}], [{3, 2}, {3, 1}]]

iex> for i <- 1..3, j <- 2..1//-1, into: %{}, do: {i,j} # ismétlődő kulcs felülír!
%{1 => 1, 2 => 1, 3 => 1}

iex> for i <- 1..3, j <- 2..1//-1, into: %{}, do: {i+j*4,j} # így a kulcsok egyediek
%{5 => 1, 6 => 1, 7 => 1, 9 => 2, 10 => 2, 11 => 2}

iex> for _i <- 1..5, into: "", do: "1" # többszörözve
"11111"

iex> for _i <- 1..5, into: "", uniq: true, do: "1" # azonosak csak egyszer
"1"

iex> for _i <- 1..5, into: <<>>, do: <<1::size 1>> # 5 bit (0b11111), 1 byte
<<31::size(5)>>

iex> for _i <- 1..5, into: <<>>, uniq: true, do: <<1::size 1>> # 1 bit (0b1), 1 byte
<<1::size(1)>>

iex> for _i <- 1..5, into: <<>>, do: <<1::size 2>> # 10 bit (0b01010101, 0b01), 2 byte
<<85, 1::size(2)>>

iex> for _i <- 1..5, into: <<>>, uniq: true, do: <<1::size 2>> # 2 bit (0b01), 1 byte
<<1::size(2)>>

Gyakorló feladatok

Listában kulcs-érték párokban előforduló értékek listája

Egy listában többféle típusú és szerkezetű elem fordul elő, köztük {:v, v} párok is, ahol a párok első tagja a :v atom, második tagja az itt v-vel jelölt, tetszőleges érték. Írjon olyan rekurzív függvényt, amely a lista elemei közül az összes {:v, v} párban található v értéket visszaadja az eredménylistában! Írhat segédfüggvényt és akkumulátort nem használó, valamint akkumulátoros segédfüggvényt használó változatot. Feltétlenül írjon egyéb változatot is for-jelöléssel (meg fog lepődni!).

Megoldás

A for-jelöléses változat a feladatlapon szerepel. Ha a generátorban a mintaillesztés sikertelen, az adott érték nem kerül be az eredménylistába, és a kiértékelés a következő listaelemmel folytatódik; ezért külön szűrőfeltétel nélkül, nagyon egyszerűen megvalósítható az elvárt működés.

defmodule Ertekek do
  @spec ertekek(xs :: [{:v::atom(), v::any()} | any()]) :: vs :: [any()]
  # Az xs lista elemei közül a {:v, v} mintára illeszkedő
  # párok 2. tagjából képzett lista vs
  def ertekek(xs), do: for({:v, v} <- xs, do: v)
end
Ertekek.ertekek([:alma, {:s, 3}, {:v, 1}, 3, {:v, 2}]) === [1, 2]

Természetes szám valódi osztói

Egy természetes szám valódi osztóinak nevezzük az 1-en és önmagán kívüli pozitív osztóit. Írjon olyan kifejezést vagy függvényt a for-jelölés felhasználásával, amely egy listában visszaadja a paraméterként átadott természetes szám valódi osztóit! Írjon többféle megoldást, használjon magasabb rendű függvényeket, definiálhatja a függvényt a def kulcsszóval is.

Tipp: egy természetes szám valódi osztóit a legegyszerűbben úgy találhatja meg, hogy a -t rendre elosztja a és közötti egészekkel, és ha az egészosztásnak nincs maradéka, akkor az adott szám osztója -nak.

# @spec proper_divisors(i :: integer()) :: ds :: [integer()]
# Az i természetes szám valódi osztóinak listája ds
proper_divisors = ...
(proper_divisors.(10) === [2, 5]) |> IO.inspect(charlists: :as_list)
(proper_divisors.(23) === []) |> IO.inspect(charlists: :as_list)
(proper_divisors.(48) === [2, 3, 4, 6, 8, 12, 16, 24]) \
 |> IO.inspect(charlists: :as_list)
(proper_divisors.(128) === [2, 4, 8, 16, 32, 64]) \
 |> IO.inspect(charlists: :as_list)

Összetett számok

Összetett számnak nevezzük azt a természetes számot, amelynek van valódi osztója. A legkisebb összetett szám a 4. Írjon olyan kifejezést vagy függvényt a for-jelölés felhasználásával, amely egy listában visszaadja a paraméterként átadott természetes számnál nem nagyobb összes összetett számot, 4-től kezdve! Felhasználhatja a valódi osztókat előállító függvényt, amelyet az előbb írt meg. Írjon többféle megoldást, használjon magasabb rendű függvényeket, definiálhatja a függvényt a def kulcsszóval is.

# @spec composite_numbers(i :: integer()) :: ns :: [integer()]
# Az i-nél nem nagyobb összetett számok listája ns
composite_numbers =
(composite_numbers.(11) === [4, 6, 8, 9, 10]) |> IO.inspect(charlists: :as_list)
(composite_numbers.(17) === [4, 6, 8, 9, 10, 12, 14, 15, 16]) \
 |> IO.inspect(charlists: :as_list)

Hatékonyabb megoldás. Írjon olyan megoldást az előző feladatra, amely nem állítja elő a valódi osztók listáját! Az természetes szám összetett, ha osztható

  1. a és közötti prímszámok bármelyikével;
  2. -vel vagy a és közötti páratlan egészek bármelyikével.

Az első módszer gyorsabb, de ha a prímszámok előállítása nem triviális, a második módszer is elég hatékony. További hatékonyságnövelő lehetőségekről olvashat pl. itt: https://en.wikipedia.org/wiki/Primality_test.

composite_numbers_faster = &for i <- 4..&1, composite?.(i), do: i
(composite_numbers_faster.(11) === [4, 6, 8, 9, 10] ) \
 |> IO.inspect(charlists: :as_list)
(composite_numbers_faster.(21) === [4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20, 21])\
 |> IO.inspect(charlists: :as_list)

Tipp: bontsa a megoldást lépésekre: állítsa elő a szám összetett voltának vizsgálatához használandó osztókat; állapítsa meg, hogy a szám összetett-e; állítsa elő az összetett számok listáját a kért tartományban. Fontolja meg az alábbi függvények használatát:

  • A Kernel modulban definiált ..///3 operátor operandusainak egész számoknak kell lenniük, ezért ha a felső határ beállítására a :math.sqrt/1 függvényt használja, egész számmá kell konvertálni (vö. round/1, floor/1, ceil/1).
  • Az Enum.to_list/1 tetszőleges felsorolható sorozatot listává alakít, így tartomány típusú értéksorozatot is.
  • Az Enum.reduce/3 egy lista összes elemére alkalmaz egy kétoperandusú függvényt. Ha pl. egy szám összetett voltát akarjuk vele vizsgálni, olyan függvényt kell átadnunk paraméterként, amely az adott számnak a sorozat egy elemével való oszthatósága esetén igaz, egyébként hamis értékkel tér vissza.
# @spec probes(i :: integer()) :: ps :: [integer()]
# A 2-t, továbbá a 3 és a :math.sqrt(i) közé eső egészeket tartalmazó lista ps
probes = ...
# @spec composite?(i :: integer()) :: b :: boolean()
# b igaz, ha i összetett szám
composite? = ...
# @spec composite_numbers_faster(i :: integer()) :: ns :: [integer()]
# Az i-nél nem nagyobb összetett számok listája ns
composite_numbers_faster = ...

Ha a javaslatot megfogadta, a megoldást három kis – esetleg több – lépésre bontotta, mindegyikre egy-egy névtelen (de változóhoz kötött) függvényt írt, így ezeket könnyebb volt megérteni, és a helyességüket könnyebb volt belátni. Mivel a funkcionális nyelvekben a függvényhívás nagyon hatékonyan van megoldva, ez a jó és követendő gyakorlat: minden kicsit is összetett függvényt több egyszerűbb függvényből rakjunk össze, és ahol csak lehet, használjuk a hatékonyan megvalósított és sokat tesztelt könyvtári függvényeket.

Forrás: dp26a-fp3ea.pdf (10–14. dia), dp26a-fp3gy.livemd

Problémamegoldási technikák

Ez a rész három általános technikát mutat be Elixir-példákon:

  • a dinamikus programozást, a Fibonacci-számok kiszámításának hat változatán;
  • a csúszóablakos technikát, egy számlista maximális összegű folytonos részlistáinak előállításán;
  • a kihagy-bevesz rekurziót, egy lista elemeinek kombinációin és egy összeg két részre osztásán.

A megoldások futási idejét a benchee-vel mérjük (Projektek, mérés, típusellenőrzés).

Forrás: dp26a-fp2ea.pdf (46. dia), dp26a-fp3ea.pdf (4., 6. dia)

Dinamikus programozás: Fibonacci-számok

Dinamikus programozás

A dinamikus programozás optimalizálási feladatok megoldására használható módszer, Richard Bellman fejlesztette ki 1950 környékén. Lényege:

  • Az eredetihez hasonló részfeladatokat tűzünk ki, amelyek akár általánosabbak is lehetnek az eredeti feladatnál.
  • A részfeladatokat általában kisebb inputra oldjuk meg először.
  • A részfeladatok eredményét eltároljuk.
  • A feladatokat úgy rendezzük sorba, hogy a későbbi feladatok megoldásánál fel tudjuk használni a korábbiak eredményét.
  • Az első néhány részfeladat legyen önmagában is könnyen megoldható.
  • A későbbi feladatok eredményét a korábbiak eredményét felhasználva kapjuk. A részfeladatokat úgy határozzuk meg, hogy ez könnyen menjen.
  • Az összes részfeladat megoldásából már könnyen megkapható az eredeti feladat megoldása.

A Fibonacci-számok

A Fibonacci-számok jól ismert matematikai definíciója:

A kiszámításukra hat változatot nézünk meg:

  • elágazó rekurzióval;
  • memoizálással (dinamikus programozás felülről lefelé haladva), Elixir Map-pel;
  • táblázattal (dinamikus programozás alulról felfelé haladva), Elixir Map-pel;
  • táblázattal, Erlang :array-jel;
  • táblázattal, Elixir List-tel;
  • az (n-2)-edik (prev) és az (n-1)-edik (curr) Fibonacci-szám nyilvántartásával.

Elágazó rekurzióval

A naiv rekurzív megoldás a matematikai definíciót követi:

defmodule Fib do

  # Tree recursion
  # O(2^n) futási idő, O(n) tárhely
  @spec fib(i :: integer()) :: n :: integer()
  # n az i-edik Fibonacci-szám
  def fib(0), do: 0
  def fib(1), do: 1
  def fib(i), do: fib(i-1) + fib(i-2)

end
Fib.fib(23) #|> IO.inspect()
28657

Az -edik Fibonacci-szám meghatározása elágazó rekurzióval nagyon rossz hatékonyságú, mert a két elágazó ágat minden egyes rekurzív lépésben újra meg újra teljesen be kell járni, azaz az -ediknél kisebb Fibonacci-számokat újra és újra ki kell számolni. A fib 5 hívási fája:

graph TD
  A["fib 5"] --> B["fib 4"]
  A --> C["fib 3"]
  B --> D["fib 3"]
  B --> E["fib 2"]
  D --> F["fib 2"]
  D --> G["fib 1"]
  F --> H["fib 1"]
  F --> I["fib 0"]
  E --> J["fib 1"]
  E --> K["fib 0"]
  C --> L["fib 2"]
  C --> M["fib 1"]
  L --> N["fib 1"]
  L --> O["fib 0"]
  H --> H1(["1"])
  I --> I1(["0"])
  G --> G1(["1"])
  J --> J1(["1"])
  K --> K1(["0"])
  N --> N1(["1"])
  O --> O1(["0"])
  M --> M1(["1"])

A fib 3 kétszer, a fib 2 háromszor számolódik ki. A kiértékelés a fát mélységben, balról jobbra járja be: a gyökértől a bal szélen indul lefelé, és a jobb szélen tér vissza a gyökérhez. A levelek értéke balról jobbra 1, 0, 1, 1, 0, 1, 0, 1, összegük 5. A futási idő -ben exponenciális, a részeredményeket viszont csak az éppen bejárt ág mentén, az egyre mélyülő veremben kell tárolni, ezért a tárigény a fa mélységével, -vel arányos.

Memoizálással (felülről lefelé)

A memoizálás a már kiszámított Fibonacci-számokat egy szótárban (mem) tárolja, és mielőtt kiszámítana egy értéket, megnézi, nincs-e már meg. A fib_m/2 a kért szám mellett a bővített szótárt is visszaadja, hogy a következő hívás felhasználhassa:

defmodule FibM do

  # Memoization (top down) – dinamikus programozás
  # O(n) futási idő, O(n) tárhely
  @spec fib_mem(i :: integer()) :: n :: integer()
  # n az i-edik Fibonacci-szám
  def fib_mem(i), do: fib_m(i, %{0 => 0, 1 => 1}) |> elem(0)

  @type mem() :: %{index :: integer() => value :: integer()}
  @spec fib_m(i :: integer(), mem :: mem()) :: {n :: integer(), uj_mem :: mem()}
  # n az i-edik Fibonacci-szám

  def fib_m(i, mem) do
    case mem[i] do # case nem váltható ki mintaillesztéssel
      nil ->
        {prev, memp} = fib_m(i-2, mem)
        {curr, memc} = fib_m(i-1, memp)
        val = prev + curr
        {val, Map.put(memc, i, prev+curr)}
      val ->
        {val, mem}
      end
  end

end
FibM.fib_mem(63) #|> IO.inspect()
6557470319842

A case itt nem váltható ki a függvényfejben végzett mintaillesztéssel, mert azt kell eldönteni, hogy a mem[i] kifejezés értéke nil-e, a kifejezés pedig nem lehet minta.

A memoizálás lépései követhetők, ha a fib_m/2 eredményéből nem a számot, hanem a szótárt vesszük ki:

# Módosított változat a memoizálási lépések követésére
defmodule FibMm do
  @spec fib_mem(i :: integer()) :: mem :: %{integer() => integer()}
  def fib_mem(i), do: FibM.fib_m(i, %{0 => 0, 1 => 1}) |> elem(1)
end
FibMm.fib_mem(5)
%{0 => 0, 1 => 1, 2 => 1, 3 => 2, 4 => 3, 5 => 5}

Táblázattal (alulról felfelé), Elixir Map-pel

A táblázatos megoldás a kisebb indexektől halad felfelé: a j-edik elemet a j-2-edik és j-1-edik elemből számítja ki, amíg el nem éri az i-ediket:

defmodule FibT do

  # Tabulation (bottom-up) – dinamikus programozás
  # O(n) futási idő, O(n) tárhely
  @spec fib_tab(i :: integer()) :: n :: integer()
  # n az i-edik Fibonacci-szám
  def fib_tab(i), do: fib_t(i, 2, %{0 => 0, 1 => 1})

  @type tab() :: %{index :: integer() => value :: integer()}
  @spec fib_t(i :: integer(), j :: integer(), tab :: tab()) :: n :: integer()
  # n az i-edik Fibonacci-szám
  def fib_t(i, j, tab) when i < j, do: tab[i]
  def fib_t(i, j, tab) do
    tab0 = Map.put(tab, j, tab[j-2] + tab[j-1])
    fib_t(i, j+1, tab0)
  end
end
FibT.fib_tab(63) #|> IO.inspect()
6557470319842

Táblázattal, Erlang :array-jel

defmodule FibAerl do

  # Tabulation (bottom-up) – dinamikus programozás
  # O(n) futási idő, O(n) tárhely
  # Erlang :array
  @spec fib_tab(i :: integer()) :: n :: integer()
  # n az i-edik Fibonacci-szám
  def fib_tab(i), do: fib_t(i, 2, :array.set(1,1,(:array.set(0,0,:array.new()))))

  @type tab(integer) :: :array.array(integer)
  @spec fib_t(i :: integer(), j :: integer(), tab :: tab(integer())) :: n :: integer()
  # n az i-edik Fibonacci-szám
  def fib_t(i, j, tab) when i < j, do: :array.get(i, tab)
  def fib_t(i, j, tab) do
    prev = :array.get(j-2, tab)
    curr = :array.get(j-1, tab)
    tab0 = :array.set(j, prev+curr, tab)
    fib_t(i, j+1, tab0)
  end
end
FibAerl.fib_tab(1023) #|> IO.inspect()
2785293550699592923938812412668093509353307352123703806913182668987369503203465183625616759613324452749958549669966882191117895425015208455469403731272652158240825628484818131485544230827304940519132195299466733282

Táblázattal, Elixir List-tel

A lista fordított sorrendben tárolja a Fibonacci-számokat: a feje a legutóbb kiszámított, a második eleme az azt megelőző. Így az új elemet olcsón, a lista elé lehet fűzni:

defmodule FibLtab do

  # Tabulation (bottom-up) – dinamikus programozás
  # O(n) futási idő, O(n) tárhely
  # Elixir List
  @spec fib_tab(i :: integer()) :: n :: integer()
  # n az i-edik Fibonacci-szám
  def fib_tab(i), do: fib_t(i, 2, [1,0])

  @spec fib_t(i :: integer(), j :: integer(), tab :: [integer()]) :: n :: integer()
  # n az i-edik Fibonacci-szám
  def fib_t(i, j, tab) when i < j, do: hd(tab)
  def fib_t(i, j, tab) do
    prev = hd(tl(tab))
    curr = hd(tab)
    tab0 = [prev+curr | tab]
    fib_t(i, j+1, tab0)
  end
end
FibLtab.fib_tab(63) #|> IO.inspect()
6557470319842

A két utolsó érték nyilvántartásával

Mivel az új értékhez csak a két utolsóra van szükség, a táblázat helyett elég ezt a kettőt (curr, prev) akkumulátorokban továbbadni; a tárigény így állandó:

defmodule FibI do
  # Space optimized (bottom up)
  # O(n) futási idő, O(1) tárhely
  @spec fib_iter(i :: integer()) :: n :: integer()
  # n az i-edik Fibonacci-szám
  def fib_iter(i), do: fib_i(i, 1, 0)

  @spec fib_i(i :: integer(), curr :: integer(), prev :: integer())
    :: n :: integer()
  # n az i-edik Fibonacci-szám
  defp fib_i(0, _curr, prev), do: prev
  defp fib_i(1, curr, _prev), do: curr
  defp fib_i(i, curr, prev), do: fib_i(i-1, prev+curr, curr)
end
FibI.fib_iter(2203) #|> IO.inspect()
11227588022178051398070623745770537746981032161033283578641889149504371902547595733548949731279174036520553510211185291521165787504965947954328861725425894532117680897067684977042503589399040135697168277407633126059586479862184815462709569351240070274187436057121550393922337505846249722123756568019538289963931388811270535294468233234206275243288823876307712381776769983580371337794399152833220102956602421639379175057893229860412359902362848104779389231572677

Futási idők összehasonlítása

Benchee.run(
  %{
    "fib tree recursive" => fn -> Fib.fib(33) end,
    "fib memoization" => fn -> FibM.fib_mem(33) end,
    "fib tabulation" => fn -> FibT.fib_tab(33) end,
    "fib tabula_array_erl" => fn -> FibAerl.fib_tab(33) end,
    "fib tabula_list" => fn -> FibLtab.fib_tab(33) end,
    "fib iterative" => fn -> FibI.fib_iter(33) end
  },
  profile_after: false
  )
:ok

Az eredmény (Elixir 1.20.2, Erlang 29.0.6, AMD Ryzen AI 9 HX 370):

Name                           ips        average  deviation         median         99th %
fib iterative            2025.02 K        0.49 μs  ±1815.45%        0.47 μs        0.67 μs
fib tabula_list          1793.18 K        0.56 μs  ±1986.49%        0.50 μs        0.82 μs
fib tabula_array_erl      507.54 K        1.97 μs   ±502.71%        1.85 μs        3.01 μs
fib memoization           337.87 K        2.96 μs   ±255.53%        2.73 μs        5.28 μs
fib tabulation            324.54 K        3.08 μs   ±305.85%        2.94 μs        5.17 μs
fib tree recursive        0.0504 K    19823.90 μs    ±14.14%    18557.61 μs    26679.49 μs

Comparison: 
fib iterative            2025.02 K
fib tabula_list          1793.18 K - 1.13x slower +0.0638 μs
fib tabula_array_erl      507.54 K - 3.99x slower +1.48 μs
fib memoization           337.87 K - 5.99x slower +2.47 μs
fib tabulation            324.54 K - 6.24x slower +2.59 μs
fib tree recursive        0.0504 K - 40143.78x slower +19823.41 μs

Az elágazó rekurzió a 33. Fibonacci-számra negyvenezerszer lassabb a leggyorsabb változatnál. A listás táblázat közel olyan gyors, mint a két értéket nyilvántartó változat, mert a lista elejéhez fűzés és a fej elérése olcsó; a Map-pel dolgozó változatok lassabbak.

Nyomkövetés a dbg-vel

A dbg/1 makró kiírja a kapott kifejezést és az értékét, majd az értéket változatlanul továbbadja, ezért egy pipe-lánc végére fűzve a lépéseket nyomon követhetjük. Az alábbi változat a fib_t/3 paramétereinek sorrendjét is megváltoztatja, hogy a táblázat a pipe-ban továbbadható legyen:

defmodule FibTdbg do

  # Tabulation (bottom-up) – dinamikus programozás
  # O(n) futási idő, O(n) tárhely
  @spec fib_tab(i :: integer()) :: n :: integer()
  # n az i-edik Fibonacci-szám
  def fib_tab(i), do: fib_t(%{0 => 0, 1 => 1}, 2, i)

  @type fib() :: %{index :: integer() => value :: integer()}
  @spec fib_t(mem :: fib(), j :: integer(), i :: integer()) :: n :: integer()
  # n az i-edik Fibonacci-szám
  def fib_t(tab, j, i) when j > i, do: tab[i]
  def fib_t(tab, j, i) do
    tab
    |> Map.put(j, tab[j-1] + tab[j-2])
    |> fib_t(j+1, i)
    |> dbg()
  end
end
FibTdbg.fib_tab(8) #|> IO.inspect()
21

Gyakorló feladat: maximális összegű intervallum

Legyen egy elemű, egész (de nem feltétlenül pozitív) számokból álló lista. Jelölje az . elemét és az . elemétől . eleméig tartó összefüggő részlistáját. Határozza meg az

értékét, azaz az legnagyobb összegű egybefüggő részlistájának az összegét! (A feladat az Algoritmuselmélet tárgy dinamikus programozásról szóló diasorában is szerepel: https://www.cs.bme.hu/~kiskat/algel/eloadas2025/DP-2025.pdf#page=9.)

Ebben a feladatban egy segédlistát állítunk elő dinamikus programozással, ahol

Valósítsa meg a segédlistát előállító maxOsszegIndul függvényt! A megoldásnak nem kell feltétlenül jobbrekurzívnak lennie.

defmodule MaxOsszeg do
  @spec maxOsszeg(xs :: [number()]) :: r :: number()
  # Az r szám az xs összefüggő részlistái közül a legnagyobb összegű elemeinek az összege.
  def maxOsszeg(xs) do
    # A maxOsszegIndul lista elemeit az >=/2 segítségével hasonlítjuk össze,
    # üres lista esetén a visszatérési érték legyen 0.
    maxOsszegIndul(xs) |> Enum.max(&>=/2, fn -> 0 end)
  end

  @spec maxOsszegIndul(xs:: [number()]) :: ys :: [number()]
  # Az ys lista i. eleme az xs i. elemétől induló összefüggő részlistái közül
  # a legnagyobb összegű elemeinek az összege.
  def maxOsszegIndul(...) do
    ...
  end
end

IO.inspect(MaxOsszeg.maxOsszeg([]) == 0)
IO.inspect(MaxOsszeg.maxOsszeg([-2, 1, -3, 4, -1, 2, 1, -5, 4]) == 6)
IO.inspect(MaxOsszeg.maxOsszegIndul([-2, 1, -3, 4, -1, 2, 1, -5, 4]))
Megoldás

Az lista hátulról építhető fel: az . elemtől induló legjobb részlista vagy csak az elemből áll, vagy -hez hozzávesszük az . elemtől induló legjobb részlistát, ha annak összege pozitív.

defmodule MaxOsszeg do
  @spec maxOsszeg(xs :: [number()]) :: r :: number()
  # Az r szám az xs összefüggő részlistái közül a legnagyobb összegű elemeinek az összege.
  def maxOsszeg(xs) do
    # A maxOsszegIndul lista elemeit az >=/2 segítségével hasonlítjuk össze,
    # üres lista esetén a visszatérési érték legyen 0.
    maxOsszegIndul(xs) |> Enum.max(&>=/2, fn -> 0 end)
  end

  @spec maxOsszegIndul(xs:: [number()]) :: ys :: [number()]
  # Az ys lista i. eleme az xs i. elemétől induló összefüggő részlistái közül
  # a legnagyobb összegű elemeinek az összege.
  def maxOsszegIndul([]), do: []
  def maxOsszegIndul([h|t]) do
    ys2 = maxOsszegIndul(t)
    case ys2 do
      [y2|_] when y2 > 0 -> [h + y2 | ys2]
      _ -> [h | ys2]
    end
  end
end
true
true
[2, 4, 3, 6, 2, 3, 1, -1, 4]

Legyen most az adott indexű elemeinél végződő összefüggő részlisták maximális összege, azaz

Valósítsa meg a segéd-adatstruktúrát előállító maxOsszegVege jobbrekurzív függvényt! A megoldásban az Elixir Map adatszerkezetét használjuk.

defmodule MaxOsszeg2 do
  @spec maxOsszeg(xs :: [number()]) :: r :: number()
  # Az r szám az xs összefüggő részlistái közül a legnagyobb összegű elemeinek az összege.
  def maxOsszeg(xs) do
    # A maxOsszegVege Map elemeit az >=/2 segítségével hasonlítjuk össze,
    # üres Map esetén a visszatérési érték legyen 0.
    maxOsszegVege(xs) |> Map.values() |> Enum.max(&>=/2, fn -> 0 end)
  end

  @spec maxOsszegVege(xs:: [number()]) :: zs :: %{ number() => number() }
  # Az zs j-hez tartozó eleme az xs j. eleménél végződő összefüggő részlistái közül
  # a legnagyobb összegű elemeinek az összege.
  def maxOsszegVege(...), do: ...
end

IO.inspect(MaxOsszeg2.maxOsszeg([]) == 0)
IO.inspect(MaxOsszeg2.maxOsszeg([-2, 1, -3, 4, -1, 2, 1, -5, 4]) == 6)
IO.inspect(MaxOsszeg2.maxOsszegVege([-2, 1, -3, 4, -1, 2, 1, -5, 4]))
Megoldás

Az elejétől haladva: a . elemnél végződő legjobb részlista vagy csak az elemből áll, vagy a . elemnél végződő legjobbhoz fűzzük hozzá, ha annak összege pozitív. A Map.fetch/2 {:ok, z} párt ad, ha van j - 1 kulcs, egyébként :error-t.

defmodule MaxOsszeg2 do
  @spec maxOsszeg(xs :: [number()]) :: r :: number()
  # Az r szám az xs összefüggő részlistái közül a legnagyobb összegű elemeinek az összege.
  def maxOsszeg(xs) do
    # A maxOsszegVege Map elemeit az >=/2 segítségével hasonlítjuk össze,
    # üres Map esetén a visszatérési érték legyen 0.
    maxOsszegVege(xs) |> Map.values() |> Enum.max(&>=/2, fn -> 0 end)
  end

  @spec maxOsszegVege(xs:: [number()]) :: zs :: %{ number() => number() }
  # Az zs j-hez tartozó eleme az xs j. eleménél végződő összefüggő részlistái közül
  # a legnagyobb összegű elemeinek az összege.
  def maxOsszegVege(xs), do: maxOsszegVege(xs, 0, %{})

  defp maxOsszegVege([], _, zs), do: zs
  defp maxOsszegVege([h|t], j, zs) do
    z1 = case Map.fetch(zs, j - 1) do
      {:ok, z} when z > 0 -> h + z
      _ -> h
    end
    zs1 = Map.put(zs, j, z1)
    maxOsszegVege(t, j + 1, zs1)
  end
end
true
true
%{0 => -2, 1 => 1, 2 => -2, 3 => 4, 4 => 3, 5 => 5, 6 => 6, 7 => 1, 8 => 5}

Forrás: dp26a-fp2ea.pdf (46–48. dia), dp26a-fp2ea-fibonacci.livemd, dp26a-fp2gy-megoldasok.livemd

Csúszóablakos technika

Egy sorozat (lista) különféle szempontok szerint kiválasztott folytonos részsorozatait csúszóablakos módszerrel állíthatjuk elő, egymásba ágyazott ismétlésekkel. Mivel a funkcionális nyelvekben nincs ciklus, az ismétlést rekurzióval valósítjuk meg, a részeredményeket egy, esetleg több akkumulátorban gyűjtjük. Mivel a gyűjtéshez plusz paraméter(ek)re van szükség, rendszerint segédfüggvényeket is definiálunk.

A példafeladat egy számlista maximális összegű folytonos részlistáinak előállítása. Például az [1, 2, 3, 4, -10, 4, 3, 2, 1] lista maximális összegű folytonos részlistái: [1, 2, 3, 4], [1, 2, 3, 4, -10, 4, 3, 2, 1] és [4, 3, 2, 1], összegük 10. A függvény visszatérési értéke olyan pár legyen, amelynek első eleme a részlisták összege, második eleme ezen maximális összegű részlisták listája; a fenti példában:

{10, [[1, 2, 3, 4], [1, 2, 3, 4, -10, 4, 3, 2, 1], [4, 3, 2, 1]]}

A reszlistak/1 függvény specifikációja:

  @spec reszlistak(xs::[integer()]) :: {max::integer(), rss::[[integer()]]}
  # rss az xs max összegű részlistáinak listája

Két megközelítést nézünk meg: az elsőben előállítjuk az összes folytonos részlistát, és a maximumot a legvégén keressük meg; a másodikban már a részlisták gyűjtése közben csak a maximális összegűeket tartjuk meg.

1. Maximumkeresés a legvégén

11. A lista elejétől kezdődő összes folytonos részlista

Az ablak bal széle a lista eleje, a jobb széle elemenként halad előre. Az ss akkumulátor fordított sorrendben tartalmazza az eddig bevett elemeket (így az új elemet olcsón elé lehet fűzni), a zss akkumulátor gyűjti az eddigi részlistákat, helyes sorrendre fordítva:

defmodule Reszlistak11 do
  def reszlistak([x|xs]), do: reszlistak(xs, [x], [[x]])
  def reszlistak([]), do: []

  def reszlistak([y|ys], ss, zss) do
    ss_uj = [y|ss]
    reszlistak(ys, ss_uj, [Enum.reverse(ss_uj) | zss])
  end
  def reszlistak([], _ss, zss), do: zss
end
Reszlistak11.reszlistak([1,2,3,4,5]) |> Enum.reverse()
[[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]]

12. A lista összes folytonos részlistája

Az ablak bal szélét is léptetni kell: a lista minden szuffixumára (a lista egyre rövidülő farkára) előállítjuk az elejétől kezdődő részlistákat:

defmodule Reszlistak12 do
  def reszlistak(xxs), do: reszlistak(xxs, [])

  def reszlistak([_x|xs]=xxs, zss) do
    reszlistak(xs, Reszlistak11.reszlistak(xxs) ++ zss)
  end

  def reszlistak([], zss), do: zss
end
Reszlistak12.reszlistak([1,2,3,4,5]) |> Enum.reverse()
[
  [1],
  [1, 2],
  [1, 2, 3],
  [1, 2, 3, 4],
  [1, 2, 3, 4, 5],
  [2],
  [2, 3],
  [2, 3, 4],
  [2, 3, 4, 5],
  [3],
  [3, 4],
  [3, 4, 5],
  [4],
  [4, 5],
  [5]
]

12x. Az összes folytonos részlista és az összegük

Kitérőként lássuk a részösszegeket is:

defmodule Reszlistak12x do
  def reszlistak(xs), do: reszlistak(Reszlistak12.reszlistak(xs), [])

  def reszlistak([xs|xss], zss), do: reszlistak(xss, [{Enum.sum(xs), xs} | zss])
  def reszlistak([], zss), do: zss
end
Reszlistak12x.reszlistak([1,2,-3,-4,5]) |> Enum.reverse()
[
  {5, [5]},
  {1, [-4, 5]},
  {-4, [-4]},
  {-2, [-3, -4, 5]},
  {-7, [-3, -4]},
  {-3, [-3]},
  {0, [2, -3, -4, 5]},
  {-5, [2, -3, -4]},
  {-1, [2, -3]},
  {2, [2]},
  {1, [1, 2, -3, -4, 5]},
  {-4, [1, 2, -3, -4]},
  {0, [1, 2, -3]},
  {3, [1, 2]},
  {1, [1]}
]

13. A maximális összegű folytonos részlisták

Az összes részlista közül a maximális összegűeket egy újabb bejárással választjuk ki. A max akkumulátor az eddigi legnagyobb összeg, a zss az ilyen összegű részlisták listája. Ha egy részlista összege nagyobb az eddigi maximumnál, új maximumot és új gyűjtést kezdünk; ha egyenlő vele, hozzávesszük; ha kisebb, eldobjuk:

defmodule Reszlistak13 do
  @spec reszlistak(xs::[integer()]) :: {max::integer(), rss::[[integer()]]}
  # rss az xs max összegű részlistáinak listája
  def reszlistak([_|_]=xs) do
    [rs|rss] = Reszlistak12.reszlistak(xs)
    reszlistak(rss, Enum.sum(rs), [rs])
  end
  def reszlistak([]), do: []

  @spec reszlistak(xs::[integer()], max::integer(), zss::[[integer()]]) :: rss::[[integer()]]
  # rss az xs max összegű részlistáinak listája; a részlistákat zss-ben gyűjtjük
  def reszlistak([xs|xss], max, zss) do
    sum = Enum.sum(xs)
    cond do
      sum > max -> reszlistak(xss, sum, [xs])
      sum == max -> reszlistak(xss, max, [xs | zss])
      true -> reszlistak(xss, max, zss)
    end
  end
  def reszlistak([], max, zss), do: {max, zss}
end

A cond kifejezés sorban kiértékeli a feltételeket, és az első igaz feltételhez tartozó kifejezés értékét adja; a true ág a „minden más” eset.

Reszlistak13.reszlistak([1,2,13,4,5]) |> IO.inspect()
Reszlistak13.reszlistak([1,2,13,4,-10,4,13,2,1]) |> IO.inspect()
Reszlistak13.reszlistak([-13,-13,-13]) |> IO.inspect()
Reszlistak13.reszlistak([0,0,0]) |> IO.inspect()
Reszlistak13.reszlistak([-13]) |> IO.inspect()
Reszlistak13.reszlistak([]) |> IO.inspect()
{25, [[1, 2, 13, 4, 5]]}
{30, [[1, 2, 13, 4, -10, 4, 13, 2, 1]]}
{-13, [[-13], [-13], [-13]]}
{0, [[0], [0, 0], [0, 0, 0], [0], [0, 0], [0]]}
{-13, [[-13]]}
[]

2. Maximumkiválasztás a részlisták gyűjtésekor

21. A lista elejétől kezdődő folytonos részlisták és összegük

defmodule Reszlistak21 do
  def reszlistak([x|xs]), do: reszlistak(xs, [x], [{x, [x]}])
  def reszlistak([]), do: []

  def reszlistak([y|ys], ss, zss) do
    ss_uj = [y|ss]
    reszlistak(ys, ss_uj, [{Enum.sum(ss_uj), Enum.reverse(ss_uj)} | zss])
  end
  def reszlistak([], _ss, zss), do: zss
end
Reszlistak21.reszlistak([1,2,3,4,5]) |> Enum.reverse() |> IO.inspect()
Reszlistak21.reszlistak([1,2,3,-3,-2,5]) |> Enum.reverse() |> IO.inspect()
[
  {1, [1]},
  {3, [1, 2]},
  {6, [1, 2, 3]},
  {10, [1, 2, 3, 4]},
  {15, [1, 2, 3, 4, 5]}
]
[
  {1, [1]},
  {3, [1, 2]},
  {6, [1, 2, 3]},
  {3, [1, 2, 3, -3]},
  {1, [1, 2, 3, -3, -2]},
  {6, [1, 2, 3, -3, -2, 5]}
]

22a. A lista elejétől kezdődő, maximális összegű folytonos részlisták

Most már gyűjtés közben csak a maximális összegű részlistákat tartjuk meg:

defmodule Reszlistak22a do
  def reszlistak([x|xs]), do: reszlistak(xs, [x], x, [[x]])
  def reszlistak([]), do: []

  def reszlistak([y|ys], ss, max, zss) do
    ss_uj = [y|ss]
    ss_uj_rev = Enum.reverse(ss_uj) # |> IO.inspect(label: "ss_uj_rev")
    sum = Enum.sum(ss_uj_rev) # |> IO.inspect(label: "sum")
    cond do
      sum > max -> reszlistak(ys, ss_uj, sum, [ss_uj_rev])
      sum == max -> reszlistak(ys, ss_uj, max, [ss_uj_rev | zss])
      true -> reszlistak(ys, ss_uj, max, zss)
    end
  end
  def reszlistak([], _ss, max, zss), do: {max, Enum.reverse(zss)}
end
Reszlistak22a.reszlistak([1,2,3,-3,-2,5]) |> IO.inspect()
Reszlistak22a.reszlistak([6,-6,1,2,3,-3,-2,5]) |> IO.inspect()
{6, [[1, 2, 3], [1, 2, 3, -3, -2, 5]]}
{6, [[6], [6, -6, 1, 2, 3], [6, -6, 1, 2, 3, -3, -2, 5]]}

22b. Ugyanez Enum.take-kel

Ez a változat a teljes listából indul, és a részlistákat az Enum.take/2-vel egyre rövidebb prefixumként állítja elő:

defmodule Reszlistak22b do
  def reszlistak(xs), do: reszlistak(xs, length(xs)-1, Enum.sum(xs), [xs])

  def reszlistak(_xs, 0, max, zss), do: {max, zss}
  def reszlistak(xs, len, max, zss) do
    ss = Enum.take(xs, len)
    sum = Enum.sum(ss)
    cond do
      sum > max -> reszlistak(xs, len-1, sum, [ss])
      sum == max -> reszlistak(xs, len-1, max, [ss | zss])
      true -> reszlistak(xs, len-1, max, zss)
    end
  end
end
Reszlistak22b.reszlistak([1,2,3,-3,-2,5]) |> IO.inspect()
Reszlistak22b.reszlistak([6,-6,1,2,3,-3,-2,5]) |> IO.inspect()
{6, [[1, 2, 3], [1, 2, 3, -3, -2, 5]]}
{6, [[6], [6, -6, 1, 2, 3], [6, -6, 1, 2, 3, -3, -2, 5]]}

23. A maximális összegű folytonos részlisták

A lista minden szuffixumára meghívjuk a 22a vagy a 22b változatot, és az eredményekből csak a maximális összegűeket tartjuk meg. Az elejétől kezdődő maximális részlistákat előállító függvényt paraméterként (mxrls) adjuk át, így a két változat ugyanazzal a kerettel használható:

defmodule Reszlistak23 do
  @type mxrls() :: ([integer()] -> [[integer()]])
  @spec reszlistak(f::mxrls(), xs::[integer()]) :: {max::integer(), rss::[[integer()]]}
  # rss az xs max összegű részlistáinak listája
  # f egy számlista elejétől kezdődő, folytonos, max. összegű részlistákat adja eredményül
  def reszlistak(mxrls, [_x|xs]=xxs), do: reszlistak(mxrls, xs, mxrls.(xxs))
  def reszlistak(_mxrls, []), do: {}

  def reszlistak(mxrls, [_y|ys]=yys, {maxsum, zss}) do
    {max, mss} = mxrls.(yys)
    cond do
      max > maxsum -> reszlistak(mxrls, ys, {max, mss})
      max == maxsum -> reszlistak(mxrls, ys, {maxsum, zss ++ mss}) # hatékonyság vs. sorrend!
      true -> reszlistak(mxrls, ys, {maxsum, zss})
    end
  end
  def reszlistak(_mxrls, [], maxlists), do: maxlists
end

A zss ++ mss megőrzi a részlisták sorrendjét, de a ++ az első listát lemásolja; a mss ++ zss olcsóbb lenne, de felcserélné a sorrendet.

(&Reszlistak22a.reszlistak/1) |> Reszlistak23.reszlistak([1,2,3,4,5]) |> IO.inspect()
(&Reszlistak22a.reszlistak/1) |> Reszlistak23.reszlistak([1,2,3,4,-10,4,3,2,1]) |> IO.inspect()
(&Reszlistak22b.reszlistak/1) |> Reszlistak23.reszlistak([1,2,3,4,5]) |> IO.inspect()
(&Reszlistak22b.reszlistak/1) |> Reszlistak23.reszlistak([1,2,3,4,-10,4,3,2,1]) |> IO.inspect()
{15, [[1, 2, 3, 4, 5]]}
{10, [[1, 2, 3, 4], [1, 2, 3, 4, -10, 4, 3, 2, 1], [4, 3, 2, 1]]}
{15, [[1, 2, 3, 4, 5]]}
{10, [[1, 2, 3, 4], [1, 2, 3, 4, -10, 4, 3, 2, 1], [4, 3, 2, 1]]}

Futási idők összehasonlítása

A futási idők mérésére véletlenszerű számok listáját használjuk. Egy 10 hosszúságú, a -5..5 tartományba eső számokat tartalmazó sorozatot például így állíthatunk elő (az eredmény minden futtatáskor más):

for _ <- 1..10, do: Enum.random(-5..5)
[-5, 4, -2, -2, -5, 0, 2, 1, 4, -3]

A mérés egy 1000 elemű véletlen listán:

xs = for _ <- 1..1000, do: Enum.random(-5..5)
Benchee.run(
  %{
    "utolag keres"  =>
      fn -> Reszlistak13.reszlistak(xs) end,
    "menet kozben, sajat fv"  =>
      fn -> Reszlistak23.reszlistak(&Reszlistak22a.reszlistak/1, xs) end,
    "menet kozben, Enum.take"  =>
      fn -> Reszlistak23.reszlistak(&Reszlistak22b.reszlistak/1, xs) end
  }# , profile_after: true
)
:ok

Az eredmény (Elixir 1.20.2, Erlang 29.0.6, AMD Ryzen AI 9 HX 370):

Name                              ips        average  deviation         median         99th %
menet kozben, sajat fv           2.26      442.21 ms     ±2.22%      438.00 ms      465.67 ms
menet kozben, Enum.take          1.42      702.56 ms     ±1.30%      702.25 ms      720.61 ms
utolag keres                     0.33     3016.53 ms    ±44.34%     3169.76 ms     4270.77 ms

Comparison: 
menet kozben, sajat fv           2.26
menet kozben, Enum.take          1.42 - 1.59x slower +260.35 ms
utolag keres                     0.33 - 6.82x slower +2574.31 ms

A menet közbeni maximumkiválasztás több mint hatszor gyorsabb, mert nem kell az összes (1000 elemű listánál félmilliónál több) részlistát egyszerre tárolni és utólag újra bejárni.

Forrás: dp26a-fp3ea.pdf (4. dia), dp26a-fp3ea-reszlistak-kihagy_bevesz_rek.livemd

Kihagy-bevesz rekurzió

A kihagy-bevesz (include-exclude, inclusion-exclusion) rekurzió a lista minden eleménél két ágra bontja a feladatot: az egyik ágban az elemet bevesszük a készülő megoldásba, a másikban kihagyjuk. Így a lista elemeinek összes részhalmazát bejárja: elemű listánál esetet.

Kombinációk

A komb/1 egy lista elemeinek összes kombinációját (részhalmazát) adja eredményül. Az acc akkumulátor a már bevett elemeket gyűjti. Minden elemnél két rekurzív hívás van: a komb(ns, acc) kihagyja az n elemet, a komb(ns, [n | acc]) beveszi; a két ág eredményét összefűzzük. Ha elfogytak az elemek, az akkumulátor egy kész kombináció:

defmodule Kombinaciok do
  def komb(ns), do: komb(ns, [])

  defp komb([n | ns], acc) do
    komb(ns, acc) ++ komb(ns, [n | acc])
  end
  defp komb([], acc), do: [acc]
end
Kombinaciok.komb([1,2,3]) |> Enum.sort
[[], [1], [2], [2, 1], [3], [3, 1], [3, 2], [3, 2, 1]]

Mivel a bevett elemeket az akkumulátor elé fűzzük, a kombinációk elemei fordított sorrendben állnak ([3, 2, 1]).

Összeg testvéries elosztása

Adott pénzérméket úgy kell elosztani két ember között, hogy a két összeg különbségének abszolút értéke a lehető legkisebb legyen (CEOI’1995 versenyfeladat; CEOI: Közép-európai Informatikai Diákolimpia, Central European Olympiad in Informatics).

Ha az érmék értékét tartalmazó lista [28, 7, 11, 8, 9, 7, 27], akkor egyikük a [9, 11, 28] érméket kapja, amelyek összege 48, másikuk a többit ([7, 7, 8, 27]), amelyek összege 49.

Az eloszt/1 először kiszámítja a teljes összeget (tot), és ennek felét egész osztással (tgt, a célösszeg). Ezután kihagy-bevesz rekurzióval kigyűjti az összes olyan részlistát, amelynek összege nem nagyobb a célösszegnél, végül ezek közül kiválogatja a maximális összegűeket. Az eredmény egy szótár, amelynek kulcsai az összegfeltételt kielégítő részlisták, értékei pedig e részlisták összege, azaz az elosztás kisebbik összege.

defmodule ElosztS do

  def sort(ls), do: Enum.sort(ls, fn (a,b) -> b < a end)

  @type p_int() :: integer()
  @type my_map() :: %{[p_int()] => p_int()}
  @spec max_osszegek(map :: my_map()) :: resmap :: my_map()
  def max_osszegek(map) do
    maxval = Enum.max(Map.values(map))
    for {k,v} <- map, v == maxval, into: %{}, do: {k, v}
  end
end

Az ElosztS.max_osszegek/1 a for-jelöléssel azokat a párokat válogatja ki a szótárból, amelyek értéke a legnagyobb.

defmodule Eloszt do

  @spec eloszt(vals :: [ElosztS.p_int()]) :: map :: ElosztS.my_map
    # Legyen halfsum a vals pozitív egészlista összegének a fele (egész osztással).
    # A map kulcs-érték párjaiban a kulcsok vals olyan max. összegű részlistái,
    # melyek összege nem nagyobb halfsum-nál, az értékek pedig e részlisták összege.

  def eloszt([_,_|_] = vals) do
    tot = Enum.sum(vals)
    |> IO.inspect(label: "Listaösszeg")
    tgt = div(tot, 2)
    |> IO.inspect(label: "Célérték")

    # vals = vals #Enum.sort(vals) #my_sort(vals) # rendezzük az értéklistát csökkenő sorrendben
    # |> IO.inspect()
    eloszt(Map.new(), vals, tgt, [], 0) # Map.new() === %{} # 0 jó-e?
    # |> IO.inspect()
    |> ElosztS.max_osszegek()
  end
  def eloszt(_), do: "A listának legalább kételeműnek kell lennie."

  @spec eloszt(map  :: ElosztS.my_map(),  # map-be gyűjtjük a részlistákat és összegüket
               vals :: [ElosztS.p_int()], # a még feldolgozandó érmelista
               tgt  :: ElosztS.p_int(),   # a célösszeg (nem változik)
               curr :: [ElosztS.p_int()], # a már összegyűjtött részlista
               sum  :: ElosztS.p_int())   # a már összegyűjtött részlista összege
          :: resmap :: ElosztS.my_map()   # a bővített map, a fv. eredménye

  defp eloszt(map, [val|vals], tgt, curr, sum) do
    curr_new = [val | curr] # az aktuális érmével bővített részlista
    sum_new = sum + val     # az aktuális érmével megnövelt összeg

    # ha az új összeg nem nagyobb a célösszegnél, berakjuk a map-be, ha kisebb, nem
    # figyeljük meg, hogyan használjuk a pipe-ot az esetleg bővített map továbbadására
    (if sum_new <= tgt, do: Map.put(map, curr_new, sum_new), else: map)
    |> eloszt(vals, tgt, curr_new, sum_new) # 1. ág: val-t bevesszük # feltételesen kihagyható
    |> eloszt(vals, tgt, curr, sum) # 2. ág: val-t kihagyjuk
  end
  defp eloszt(map, [], _tgt, _curr, _sum), do: map
end

Az eloszt/5 a szótárt (map) a pipe-pal adja tovább: az első rekurzív hívás (a bevevő ág) a bővített szótárral tér vissza, és ezt kapja meg első paraméterként a második rekurzív hívás (a kihagyó ág). Így a két ág eredménye egyetlen szótárban gyűlik.

Eloszt.eloszt([1, 2, 3, 4])
Listaösszeg: 10
Célérték: 5
%{[3, 2] => 5, [4, 1] => 5}

További példák (a Listaösszeg és Célérték sorok nélkül):

Eloszt.eloszt([28, 7, 11, 8, 9, 7, 27]) |> IO.inspect()
Eloszt.eloszt([1,2,3,4,5]) |> IO.inspect()
Eloszt.eloszt([4,1,2,5,3]) |> IO.inspect()
Eloszt.eloszt([4,1,2,5,6,3,7]) |> IO.inspect()
%{[9, 11, 28] => 48}
%{[4, 2, 1] => 7, [4, 3] => 7, [5, 2] => 7}
%{[2, 1, 4] => 7, [3, 4] => 7, [5, 2] => 7}
%{
  [3, 5, 2, 4] => 14,
  [3, 6, 1, 4] => 14,
  [3, 6, 5] => 14,
  [6, 5, 2, 1] => 14,
  [7, 2, 1, 4] => 14,
  [7, 3, 4] => 14,
  [7, 5, 2] => 14,
  [7, 6, 1] => 14
}

A kulcsokban az érmék a bevétel fordított sorrendjében állnak. Egyelemű vagy üres listára a függvény a "A listának legalább kételeműnek kell lennie." sztringet adja.

Gyakorló feladatok

  • A bemutatott megoldás először kigyűjti az összes olyan részlistát, amelynek összege nem nagyobb a teljes listaösszeg felénél, és csak ezután válogatja ki közülük a maximális összegűeket. Írjon egy vagy több olyan változatot, amely nem tartja meg az összes részlistát, hanem már menet közben eldobja az aktuális maximumnál kisebb összegűeket!
  • Ezután próbáljon olyan megoldást írni, amely az egyszer már kiszámolt összegű részlisták összegét nem számolja ki újra!
  • Hasonlítsa össze az egyes változatok futási idejét a benchee segítségével, és becsülje meg a tárigényüket!

Forrás: dp26a-fp3ea.pdf (6. dia), dp26a-fp3ea-reszlistak-kihagy_bevesz_rek.livemd, dp26a-fp3gy.livemd