Closures in Java - een introductie

Inleiding
Er is al een hele tijd een discussie over het toevoegen van closures aan Java - dit zou, samen met onder meer Properties, moeten gebeuren in Java 7. Omdat er veel meningsverschillen zijn over wat de syntax en semantiek van closures moeten zijn zijn er drie concrete voorstellen: BGGA, CICE en FCM. Uit het BGGA-kamp kwam deze week een eerste prototype van javac die closures ondersteunt. Dat wil zeggen dat er eindelijk geëxperimenteerd kan worden! Dit artikel is het resultaat van een middag experimenteren.


Wat is ook alweer een closure?
Maar voor we te diep op de materie induiken, kijken we nog even naar wat onder een closure wordt verstaan. Simpel gezegd is een closure een functie in een bepaalde context, hoewel de context niet gebruikt hoeft te worden. De volgende code geeft een voorbeeld van beide:

{int, int => int} plus = {int a, int b => a + b}; // geen context
 
String naam = "wereld";
{=> void} groeter = {=> System.out.printf ("Hallo %s!", naam);}; // gebruikt naam

Wat een rare syntax!
Het voorbeeld hierboven gebruikt de BGGA-syntax. Over smaak valt niet te twisten, maar zeker met geneste closures wordt de leesbaarheid er niet beter op (zie ook de voorbeelden bij de download). De symbolencombinatie => moet gelezen worden als retourneert. De kommagescheiden expressies ervoor geven de argumenten van de closure aan. In de declaratie geeft het type achter => het type van de terugkeerwaarde aan; {int, int => int} leest dan als een closure met twee int-argumenten die een int teruggeeft. In de implementatie komt na => de methodeinhoud: normale Java-statements, waarbij het laatste statement (dat een waarde teruggeeft) niet eindigt op een puntkomma.

Functioneel programmeren
Een closure hoeft niet per se gedeclareerd, geïmplementeerd en aangeroepen te worden in een en dezelfde methode - overal waar een type gebruikt kan worden (als, methodeargument, als terugkeerwaarde of als superinterface bijvoorbeeld) kan een functietype (de officiële BGGA-term) gebruikt worden. De mogelijkheid een functie aan een methode mee te geven opent veel mogelijkheden voor functionele toepassingen:

import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
 
public class FP {
    public static void main (String... parameters) {
        List<Integer> lijst = Arrays.asList (1, 2, 3, 4, 5);
 
        System.out.printf ("lijst: %s%n", map (lijst, {Integer x => x}));
        System.out.printf ("verdubbeld: %s%n%n", map (lijst, {Integer x => x * 2}));
 
        System.out.printf ("deelbaar door twee: %s%n%n", filter (lijst, {Integer x => x % 2 == 0}));
 
        System.out.printf ("som: %s%n", foldl (lijst, 0, {Integer r, Integer x => r + x}));
        System.out.printf ("product: %s", foldl (lijst, 1, {Integer r, Integer x => r * x}));
    }
 
    private static <X, Y> List<Y> map (List<X> bronlijst, {X => Y} functie) {
        List<Y> doellijst = new ArrayList<Y> (bronlijst.size ());
 
        for (X x: bronlijst) {
            doellijst.add (functie.invoke (x));
        }
 
        return doellijst;
    }
 
    private static <X> List<X> filter (List<X> bronlijst, {X => boolean} functie) {
        List<X> doellijst = new ArrayList<X> ();
 
        for (X x: bronlijst) {
            if (functie.invoke (x)) {
                doellijst.add (x);
            }
        }
 
        return doellijst;
    }
 
    private static <X> X foldl (List<X> bronlijst, X beginwaarde, {X, X => X} functie) {
        X resultaat = beginwaarde;
 
        for (X x: bronlijst) {
            resultaat = functie.invoke (resultaat, x);
        }
 
        return resultaat;
    }
}

Deze code gebruikt drie bekende functionele constructies op een lijst: map (voer een bewerking uit op elk element), filter (bewaar alleen die elementen die aan een bepaalde voorwaarde voldoen) en foldl (vouw elementen samen).

RAII
Een andere toepassing voor closures is er voor zorgen dat bronnen die gebruikt worden binnen de closure aangemaakt en weer vrijgegeven worden in een methode die de closure als argument heeft. Deze toepassing heet Resource Acquisition Is Initialisation of kortweg RAII en wordt gedemonstreerd in het volgende voorbeeld:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
 
public class RAII {
    public static void main (String... parameters) throws IOException {
        metBestand ("test.txt", {FileOutputStream uitvoer =>
            uitvoer.write ("Hallo!".getBytes ());
        });
 
        metBestand ("test2.txt", {FileOutputStream uitvoer =>
            throw new IOException ();
        });
    }
 
    private static void metBestand (String bestandsnaam, {FileOutputStream => void throws IOException} blok) throws IOException {
        FileOutputStream uitvoer = null;
 
        try {
            uitvoer = new FileOutputStream (new File (bestandsnaam));
 
            blok.invoke (uitvoer);
        } finally {
            if (uitvoer != null) {
                System.out.printf ("%s gesloten%n", bestandsnaam);
 
                uitvoer.close ();
            }
        }
    }
}

We zien hier voor het eerst een closuredeclaratie die aangeeft dat er een uitzondering kan optreden - een tekortkoming waar bij het gebruik van Runnable nog weleens tegenaan gelopen wordt. Het uitvoeren van dit voorbeeld toont dat test2.txt wordt afgesloten, ook al vindt er een uitzondering plaats. Dit versimpelt de aanroepende code enorm.

Conclusie
Hoewel er nog een aantal mogelijkheden ontbreken in het eerste prototype (zoals het gebruik van bestaande methodes als closure of het veranderen van variabelen in de context) en er naar mijn mening nog wel aan de syntax geschaafd zou mogen worden belooft een middag experimenteren al wel veel goeds. Het is bijvoorbeeld nu al mogelijk een extended for-loop te schrijven waarbij er wel een indexvariabele beschikbaar is. In een vervolgartikel gaan we bij een completere implementatie kijken naar meer toepassingen van closures en beantwoorden we de vraag of het voordeel van closures de enorm veel complexer geworden Java-syntax rechtvaardigt.

—————————————————————————————
Meer weten over Java-specialist Finalist IT Group?


7 reacties »

  1. Leuk!

    Ik vind dat overigens dat juist het RAII voorbeeld de ‘veel complexer geworden’ Java-syntax rechtvaardigt; terwijl de syntax niet echt veel complexer wordt… ben benieuwd naar het vervolg!

    Peter Maas - november 7, 2007 17:44

  2. Gaaf artikel!
    Sluit mij dan ook aan bij Peter Maas: “ben benieuwd naar het vervolg!”

    Moest het wel even 2 keer lezen om het mijzelf eigen te maken.
    Ik ga het binnenkort is uitproberen…

    Bram Doornbos - november 9, 2007 16:36

  3. Ey interessant, misschien eens aanvullen met closures in Javascript, da’s ook een behoorlijk onbegrepen gebied.

    rikkert - november 12, 2007 15:36

  4. Tja, leuk en aardig deze closures, maar het zal toch echt niet gaan werken in java

    in jouw voorbeeld code is het probleem al te zien.

    private static List map (List bronlijst, {X => Y} functie) {
    List doellijst = new ArrayList (bronlijst.size ());

    Het probleem zit ‘m in de new ArrayList. FP gaat om lazy evaluation en dat ondersteunt java niet doordat de zoals in .Net bekende Yield ontbreekt. Het komt erop neer dat de volledige lijst meteen greedy uitgerekend moet worden. En gezien elke FP functie met een lijst operatie een nieuwe lijst (of 1 enkel element) dient op te leveren betekent dat dus dat de orginele lijst niet aangepast mag worden.
    Zie de map bijvoorbeeld. Alle elementen van de argument lijst worden omgerekend en in een resultaat lijst gezet. Deze resultaatlijst moet gemaakt worden, in dit geval wordt dit dus een Arraylist.

    en dat is dus niet goed.

    stel we hebben een lijst representatie die intern als een tree is geimplementeerd, of er gelden ordeningen, filteringe and whatnot in ons EIGEN lijst implementatie. Vervolgens gooien we deze lijst als argument in de Map functie. het resultaat is dan een Arraylist…. er gaat een Type A in en er komt een Arraylist uit. Het interne gedrag van de lijst en diens eigenschappen gaan verloren door het aanroepen van de map methode.
    Het is ook verstandiger om niet List als argument te geven omdat alles wat een lijst vorm heeft (dus (x:xs)

    rmlinden - november 28, 2007 12:35

  5. …. element op de kop van een lijst) als argument gegeven moet kunnen worden. Dus ook Set, Tree etc.

    alles wat dus itereerbaar. Enumerble of Iterable. het dient ook meteen voor arrays te werken. alles wat voldoet aan de eigenschap (x:xs). Hoe dan ook dient het return type hetzelde type te zijn als het argument type.

    Kernmerk van een closure is dat het een operatie is zonder side effects. het doet 1 ding en niet meer dan dat. En met het list type probleem is er dus een side effect, de closure is dus niet puur.

    Overigens zijn closures prima te maken in vrijwel elke object oriented taal. Het is enkel het “liften” van een operatie naar een class. In haskell kan je functies als argumenten aan een ander functie geven. In java kan dat niet. Je kan niet ‘+’ of ‘Math.abs(x)’ meegeven aan een andere functie (behalve met reflectie).

    public interface Func{
    public O eval(I input);
    }

    public static Iterable map(Func f, Iterable xs){
    Iterable result = new ArrayList(); //

    rmlinden - november 28, 2007 12:44

  6. … zucht .. leve max input length

    public interface Func{
    public O eval(I input);
    }

    public static Iterable map(Func f, Iterable xs){
    Iterable result = new ArrayList(); // filter(Func
    f, Iterable xs){
    Iterable
    result = new ArrayList(); // even = new Func(){
    public Boolean eval(Integer input){
    return input % 2 == 0;
    }
    }

    List result = map(even, Arrays.asList(1… 100);

    rmlinden - november 28, 2007 12:50

  7. De voorbeelden waren puur en alleen om het concept te illustreren. In plaats van een ArrayList zou er ook een LazyList (Commons Collections/eigen implementatie) teruggegeven kunnen worden.

    Ik ben het niet met je eens dat een closure geen neveneffecten zou mogen hebben - soms is het juist de bedoeling om een lokale variabele van waarde te doen veranderen. Dit gaat ook ondersteund worden in de BGGA-implementatie (maar werkte nog niet toen ik dit artikel schreef).

    Het is waar dat het eenvoudig is functors te schrijven (zie bijvoorbeeld Commons Functor of FunctionalJ), maar dat wil nog niet zeggen dat een taaluitbreiding met dit doel overbodig is. Maar meer daarover in het vervolgartikel.

    Levi Hoogenberg - december 1, 2007 18:15

Reageer

RSS feed for comments on this post · TrackBack URI