mandag 27. februar 2012 Polyglot Rebol
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.
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 ]
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.
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 ]
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.
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
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.