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ᵢ
pattern <- listalakú generátor, vagy- predikátum (igazságérték-eredményű függvény, feltétel);
- legalább egy qᵢ-nek generátornak kell lennie;
- a
patternmintának illeszkednie kell alistlista kiválasztandó elemeire, és ki kell elégítenie az adottpattern <- listgenerátortól jobbra álló összes qᵢ predikátumot; - az
exptetszőleges, apatternmintá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 azEnum.map/2-t, hanem azEnum.reduce/3-at váltja ki. - Az acc₀ az eredményt gyűjtő akkumulátor kezdőértéke.
- A
do ... endközött egy névtelen függvényt kell megadni (azfnés a hozzá tartozóendné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 // -2tartomány elemei 0 és -2; az1..xtartomány a függő generátorbanxértékétől függ (1..0, illetve1..-2, csökkenő tartományok). - Az
xy = {x,y}qᵢ-ként mintaillesztéssel köt változót, amely ado:utáni kifejezésben használható. - Az utolsó példában a
rem(x,3) !== 0szűrő az első generátorhoz, azx > ya 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ó
- a és közötti prímszámok bármelyikével;
- -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
Kernelmodulban definiált..///3operátor operandusainak egész számoknak kell lenniük, ezért ha a felső határ beállítására a:math.sqrt/1függvényt használja, egész számmá kell konvertálni (vö.round/1,floor/1,ceil/1). - Az
Enum.to_list/1tetszőleges felsorolható sorozatot listává alakít, így tartomány típusú értéksorozatot is. - Az
Enum.reduce/3egy 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