Mathematica — detaljnije o patternima

Već viđeno: imenovani i neimenovani blankovi

Vidjeli smo da patterni s lijeve strane u pravilima i pridruživanjima mogu biti neimenovani (_) i imenovani (x_). Pomoću njih već možemo forsirati neke strukturalne zahtjeve na argumente funkcije: pomoću _ možemo zahtijevati određeni broj argumenata,

broji[_]=1
broji[_,_]=2

broji[4]

broji[x,x]

broji[x,y,z]

… dok pomoću x_ možemo zahtijevati i da neki od njih budu jednaki (ako upotrijebimo isto ime):

usporedi[_,_]="nisu isti"
usporedi[x_,x_]="isti"

usporedi[1+6,3+4]

usporedi[s,t]

Istoimeni blank može matchati samo vrijednosti koje se automatski evaluiraju u doslovno iste objekte (Napomena: distributivnost se ne primjenjuje automatski jer može dovesti do eksponencijalne eksplozije duljine izraza, a izlučivanje se ne primjenjuje automatski jer je faktorizacija općenito komplicirana, i može zahtijevati puno vremena):

usporedi[a(b+c),a b+a c]

Redoslijed primjene pravila zadanih pridruživanjem

??usporedi

Primijetimo da Mathematica interno slaže pravila redom od specifičnijih prema generalnijima. To pomaže prilikom definiranja rekurzivnih funkcija:

sumaprvih@n_:=sumaprvih[n-1]+n
sumaprvih@0:=0

sumaprvih@100

??sumaprvih

Međutim, to nije totalni uređaj, a ponekad (kod jako kompliciranih patterna) Mathematica ne može odrediti koje pravilo je specifičnije. Tada se pravila primjenjuju onim redom kojim su zadana:

usporedi[_,x_,x_]="zadnji su isti"
usporedi[x_,x_,_]="prvi su isti"

usporedi[3,3,3]

Zaključak: ako nismo sigurni, dobro je pisati pridruživanja za isti head redom od specijalnijih prema generalnijima (dakle, ne onako kako smo gore pisali! To je samo bilo za primjer kako Mathematica zna uvesti reda i u takva pridruživanja, ali ljudima koji čitaju kod može djelovati zbunjujuće).

Restringiranje prema headu agrumen(a)ta

Kao što prije blanka _ možemo navesti njegovo ime (koje utječe na njegov match tako što ga forsira da bude isti kao i svi ostali izrazi koji matchaju istoimeni blank), tako nakon njega možemo navesti head koji njegov match mora imati (i na taj način donekle utjecati na "tip" izraza koji tu treba stajati u pozivu funkcije):

tip@_List="lista"
tip@_f="poziv funkcije f"

tip@{2,3,4}

tip@f@4

duljinaliste@x_List:=Length@x
duljinaliste@_:="to nije lista"

duljinaliste@{3,4,6,7}

duljinaliste[a+b+c]

Headovi atoma

Vidjeli smo da izrazi koji nisu atomi imaju head, i još neke dijelove (parts). Važno je znati da i atomi imaju head, pomoću kojeg je moguće restringirati funkcije kako je gore opisano. Ono po čemu se atomi razlikuju je da ostali dijelovi nisu "parts" (i ne dobivaju se operatorom [[]]), već imaju posebna imena. Tablica često korištenih atoma, s njihovim headovima i funkcijama kojima ih možemo "rastaviti":

({{atom, primjeri, head, dijelovi}, {simbol, x,List,π, Symbol, Context,SymbolName}, {cijeli broj, 3,-543, Integer, Sign,IntegerDigits}, {(netrivijalni) razlomak, (1/2),((-56)/5), Rational, Numerator,Denominator}, {(približni) realni broj, 4.,1.732, Real, Sign,RealDigits}, {kompleksni (nerealni) broj, i,2+5.6i, Complex, Re,Im}, {string, "proba","", String, Characters (ToCharacterCode)}})

Primijetimo da su to disjunktne klase — svaki izraz ima samo jedan head, dakle 5 nije Rational (jer je Integer).
Također primijetimo da je to strukturalni rastav, ne matematički (Mathematica ima ugrađeno i prepoznavanje matematičkih tipova objekata, ali o tome kasnije — naravno, to je puno kompliciranije):

Head/@{π,(Sqrt[3]),(x/2),a+b I}

tip@_Integer="cijeli broj"
tip@_String="niz znakova"

tip/@{f[3,4],{3,4},56,"Danas je lijep dan.",(2/3)}

zamijeni[x_Symbol,a_->b_]:=If[x==a,b,x,x]
zamijeni[x_List,r_Rule]:=x/.r
zamijeni[x_String,r_Rule]:=StringReplace[x,r]
zamijeni[x_Integer,r_Rule]:=Sign@x FromDigits[IntegerDigits@x/.r]

zamijeni["Proba","r"->"l"]

zamijeni[x,y->z]

zamijeni[-2345432,3->9]

Aritmetički patterni

Što je s oduzimanjem, dijeljenjem i korjenovanjem?

Vidjeli smo gore da je Head@(x/2)==Times. Kako to objasniti?

(x/2)//FullForm

Vrijeme je da otkrijemo jednu tajnu: Mathematica kao osnovne operacije ima samo zbrajanje, množenje i potenciranje. Oduzimanje, dijeljenje i korjenovanje se svode na njih (logaritmiranje je posebna priča:).

TreeForm/@{a-b,(a/b),(b^(1/a))}

Dakle, vidimo čarobni broj -1 (obični Integer), pomoću kojega se implementiraju promjena predznaka i recipročna vrijednost, a onda i gornje 3 operacije pomoću osnovnih. (Inače, ni logaritam nije toliko strašan; postoji primitivna operacija Log za ln, te se pomoću dijeljenja dobiju logaritmi po proizvoljnoj bazi.) To je važno znati kad hoćemo definirati patterne koji imaju izvedene operacije u sebi:

a+b/.a+x_:>aplus@x

a-b/.a+x_:>aplus@x

a-b/.a-x_:>aminus@x

a+b/.a-x_:>aminus@x

Prvi pattern radi kao što bi trebao, tu nema iznenađenja. Drugi pattern radi kako treba, što je jasno kad smo objasnili gore kako izgleda oduzimanje: a-b je zapravo a+(-b). Treći pattern je možda iznenađujuće da radi, dok ne shvatimo da se gornja redukcija - na + obavlja i u samom patternu, pa pattern ima jednak oblik kao i lijeva strana od /.. Ali četvrti pattern ne radi (iako bismo matematički vjerojatno htjeli dobiti aminus[-b]), jer Plus[a,b] nije oblika Plus[a,Times[-1,_]] (jer b nema head Times, već Symbol). Zaključak: u patternima ne smijemo pisati □-□, (□/□), ni (□^(1/□)), osim ako smo sigurni da se u izrazu na koji pattern želimo primijeniti koriste baš te operacije. Često je bolje koristiti □+□, □□ i (□^□), te na desnoj strani kompenzirati za promijenjen predznak ili recipročnu vrijednost koju dobijemo.

TreeForm/@Hold/@Unevaluated@{a+b/.a+x_:>aplus@x,a-b/.a+x_:>aplus@x,a-b/.a-x_:>aminus@x,a+b/.a-x_:>aminus@x}

Zbrajanje, množenje i potenciranje neutralnim elementima

Vidimo da se 3 izvedene operacije evaluiraju pomoću 3 osnovne, i zato je dobro ne pisati ih u patternima, osim ako detaljno znamo strukturu izraza na koji pattern želimo primijeniti. No ponekad se i osnovne operacije evaluiraju; na primjer, a+0 postaje a. To znači da ovdje (na poziciji 2) nemamo match:

Plus[]

{a+(-1),a+0,a+1,a+2}/.a+b_:>aplus@b

Tome možemo doskočiti specificiranjem da određeni sumand, faktor, ili eksponent može biti neutralni element. To činimo tako da nakon blanka dodamo točku (.):

{a-1,a,a+1}/.a+b_.:>aplus@b

{(a/2),a,2a}/.a b_.:>aputa@b

{(a^(1/3)),(Sqrt[a]),(3/a),a,a a}/.(a^(b_.)):>ana@b

a/.((((a^(b_.))c_.+d_.))^(e_.))f_.+g_.+h_.:>{a,b,c,d,e,f,g,h}

Perverzije

Naravno, sad možemo otići korak dalje, i zahtijevati da se 0 prepozna kao umnožak nule bilo čim, ili da se 1 prepozna kao potencija s eksponentom 0. To neće raditi, iz jednostavnog razloga što zahtijevane baze ili drugog faktora uopće nema u evaluiranom obliku potencije ili produkta (što ubija cijelu svrhu pattern matchinga; na taj način bi bilo koji x mogao matchati a unutar sebe, jer bi se mogao zapisati u obliku 0a+x, ili (a^0)x ☺). Na primjer, ovdje s lijeve strane stoji obični Integer 1, u kojem uopće ne postoji a:

(a^0)/.(a^(b_.)):>ana@b

No ako zadržimo evaluaciju, potencija je prepoznata:

Defer@(a^0)/.(a^(b_.)):>ana@b

Uvijek možemo to navesti i kao posebni slučaj:

{(a^(1/3)),(Sqrt[a]),1,a,(a^4)}/.{(a^(b_.)):>ana@b,1->ana@0}

U takvim je slučajevima ponekad bolje odustati od strukturalnog pristupa i primijeniti matematički:

ana/@Log[a,{(a^(1/3)),(Sqrt[a]),1,a,(a^4)}]//PowerExpand

Restringiranje prema unarnim "questionima"

Što su brojevi?

Vidjeli smo gore da prema headu možemo lako prepoznati liste, stringove,… a i brojeve ako znamo točno kojeg su tipa. No što ako jednostavno želimo reći da argument funkcije mora biti "bilo kakav" broj? Naravno, možemo definirati funkciju posebno za Integer, posebno za Rational, posebno za Real i posebno za Complex, ali postoji i bolji način: funkcija NumberQ, koja provjerava je li njen argument broj jednog od 4 gornja tipa (još su uključeni neki specijalni slučajevi, ali ništa bitno):

NumberQ/@{-5,(2/7),3.14*^30,I}

Ipak, mnoge stvari "jesu" brojevi, ali nisu gornjeg tipa (Head im nije iz skupa {Integer,Rational,Real,Complex}). Za njih služi generalnija funkcija, NumericQ:

NumberQ/@{π,(Sqrt[2]),Gamma[3,4,5],Khinchin}

N/@{π,(Sqrt[2]),Gamma[3,4,5],Khinchin}

NumericQ/@{π,(Sqrt[2]),Gamma[3,4,5],Khinchin}

Što se Mathematice tiče, beskonačnosti i neodređeni oblici nisu brojevi, ali objekti koji predstavljaju prevelike ili premale brojeve jesu:

NumericQ/@{∞,Indeterminate,Underflow[],Overflow[]}

Način na koji NumericQ radi je da pogleda je li head njegovog argumenta "numerička funkcija" (NumericFunction), što znači da vraća broj ako su njeni argumenti brojevi, pa ako jest, rekurzivno se poziva na argumentima. Na atomima djeluje slično kao NumberQ, s tim da ga je na korisničkim simbolima moguće eksplicitno postaviti na True i time propisati da je određeni simbol broj:

NumericQ@broj=True

NumericQ/@{5Sin@(broj^3)+2,nekafunkcija@broj,nekisimbol,broj-nekisimbol,broj∧broj}

Neki drugi questioni

Općenito, funkcije koje vraćaju True ili False ovisno o tome imaju li njihovi argumenti neka svojstva, zovu se "questions" (pitanja), i obično završavaju na Q (dobro je to pravilo primjenjivati i na funkcije koje mi definiramo). Zajedničko im je to što obično "igraju na sigurno", odnosno vraćaju False ako nisu u stanju strukturalno utvrditi svojstva:

IntegerQ[(((1+(Sqrt[2])))^2)-2(Sqrt[2])]

IntegerQ@Expand[(((1+(Sqrt[2])))^2)-2(Sqrt[2])]

EvenQ/@{0,-56,2k,2k+1,"bla",∞,2I}

Evo liste svih simbola u System kontekstu koji završavaju na Q, prije kojeg je malo slovo (* znači nula ili više znakova, @ jedno ili više malih slova):

??System`*@Q

??@*

Još neke funkcije koje vraćaju Booleans

…ali nisu questions, jer mogu ostati neevaluirane ako nemaju dovoljno podataka, su Positive,Negative,NonPositive,NonNegative,∈,<,>,==,≠,≤,≥,…. Od njih su prve 4 unarne, i zato nama zanimljive (zasad).

Positive/@{π,12,-5.6,I,0,broj,(((1+(Sqrt[2])))^2)-2(Sqrt[2])}

Questioned patterns

Sve je to lijepo, ali kako ograničiti patterne da match-aju samo ono što zadovoljava pojedini question (ili općenito vraća True nekoj unarnoj funkciji)? Pomoću operatora "?" koji stavimo nakon blanka i eventualno zahtijevanog heada:

korijen@x_?NonNegative:=(Sqrt[x])

korijen@{5,9,1,0,-1,-4}

korijen/@{5,9,1,0,-1,-4}

kvadriraj@m_?MatrixQ:=m\[Transpose].m

kvadriraj@(({{1, 2, 3}, {4, 5, 6}}))

MatrixForm@%

kvadriraj@{{1,2},{3}}

aproksimiraj@x_?ExactNumberQ:=N[x,20]

aproksimiraj@((5+I)/31)

aproksimiraj@3.14

Općeniti uvjeti na patterne

Naravno, ako nas gornji oblici ne zadovoljavaju, uvijek možemo Mathematici reći da provjeri neki općeniti uvjet na ono što je match-nuto imenovanim blankovima, da bi pattern mogao match-nuti naš izraz. To činimo operatorom /;.

naziv@1="jedan"
naziv@2="dva"
naziv@0="nula"
naziv@x_Integer?Negative="minus…"
naziv@x_Integer/;2<x≤5="nekoliko"
naziv@x_Integer="previše"

Ako uvjet ne ovisi samo o jednom imenovanom blanku nego o više njih, moramo ga napisati nakon čitave lijeve strane pridruživanja ili pravila:

oddo[x_Integer,y_Integer]/;x<y:=Append[oddo[x,y-1],y]
oddo[x_Integer,y_Integer]/;x>y:=Append[oddo[x,y+1],y]
oddo[x_Integer,x_]:={x}

oddo[9,2]

oddo[-4,7]

naziv/@%

Ako imamo odgođena pravila ili pridruživanja (s dvotočkom), /; možemo staviti i na kraju desne strane:

jedinicni@φ_:=ComplexExpand@(E^(I φ))/;0≤φ<2π

jedinicni@((7π)/4)

jedinicni@-3

Ili čak unutar blokovskih konstrukta (With, Module, Block,…) ako je neki od njih head desne strane:

triprostafaktora@x_:=With[{l=FactorInteger@x},(Subscript[l, [[;;3,1]]])/;Length@l≥3]

triprostafaktora[99!]

triprostafaktora@(6^6)

Ponavljanja

Sequence

Sequence je head koji automatski nestaje kad se nađe unutar bilo kojeg drugog heada (dobro, gotovo bilo kojeg); svi njegovi ostali dijelovi ostaju unutar tog heada, na mjestu gdje je čitav Sequence bio:

Sequence@3

f@%

Zašto je to korisno? Osnovni razlog je što Sequence može imati duljinu i veću od 1. Na taj način dobijemo jedan objekt koji predstavlja više argumenata neke funkcije, odvojenih zarezima.

dvaipet=Sequence[2,5]

f@dvaipet

oddo@dvaipet

N@dvaipet

ConstantArray@dvaipet

PowerMod[dvaipet,7]

PowerMod[7,dvaipet]

{3,7,dvaipet,1,-3}

1+dvaipet

3dvaipet

(7^dvaipet)

(dvaipet^3)

FactorInteger@%

Iz posljednjeg primjera vidimo da tu nema nikakvih "zagrada" oko 2 i 5... sintaksa i prioriteti su točno kakvi su bili, samo kao da smo umjesto "dvaipet" napisali "2,5" u FullForm.

Naravno, Sequence može imati i veću duljinu, pa čak i duljinu 0. Sequence[] predstavlja objekt koji nestaje čim se nađe unutar nekog većeg izraza:

praznina=Sequence[]

{3,4,praznina,7}

(praznina^17)

Sin@praznina

Ponavljajući blankovi

Sada već znamo definirati funkcije koje primaju razne vrste i brojeve argumenata, no još uvijek ne znamo definirati funkciju koja prima proizvoljno mnogo argumenata, poput Plus. Tu će nam pomoći Sequence. U tu svrhu služe ponovljeni blankovi, __ i ___ (dva i tri blanka, redom). __ označava bilo koji neprazan Sequence, dok ___ može biti i prazan.

gutasve@__="Mljac!"

gutasve[5,6,3,2,4]

gutasve[]

gutasveinista@___="Hmmm…"

gutasveinista[]

gutasveinista[π,-∞,a+b]

Naravno, te Sequence, kao i obične blankove, možemo i imenovati:

staviunule@x___:={0,x,0}

staviunule[4,5,2,1]

staviunule[]

Sequence-ova magija nas ponekad može i iznenaditi; čak ni Length mu ne odolijeva:

prosjek@x__:=((Plus@x)/(Length@x))

prosjek[2,3,3]

prosjek@x__:=((Plus@x)/(Length@{x}))

prosjek[2,3,3]

Sequence-ovima možemo davati i uvjete na head, kao i questione koje moraju ispunjavati; u tim slučajevima, svi elementi koji bi htjeli biti u Sequence-u, moraju imati traženi head, odnosno odgovoriti True na zadani question:

prosjekocjena@x__Integer?Positive:=N[((Plus@x)/(Length@{x})),3]

prosjekocjena[3,4,5]

prosjekocjena[4.1,2]

prosjekocjena[-3,2,5]

Naravno, htjeli bismo da prosjekocjena ne može primiti argumente veće od 5:

prosjekocjena@8

Ali zasad ne znamo to napraviti; za razliku od provjere heada i questiona, /; se ne distribuira na Sequence-e dovoljno dobro:

Clear@prosjekocjena

prosjekocjena[x__Integer?Positive/;x≤5]:=N[((Plus@x)/(Length@{x})),3]

prosjekocjena[3,2]

ocjenaQ@x_Integer:=1≤x≤5

prosjekocjena@x__Integer?ocjenaQ:=N[((Plus@x)/(Length@{x})),3]

prosjekocjena[3,2]

prosjekocjena[3,2,6]

prosjekocjena[3,-1,6]

prosjekocjena[3,3.5,6]

Dakle, jedno od rješenja je definirati svoj question. Primijetimo da je ocjenaQ definirana samo za Integer-e, ali to je u redu jer ? samo uzima one koji vraćaju True (ako vraćaju bilo što drugo, uključujući False ili same sebe nepromijenjene, ne postoji match).

Ostali ponavljajući patterni

.. općenito znači jedan ili više primjeraka upravo navedenog patterna. ... znači nula ili više, dakle match-a i prazan Sequence. Možemo zamisliti da je __ skraćenica za _.., te ___ skraćenica za _..., s dodatnim zgodnim detaljima poput uvjeta na head i questione.

gutaaove[a..]="Mljac!"

gutaaove[a,b]

gutaaove[a,a,a,a]

relacijaQ[{_,_}...]=True
relacijaQ@__=False

relacijaQ[{2,3},{5,6},{6,7}]

relacijaQ[3]

relacijaQ[{2,3},{5,6,7}]

Ako imenujemo pattern (ili neki njegov dio) pod operatorom .., on u svim primjercima koji se matchaju mora imati istu vrijednost:

konstantaQ[{_,k_}..]=True
konstantaQ@___=False

konstantaQ[{2,3},{5,3},{6,5}]

konstantaQ[{2,3},{5,3},{6,3}]

Imenovanje općenitih patterna

Da bismo mogli imenovati gore navedene patterne, služi nam operator :. Njemu s lijeve strane stoji ime, a s desne pattern. Možemo smatrati da je x_ skraćenica za x:_.

{f[2,3,4],f[5,6],f[7],f[],f[(1/2),3]}/.t:f@__Integer:>(1/t)

Opcionalni argumenti

Ako se : nalazi s desne strane patterna, ona označava opcionalni argument. Njegova defaultna vrijednost navodi se nakon dvotočke:

palindromQ[n_Integer?Positive,baza_Integer:10]/;baza≥2:=With[{z=IntegerDigits[n,baza]},z==Reverse@z]

palindromQ@232

palindromQ[27,2]

Opcionalni argument se može jedino ne navesti, ili navesti prema pravilima patterna. Defaultna vrijednost koristi se samo ako argument nije naveden; inače nemamo match.

palindromQ[27,5.6]

Opcionalni argumenti ne moraju biti zadnji:

Clear@korijen

korijen[ex:(_Integer?Positive):2,x_]:=(x^(1/ex))

korijen[5,19]