Lispy C# (og hva er en closure)


onsdag 4. august 2010 Lisp C#

I et forsøk på å gi et bedre innblikk i hva Lisp er for noe har jeg skrevet litt lisp-inspirert C#-kode. Jeg snakker ikke om Lisp-syntaks, men mer programmering i et Lisp-tankemønster. Koden illustrerer hvordan jeg tenker når jeg programmerer i Lisp (og til dels funksjonelle språk generelt). Eksperimentet er mest for gøy, men om det kan gi noen en a-ha-opplevelse hadde det også vært kjekt.

Programmet nedenfor kalkulerer og skriver ut power of 2 for tallene 1 til 10. For å gjøre dette har jeg definert endel lambda-uttrykk, og kombinerer dem for å oppnå ønsket resultat. 

static void Main(string[] args)
{
    Action<int, Action> Times = (n, action) =>
    {
        for (int i = 0; i < n; i++)
            action();
    };
 
    Action<IEnumerable<string>> Output = (s) =>
    {
        foreach (var item in s)
            Console.Write("{0} ", item);
        Console.WriteLine();
    };
 
    Func<int, Func<IEnumerable<string>>> PowerStringIterator = (powerbase) =>
    {
        int x = 0;
        return () =>
        {
            x++;
            return new[]
            {
                powerbase.ToString(), "to the power of", 
                x.ToString(), "is", 
                Math.Pow(powerbase, x).ToString()
            };
        };
    };
 
    var iterator = PowerStringIterator(2);
 
    Times(10, () => Output(iterator()));
}

Output:

2 to the power of 1 is 2
2 to the power of 2 is 4
2 to the power of 3 is 8
2 to the power of 4 is 16
2 to the power of 5 is 32
2 to the power of 6 is 64
2 to the power of 7 is 128
2 to the power of 8 is 256
2 to the power of 9 is 512
2 to the power of 10 is 1024

Legg spesielt merke til høyereordens-funskjonen PowerStringIterator: Den er en slags factory som returnerer en ny funksjon som er en closure rundt variabelen x, og som dermed kan brukes som en iterator som gir meg en uendelig rekke med output (hvorav jeg skriver ut de 10 første). Variabelen x er "ute av scope" når PowerStringIterator returnerer, og det er ingen måte å få tak på den igjen på. Men den er like vel ikke borte – den nye iterator-funksjonen bruker den som sin state-variabel, og x blir derfor ikke borte sålenge funksjonen er der og "lukker seg rundt den".

Wikipedia: A closure is a first-class function with free variables that are bound in the lexical environment. Such a function is said to be "closed over" its free variables. A closure is defined within the scope of its free variables, and the extent of those variables is at least as long as the lifetime of the closure itself. The explicit use of closures is associated with functional programming and with languages such as ML and Lisp. Closures are used to implement continuation passing style, and in this manner, hide state.

Vær obs på at closures ofte brukes feilaktig som et annet ord for anonyme funksjoner (jeg kan selv dokumentere å ha gjort det for ikke så lenge siden). Anonyme funksjoner kan danne closures (i mange språk), men det er likevel to separate begrep.

.NET-rammeverket støtter forresten egentlig ikke "ekte" closures, men simulerer det ved å opprette en klasse som holder på state (variabelen x). Action og Func, lambdaer i .NET, er jo egentlig klasser/objekter. Tar vi en titt på den kompilerte assemblien med Reflector finner vi klassen som er gjengitt nedenfor. Kompilatoren har generert denne for å representere iterator-funksjonen som PowerStringIterator returnerer:

[CompilerGenerated]
private sealed class <>c__DisplayClassa
{
    // Fields
    public int powerbase;
    public int x;

    // Methods
    public IEnumerable<string> <Main>b__3()
    {
        this.x++;
        return new string[] {
          this.powerbase.ToString(),
          "to the power of",
          this.x.ToString(),
          "is",
          Math.Pow((double) this.powerbase, (double) this.x).ToString()
        };
    }
}

Men nok om closures, og over til Clojure (som faktisk uttales closure!). Nedenfor har jeg implementert det samme programmet – strukturelt sett er det identisk med C#-varianten, så sammenlign dem gjerne for å lære litt mer om Clojure/Lisp.

(defn times [n action]
  (dotimes [_ n] (action)))

(defn output [args]
      (doseq [s args]
             (print s ""))
      (newline))

(defn power-string-iterator [powerbase]
      (let [x (atom 0)]
        (fn []
            (reset! x (inc @x)) ; mutate x
            (list powerbase "to the power of" @x "is" 
                  (int (Math/pow powerbase @x))))))

(def iterator (power-string-iterator 2))

(times 10 
       (comp output iterator))

Merk at jeg normalt ikke ville ha definert funksjonene times og output – disse er her for å gjøre eksempelet likt C#-varianten, og tilsvarende funksjoner finnes allerede i Clojure-språket.

Fordi jeg ønsker å mutere (endre verdien på..) x har jeg valgt å opprette det som et atom. Du kan lese mer om det her.

Spesielt interesserte kan også merke seg funksjonen comp på siste linje. Dette er en funksjon som komponerer to eller flere funksjoner, og tilsvarer på en elegant måte hvordan jeg i C# lagde en ny lambda som kombinerte to andre lambdaer. Se gjerne Komponere funksjoner i F#, som også omhandler komposisjon av funksjoner i funskjonelle språk.

Jeg vet ikke.., inspirerte dette til noen nye tanker? Ser du at C#-koden er "lispy"? Om du ikke skjønte noe mer Lisp av dette så fikk jeg i alle fall endelig snakket litt om hva closures er for noe, så jeg er fornøyd :P


comments powered by Disqus