Szintaxis
Egy függvény a q-ban megfelel egy matematikai leképezésnek, amit egy algoritmus definiál. Egy függvény kifejezések sorozata, opcionális bemenő paraméterekkel és egy visszatérési értékkel. A függvény törzsét kapcsos zárójelek közé írjuk. A függvény törzse kifejezések szekvenciájából áll, pontosvesszővel elválasztva. A függvény input paramétereinek és visszatérési értékének a típusát nem határozhatjuk meg.
Más nyelvekkel ellentétben a q-ban nincs "függvénydefiníció" mint nyelvi elem. Csak lambda kifejezés van, amelyet értékül adhatunk egy változónak. A változónevet ezután tekinthetjük a függvény "nevének".
A következő példa egy teljes specifikációja egy függvénynek, amely visszatér az inputja négyzetével. A whitespace a paraméter után opcionális:
f:{[x] x*x}
Az f függvény meghívása úgy történik, hogy a függvény neve után szögletes zárójelekbe írjuk annak paramétereit:
f[3]
9
Egyparaméteres függvénynél elhagyhatjuk a zárójeleket, de ekkor vigyázzunk a kifejezések kiértékelési sorrendjére.
f 3 + 4
49
A lista indexeléséhez hasonlóan lehetőség van paraméterek elhagyására is, ilyenkor részlegesen applikált függvényt kapunk. A függvény törzse csak akkor kerül végrehajtásra, ha már minden paraméterét megadtuk. (Nulla paraméterű függvénynél is használni kell a [] jeleket, vagy átadni egy tetszőleges "dummy" paramétert.)
g:{[x;y;z] x+y+z}
g[1;;3][5]
9
A legfeljebb három paraméterű függvények esetén elhagyhatjuk a paraméterlistát. Ilyenkor az x, y és z azonosítókkal hivatkozhatunk a paraméterekre, a q automatikusan kitalálja, hány paraméteresnek kell lennie a függvénynek. A következő példa a négyzetre emelő függvény, amelyből elhagytuk az opcionális részeket:
{x*x}[5]
25
A példákból láthatjuk, hogy a függvénydefiníciók szintaxisa a következő:
{[p1;...;pn] e1; ...; en}
ahol p1, ..., pn opcionális paraméterek, e1, ..., en pedig kifejezések sorozata, melyek balról jobbra értékelődnek ki.
Egy többsoros függvénydefiníció így a következőképpen néz ki:
f:{[p1;...;pn]
e1;
...;
en}
A függvények rendelkezhetnek lokális változókkal. Ha a függvényben értéket adunk egy olyan változónak, amely még nem létezett, akkor az automatikusan lokális változóként jön létre:
f:{a:x+y+z; :a%3}
A lokális változót csak az adott függvényből érhetjük el, a függvény belsejében definiált más függvényekben már nem.
f:{a:1 2 3 4; {x+max a}[5]} /hiba
f:{a:1 2 3 4; {[a;x] x+max a}[a;5]} /így már jó
A névtérnévvel ellátott globális változók a szokásos módon felhasználhatók és módosíthatók függvényekben is:
f:{.foo.bar:x}
g:{:.foo.bar+x}
f 6
.foo.bar
6
g 5
11
A névtérnév nélküli globális változók kezelése problémásabb. Olvasáskor mindig abban a névtérben keresi a változót, amely a függvény definiálásakor éppen aktív volt, nem pedig magának a függvénynek a névterében, sem a hívás névterében:
.foo.a:5
.bar.a:6
.baz.a:7
\d .bar
.foo.g:{:a}
\d .baz
.foo.g[]
6
\d .foo
g[]
6
Az értékadás nem működik az egyszerű ":" operátorral, mert az lokális változót hoz létre. Helyette a "::" operátort kell használnunk. Kivétel a lista, szótár stb. elemenkénti értékadása - ebben az esetben már működik a sima ":" is.
{a:5}[]
5
a
'a
{a::5}[]
a
5
b:1 2 3
{b[1]:10}[]
b
1 10 3
Mindezek miatt a névtérnév nélküli globális változók használata a függvényekben kerülendő.
Paraméterátadás
Q-ban az összes paraméter (listák, táblák is) másolva kerül átadásra.
Túlterhelés
A függvények paramétereinek nem adhatunk meg típust, se nem konstansságot, de nem is ez a lényeg itt, mert ettől még paraméterszámban túlterhelhetnénk függvényt.
Az az igazság, hogy q-ban minden függvény "névtelen". Egy változónak lehet egy függvényt értékül adni, ilyenkor annak a változónak a típusa függvény lesz. Ezt a változót aztán
átadhatjuk más függvényeknek, tárolhatjuk listában, stb. Máshogy szólva q-ban igazából csak függvénypointerekről beszélhetünk, amelyek névtelen függvényekre mutatnak.
Ezért aztán amikor ugyanolyan névvel próbálunk definiálni egy másik függvényt, akkor igazából egy változó értékadás történik, tehát a függvénypointerünk egy új függvényre fog mutatni.
Ez a viselkedés kizárja a túlterhelés lehetőségét.
Rekurzió
A q nyelv erősen támogatja a funkcionális programozást, így természetesen lehetőség van benne rekurzióra. Itt azonban vigyáznunk kell, mivel minden függvény névtelen.
fact:{$[x;x*fact x-1;1]}
Első ránézésre ez egy jó rekurzív függvénynek tűnik, de problémák merülhetnek fel vele:
perm:fact
perm
{$[x;x*fact x-1;1]}
A függvény értékadás után még mindig a régi névre hivatkozik, ami hibaforrás lehet. Ezért a q-ban létezik egy speciális azonosító, a .z.s ("self"). Ez mindig az éppen végrehajtás alatt álló függvényt tartalmazza, segítségével megoldható a névtelen rekurzív függvény írása.
fact:{$[x;x*.z.s x-1;1]}
Infix függvények
Általában ha egy függvényt definiálunk, azaz értékül adjuk egy változónak, a változónév a q szintaxisa szerint főnév lesz. Ezért csak a jobb oldalán állhat paraméter.
pow:{prd y#x} / x összeszorzása y-szor
pow[2;8]
256
(pow 2) 8
256
2 pow 8
'type
Hiába mutatna jól, hogy a "pow" nevet mint operátort a két argumentum közé írjuk, a szintaxis ezt nem engedi meg. Két kivétellel: az egyik lehetőség, hogy egy módosítószóval együtt használjuk a függvény nevét, ezáltal automatikusan infix operátor lesz belőle. A másik abból a megfigyelésből adódik, hogy a q-ban van néhány beépített függvény, amely lambda kifejezéssel van megadva, de mégis infix módon használható:
", " sv ("alfa"; "beta"; "gamma")
"alfa, beta, gamma"
sv
k){x/:y}
Mégis, ha értékül adjuk egy saját változónak az "sv" értékét, a kapott változó nem lesz infix módon használható. A titok nyitja az, hogy a beépített függvények a .q névtérben vannak definiálva. Ez a névtér két speciális tulajdonsággal is bír: a benne levő függvények nevében egyrészt nem kell kiírni a .q névtérnevet, másrészt kétparaméteres függvény esetén ilyenkor infix módon használhatóak. Ha a saját függvényünket a .q névtérbe rakjuk, akkor az is rendelkezni fog ezekkel a tulajdonságokkal:
.q.pow:{prd y#x} / x összeszorzása y-szor
2 pow 8
256
Ez azzal is jár, hogy az egyparaméteres applikációnál a függvénynév és a paraméter sorrendje megcserélődik, pl. (2 pow) helyes részleges applikáció, míg (pow 2) már hiba. Másrészt viszont a q fejlesztői fenntartják maguknak a .q és az összes egyéb egybetűs névteret speciális használatra, így semmi se garantálja, hogy a következő q verzióban nem lesz olyan nevű függvény, amelyet éppen be szeretnénk tenni a .q névtérbe, szóval nem ajánlott ennek a technikának a használata.
Atomi műveletek és módosítószavak
A q-ban a legtöbb beépített operátor rendelkezik az atomi tulajdonsággal. Ez azt jelenti, hogy a művelet mindig csak atomok között kerül végrehajtásra. Ha nem atomi paramétert adunk nekik, akkor automatikusan kiterjesztésre kerülnek az alábbi módon.
Ha egy atomi műveletnek egy atomot és egy listát adunk át, akkor a művelet a lista egyes elemei és az atom között kerül végrehajtásra:
1 2 3 4+100
101 102 103 104
100-1 2 3 4
99 98 97 96
Ha két listát adunk át, akkor páronként hajtódik végre a művelet. Ebben az esetben a listáknak azonos hosszúaknak kell lenniük:
1 2 3 4+ 10 30 20 40
11 32 23 44
100 101-1 2 3 4
'length
Ez a kiterjesztés rekurzív, ezért működik mátrixokra, szótárakra és táblákra is.
Saját függvény esetén a kiterjesztés nem működik automatikusan, mivel a paramétereket nem lehet megjelölni, hogy elfogadhatnak-e listát. Ezért a lista egy az egyben átadódik a paraméterben. A függvényben természetesen továbbadhatjuk a listát egy atomi műveletnek, így az kiterjesztésre kerül. A hívás helyén is lehetőség van az alapértelmezett viselkedés felülbírására a módosítószavak (adverb) segítségével. A q 6 beépített módosítószót tartalmaz, újakat nem tudunk megadni. Ezeket a szimbólumokat "tapadószóként" a módosítandó függvény mögé írjuk, szóköz nélkül. Az így keletkezett függvény infix operátor lesz, ha még nem volt az.
- Az atomi kiterjesztést engedélyezhetjük egy tetszőleges kétparaméterű függvényre az ' (each-both) módosítószóval.
pow:{prd y#x}
2 pow' 1 2 3 4
2 4 8 16
2 3 4 pow' 4 3 2
16 27 16
Ez azonban csak egy szintig működik. Lehetőségünk van a módosítószavak halmozására, ha többszintű listákkal kell dolgoznunk.
(2 3 4;3 2 3) pow'' (4 3 2;4 3 2)
16 27 16
81 8 9
Emlékezzünk rá, hogy az infix operátorok balra kötnek. Ez kicsit furcsa lehet egyparaméteres függvény esetén. Az "each" beépített függvény gyakran használt szinonima erre az esetre.
.Q.s1 1 2 3 /.Q.s1 : objektum -> q adatkonstruktor string formában
"1 2 3"
.Q.s1' 1 2 3 / rossz
'
1 2 3 .Q.s1' / jó
,"1"
,"2"
,"3"
.Q.s1 each 1 2 3 /ugyanazt jelenti
,"1"
,"2"
,"3"
-
Ha az egyik paraméter egy lista, a másik pedig egy atom, akkor kikényszeríthetjük a listára való kiterjesztést a /: (each right) és a \: (each left) módosítószavakkal. A ferde vonal mindig a kiterjesztendő lista felé "dől".
":" vs/: ("1:2";"3:4") / vs: string felbontása részekre adott elválasztójellel
,"1" ,"2"
,"3" ,"4"
(count;max) @\: 1 3 5 2 4 3 / @ : egyparaméteres applikáció
6 5
A két módosítószó kombinációjával készíthetünk pl. szorzótáblát vagy két lista Descartes-szorzatát mátrix formájában. Ha kell, kilapíthatjuk a "raze" függvénnyel.
(1+til 10) */:\: (1+til 10) / szorzótábla 10-ig
/ til: számok 0-tól x-1-ig
1 2 3 4 5 6 7 8 9 10
2 4 6 8 10 12 14 16 18 20
3 6 9 12 15 18 21 24 27 30
4 8 12 16 20 24 28 32 36 40
5 10 15 20 25 30 35 40 45 50
6 12 18 24 30 36 42 48 54 60
7 14 21 28 35 42 49 56 63 70
8 16 24 32 40 48 56 64 72 80
9 18 27 36 45 54 63 72 81 90
10 20 30 40 50 60 70 80 90 100
raze 2 3 6 ,/:\: 1 4 5 / két lista Descartes-szorzata
/ raze: lista lapítása
/ , : konkatenáció, itt épp rendezett párokat készít
2 1
2 4
2 5
3 1
3 4
3 5
6 1
6 4
6 5
-
Az ': (each prior) módosítószó egy kétargumentumú függvényt hajt végre egy lista szomszédos elemei között, vagyis minden x. és x-1. elem között. Ezzel lehet pl. a szomszédos elemek különbségeit (deltas) előállítani. A módosított függvény használható egy- és kétparaméteres formában. Ez utóbbi esetben az első paraméter a lista "mínusz egyedik" elemeként funkcionál.
{x-y}':[1 5 6]
1 5 6 {x-y}': /ez a két alak ekvivalens
0N 4 1
3 {x-y}':1 5 6
-2 4 1
-
A / (over) módosítószó (vigyázzunk, nehogy komment legyen belőle) egy kétargumentumú függvényt hajt végre egy listára végiggörgetve, tehát a funkcionális nyelvek "foldl" függvényének felel meg. Szintén használható egy- és kétparaméteres formában.
+/[1 2 3]
1 2 3 +/ /ez a két alak ekvivalens
6
+/[10;1 2 3]
10 +/ 1 2 3 /ez a két alak ekvivalens
16
Egyparaméteres függvény esetén addig iterálja a függvényt, amíg egy eredmény meg nem ismétlődik. Az ismétlődés előtti utolsó eredményt adja vissza.
1 2 3 4 rotate[1]/ /rotate: lista forgatása. Lerögzítjük az első paraméterét, így egyparaméteres lesz.
4 1 2 3
-
A \ (scan) módosítószó ugyanazt csinálja, mint az "over", de visszaadja a részeredményeket is. Tehát a funkcionális nyelvek "scanl" függvényének felel meg.
+\[1 2 3]
1 2 3 +\ /ez a két alak ekvivalens
1 3 6
+\[10;1 2 3]
10 +\ 1 2 3 /ez a két alak ekvivalens
11 13 16
1 2 3 4 rotate[1]\
1 2 3 4
2 3 4 1
3 4 1 2
4 1 2 3