8. Anonymní funkce, funkcionální programování

Tři užitečné funkce

Na úvod zmíníme ještě tři funkce pro práci se seznamy, které se nám budou hodit.

Rest[{1,2,3}] (* odstraní první prvek seznamu a vrátí zbytek *)

Most[{1,2,3}] (* vrátí všechny prvky seznamu kromě posledního *)

Flatten[{{1,2,3},{4,5}}] (* odstraní všechny vnitřní složené závorky v seznamu, jehož prvky jsou seznamy *)

Anonymní funkce (pure functions)

V běžném matematickém zápisu rozlišujeme mezi funkcemi a jejich hodnotami:

({{Funkce, Hodnota}, {f, f(x)}, {sin, sin x}, {x↦(x^2), (x^2)}})

Funkce „umocňování na druhou“ nemá žádné standardní označení. Chceme-li se odkazovat na takovéto funkce, obvykle píšeme proměnná↦předpis funkce (na rozdíl od samotného předpisu, což je funkční hodnota).

Také Mathematica rozlišuje mezi funkcemi a funkčními hodnotami. Zde je předchozí tabulka přeložená do syntaxe Mathematiky:

({{Funkce, Hodnota}, {f, f[x]}, {Sin, Sin[x]}, {x|->x^2
Function[x,(x^2)]
Function[#^2]
#^2&, x^2}})

Poslední řádek tabulky ukazuje, jak se lze odvolávat na funkce, které nejsou pojmenované – jsou to tzv. anonymní funkce. Existují čtyři ekvivalentní varianty zápisu:

proměnná|->předpis (symbol |-> lze vložit jako trojici znaků |-> nebo pomocí klávesové zkratky EscfnEsc)

Function[proměnná,předpis]

Function[předpis, kde místo názvu proměnné píšeme #]

(předpis, kde místo názvu proměnné píšeme #)&

V minulé lekci jsme řešili následující úlohu: Vyberte ze seznamu {1, ..., 100} všechna prvočísla tvaru 4k+1.

Řešení pomocí Select z minulé lekce:

p[n_]:=(Mod[n,4]==1 && PrimeQ[n])

Select[Range[100],p]

Clear[p]

Select očekává ve druhém argumentu funkci (nikoliv předpis,  což je funkční hodnota), proto jsme definovali pomocný predikát p. Tomu se můžeme vyhnout pomocí anonymní funkce.

4 varianty zápisu:

Select[Range[100],n|->PrimeQ[n]&&Mod[n,4]==1]

Select[Range[100],Function[n,PrimeQ[n]&&Mod[n,4]==1]]

Select[Range[100],Function[PrimeQ[#]&&Mod[#,4]==1]]

Select[Range[100],(PrimeQ[#]&&Mod[#,4]==1)&]

Existují též anonymní funkce více proměnných, opět lze vybírat ze čtyř ekvivalentních variant zápisu:

seznam proměnných|->předpis

Function[seznam proměnných, předpis]

Function[předpis, kde místo názvů proměnných píšeme #1, #2, atd.]

(předpis, kde místo názvů proměnných píšeme #1, #2, atd.)&

Triviální příklad:

(#1+#2)&[3,5]

Vysvětlení: (#1+#2)& je anonymní funkce dvou proměnných, která vrací jejich součet. Pokud ji zavoláme s argumenty 3, 5, dostaneme součet 3+5.

Funkcionální programování

V Mathematice máme na výběr z různých programátorských stylů:

přepisovací pravidla a rekurze (už známe)

funkcionální programování (viz dnešní lekce)

procedurální programování (probereme v budoucnosti)

Pro funkcionální programování je typická práce s anonymními funkcemi, seznamy a následujícími funkcemi:

Map a MapThread

Map zavolá funkci jedné proměnné na všechny prvky daného seznamu, výsledkem je seznam funkčních hodnot.

Map[f,{x1,x2,x3}]

Ekvivalentní zápis:

f/@{x1,x2,x3}

Příklad:

Map[Sqrt,{2,3,5}]

Použití Map je v tomto případě zbytečné, neboť Sqrt má atribut Listable:

Sqrt[{2,3,5}]

Většina funkcí pro práci se seznamy nemá atribut Listable. Dokládají to následující příklady (rozmyslete si, jak fungují):

Map[Reverse,{{a,b},{c,d}}]

Reverse[{{a,b},{c,d}}]

Map[Last,{{a,b},{c,d}}]

Last[{{a,b},{c,d}}]

Map se často používá ve spojení s anonymními funkcemi. 
Typické použití: Máme data uložená v seznamu. S každým prvkem seznamu chceme provést nějakou operaci, pro kterou v Mathematice není k dispozici zabudovaná funkce. V tom případě můžeme operaci specifikovat jako anonymní funkci v prvním argumentu Map.

Příklad: Numerické řešení diferenciální rovnice x'(t)=-x(t)+t pro více počátečních podmínek: x(0)∈{0,1,2,3,4}.

reseni=Map[NDSolveValue[{x'[t]==-x[t]+t,x[0]==#},x[t],{t,0,5}]&,Range[0,4]]

Plot[reseni,{t,0,5}]

Clear[reseni]

MapThread je analogií Map pro funkce více proměnných. Prvním argumentem je funkce n proměnných, druhým argumentem seznam n stejně dlouhých seznamů. MapThread postupně volá zadanou funkci na první prvky, druhé prvky, atd.

MapThread[f,{{x1,x2,x3},{y1,y2,y3}}]

MapThread[Max,{{1,2,3},{3,2,1}}]

Příklad: Ve 4. lekci jsme si ukázali, jak naprogramovat připojení vektoru jako nového sloupce k zadané matici. Zde je elegantnější řešení využívající MapThread (rozmyslete si, proč funguje).

pridejSloupec[M_,v_]:=MapThread[Append,{M,v}]

pridejSloupec[{{1,2},{3,4}},{5,6}]

Příklad: Nechť a,b>0. Cassiniho ovál je množina všech bodů v rovině, pro které je součin vzdáleností od bodů [-a,0] a [a,0] roven (b^2) (tj. je konstantní). 
Rovnice: (Sqrt[(((x-a))^2)+(y^2)])(Sqrt[(((x+a))^2)+(y^2)])=(b^2), neboli ((((x-a))^2)+(y^2))((((x+a))^2)+(y^2))=(b^4)
Tvar Cassiniho oválu závisí na tom, zda a<b, a=b nebo a>b. Chceme vykreslit ovály pro a=1 a b∈{0.95, 1, 1.05}. Kromě toho chceme, aby každý ze tří obrázků měl vhodný popis (volba PlotLabel).

Zdlouhavé řešení:

{ContourPlot[((x-1)^2+y^2)((x+1)^2+y^2)==0.95^4,{x,-2,2},{y,-1,1},AspectRatio->Automatic,ContourStyle->Red,PlotLabel->"a<b"],
ContourPlot[((x-1)^2+y^2)((x+1)^2+y^2)==1^4,{x,-2,2},{y,-1,1},AspectRatio->Automatic,ContourStyle->Red,PlotLabel->"a=b"],
ContourPlot[((x-1)^2+y^2)((x+1)^2+y^2)==1.05^4,{x,-2,2},{y,-1,1},AspectRatio->Automatic,ContourStyle->Red,PlotLabel->"a>b"]}

Totéž elegantněji pomocí MapThread:

MapThread[ContourPlot[((x-1)^2+y^2)((x+1)^2+y^2)==#1^4,{x,-2,2},{y,-1,1},AspectRatio->Automatic,ContourStyle->Red,PlotLabel->#2]&,{{0.95,1,1.05},{"a<b","a=b","a>b"}}]

Nest a NestList

Máme-li funkci f jedné proměnné, pak jejím skládáním se sebou samou vznikají tzv. iterace. První iterace je f, druhá iterace je f∘f, třetí iterace je f∘f∘f, atd.

Nest počítá iterace zadané funkce:

Nest[f,x,3] (* hodnota třetí iterace f v bodě x *)

Nest[Cos,1,3]

Použití: Nest se hodí pro počítání členů posloupností definovaných rekurentním vzorcem typu (Subscript[a, n+1])=f((Subscript[a, n])). Známe-li (Subscript[a, 0]), pak (Subscript[a, 1])=f((Subscript[a, 0])), (Subscript[a, 2])=f((Subscript[a, 1]))=f(f((Subscript[a, 0]))), atd.; obecně platí, že (Subscript[a, n]) je hodnota n-té iterace f v bodě (Subscript[a, 0]).

Příklad: Spořící účet, úrok 3 % ročně připisován měsíčně, poplatky 30 Kč měsíčně. Vklad 20 000 Kč, vypočítejte zůstatek po 12 měsících.

Jestliže (Subscript[a, n]) je zůstatek po n měsících, pak (Subscript[a, 0])=20000 a (Subscript[a, n+1])=f((Subscript[a, n])), kde f(x)=(1+(0.03/12))x-30.

Řešení pomocí přepisovacích pravidel:

a[0]:=20000;
a[n_]:=((1+0.03/12)*a[n-1]-30)

a[12]

Clear[a]

Ekvivalentně pomocí Nest a anonymní funkce:

Nest[((1+0.03/12)*#-30)&,20000,12]

NestList[f,x,n] vrací seznam všech iterací od nulté po n-tou.

NestList[f,x,3]

NestList[((1+0.03/12)*#-30)&,20000,12](* vývoj zůstatků během prvních 12 měsíců *)

Grafické znázornění:

ListPlot[NestList[((1+0.03/12)*#-30)&,20000,12*3],Filling->Axis,AxesLabel->{"měsíc","částka"}]

Pomocí Manipulate (budeme probírat později) lze interaktivně měnit parametry:

Manipulate[ListPlot[NestList[((1+urok/12)*#-poplatek)&,20000,pocet],PlotRange->{10000,37000},Filling->Axis,AxesLabel->{"měsíc","částka"}],{{urok,0.03,"úrok p.a."},0,0.1,Appearance->"Labeled"},{{poplatek,30},0,100,Appearance->"Labeled"},{{pocet,3*12,"počet měsíců"},12,6*12,1,Appearance->"Labeled"}]

Příklad: Ve cvičení k 6. lekci bylo úkolem naprogramovat náhodnou procházku v rovině. Zde je elegantní řešení pomocí NestList.

walk=NestList[#+RandomReal[{-1,1},2]&,{0,0},1000];

ListLinePlot[walk,AspectRatio->Automatic]

Fold a FoldList

Fold a FoldList jsou varianty Nest a NestList pro funkce dvou proměnných. Opakovaně volají zadanou funkci f, do které dosazují vždy výsledek předchozího kroku (nebo počáteční hodnotu) a prvky zadaného seznamu. Výpočet má tolik kroků, kolik je délka zadaného seznamu.

FoldList[f,x,{b1,b2,b3}] (* FoldList vrací kompletní průběh výpočtu *)

Fold[f,x,{b1,b2,b3}] (* Fold vrací pouze výsledek *)

Použití: Počítání členů posloupnosti definované rekurentním vzorcem typu (Subscript[a, n+1])=f((Subscript[a, n]),(Subscript[b, n])), kde {(Subscript[b, 1]),(Subscript[b, 2]), (Subscript[b, 3]), …} jsou zadané parametry.

Příklad: Spořící účet s variabilní úrokovou mírou 0.03, 0.03, 0.02, 0.02 během prvních 4 měsíců, počáteční vklad 20 000 Kč, měsíční poplatek 30 Kč.

Fold[((1+#2/12)*#1-30)&,20000,{0.03,0.03,0.02,0.02}]

FoldList[((1+#2/12)*#1-30)&,20000,{0.03,0.03,0.02,0.02}]