2. helmikuu 2012, 17:25

Ranskalaisten viivojen parsimista Clojurella

Kirjoitin männäviikolla kokeilumielessä ohjelman, joka kääntää ranskalaiset viivat Beamer-Latexiksi. Ideana olisi tuottaa esityksen lihapuoli nopeasti rustaamalla tiivistä merkintää ja sitten kääntää se hyvinmuotoilluksi LaTeX-koodiksi, jossa voi sitten lisätä mausteet.

Koska projektilla ei sitten loppupeleissä taida olla kamalasti merkitystä, niin se jäi vähän hautomoon. Itse ranskalaisten viivojen muuntaminen tietorakenteiksi oli tämän ohjelman punainen lanka. Asian voi ratkaista varmasti aika monella tavalla, kun oletetaan sisennysten määrä konsistentiksi. Toinen ongelma oli lisäsisennys, mikäli kirjoittaa siistiä tekstiä ja teksti kaartuu usealle riville.

Siis seuraavanlaista hahmotelmaa pitäisi parsia tietorakenteiksi:

- Hiilarit
    - Makaroni
    - Leipä
        - ruisleipä, joka päläpälä...
          ...tarina jatkuu tähän
- Protskut
    - Pihvi
    - Soija

Siitä pitäisi parsia sitten karheasti ottaen seuraavanlainen listarakenne:

["Hiilarit"
   ["Makaroni"
    "Leipä"
       ["ruisleipä, joka päläpälä... ...tarina jatkuu tähän"]
   ]
 "Protskut"
   ["Pihvi"
    "Soija"]
]

Enpä ole edes varma, onko kyseinen malli se fiksuin mahdollinen, koska se ei ole laajennettavissa. Kuitenkin, siirrytäänpä itse toteutuksen pohdiskeluun. Clojuressa tottakai mennään rekursiolla eteenpäin. Funktio saa kyseisen listauksen vektorina, joten regexp ei suoriltaan tuntunut hyvältä ajatukselta. Kaikkinensa rekursion eleganttius hukkui nopeasti lisävaatimusten alle.

Ideana on tarkastella annetun listan ensimmäistä riviä, eli tässä tapauksessa “- Hiilarit” -riviä. Lasketaan sen sisennys (tässä 0). Sen avulla luodaan sulkeuma (closure), deeper?, joka kertoo tokikin, onko annettu rivi enemmän sisennetty kuin tämä alkuperäinen rivimme. Sitten aloitamme kakkosrekursion (niin, tässä itse asiassa käytetään kahta rekursiota laiskuuttani) tuosta loop-rivistä alkaen. Käymme siis listan läpi rivi kerrallaan. Ei mitään ihmeellistä. Nyt tarkastelemme sen listan kunkin rivin sisennyksen. Jos löydetään sellainen alkio, joka on sisennetympi, niin keräämme kaikki sisennetyt rivit talteen ja kutsumme walk-funktiota uudestaan, siis rekursiota kahdessa tasossa. Tämänlaisen funktion tekeminen vaatii aina rekursiota, oli se Pythonilla tai Basicilla toteutettu, koska emme tiedä listan syvyyttä etukäteen. Tuo loop-rekursio on perusiterointi, jonka voisi toteuttaa kai miten haluaa.

(defn- walk
  "Walk through a flat list of strings and parse the items into a nested vector
  structure basing on indentation. Finally does a transformer of the element
  using supplied function, if any."
  ([transformer the-list]
   (when-let [initial-indent (indentation (first the-list))]
     (letfn [(deeper? [item]
               (> (indentation item) initial-indent))]
       (loop [ll the-list
              acc []]
         (if-let [item (first ll)]
           (if (deeper? item)
             (if (.startsWith (str/trim item) "- ")
               ; genuine sublist or -item
               (recur (drop-while deeper? ll)
                      (vec (conj acc (walk transformer (take-while deeper? ll)))))
               ; just a line continuation
               (recur (rest ll)
                      (assoc acc
                             (dec (count acc))
                             (str (last acc) \ (str/trim item)))))
             (recur (rest ll)
                   (conj acc (transformer item))))
           (vec acc))))))
  ([the-list]
   (walk identity the-list)))

Tässä koodissa joudutaan ottamaan huomioon myös mahdollinen rivinjatko (line continuation), joka hieman epäselventää kokonaiskuvaa. Näiden rivinjatkojen takia joudumme manipuloimaan akkumulaattoria acc. Se on tietysti sallittua toimintaa, mutta rekursio täytyy rakentaa juuri näin päin tästä syystä.

Viimeisenä jouduin hieman yleistämään funktiotani epäortogonaalisella (myöhemmin huomatakseni, että epäortogonaalinen yleistys olikin turhaa… Olisi pitänyt vain uskoa algoritmisuunnitteluun.) lisäparametrilla transformer, joka tässä tapauksessa trimmaisi rivin tyhjät pois sekä poistaa ranskalaisen viivan loppusilaukseksi. Oletusasetuksena muuntokuvaus on perinteinen identity, eli lista jää ennalleen.

Tästä olisi kiva nähdä vaihtoehtoisia ratkaisuja. Eiköhän joku huimapää osanne regexpilläkin tämän tehdä.

Tageja: ,

---
---

---

Aiheen vierestä