Programmation fonctionnelle en Objective-C

Programmation fonctionnelle en Objective-C

Dans un article précédent, nous avons entrevu les possibilités offertes par l’utilisation des Blocks en Objective-C. Leur similarité avec les expressions lambda de la programmation fonctionnelle que vous pouvez retrouver dans Java 8, Scala ou bien encore JavaScript est évidente. Cependant, nous ne sommes pas habitué avec Objective-C ...

iOS

Dans un article précédent, nous avons entrevu les possibilités offertes par l’utilisation des Blocks en Objective-C. Leur similarité avec les expressions lambda de la programmation fonctionnelle que vous pouvez retrouver dans Java 8, Scala ou bien encore JavaScript est évidente. Cependant, nous ne sommes pas habitué avec Objective-C à penser ou bien écrire dans un style fonctionnel. Cependant, il existe des libraries qui permettent de faciliter l’usage de l’approche fonctionnelle. Plusieurs projets ont même fleuri depuis la mise à disposition des blocks avec la sortie d’iOS4.
En JavaScript, la librairie underscore.js est très appréciée par les développeurs web pour sa simplicité et son efficacité. Cette librairie a d’ailleurs tellement de succès qu’elle a traversé la frontière des langages pour être implémentée en Objective-C! Il en existe à ce jour au moins deux implémentations, toutes les deux sous license MIT.

Le projet Underscore.m semble fournir un support plus abouti des fonctionnalités proposées par la librairie JavaScript originale, et surtout propose un site documentaire complet qui permet de démarrer rapidement et de trouver un grand nombre d’exemples.

Installation

Pour démarrer un projet avec Underscore.m, rien de plus simple, il suffit d’utiliser CocoaPods que nous avons découvert dans un article précédent.

Pour rappel, si vous n’avez pas encore installé CocoaPods, il suffit de lancer les commandes suivantes pour installer l’outil (A condition d’utiliser une version 1.9 de Ruby):

$ gem install cocoapods
$ pod setup

Avant d’utiliser CocoaPods, vous devrez créer un projet en ligne de commandes pour OSX via XCode:

xcode

Ensuite, vous aurez à créer un fichier Podfile à la racine de votre projet XCode:

platform :osx
pod 'Underscore.m', '0.1.0'

Puis, vous devrez exécutez la commande suivante:

pod install

Il faudra alors relancer votre projet en ouvrant le fichier *.XCodeWorkspace, plutôt que le fichier *.XCodeProject. Vous serez alors prêt à utiliser la librairie Underscore.m, et vous pourrez commencer à expérimenter les possibilités la librairie.

Mise en oeuvre

Le contexte d’utilisation de la librairie Underscore.m est idéal pour mettre en oeuvre le nouveau support des syntaxes simplifiées de déclaration des tableaux et dictionnaires Objective-C. En effet, la librairie manipule principalement les classes NSArray et NSDictionary à travers l’utilisation de wrappers. L’encapsulation de ces objets permet à la librairie une liberté totale de manipulation de ces structures de données sans être contraint par leur interfaces et implémentations.

Ainsi, si vous voulez manipuler des objets du type NSDictionnary, vous devrez les wrapper comme suit:

NSDictionary *dictionary = @{
  @"en": @"Hello world!", @"fr": @"Bonjour le monde!", @"de": @"Hallo Welt!"
}

USDictionaryWrapper *wrapper = _dict(dictionary);

Pour un tableau, la déclaration sera la suivante:

NSDictionary *dictionary = @[ @"en", @"fr", @"de"];

USArrayWrapper *wrapper = _array(array);

Les objets obtenus, du type USDictionaryWrapper ou USArrayWrapper, sont des objets fournis par la librairie qui représentent la version wrappée des types NSDictionnary et NSArray. Ils permettent le chainage des appels ce qui apporte une syntaxe à la fois compacte et expressive.

Les fonctions mises à disposition par la librairie sont les suivantes:

L’utilisation la plus basique, comme vu plus haut veut qu’un objet NSArray ou bien NSDictionnary soit wrappé avec les méthode _array ou _dict. Sur l’objet en résultant, un objet de type USDictionaryWrapper ou USArrayWrapper, Il est possible de chaîner plusieurs appels. A l’issue de l’exécution de ces appels, si le résultat attendu est également un tableau ou bien un dictionnaire, il est nécessaire de dé-wrapper l’objet avec la méthode unwrap.

Il est ainsi possible de filtrer un tableau en une ligne avec le code suivant:

NSArray *dictionaries = _array(objects).filter(Underscore.isDictionary).unwrap;

Ou bien d’obtenir toutes les valeurs d’un dictionnaire avec la déclaration suivante:

NSArray *values = _dict(dictionary).values.unwrap;

Un cas concret

Dans un projet professionnel, vous êtes amené à travailler régulièrement si ce n’est en permanence avec des tableaux ou bien des dictionnaires. Imaginez que vous ayez à travailler avec l’API REST de GitHub pour afficher des informations de social coding. Vous disposez au sein de l’API GitHub d’une ressource REST correspondant à la liste des repositories mis à disposition par une organisation. Dans le cas facebook, vous obtenez une liste d’une vingtaine de repositories en accédant à la ressource suivante: https://api.github.com/orgs/facebook/repos.

[
 {
    "default_branch": "master",
    "homepage": "http://three20.info/",
    "html_url": "https://github.com/facebook/three20",
    "owner": {
      ...
    },
    "has_downloads": false,
    "created_at": "2009-02-19T06:31:51Z",
    "watchers": 6961,
    "has_issues": true,
    "description": "Three20 is an Objective-C library for iPhone developers",
    "pushed_at": "2012-10-18T09:30:54Z",
    "forks": 1207,
    "git_url": "git://github.com/facebook/three20.git",
    "ssh_url": "git@github.com:facebook/three20.git",
    "svn_url": "https://github.com/facebook/three20",
    "has_wiki": true,
    "master_branch": "master",
    "clone_url": "https://github.com/facebook/three20.git",
    "size": 4704,
    "forks_count": 1207,
    "fork": false,
    "updated_at": "2012-10-23T19:37:21Z",
    "watchers_count": 6961,
    "name": "three20",
    "open_issues": 252,
    "url": "https://api.github.com/repos/facebook/three20",
    "private": false,
    "id": 132321,
    "language": "Objective-C",
    "mirror_url": null,
    "open_issues_count": 252,
    "full_name": "facebook/three20"
  }
...
]

Si vous souhaitez filtrer la liste des repositories correspondant à un langage particulier, vous allez devoir:

  • Créer une liste mutable du résultat
  • Itérer sur l’ensemble des éléments de la liste
  • Comparer le langage de chaque élément de la liste avec le langage qui vous intéresse
  • Ajouter l’élément dans la liste de résultats si celui-ci match.

Le travail peut rapidement se révéler fastidieux, là où la librairie Underscore.m vous permet de l’exprimer très simplement:

NSArray *jsonObjcRepos = _array(jsonRepos).filter(^BOOL (NSDictionary *repository) {
        return [@"Objective-C" isEqualToString:[repository valueForKey:@"language"]];
    }).unwrap;

L’exemple complet de code permettant de compter le nombre de projets facebook sur GitHub utilisant le langage Objective-C est le suivant:

#import <foundation /Foundation.h>
#import "Underscore.h"

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        UnderscoreTestBlock IsObjectiveCPredicate = ^BOOL (NSDictionary *repository) {
            return [@"Objective-C" isEqualToString:[repository valueForKey:@"language"]];
        };

        NSURL *fbReposUrl = [NSURL URLWithString:@"https://api.github.com/orgs/facebook/repos"];
        NSData *data = [NSData dataWithContentsOfURL:fbReposUrl];
        NSArray *jsonRepos = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:NULL];
        NSArray *jsonObjcRepos = _array(jsonRepos).filter(IsObjectiveCPredicate).unwrap;
        NSLog(@"Nombre de repositories utilisant le langage Objective-C: %ld", jsonObjcRepos.count);
    }
    return 0;
}

Dans cet exemple un prédicat est défini dans un premier temps afin de le rendre réutilisable.

Vous pouvez obtenir tout aussi facilement d’autres informations, telles que la liste des langages utilisés par facebook en utilisant le fonction pluck qui permet d’obtenir une liste correspondant à l’extraction d’un attribut spécifié de chaque élément de la liste fournie en entrée.

...
NSArray *languages = _array(jsonRepos).pluck(@"language").unwrap;

Le support des fonctions proposées par la librairie n’égale pas encore la richesse de la librairie originale. Un certain nombre de fonctions manquent encore à l’appel. Par exemple, il n’existe pas de fonction dédiée pour obtenir une liste d’éléments uniques sans doublon ce qui aurait été appréciable dans le traitement précédent.

Dans un but didactique, nous allons enrichir la classe USArrayWrapper avec la fonction uniq. Pour cela, il faut tout d’abord déclarer la propriété uniq en readonly dans l’interface:

@property (readonly) USArrayWrapper *uniq;

Puis nous allons ajouter l’implémentation:

- (USArrayWrapper *)uniq;
{
    NSSet* uniqSet = [NSSet setWithArray:self.array];
    NSArray* result = [uniqSet allObjects];

    return [USArrayWrapper wrap:result];
}

La méthode wrap attend en paramètre un block, il est donc nécessaire de créer un block nommé uniq qui sera passé à la méthode wrap.

Une fois la méthode définie, il est possible d’employer la méthode pluck:

NSArray *languages = _array(repos).pluck(@"language").unwrap;
NSLog(@"Langages unique utilisés (non uniques) par FaceBook: %@", languages);

Le résultat obtenu est le suivant:

”“, “Objective-C”, C, Python, “C++”, Python, “C++”, JavaScript,
““, ““, ““, PHP, “Emacs Lisp”, Haskell, Haskell,
JavaScript, “C++”, Python, ActionScript, JavaScript, “Objective-C”,
“C++”, PHP, “C++”, Python, “C++”, C, PHP, PHP, Java

Certains langages sont présents de multiples fois, nous allons donc utiliser la fonction uniq pour obtenir une liste des langages utilisés sans doublons:

NSArray *uniqLanguages = _array(repos).pluck(@"language").uniq.unwrap;
NSLog(@"Langages utilisés par FaceBook: %@", uniqLanguages);

La résultat est plus satisfaisant, cependant, il reste encore des valeurs nulles. Pour corriger le résultat, nous allons supprimer toutes les valeurs nulles de la liste en utilisant la fonction reject qui permet de retourner toutes les valeurs pour lesquelles le prédicat passé en paramètre renvoie faux, c’est à dire ici, toutes les valeurs non nulles:

NSArray *uniqNotNullLanguages = _array(repos).pluck(@"language").reject([Underscore isNull]).uniq.unwrap;
NSLog(@"Langages uniques utilisés par FaceBook: %@", uniqNotNullLanguages);

Curryfication

La curryfication est un procédé associé à la programmation fonctionnelle qui permet de créer une seconde fonction qui en résout partiellement une première.

Imaginons que nous ayons la déclaration suivante:

typedef int(^AddBlock)(int x, int y);

AddBlock add = ^(int x, int y) {
        return x + y;
};

int x = 24;
int y = 25;

NSLog(@"add(x[%d], y[%d])= %d", x, y, add(x, y));

Dans un contexte de travail donné, partons du postulat que la valeur de X est fixée. Nous souhaitons curryfier la fonction add, c’est à dire la résoudre partiellement, pour simplifier la suite du problème.

Nous allons déclarer un block qui va permettre de passer les paramètres x, puis y via 2 appels successifs. Le block renvoyé est dit curryfié, car il ne prend plus qu’un seul paramètre au lieu de deux:

typedef int(^AddYBlock)(int y);

AddYBlock(^addX)(int x) = ^(int x) {
    return ^(int y) {
       return x + y;
    };
};

La fonction retournée par l’exécution du block addX fait référence au paramètre de fonction x. Comme vu précédemment, un block en Objective-C est une closure. Il garde donc l’état du contexte dans lequel il a été créé. Le block retourné par l’exécution du block addX gardera donc la valeur de x lors de sa création. Ce principe, nous permet de renvoyer un block exécutable qui connaît déjà x, et qui ne prend qu’une variable y en paramètre.

int x = 24;
int y = 25;

NSLog(@"add(x[%d], y[%d])= %d", x, y, add(x, y));
NSLog(@"add(x[%d])= %@", x, addX(x));
NSLog(@"add(x[%d])(y[%d])= %d", x, y, addX(x)(y));
Les logs renvoyés sont les suivants:

2012-10-26 22:13:29.904 underscore[1117:1307] add(x[24], y[25])= 49
2012-10-26 22:13:29.928 underscore[1117:1307] add(x[24])= <__nsmallocblock__: 0x10b4035b0>
2012-10-26 22:13:29.929 underscore[1117:1307] add(x[24])(y[25])= 49

Nous voyons bien que le premier appel qui prend la variable x en paramètre renvoie un block. Ce block renvoyé est alors appelé et permet de résoudre le calcul. Ce block aurait très bien pu être stocké pour une utilisation ultérieure.

Limitations

Il n’est pas possible d’éviter la déclaration de block explicite pour le type de manipulation vue précédemment. L’écriture du code en est donc alourdie, et les limitations des blocks sont atteintes. Il ne sera ainsi pas possible d’écrire le code suivant ou quoi que ce soit de similaire qui soit valide:

(int(^)(int y))(^addX)(int y) = ^(int x) {
        return ^(int y) {
            return x + y;
        };
    };

Le type de retour d’un block ne peut être inliné si celui-ci est également un block, il doit être déclaré au préalable.

Conclusion

Même si le langage Objective-C ne se prête pas à première vue au jeu de la programmation fonctionnelle, les blocks apportent une réponse intéressante. Les blocks sont l’équivalent de fonctions dites de premier ordre, puisqu’il est possible de les passer en paramètre à des appels de fonctions et qu’ils peuvent être retournés par des fonctions. Les blocks offrent des possibilités d’autant plus intéressantes qu’ils fonctionnent tel des closures. L’univers de la programmation fonctionnelle est encore peu explorée en Objective-C, mais des librairies comme Underscore.m permettent de donner un peu plus de saveur au code tout en bénéficiant de la simplicité que peut apporter l’usage de la programmation fonctionnelle.