Još o sintaksi
Postoji još par pojedinosti Haskel sintakse koje nismo do sada obradili, a koje ćemo u nastavku prezentovati.
Podudaranje oblika
Nakon što smo se upoznali sa algebarskim tipovima, možemo dati nešto precizniji opis mehanizma podudaranja oblika. Podudaranje oblika smo koristili pri definisanju funkcija. U opštem slučaju, ovakve definicije su sačinjene od više linija koje predstavljaju primenu funkcije na oblike1. Svaka linija ima formu jednakosti f x₁ x₂ … xₘ = y
gde su x₁
, x₂
itd. neki oblici. Oblik može biti nešto od sledećeg:
- Literal broja, odnosno izraz poput
2
,3.14
,0x3A
, itd... - Literal karaktera, odnosno izraz poput
'a'
,'B'
,'ш'
, itd... - Literal niske, odnosno izraz poput
"hello"
,"world!!"
, itd... - Proizvoljno ime, odnosno izraz poput
x
,ys
,n
, itd... - Džoker
_
- Literal uređene \(n\)-torka oblika, odnosno izraz poput
(1, 'a')
,(x, y, z)
,(_,1,_,x)
itd... - Literal liste oblika, odnosno izraz poput
[]
,[5]
,[x,y,_]
, itd... - Konstruktor primenjen na oblike ili zapise, odnosno izraz poput
True
,Maybe x
,Vektor x y _
itd...
Svako podudaranje oblika koje možemo konstruisati samo kombinovanjem prethodno navedenih oblika. Primetimo da oblici \(n\)-torke, liste i algebarskog tipa podatka, dozvoljavaju da se konstruišu oblici proizvoljne složenosti2.
Numerički literal, karakter ili niska u obliku će se podudariti sa argumentom funkcije, ako i samo ako je taj argument jednak3 tom literalu. Oblik sačinjen od imena, podudariće se sa svakim argumentom, i to ime će imati vrednost argumenta sa desne strane jednakosti. Jedno ime može se koristi samo na jednom mestu sa leve strane jednakosti4. Džoker _
takođe se podudara sa svakom vrednošću argumenta, ali za razliku od imena džoker se može pojavljivati na više mesta sa leve strane jednakosti. Vrednost argumenata koji se podudario sa džokerom, ne može se iskoristiti sa desne strane jednakosti.
Oblici sa listama, \(n\)-torkama, i konstruktorima uglavnom u sebi sadrže neke druge podoblike (npr. literale). Ovi oblici će se podudariti sa argumentom funkcije ako i samo ako se svi podoblici podudare sa odgovarajućim vrednostima.
Primetimo da se liste mogu podudariti na dva načina: ili preko literala liste (npr [1,2,3]
) ili korišćenjem konstruktora liste (npr: x:xs
). Nekad se ova dva pristupa podudaranju lista koriste istovremeno (npr. u obliku x:[_]
). Ovo takođe važi i za uređene n
-torke koji se mogu podudariti korišćenjem konstruktora poput (,)
, (,,,)
. itd. ali se ovakav pristup retko koristi u praksi.
As sintaksa
Konstruktori se mogu koristiti na dva načina: kao funkcije pomoću kojih se konstruišu vrednosti nekog tipa (otuda i samo ime), ali i za dekonstrukciju vrednosti podudaranjem oblika. Neretko se dešava da u definiciji funkcije neki argument dekonstruišemo da bi u telu funkcije istu tu vrednost konstruisali.
Neka je dat tip koji predstavlja petodimenzionalni vektor:
data Vektor = Vektor Float Float Float Float Float
Neka su date i dve funkcije g1, g2 :: Vektor -> Float
. Zamislimo da je potrebno napisati funkciju f :: Vektor -> Int
koja za dati vektor v
vraća vraća vrednost g1 v
ako je prva koordinata vektora v
pozitivna, a suprotnom g2 v
. Jedna implementacija tražene funkcije bi mogla da glasi ovako:
f :: Vektor -> Float f (Vektor x y z w q) = if x >= 0 then g1 (Vektor x y z w q) else g2 (Vektor x y z w q)
Vidimo da u kodu nepotrebno uvodimo promenljive y
, z
, w
, q
. Vrednosti koje predstavljaju ove promenljive nisu bitne za samu funkciju f
. Situacija bi nešto bolja bila da smo iskoristili let in
ili where
konstrukciju, ali i osnovni problem bi i dalje ostao: istu vrednost dekonstruišemo pa konstruišemo.
As sintaksa nam omogućuje da isti argument istovremeno vežemo za jedno ime ali i da iskoristimo podudaranje oblika. Sintaksa ima oblik ime@oblik
, pri čemu se ime
ne nalazi u obliku. Na ovaj način vrednost čitavog argumenta će biti dostupna kao ime
sa desne strane jednakosti.
Koristeći as sintaksu za prethodni primer, funkciju f
možemo definisati kao
Zapravo, kako nas vrednosti y
, z
, w
, q
uopšte ne interesuju, možemo ih zameniti džokerima:
f :: Vektor -> Float f v@(Vektor x _ _ _ _) = if x >= 0 then g1 v else g2 v
Case notacija
Podudaranje oblika je do sada bilo isključivo vezano za definicije funkcija: da bismo iskoristili podudaranje oblika, morali smo da definišemo novu funkciju5. Case konstrukcija nam omogućava podudaranje oblika unutar izraza, bez potrebe za definicijom funkcije.
Case izraz započinje sa kodom case x of
gde je x
vrednost čiji oblik želimo da ispitamo. Nakon ovog početka, potrebno je navesti blok sačinjen od oblika i rezultata razdvojenih strelicom ->
.
Za blok oblika koji sledi nakon koda case x of
važe iste napomene kao i za blokove poput where
ili let in
blokova. Blok se može napisati i u jednoj liniji razdvajanjem slučajeva sa znakom ;
. Ako se blok prelomi u više linija, tada počeci svih linija moraju biti poravnati po vertikali.
Iskoristimo primer iz prethodne glave za demonstraciju različitog preloma case izraza. Sve naredne definicije definišu istu funkciju
data Dužina = Metar Float | Milja Float | SvetlosnaSekunda Float deriving (Show) uMetre' :: Dužina -> Dužina uMetre' l = case l of Milja m -> Metar (1609.344 * m) SvetlosnaSekunda s -> Metar (299792458 * s) x -> x uMetre'' :: Dužina -> Dužina uMetre'' l = case l of Milja m -> Metar (1609.344 * m) SvetlosnaSekunda s -> Metar (299792458 * s) x -> x uMetre''' :: Dužina -> Dužina uMetre''' l = case l of Milja m -> Metar (1609.344 * m); SvetlosnaSekunda s -> Metar (299792458 * s); x -> x
Možda isprava ne deluje kao mnogo korsina, ali case notacija može skratiti kôd.
Zadatak 1. Napisati funkciju moždaZbir :: Maybe [Maybe Int] -> Int
koja sabira sve vrednosti različite od Nothing
u datom možda-nizu možda-brojeva. Iskoristiti case notaciju
Pragme i ekstenzije
Pragma je instrukcija za Haskel kompajler smeštena u kodu. Pragme se zapisuju u obliku {-# WORD ... #-}
, gde WORD
označava tip pragme, nakon čega mogu slediti dodatne informacije. Neki od dostupnih, i najčešće prisutnih, tipova pragmi su:
LANGUAGE
omogućava ekstenzije Haskel jezika. O ekstenzijama ćemo govoriti u nastavku.OPTIONS_GHC
prosleđuje opcije GHC kompajleru, koje bi se inače prosledile kao opcije pri pokretanju kompajlera.WARNING
iDEPRECATED
omogućava ispis poruka upozorenja prilikom kompilacije koda.MINIMAL
određuje minimalan skup metoda koje instanca neke klase mora da implementira.INLINE
iNOINLINE
sugerišu kompajleru da (ne) izvrši određenu optimizaciju (inlining) prilikom generisanja izvršne datoteke.
Mi smo se u prošloj lekciji upoznali sa MINIMAL
pragmom koju ćemo imati prilike ponekad da koristimo. Ostale pragme ćemo ređe koristiti, osim LANGUAGE
pragme koja je verovatno najčešće korišćena pragma i koja je prisutna u gotovo svakom netrivijalnom Haskel projektu.
Ekstenzije
Pragma LANGUAGE
omogućuje aktivaciju Haskel ekstenzije koja na neki način menja Haskel jezik. Ekstenzije mogu da dozvole određenu sintaksu koja nije validna bez te ekstenzije, da promene neku pojedinost sistema tipova, sistem modula, integraciju sa drugim jezicima, ili jednostavno da zabrane neke konstrukcije, itd.. Skup ekstenzija se menjao tokom decenija: nove ekstenzije su se dodavale i nastavljaju da se dodaju, neke ekstenzije su prevaziđene, a neke su postale deo standardnog jezika.
Pragma LANGUAGE
mora se navesti na vrhu datoteke, pre dekleracije modula, i odnosi se samo na modul u kom je navedena:
Većina ekstenzija je van domašaja ove lekcije, ali za sada možemo razumeti naredne:
BinaryLiterals
dozvoljava pisanje celobrojnih literala u binarnom obliku. Ovakvi literali moraju se započeti sa0b
ili0B
. Na primer, literal0b100
predstavlja \(4\).DuplicateRecordFields
dozvoljava da različiti zapisi imaju polja sa istim imenom.HexFloatLiterals
dozvoljava korišćenje heksadecimalnih brojeva sa zarezom: literal0x0.1
predstavlja \(\frac{1}{16}\), literal0x0.0F
predstavlja \(\frac{15}{256}\), itd...InstanceSigs
dozvoljava da se prilikom instanciranja klase navedu tipovi metoda.LambdaCase
dozvoljava da se lambda izraz oblika\x -> case x of {p₁ -> e₁; ...; pₙ -> eₙ}
zapiše kao\case {p₁ -> e₁; ...; pₙ -> eₙ}
. Ovo je jedna od najkorišćenijih ekstenzija.MagicHash
dozvoljava da se na kraju imena postavi znak#
, i uvodi još neke oblike literala. Ova ekstenzija se najčešće koristi pri radu sa nekim low-level tipovima Haskel jezika.NoImplicitPrelude
stopira implicitni uvoz Prelude modula. Ovo oslobađa prostor imena.NumericUnderscores
dozvoljava zapis numeričkih literala sa znakom_
radi bolje čitljivosti. To omogućava da npr. vrednost milion zapiše kao1_000_000
. Ako je ova ekstenzija aktivirana, znak_
u numeričkom literalu se u potpunosti ignoriše od strne kompajlera.OverloadedRecordDot
dozvoljava da se poljub
nekog zapisaa
pristupi saa.b
umesto sab a
. Ovakva sintaksa je analogna drugim jezicima gde se sa tačkom pristupa poljima nekog objekta (strukture).UnicodeSyntax
Dozvoljava da se umesto ASCII nizova poput::
,->
.<-
,=>
, itd, koriste UTF simboli poput∷
,→
,←
,⇒
, itd... Ovo omogućava zapise poputf ∷ Num a ⇒ a → Int
.
Ekstenzije su moćan deo Haskel jezika koje omogućuju programerima da olakšaju programiranje, ali i da istraže nove pravce u kojima bi Haskel jezik mogao da se razvija u budućnosti. Sa velikom moći dolazi i velika odgovornost, te je stoga potrebno razmisliti o posledicama6 pre aktiviranja ekstenzija. U praksi, neke ekstenzije su često prisutne na projektima (npr. LambdaCase
), dok se neke ekstenzije zaobilaze u širokom luku (npr. UnicodeSyntax
).
Zadatak 2. Uključiti ekstenziju LambdaCase
i ponovo definisati funkciju uMetre :: Dužina -> Dužina
koristeći novu sintaksu.
Programiranje bez tačaka
Tehnika programiranja bez tačaka7 je stil pisanja programskog koda u kom se ne navode parametri funkcija. Ova tehnika je moguća zahvaljujući osobinama sintakse Haskela zbog čega je navodimo ovde (iako sama po sebi nije deo Haskel sintakse).
Suština programiranja bez tačaka leži u osobini da se lambda funkcija \x -> f x
može zameniti sa funkcijom f
. Zaista, funkcija koja primenjuje f
na argument x
i vraća dobijeni rezultat, ne razlikuje se od same funkcije f
, i možemo ih smatrati istima8. Zamena \x -> f x
naziva se eta konverzija.
Eta konverzija je jednostavan princip, ali u kodu često koristimo podudaranje oblika umesto lambda izraza, zbog čega prilika za eta redukcijom može biti skrivena "ispred nosa". Na primer, neka je data funkcija h :: Int -> Int -> Bool
i neka je funkcija g :: Int -> Bool
definisana sa g x = h 10 x
. Ovakvo jednostavno podudaranje oblika nije ništa drugo nego skraćenica za definiciju sa lambda izrazom g = (\x -> h 10 x)
. Uzimajući u obzir da je h 10 x
zapravo (h 10) x
, vidimo da lambda izraz (\x -> (h 10) x)
možemo eta konverzijom da svedemo na h 10
. Samim tim originalna definicija funkcije g
je postala sada g = h 10
. Dakle, eta konverzija nam omogućuje "skraćivanje" najdesnijeg parametra u definicijama oblika g x₁ ... xₙ = IZRAZ xₙ
, pod pretpostavkom da se xₙ
ne pojavljuje u IZRAZ
9.
Funkciju zbir :: [Int] -> Int
koja vraća zbir svih elemenata liste, možemo definisati:
zbir xs = foldr (+) 0 xs
"Skraćivanjem" parametra xs
dobijamo kod
zbir = foldr (+) 0
Primetimo da u definiciji sum = foldr (+) 0
iz primera, ne pojavljuje se parametar funkcije sum
. Stoga za ovu definiciju kažemo da je napisana u stilu bez tačaka. Izraz bez tačaka potiče iz matematike, gde se često za argument funkcije kaže tačka10 (iako možda ta funkcija nema veze sa geometrijskim tačkama). Kôd bez tačaka može biti elegantniji od "koda sa tačkama", ali ta elegantnost može vrlo lako da preraste u nečitljivost11. Ipak, dobro je razumeti ovu tehniku, jer je popularna među Haskel programerima i može se videti u svim projektima.
Napišimo definiciju f x = 2 * x - 1
u stilu bez tačaka. Prvo možemo izdvojiti operator oduzimanja u prefiksnu poziciju čime dobijamo f x = (-) (2 * x) 1
. Pošto je za eta konverziju neophodno da parametar bude smešten na desnoj strani izraza, iskoristićemo flip :: f -> a -> b -> b -> a
kombinator koji obrće redosled parametara funkcija. Kombinator ćemo primeniti na funkciju (-)
, čime stižemo do definicije f x = flip (-) 1 (x * 2)
. Ako postavimo i *
u prefiksnu poziciju dobijamo f x = flip (-) 1 ((*) 2 x)
. Vidimo da se ovde radi o kompoziciji funkcija flip (-) 1
i (*) 2
primenjenoj na x
, pa je f x = ((flip (-) 1) . ((*) 2)) x
. Prostom eta redukcijom stižemo do f = (flip (-) 1) . ((*) 2)
U ovom primeru vidimo da ne treba po svaku cenu pisati kod u stilu bez tačaka12.
Ironično, u stilu bez tačaka često se pojavljuje operator kompozicije koji se zapisuje sa tačkom. Ovo često buni početnike, te je dobro napomenuti da stil bez tačaka samo podrazumeva da se funkcije definišu bez navođenja parametara, a da ne podrazumeva ništa o upotrebi operatora kompozicije.
Pokušajmo sad da rešimo "problem" koji se često može sresti u Haskel kodu. Neka su date funkcije f' :: A -> B -> C
i g' :: C -> D
. U kodu nam je potrebna funkcija h = \x y -> g' (f' x y)
, i voleli bismo ovu funkciju napišemo u stilu bez tačaka. Prva ideja koja nam pada na pamet je da iskoristimo operator kompozicije .
i definišemo h = g' . f'
. Međutim dobićemo tipsku grešku čim ovako definišemo h
. Razlog tome je što je kodomen funkcije f
tip B -> C
13. To znači da domen funkcije g'
mora biti isto B -> C
ako želimo izvršimo kompoziciju, što naravno nije za naš primer. Dakle, funkcije više promenljivih ne možemo uvek lako komponovati sa .
.
Pri traženju definicije bez tačaka, uvek je dobro krenuti od definicije za koju znamo da je dobra h x y = g' (f' x y)
. Videli smo da h
nije kompozicija g'
i f'
, ali h x
jeste kompozicija funkcija g'
i f' x
: h x y = (g' . (f' x)) y
. Sada možemo izvršiti eta konverziju, čime dobijamo h x = g' . (f' x)
. Pomeranjem operatora kompozicije u prefiksnu poziciju dobijamo h x = (.) g' (f' x)
, što se može shvatiti kao primena funkcije (.) g'
na f' x
. Drugim rečima, ovde se radi o kompoziciji h x = (((.) g') . f') x
koja se može eta konvertovati u h = ((.) g') . f'
. Ovim smo stigli do definicije "kompozicije" f'
i g'
koja je u stilu bez tačaka.
14
Ovaj postupak možda želimo da proizvoljne funkcije f :: a -> b -> c
i g :: c -> d
, a ne samo određene f'
i g'
. Stoga definišemo kombinator comp_1_2 :: (c -> d) -> (a -> b -> c) -> (a -> b -> d)
sa comp_1_2 g f = ((.) g) . f
. Jasno, za funkciju h
iz prethodnog paragrafa važi h = comp_1_2 g' f'
. Međutim, definicija comp_1_2
više nije u stilu bez tačaka. Stoga transformišimo i ovu definiciju u stil bez tačaka. Prebacivanjem operatora kompozicije u prefiksnu poziciju od comp_1_2 g f = ((.) g) . f
dobijamo comp_1_2 g f = (.) ((.) g) f
, odakle eta konverzijom dobijamo comp_1_2 g = (.) ((.) g)
. Ovde imamo kompoziciju dve (.)
funkcije, odnosno comp_1_2 g = ((.) . (.)) g
. Poslednjom eta konverzijom dobijamo comp_1_2 = (.) . (.)
.
Zadatak 3. Napisati funkciju koja računa aritmetičku sredinu dva broja u stilu bez tačaka.
Zadatak 4. U stilu bez tačaka dati definiciju kombinatora koji vrši kompoziciju jedne unarne funkcije i jedne ternarne funkcije comp_1_3 ::(d -> e) -> (a -> b -> c -> d) -> (a -> b -> c -> e)
Zadatak 5. U stilu bez tačaka dati definiciju kombinatora koji vrši kompoziciju tri unarne funkcije comp_1_1_1 :: (c -> d) -> (b -> c) -> (a -> b) -> (a -> d)
.