Closure et fonctions lambda en Objective-C

written by netinfluence 12 novembre 2009
Closure et fonctions lambda en Objective-C

Définitions

De nombreux langages de scripts permettent l’utilisation de «fonctions lambdas», ou encore «fonctions anonymes», concept généralement lié au phénomène appelé «closure».
Il s’agit de concepts bien connus notamment en JavaScript, ActionScript, ou encore en PHP depuis sa version 5.3.
Le langage Objective-C offre depuis peu une implémentation de ces deux concepts, appelée «blocks».
Les blocks sont disponibles depuis Mac OS X 10.6 et l’adoption de Clang.

Fonctions anonymes

Comme son nom l’indique, une fonction anonyme est une fonction ne possédant pas de nom, ou d’identifiant. Elle ne contient que son contenu (body), et peut être associée à une variable, pour être ré-utilisée, ou passée en argument d’une autre fonction.
Ce phénomène est très souvent utilisé dans des langages de scripts, notamment pour des callbacks.
En JavaScript par exemple, imaginons une fonction standard nommée «foo», prenant en paramètre un callback, et l’exécutant dans son body:
function foo( callback )
{
    callback();
}
Il est parfaitement possible de définir une autre fonction standard, et de la passer en paramètre de notre première fonction:
function bar()
{
    alert( 'Hello World!' );
}

foo( bar );
Le problème dans un tel cas est que nous déclarons une fonction nommée «bar» dans le scope global. Il en découle un risque de collision avec une éventuelle autre fonction qui porterait le même nom.
Le langage JavaScript nous permet donc de déclarer la fonction utilisée comme callback lors de l’appel:
foo(
    function()
    {
        alert( 'Hello World!' );
    }
);
Notre callback n’a ici pas d’identifiant. Il n’existera pas dans le scope global, et donc ne risquera pas d’entrer en conflit avec une fonction existante.
Il est également possible de définir le callback comme variable. Il n’existera toujours pas dans le scope global, mais pourra ainsi être ré-utilisé à volonté via cette variable:
myCallback = function()
{
    alert( 'Hello World!' );
};

foo( myCallback );

Closure

Le phénomène appelé «closure» consiste en la possibilité, pour une fonction, d’accéder aux variables disponibles dans son contexte de déclaration, ceci même dans le cas où son contexte d’exécution est différent.
Toujours en JavaScript, imaginons le code suivant:
function foo( callback )
{
    alert( callback() );
}

function bar()
{
    var str = 'Hello World';

    foo(
        function()
        {
            return str;
        } 
    );
}

bar();
Notre callback, passé à la fonction «foo» depuis le contexte d’exécution de le fonction «bar» retourne une variable nommée «str».
Or, cette variable, déclarée dans le contexte de la fonction «bar», est une variable locale à cette dernière. Autrement dit, elle n’existe qu’à l’intérieur de ce contexte.
Comme notre callback est exécuté depuis un contexte différent du contexte de la déclaration de cette variable, on pourrait penser que le code-ci dessus n’affiche rien.
Mais c’est ici que le phénomène de closure intervient.
Quel que soit son contexte d’exécution, une fonction conserve un accès aux variables disponibles lors de sa déclaration.
Notre callback aura donc bien accès à la variable «str», même s’il est effectivement appelé depuis la fonction «foo», et que cette dernière n’y a pas accès.

Implémentation en Objective-C

Objective-C implémente ce genre de concepts, même s’il existe un certain nombre de subtilités, venant du fait qu’il s’agit d’un langage compilé dérivé du C, en restant très proche, donc très différent d’un langage de script interprété.
Il est à noter que le support des blocks est aussi disponible en C pur, ou en C++ (et donc en Objective-C++).
De même qu’une fonction standard en C, la déclaration d’un block (fonction anonyme) se doit d’être précédée par la déclaration de son prototype.
La syntaxe de déclaration d’un block n’est pas la plus évidente, mais on s’y fait relativement vite.
Voici par exemple le prototype d’un block:
NSString * ( ^myBlock )( int );
Nous déclarons ici le prototype d’un block («^»), destiné à être nommé «myBlock», prenant un argument unique de type «int», et retournant un pointeur sur un objet de type «NSString».
Nous pouvons maintenant déclarer le block:
myBlock = ^( int number )
{
    return [ NSString stringWithFormat: @"Passed number: %i", number ];
};
Nous assignons donc à la variable «myBlock» le body d’une fonction, prenant comme argument «number» un entier. Cette fonction retourne un objet «NSString», dans lequel sera affiché cet entier.
Attention, ne pas oublier le point virgule à la fin de la déclaration du block!
Si cela est facultatif dans des langages de scripts, c’est absolument nécessaire pour un langage compilé comme Objective-C.
L’oublier résulterait en une erreur du compilateur, qui refuserait de générer l’exécutable final.
Le block peut désormais être utilisé, comme une fonction standard:
myBlock();
Voici le code complet d’un programme Objective-C, reprenant l’exemple précédent:
#import <Cocoa/Cocoa.h>

int main( void )
{
    NSAutoreleasePool * pool;
    NSString * ( ^myBlock )( int );

    pool = [ [ NSAutoreleasePool alloc ] init ];
    myBlock = ^( int number )
    { 
        return [ NSString stringWithFormat: @"Passed number: %i", number ];
    };

    NSLog( @"%@", myBlock() );

    [ pool release ];

    return EXIT_SUCCESS;
}

Un tel programme peut être compilé à l’aide de la commande suivante (Terminal):

gcc -Wall -framework Cocoa -o test test.m
Cela générera un fichier exécutable nommé «test», à partir du fichier source «test.m».
Pour lancer l’exécutable:
./test
La déclaration du prototype d’un block peut être omise dans le cas où le block n’est pas assigné à une variable, comme par exemple s’il est transmis directement comme argument.
Par exemple:
someFunction( ^ NSString * ( void ) { return @"Hello World!" } );
Il est à noter que dans un tel cas, le type de retour doit être déclaré. Ici, il s’agit d’un objet de type «NSString».

Passage d’un block comme argument

Un block peut bien sûr être passé comme argument d’une fonction C.
Là aussi, la syntaxe peut être quelque peu déroutante à première vue:
void logBlock( NSString * ( ^theBlock )( int ) )
{
    NSLog( @"Block returned: %@", theBlock() );
}
Evidemment, comme Objective-C est un langage fortement typé, une fonction recevant un block en argument doit également déclarer son type de retour et le type de ses éventuels arguments.
Il en va de même dans le cas d’une méthode d’une classe Objective-C:
- ( void )logBlock: ( NSString * ( ^ )( int ) )theBlock;

Closure

Le phénomène de closure est également présent en Objective-C, même si son comportement est évidemment différent de celui des langages interprétés.
Imaginons le programme suivant:
#import <Cocoa/Cocoa.h>

void logBlock( int ( ^theBlock )( void ) )
{
    NSLog( @"Closure var X: %i", theBlock() );
}

int main( void )
{
    NSAutoreleasePool * pool;
    int ( ^myBlock )( void );
    int x;

    pool = [ [ NSAutoreleasePool alloc ] init ];
    x = 27;

    myBlock = ^( void )
    {
        return x;
    };

    logBlock( myBlock );

    [ pool release ];

    return EXIT_SUCCESS;
}
La fonction «main» déclare un entier, de valeur 27, ainsi qu’un block, retournant cette même variable.
Le block set ensuite passé à la fonction «logBlock», qui affiche sa valeur de retour.
Même dans le contexte d’exécution de la fonction «logBlock», le block déclaré dans la fonction «main» continue d’avoir accès à l’entier «x», et peut donc sans aucun problème en retourner la valeur.
Il est à noter que les blocks ont également accès aux variable globales, même statiques si elles sont disponibles dans le contexte du block.
Il existe ici une première subtilité. En effet, les variables disponibles depuis un block par le phénomène de closure sont de type «const». Autrement dit, elle ne peuvent être modifiées depuis l’intérieur du block.
Par exemple, imaginons que notre block incrémente la valeur de x avant de la retourner:
myBlock = ^( void )
{
    x++
    return x;
};
Le compilateur refusera ici de compiler le programme, puisque la variable «x» n’est disponible qu’en lecture pour notre block.
Pour qu’une variable puisse être modifiée depuis un block, il faut la déclarer avec le mot clé «__block».
Ainsi, le codé précédent est valide si l’on déclare la variable x ainsi:
__block int x;

Gestion de la mémoire

Au niveau C, un block est une structure, pouvant être copiée et détruite.
Deux fonctions C sont à disposition pour cet usage: «Block_copy()» et «Block_destroy».
En Objective-C, un block peut également recevoir les messages «retain», «release» et «copie», comme un objet.
C’est aspect peut-être extrêmement important dans le cas où un block doit être conservé pour une utilisation ultérieure (et par exemple stocké dans une propriété d’une classe).
Ne pas copier le block, ou ne pas effectuer de «retain» dans un tel cas peut en effet induire une erreur de segmentation.

Exemple d’utilisation

Les blocks peuvent être utilisés dans de très nombreux contextes, afin de simplifier le code, et réduire le nombre de fonctions déclarées.
Voici un exemple simple illustrant l’utilisation des blocks.
Nous allons ajouter à la classe «NSArray» une méthode statique permettant de générer un tableau en filtrant les éléments d’un autre tableau, à l’aide d’un callback.
Pour les amateurs de PHP, il s’agit ici d’un équivalent de la fonction «array_filter()».
Nous allons commencer par la déclaration d’une catégorie de la classe «NSArray». Pour rappel, une catégorie permet l’ajout de méthodes dans une classe existante, évitant ainsi le besoin de créer une sous-classe.
@interface NSArray( BlockExample )

+ ( NSArray * )arrayByFilteringArray: ( NSArray * )source withCallback: ( BOOL ( ^ )( id ) )callback;

@end
Nous déclarons ici une méthode retournant un objet de type «NSArray», et prenant en argument un autre objet «NSArray», ainsi qu’un callback, sous forme de block.
Ce callback sera exécuté pour chaque élément du tableau passé en argument. Il doit retourner une valeur booléenne, afin de savoir si l’élément courant du tableau source doit être conservé dans le tableau retourné. Le block prend donc comme unique argument l’objet courant du tableau source.
Voyons maintenant l’implémentation de cette méthode:
@implementation NSArray( BlockExample )

+ ( NSArray * )arrayByFilteringArray: ( NSArray * )source withCallback: ( BOOL ( ^ )( id ) )callback
{
    NSMutableArray * result;
    id element;

    result = [ NSMutableArray arrayWithCapacity: [ source count ] ];

    for( element in source ) {
        
        if( callback( element ) == YES ) {
            [ result addObject: element ];
        }
    }

    return result;
}

@end
Nous créons en premier lieu un tableau de taille dynamique («NSMutableArray»), en lui allouant une capacité initiale correspondant au nombre d’entrées du tableau source.
Ensuite, nous itérons chaque élément du tableau source, et nous ajoutons l’élément courant dans le cas où le résultat du callback retourne la valeur booléenne «YES».
Voici un exemple de programme utilisant une telle méthode.
Ici, nous nous servons du callback pour créer un tableau ne contenant que les objets de type «NSString» du tableau source:
#import <Cocoa/Cocoa.h>

@interface NSArray( BlockExample )

+ ( NSArray * )arrayByFilteringArray: ( NSArray * )source withCallback: ( BOOL ( ^ )( id ) )callback;

@end

@implementation NSArray( BlockExample )

+ ( NSArray * )arrayByFilteringArray: ( NSArray * )source withCallback: ( BOOL ( ^ )( id ) )callback
{
    NSMutableArray * result;
    id element;

    result = [ NSMutableArray arrayWithCapacity: [ source count ] ];

    for( element in source ) {

        if( callback( element ) == YES ) {
            [ result addObject: element ];
        }
    }

    return result;
}

@end

int main( void )
{
    NSAutoreleasePool * pool;
    NSArray * array1;
    NSArray * array2;

    pool = [ [ NSAutoreleasePool alloc ] init ];
    array1 = [ NSArray arrayWithObjects: @"Hello World!", [ NSDate date ], @"Hello Universe!", nil ];
    array2 = [ NSArray
        arrayByFilteringArray: array1
        withCallback: ^ BOOL ( id element )
        {
           return [ element isKindOfClass: [ NSString class ] ];
        }
    ];

    NSLog( @"%@", array2 );

    [ pool release ];

    return EXIT_SUCCESS;
}
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license can be found at: http://www.gnu.org/copyleft/fdl.html

You may also like

3 comments

Romac 1 février 2010 at 13 h 03 min

Excellent article, bravo !

Il y a juste une petite coquille me semble-t-il dans l’exemple de code n°4.
Tu y dis que la variable myCallback n’existera pas dans le scope global, alors qu’elle est déclarée sans le mot-clé « var ».

En espérant lire d’autres articles de ta part,

Romain

Reply
Lunack 29 février 2012 at 12 h 08 min

Vraiment très bien cet article ! j’ai pu bien assimilé les blocks

Reply
Axel 10 novembre 2012 at 9 h 45 min

Très, très bon article, merci.

Reply

Leave a Comment