fka-00.png

Lisp-noder i KNIME

Den som inte provat KNIME Analytics Platform kan tänka på det som en arbetsyta där man stoppar in lådor som skickar och tar emot tabeller sinsemellan via små streck. De där lådorna kallas noder och det finns massor av noder som gör alla möjliga saker. KNIME är ett superbra verktyg som jag använder nästan varje dag för att vrida och vända på data av olika slag. KNIME läser och skriver alla möjliga datakällor och när data väl finns i programmet ser det likadant ut oavsett var ifrån det härstammar. Detta besparar mig många "engångs-program" för läsning, bearbetning och skrivning, som jag annars behövt tänka ut och plita ned utan att göra något misstag.

Det fungerar för det mesta att använda de noder som skickas med i grundinstallationen. Joiner, GroupBy, Concatenate, Rule-Based Row Filter och så vidare, vanliga operationer som känns igen från relationsdatabaser finns där. När standardnoderna inte löser uppgiften finns script-noder att ta till, exempelvis noden Java Snippet. De där script-noderna är tyvärr frustrerande att jobba med och det är lätt att börja ta genvägar för att komma vidare innan "tanke-tåget" hunnit lämnat station. Till slut sitter man där med en soppa som är svår att förstå sig på.

Bilden ovan visar bland annat en nod med etiketten "Normalize". Det är en Lisp-nod som fått till uppgift att normalisera text i alla kolumner av sträng-typ. Om jag istället använt noden Java Snippet hade jag varit tvungen att namnge de kolumner som skall bearbetas, det går absolut inte att säga "alla av sträng-typ". Det går förstås att åstadkomma motsvarande med standardnoder, men det skulle antagligen ta en timma och bli riktigt tjorvigt.

Ända sedan jag först började använda KNIME har jag funderat på om det skulle gå att knyta in Common Lisp på ett bra sätt. KNIME är en Eclipse-applikation och därmed skrivet i Java. Armed Bear Common Lisp är en implementation av Common Lisp som kör på Java Virtual Machine och som jag använt mig mycket av. Tänk vilken fördel att kombinera KNIME och Armed Bear! De båda miljöerna är i grunden interaktiva, och båda ger mig som programmerare en känsla av att föra en dialog med programmet, som om jag kunde "ta på" den data som ligger i minnet just för tillfället.

Låt mig presentera: Floatp KNIME Armed Bear

Det är en plugin till KNIME som jag har gjort. Den består av kod i Java och Lisp för att knyta ihop KNIME's ramverk för noder med Lisp-världen. Tyvärr vill inte Armed Bear Common Lisp i grundutförande fungera när det laddas från en OSGi-bundle, den mekanism som Eclipse extension-ramverk bygger på, så med i pluginen finns också en version av Armed Bear Common Lisp som jag petat på lite grand för att råda bot på det.

Jag har strävat efter att hålla Java-sidan så enkel som möjligt och i stället lägga allt verkligt arbete på Lisp-sidan. Java-sidan ansvarar för ladda och konfigurera pluginen samt att vidarebefordra meddelanden mellan KNIME och Lisp-sidan. På Lisp-sidan ligger ansvaret att förstå vad KNIME säger och svara på ett korrekt sätt. På grund av hur ramverket för noder fungerar så är det enklast att lägga till noder med ett förbestämt antal portar in/ut. Jag har lagt till fem noder med upp till två portar in och upp till två portar ut. Den enda skillnaden mellan noderna på Java-sidan är faktiskt just antalet portar in/ut.

fka-01.png

Figure 2: En egen kategori i "Node Repository"

Varje nod i KNIME konfigureras med en dialog-ruta, särskilt utformad för noden i fråga. Jag har valt att låta alla Lisp-noder använda samma konfigurationsdialog. Ramverket kräver att noder i slutänden skall kunna konfigureras och exekveras, så i dialogen anges vilka Lisp-funktioner som skall utföra respektive uppgift. Dialogen har även ett fält som måste innehålla ett symboliskt uttryck, och detta används för att ge noden kompletterande information. Det här tillvägagångssättet är så pass generellt att jag valt att lägga till en mekanism för konfigurationsmallar, annars blir det för mycket knappande. Mallarna läggs till från Lisp-sidan och det går sedan att välja mall i dialogens rullgardin. Vilka mallar som är valbara i rullgardinen avgörs av hur många portar in/ut den aktuella noden har.

fka-02.png

Figure 3: Samma konfigurationsdialog för alla Lisp-noder

DataTableSpec som symboliskt uttryck

Jag har inte implementerat stöd för "vilken datatyp som helst". För att gå från Java-världen till Lisp-världen och sedan tillbaka igen behöver översättning göras, och här har jag valt att lägga ribban lågt. De vanligaste datatyperna finns med och det är enkelt att justera om något skulle fattas genom att lägga fler översättare på en variabel, vilket är standardförfarande i Lisp.

De enkla celldatatyperna Boolean, Int, Long, Double, String och LocalDateTime, samt de sammansatta List och Set finns givetvis med. Samlings-datatyperna fungerar rekursivt.

Det är enkelt att skriva och läsa tabeller som symboliska uttryck med Lisp-noderna. Sink- och Source-noderna använder samma format, och det har visat sig smidigt att kunna skriva ner en tabell som ett symbolikst uttryck, rätta till något för hand (kanske också versionshantera resultatet), och sedan läsa upp tabellen och jobba vidare i KNIME. Det går förstås fint att läsa och skriva tabeller till och från Lisp-variabler. Det kanske viktigaste motivet till Lisp-noder i KNIME var från början att ersätta den omständiga noden Java Snippet med manipulator-noder i Lisp, som kan vara mer flexibla i sättet att välja kolumner och hur värden skall bearbetas. Men det är bara förnamnet, full frihet att fixa det som enklast fixas i Lisp är väl efternamnet, antar jag.

Exempel på skrivning med Lisp Writer Sink

Se skärmdumpen och listningen nedan för ett exempel på hur filformatet ser ut. Har en datafil kommit in från annat håll än KNIME så är det enkelt att komplettera den med en kolumnspecifikation för hand.

fka-03.png

Figure 4: Data från Table Creator och skrivning till fil

;; -*- mode: lisp; coding: utf-8-unix; tab-width: 8; -*-

;; 2023-01-06T01:06:39.353619661

((:STRING "Namn") (:INT "Ålder"))

("Sten" 41)
("Pelle" 23)
("Stina" 8)
("Petronella" 55)

Filen som skrivs i exemplet ser ut som listningen ovan och kan läsas av Lisp Reader Source. Som synes så skrivs först kolumnspecifikationen som ett uttryck och sedan varje rad som ytterligare uttryck, top level i filen. Det sker på det sättet för att kunna läsa och skriva tabellerna i en någorlunda konstant minnesrymd. I de fall läsning och skrivning sker till variabler (eller läsning från en funktions-retur) så omsluts uttrycken av en lista och "stor-uttrycket" får då helt enkelt uppta den plats som krävs i minnet. Har man en lista med data är det enkelt att skriva en kolumnspecifikation för hand och läsa med Reader Source eller S-expression Source. Det händer ofta att jag har meckat ihop listor i Emacs som jag vill jobba vidare med i KNIME, och då fungerar det här sättet utmärkt.

Exempel på läsning med Lisp Source

Läsning med noden Lisp Source kan ske från fil, Lisp-variabel, genom anrop till en Lisp-funktion, eller att ett symboliskt uttryck helt enkelt klistras in direkt i konfigurationsdialogens text-ruta. Här kommer ett exempel på hur det går till när data finns i en variabel. Vi definerar en funktion som lägger till en kolumn-specifikation och sedan konfigurerar vi S-expression Source för att anropa funktionen. Pang, så har vi en tabell i KNIME och kan jobba vidare därifrån.

(defparameter *data*
  '(("a" 1 ("a" "b" nil) (1 2 nil) ((1 2 3) (4 5 6))))
  "A list containing one data row.")

(defun data ()
  "This function may be called by S-expression Source."
  (cons
   ;; Prepend the rows with a table column specification ...
   '((:string "a")
     (:int "x")
     ((:list :string) "a,b,nil")
     ((:set :int) "x,y,nil")
     ((:list :list :int) "nested"))
   ;; ... and then continue with all rows found in the variable `*data*'.
   *data*))

fka-04.png

Figure 5: Noden Lisp Source konfigureras med mallen S-expression Source för att anropa funktionen

Klart slut

Ja, det var det om det, i rätt så svepande drag. Lisp-noder i KNIME är både roligt och tidsbesparande!

Date: 2023-01-05

Author: Gunnar Lingegård

Validate