Opus Polyglotis II: Rebol


mandag 27. februar 2012 Polyglot Rebol

act3

OPUS POLYGLOTIS II, Act 3
Tagline: "Fjern støttehjulet, heng opp Wunderbaumen, og slå ut håret!"
På scenen: Rebol

Nå blir det gjensyn med en rebell jeg presenterte første gang i Julekalenderen 15. desember. Jeg vet ikke akkurat hva for noe nytt det vil tilføre i forhold til denne bloggserien, men jammen var det gøy å bruke dette språket!

Selve løsningen er vel mer eller mindre funksjonell (som Python-løsningen i akt 1), men bruker i tillegg et globalt objekt hvor de ulike formatene kan registrere seg. Dermed følger jeg Open/Closed-prinsippet (som i Ruby-løsningen i akt 2), og behøver ikke å endre noe dispatch-kode for å legge til flere formater. I tillegg demonstrerer jeg Rebol's ganske geniale parse-funksjon, som jeg også benytter da jeg lagde en DSL på selveste julaften i fjor.

Jeg kommer ikke til å forklare alle detaljer i koden – jeg vil i stedet at du forsøker å tolke mye av det selv. Er du interessert i språket kan du lære endel av det.

Men først må vi tette et lite hull

Rebol har ikke noen map-funksjon. Og jeg er glad i å mappe! Derfor måtte jeg lage min egen map. Funksjonen du ser nedenfor virker avansert, men det er fordi den kan brukes på to måter: Den kan mappe en funksjon over én serie med elementer, eller den kan mappe en funksjon over to serier.

Jeg kunne nok ha gjort den enda mere generell, men nå dekket den behovet mitt for denne løsningen, så jeg stoppet der.

 23 map: func [
 24   { Returns a block consisting of the result of applying
 25     fun to the items in a series. }
 26   fun         [function!] "Function to map on"
 27   series      [block!]    "Elements to map"
 28   /two        "Use two series (fun must take two elements)"
 29   series2     [block!]    "The second series of elements"
 30   /local temp "Temp var to hold mapped values"
 31 ] [
 32   temp: copy []
 33   either two [
 34     " Map over both series and series2"
 35     while [not tail? series] [
 36       append temp fun first series
 37                       pick series2 index? series
 38       series: next series
 39     ]
 40     series: head series
 41     temp
 42   ] [
 43     " else: map only the first series, which is simple "
 44     foreach x series [ append temp fun x ]
 45   ]
 46 ]

En DSL for registrering av nye formater

Og da kan vi begynne med selve løsningen. Jeg startet med å opprette et objekt – opus-polyglotis – som har en slot som holder en liste med format-funksjoner med tilhørende format-nøkler. Objektet har i tillegg to funksjoner, én for å legge til et format og én for å hente ut et format.

 48 opus-polyglotis: context [
 49   formaters: []
 50 
 51   add-format: func [key definition] [
 52     append formaters key
 53     append formaters :definition
 54   ]
 55 
 56   get-format: func [key] [
 57     select formaters key
 58   ]
 59 ]

I neste kodeblokk ser du hvordan jeg bruker dette objektet til å definere og registrere YAML-formatet:

 61 opus-polyglotis/add-format "yaml"
 62 
 63 func [headers records] [
 64   "YAML not supported yet"
 65 ]

Det ser ut som om jeg har opprettet en anonym funksjon uten å beholde noen referanse til den, men den er altså egentlig det andre argumentet til kallet til add-format funksjonen. Ganske kult egentlig – Rebol's syntaks er så løs og ledig at dette helt utilsiktet ble en aldri så liten intern DSL.

XML-formatet

Nå kommer koden for å formatere dataene mine (som jeg ikke har presentert enda) som XML. Formatfunksjonen har to hjelpefunksjoner: xml-tag og xml-record. Bruk litt fantasi så tror jeg du skjønner hvordan XML'en genereres. For å holde eksempelet ryddig og forståelig droppet jeg å legge til indentering og linjeskift.

 67 opus-polyglotis/add-format "xml"
 68 
 69 func [headers records /local xml-tag xml-record] [
 70 
 71   xml-tag: func [tag body] [
 72     tag: replace tag " " "_"
 73     rejoin [ "<" tag ">" body "</" tag ">" ]
 74   ]
 75 
 76   xml-record: func [row] [
 77     xml-tag "record"
 78             rejoin map/two :xml-tag headers row
 79   ]
 80 
 81   xml-tag "records"
 82           rejoin map :xml-record records
 83 ]

json-formatet

Det var litt mere komplisert å produsere JSON. Hvis du har problemer med å forstå denne koden så er det helt greit. Men ta en ekstra titt på numeric?-funksjonen, som skjekker om en streng inneholder et tall. Rebol har ikke regulære uttrykk (som jeg brukte i Python og Ruby), men tilbyr i stedet en mye sterkere parse-funksjon.

101 opus-polyglotis/add-format "json"
102 
103 func [
104   headers records
105   /local numeric? json-object json-value clear-from-tail
106 ] [
107 
108   chop-last-comma: func [str] [
109     clear skip str (length? str) - 2
110     return str
111   ]
112 
113   numeric?: func [x] [
114     digit:   charset "0123456789"
115     integer: [ some digit          ]
116     float:   [ integer "." integer ]
117     parse x  [  float  |   integer ]
118   ]
119 
120   json-value: func [v] [
121     either numeric? v
122       [               v        ]
123       [ rejoin [ "^"" v "^"" ] ]
124   ]
125 
126   json-object: func[values] [
127     append compose ["^{" (
128       chop-last-comma rejoin map/two func[key value] [
129         rejoin [ "^"" key "^":" json-value value ", " ]
130       ] headers values
131     ) "^}," ] newline
132   ]
133 
134   rejoin compose [
135     "[" (chop-last-comma rejoin map :json-object records) "]"
136   ]
137 ]

Det jeg gjør i numeric? er å bygge opp definisjonen på et knøttlite språk. Er du kjent med BNF-grammars bør du se hvordan dette fungerer. Jeg sier at et digit er et tegn i serien "0123456789". Så sier jeg at en integer består av noen digits. Deretter sier jeg at en float består av en integer, etterfulgt av punktum, etterfulgt av en ny integer. Til slutt kaller jeg parse på teksten jeg skal teste, og sier at input skal være float eller integer.

Dette er bare en ørliten smakebit av hva parse kan gjøre.

Resten av programmet

Nå må jeg hente kommandolinje-argumentene: formatet som er valgt for output, og filen som skal leses:

140 selected-format: first system/options/args
141 csv-file: to-rebol-file second system/options/args

Så skal jeg parse CSV-filen. Igjen bruker jeg parse-funksjonen, som skjønner CSV-filer helt utmærket sålenge jeg forteller den hvilket tegn som brukes som separator. Resultatet lagrer jeg i variablene headers og records.

144 records: []
145 foreach line read/lines csv-file [
146   append/only records parse/all line ";"
147 ]
148 headers: first records
149 records: skip records 1

Og så kommer dispatch-koden. Jeg henter ut formatet fra opus-polyglotis, sender inn dataene til funksjonen, og printer resultatet. Hey Presto!

152 formater: opus-polyglotis/get-format selected-format
153 print formater headers records

Oppsummering

Som sagt vet jeg ikke om Rebol tilførte så mye nytt i forhold til problemet jeg jobber med, men jeg føler språket viste seg fra en bra side. Rebol er et lite språk med få funksjoner og enkle regler, men funksjonene er kraftige og kan settes sammen til å gjøre fantastiske ting med forholdsvis lite kode. Det var stimulerende og gøy å implementere dette programmet.


comments powered by Disqus