Future & Promises (PromiseKit, Bolts, ...)

AliGatorAliGator Membre, Modérateur
février 2015 modifié dans Objective-C, Swift, C, C++ #1
Hello !

Ca fait un bout de temps que je m'intéresse au Design Pattern "Futures" & "Promises" (qui ont l'air un peu similaires en fait), et que je voulais le tester en pratique.

En particulier les libs/pods les plus connus sur le sujet sont PromiseKit et Bolts. On trouve également un peu le concept dans ReactiveCocoa.


Quelqu'un les as déjà  utilisé ? A déjà  un retour d'expérience sur la question ?
Y'en a-t-il un mieux pensé / plus fini / plus simple à  utiliser qu'un autre ?

---

En particulier je ne suis pas fan de ReactiveCocoa de par sa syntaxe qui fait très C et de par la multitude de RAC_xxx qu'il introduit partout et finit par faire produire un code que je trouve très peu lisible, surtout comparé à  PromiseKit par exemple... Et puis en plus j'ai l'impression que ReactiveCocoa est devenu un peu un fourre-tout, qui contient les concepts de Futures/Promises mais aussi beaucoup d'autres trucs, et si on passe à  RAC on est un peu obligé de prendre tout le package RAC sans exception...

---

Quand je vois ce que PromiseKit par exemple donne en Swift, ça fait rêver (ça devient à  la fois hyper lisible et l'utilisation des Generics est tout à  fait adaptée, etc). La version Objective-C est un peu moins sexy mais bon... Et l'avantage de PromiseKit est qu'il a l'air très bien documenté, avec un guide de prise en main et tout !
«1

Réponses

  • Je n'ai testé aucun de ces frameworks, mais le concept de Futures a l'air sympa ! Merci de l'info ;-)


  • Nos serveurs étant en NodeJs nous utilisons beaucoup le système de Promise que je trouve vraiment bien dans ce cas.


    En iOS j'ai testé RAC mais comme tu le soulignes le code final deviens vraiment illisible. Mais depuis l'arrivé de Swift je lorgne sur PromiseKit mais je n'ai pas encore eu le temps de le tester.


     


    Au plaisir de lire ton retour d'expérience :) 


  • AliGatorAliGator Membre, Modérateur
    février 2015 modifié #4
    J'ai fait un petit test du concept du pattern Promises sur mon GIST : https://gist.github.com/AliSoftware/9e4946c8b6038572d678#file-promises-swift

    A la limite je vous invite plutôt à  le lire de bas en haut, c'est à  dire regarder le code de démo que j'ai mis tout en bas pour voir comment on va finir par utiliser le pattern, puis remonter pour comprendre comment j'ai implémenté mes méthodes asynchrones fetchCurrentCity(), fetchWeather() et fecth() et les structures de données que j'ai utilisé... et enfin remonter en haut pour voir comment marche une Promise<T> sous le capot.

    On voit qu'on peut ainsi avoir des constructions à  la fin comme
    fetchCurrentCity().then(fetchWeather).then(println)
    Pour dire "Récupère moi ma ville actuelle via un WebService, puis ensuite quand tu as la réponse récupère moi la météo pour cette ville avec un autre WebService, et ensuite quand tu as la réponse affiche-la dans la console"...

    Je trouve ça juste magique :D et surtout ça évite d'avoir 36 niveaux d'indentation imbriqués comme on a quand on utilise des completionBlocks :)


    Attention, la classe Promise<T> que j'ai créé au début du fichier d'exemple est une implémentation TRES simpliste du pattern, qui ne gère pas le multi-thread, qui ne gère pas quand le fulfill est appelé avant le then, qui ne gère pas plein d'autres choses... c'est une version super-simpliste juste pour montrer le concept vite fait dans un Playground, et en particulier pour voir ce que ce pattern Promises permet de faire à  l'usage.
    Dans un vrai projet je vous invite évidemment à  utiliser un vrai framework qui implémente ça bien et proprement, comme PromiseKit justement, qui gère tous les cas de la vraie vie.
  • AliGatorAliGator Membre, Modérateur
    février 2015 modifié #5
    J'ai amélioré un peu mon GIST pour pousser la démo plus loin.

    A la fin ça donne la possibilité de faire ce genre de choses :
    fetchCityFromIP()
    .then(printResult("Your current position is: "))
    .then(fetchCurrentWeather)
    .then(printResult("Current weather there: "))
    .then(getWeatherTip)
    .then(printResult("Tip of the day: "))
  • JegnuXJegnuX Membre
    février 2015 modifié #6

    J'ai arrêté de lire le site de PromiseKit quand j'ai vu qu'il fallait par exemple remplacer ça :



    [UIView animateWithDuration:0.3 animations:^{
    self.label.alpha = 1;
    } completion:^{
    // this is the end point
    // add code to happen next here
    }];

    Par ça :



    [UIView promiseWithDuration:0.3 animations:^{
    self.label.alpha = 1;
    }];

    // Il manque le completion mais c'est par ce qu'il est géré ailleurs dans un "then"

    "promiseWithDuration:" j'aime pas du tout. C'est le coup à  ce retrouver avec un code remplis de "promise".


    Je comprends pas pourquoi ils ne passent pas plutôt par un proxy et du message forwarding pour faire un truc du style :



    [[UIView promise] animateWithDuration:0.3 animations:^{
    self.label.alpha = 1;
    }];

    Y'a plus qu'à  faire un nouveau Framework... :D


  • AliGatorAliGator Membre, Modérateur
    Pas besoin de faire un nouveau framework il suffit d'ajouter une catégorie de 2 lignes et tu as ton proxy.


  • Pas besoin de faire un nouveau framework il suffit d'ajouter une catégorie de 2 lignes et tu as ton proxy.




     


    sauf que du coup il faut que la categorie sache que le message "animateWithDuration:" soit redirigée sur "promiseWithDuration:" si on veut rester avec du PromiseKit.


    Du coup ça devient un vrai bordel à  gérer...

  • AliGatorAliGator Membre, Modérateur
    ? Je vois pas trop ce que tu veux dire. C'est à  ça que sert ton proxy object non?
  • AliGatorAliGator Membre, Modérateur
    février 2015 modifié #10
    @interface UIViewPromiseProxy : NSProxy
    @property(weak) UIView* view;
    @end
    @implementation UIViewPromiseProxy
    - (PMKPromise*)animateWithDuration:(NSTimeInterval)duration animations:(void(^)(void))animations {
    return [self.view promiseWithDuration:duration animation:animations];
    }
    @end

    @implementation UIView(AnimationPromises)
    - (UIViewPromiseProxy*)promise {
    UIViewPromiseProxy* proxy = [UIViewPromiseProxy new];
    proxy.view = self;
    return proxy;
    }
    @end
    Et en Swift c'est même encore plus simple : si on considère que la signature de la méthode c'est "func promiseWithDuration(duration: Double, animation: Void->Void) -> Double", alors l'extension peut même retourner un tuple de fonctions directement :

    extension View {
    typealias Proxy = (
    view: View,
    animateWithDuration: (Double, animation: Void->Void) -> Double
    )
    var proxy: Proxy { return (
    view: self,
    animateWithDuration: View.promiseWithDuration(self)
    )}
    }
    Et du coup ces 2 lignes sont alors totalement équivalentes :

    myView.promiseWithDuration(5) { ... }
    myView.proxy.animateWithDuration(3) { ... }
    ---

    Mais bon, je vois pas trop l'intérêt de tout ça, c'est tellement plus simple la façon dont ils l'ont implémentée de leur côté avec directement "promiseWithDuration:animation:". Je comprend pas pourquoi tu veux t'entêter à  passer par un proxy object, je préfère leur solution plus simple et moins prise de tête.
  • J'aime bien l'idée de remettre à  plat les completion block imbriqués (car cela devient vite illisible) mais j'hésite un peu à  utiliser un framework third party pour quelque chose qui va être l'epine dorsal du code.

    L'idéal serait d'avoir un framework Apple, une sorte d'extension du principe des blocks, je ne sais pas si c'est possible.
  • MagiicMagiic Membre
    février 2015 modifié #12

    Je pense pareille. Il est clair que le Pattern Promise est intéressant mais utiliser un framework supplémentaire pour faire ceci me paraà®t lourd pour moi qui préfère jouer avec les API disponibles par Apple.


     


    D'ailleurs c'est peut être envisagé dans leurs équipes quand on voit qu'ils veulent déjà  le faire pour les conditions sur swift 1.2.


  • AliGatorAliGator Membre, Modérateur
    Swift 1.2 apporte déjà  son lot de fonctions issues du monde du FP (Fonctional Programming), comme "map", "reduce", etc. Et c'est déjà  top !
    Ca va être une autre façon de voir et d'appréhender les choses pour ceux qui viennent uniquement du monde de la POO et d'ObjC, donc ça peut être déroutant au début, mais ça a beaucoup d'avantages

    Du coup, puisqu'on est sur le chemin du FP, effectivement les Promises sont une suite logique envisageable, ça va un peu dans la lignée. Espérons...
  • Swift 1.2 apporte déjà  son lot de fonctions issues du monde du FP (Fonctional Programming), comme "map", "reduce", etc. Et c'est déjà  top !

    Certes, mais on est encore loin d'un langage comme F# (que j'ai commencé à  utiliser au taf). "map", "reduce" ce n'est que la surface de la programmation fonctionnelle. Les optionals et discriminated unions en font également partie.

    Alors, effectivement, il reste possible d'implémenter en Swift la plupart des outils fonctionnels (Functors, Applicative Functors, Monads), mais souvent sans les sucres syntaxique qui les rendent facilement utilisables.
  • AliGatorAliGator Membre, Modérateur
    Je suis d'accord Swift à  encore du chemin à  faire avant de fournir en standard tous les outils du FP. J'espère qu'ils seront intégrés un jour.


    C'est aussi pour ça que je ne me fais pas trop d'illusions, je ne pense pas que les Promises seront intégrées dans Swift du moins pas dans un futur proche. Il y a encore de quoi faire avant qu'Apple le propose en standard.
  • Salut,


    Je trouve ton exemple très intéresant, ça démontre bien la force du pattern. Je n'en avais jamais entendu parlé.


    Je me dis que c'est sympa pour organiser les blocks et de ne plus les mettre en vrac dans le code. J'imagine que ça a été pensé pour ça. Ca promet pour les prochaines versions de Swift :)


  • Je ne connais aucun de ces frameworks, mais dans le développement javascript, cela est quelque chose d'assez connu.


    Même la méthode ajax de jQuery fonctionne sur ce principe.


    C'est vrai que les promises apporte un véritable avantage, dans de la programmation evenementielle.


    Mais moi j'ai plus été séduit par reactivecocoa. Le concept des signaux est proches des promesses, mais il apporte tout un tas d'opérateur pour manipuler ces signaux, que je trouve cela vraiment pratique.


     


    Mais encore une fois, je ne l'ai jamais vraiment utilisé, juste parcouru, et c'est vrai que niveau code produit on se retrouve avec un max de "rac_".


  • jojolebgjojolebg Membre
    février 2015 modifié #18

    Par contre je suis surpris que tu n'ai pas parlé d'un autre avantage des promises, à  savoir la gestion des erreurs.


    Dans la programmation orienté-objet, on utilise souvent les exceptions pour gérer les erreurs (pas très utilisé en objective-c par contre).


    Mais avoir une véritable gestion d'exception est un casse tête dans de la programmation avec des blocks imbriqués.


    Les promises, apportent en plus une gestion des erreurs, qui s'apparente aux exceptions de l'orienté-objet, mais plus adaptée à  la programmation asynchrone.


  • AliGatorAliGator Membre, Modérateur
    RAC c'est pas juste du FP et des Promises, c'est carrément de la FRP (Programmation Réactive Fonctionnelle), pour définir ton application sous forme de flux, un peu comme des bindings.

    Ce qui me gêne de RAC c'est que c'est un peu un bulldozer qui embarque tout et est assez "épine dorsale" :
    - Quand tu migres vers RAC, souvent toute ton application va alors devoir utiliser RAC, c'est un peu inventif
    - Quand tu utilises RAC, tu te trouves avec des "rac_xxx" partout
    - Tu te trouves aussi à  mélanger des crochets Objective-C avec des macros et fonctions C, genre : RAC(button, enabled) = [RAC combine:@[truc.rac_Signal, bidule.rac_signal] reduce:^(...) { }]

    Au final (mais ce n'est que mon avis, sans doute parce que je n'ai pas assez pratiqué RAC ?) je trouve le code difficilement lisible. C'est peut-être aussi un peu la faute d'Objective-C, et quand RAC supportera Swift, avec une API/syntaxe adaptée à  Swift, ça sera peut-être plus lisible, mais bon en attendant c'est moyennement propre et lisible.
    Ceci dit les Promises de PromiseKit (ou pire, de Bolts) sont aussi moins sexy sous ObjC que sous Swift, mais bon, ça reste quand même plus propre je trouve que le mix des rac_Machin et RACTruc() avec les crochets...
  • AliGatorAliGator Membre, Modérateur
    février 2015 modifié #20

    Par contre je suis surpris que tu n'ai pas parlé d'un autre avantage des promises, à  savoir la gestion des erreurs.
    Dans la programmation orienté-objet, on utilise souvent les exceptions pour gérer les erreurs (pas très utilisé en objective-c par contre).
    Mais avoir une véritable gestion d'exception est un casse tête dans de la programmation avec des blocks imbriqué.

    Les promises, apportent en plus une gestion des erreurs, qui s'apparente aux exceptions de l'orienté-objet, mais plus adaptée à  la programmation asynchrone.

    Je suis entièrement d'accord.

    La gestion des Promises dans Promise/A+ (et dans Bolts) n'est pas terrible je trouve, car il faut passer à  la fois le bloc de succès et le bloc de failure à  chaque fois, même si y'a un chaà®nage possible, mais ça fait un truc très verbeux.

    Par contre je suis fan de celle de PromiseKit, où tu peux mettre un "catch()" à  la toute fin de la chaà®ne seulement si par exemple (cas assez fréquent) tu veux traiter toutes les erreurs de la chaà®ne de la même façon, par exemple que l'erreur soit intervenue lors de la requête réseau, lors du parsing JSON, lors du stockage du JSON dans CoreData, ou autre... Du coup le traitement des erreurs avec catch() est séparé du traitement du succès avec then(), alors qu'avec Bolts ou dans les bases de Promises/A+ c'est dès le then() que tu fournis à  la fois fulfiller+rejecter ce qui donne des syntaxes peu sexy.
  • Je pensais que la méthode "catch" était présente d'office dans les promises. Si PromiseKit l'implémente il se rapproche alors de l'implementation Q de kriskowal, une implémentation connue en Javascript.


    http://documentup.com/kriskowal/q/


  • Effectivement la documentation de PromiseKit donne envie


  • AliGatorAliGator Membre, Modérateur
    février 2015 modifié #23
    De ce que je comprends de Bolts, il n'y a pas à  fournir les 2 blocks (succès et failure) pour chaque action qu'on veut enchaà®ner ("then(successBlock, failureBlock)") mais seulement un block, par contre tu passes un seul block à  continueWithBlock() qui prend une BFTask et c'est à  toi de tester si task.error est nil ou pas, ou si task.result a une valeur, à  chaque fois dans chaque block.

    Le fait d'avoir une BFTask à  chaque fois et de devoir inspecter le result (qui est un "id", donc très générique), plutôt que d'avoir directement le résultat dans un then() et l'erreur dans un catch(), n'est pas très user-friendly je trouve.

    Il se trouve qu'en interne on a un framework qui utilise déjà  le même concept que ce que fait Bolts (sauf qu'à  l'époque on ne le connaissait pas " je crois qu'il n'existait même pas) : on a implémenté un truc similaire, qui retourne des MachinTask, et ces objets MachinTask ont une méthode setCompletionBlock qui passe un id. Et justement avec l'expérience on trouve ça lourd et pas pratique, notamment pour la gestion des erreurs ou le fait que ça génère encore des pyramids of doom (trop de niveaux d'indentation). C'est aussi pour cela que j'ai commencé à  creuser les pistes comme PromiseKit.

    Du coup pour moi même si Bolts dit implémenter le mécanisme de Future & Promises, ça ne répond pas au besoin que l'on a d'avoir du code lisible qui s'enchaà®ne sans faire 15 niveaux d'indentation, ni au besoin de centraliser la gestion d'erreur dans la plupart des tunnels de traitement. Alors que PromiseKit répond parfaitement à  cette problématique.



    Pour l'exercice (tant pour m'entraà®ner à  Swift qu'à  utiliser PromiseKit), j'ai implémenté un petit module pour s'interfacer avec la StarWars API (swapi.co), et vu le look qu'a mon code avec PromiseKit par rapport à  s'il avait été implémenté avec les completionBlocks ou même avec Bolts, je suis déjà  séduit. Je mettrais le projet sur mon GitHub un de ces 4 à  des fins de démo / exemple.
  • Merci pour ton retour Ali, je suis très intéressé par ton POC avec la swapi.co !


  • AliGatorAliGator Membre, Modérateur
    février 2015 modifié #25
    C'est encore en Work In Progress, mais comme promis, j'ai mis ma petite expérimentation de PromiseKit + Swift + StarWars API sur GitHub

    Ce n'est pas complètement fini, mais le plus long était de réfléchir à  l'API, de faire le squelette, et de réfléchir sur la classe ResourceFetcher<T> puis l'implémenter, car finalement c'est elle qui fait les choses les + intéressantes.

    Le reste, en particulier créer les struct représentant les différentes ressources (Person, Starship, Film, Species, Vehicle, ...) est très rapide, car c'est juste des struct où je liste les champs de l'API + fournit une méthode init() pour les créer à  partir d'un JSON.

    J'ai juste fait Person & Starship pour l'instant mais compléter ceux qui manquent devrait être très facile ("laissé en exercice au lecteur" comme ils disent dans les bouquins :D)




    Je vous laisse lire le README et inspecter le code pour ceux que ça intéresse.
    N'hésitez pas à  faire des Merge Requests pour rajouter les struct manquantes (et vous entraà®ner à  Swift au passage :)) : il manque Planet, Species, Vehicle et Film.

    Je rajoutera + de code de démo d'utilisation dans les jours à  venir (car pour l'instant y'a pas grand chose dans ViewController.swift !) et peut-être une petite UI de base si je suis motivé.
  • Je regarde ca en détail demain matin, merci beaucoup :)


  • AliGatorAliGator Membre, Modérateur
    février 2015 modifié #27
    Projet de démo mis à  jour, j'ai implémenté les structures du modèle qui manquaient (Vehicle, Planet, Film...).
    • Le code modèle et métier est maintenant complet, tous les types de ressources (Planet, Person, Species, Film, ...) fonctionnent
    • Le code de démo est un peu light : je n'ai toujours pas d'UI (car le but c'est plutôt de montrer le Pattern et du code), le code qui utilise le tout et que j'ai mis dans ViewController.swift irait mieux dans des Tests U plutôt que dans le VC, et faudrait rajouter + d'exemples de cas d'usage pour montrer un peu + de cas pratiques
    • Il y aurait moyen d'optimiser le parsing, en particulier Vehicle et Starship partagent plusieurs champs, on pourrait faire un héritage. Sauf que ce sont des struct (et que les struct ne peux pas hériter d'une struct parente).
      • Donc soit on les bascule en classe (mais ça me plait pas trop, je préfère la vision de Andy Matuschak sur le sujet, vidéo que je vous invite fortement à  regarder, très intéressante)
      • Soit on fait de la composition, avec une classe commune interne qui se charge du parsing des champs communs
    • Bref c'est un point d'amélioration qui soulève des questions d'archi intéressantes et qu'il pourrait être sympa de creuser
    Lien direct vers le code qui montre un exemple d'usage


  •  



      • Donc soit on les bascule en classe (mais ça me plait pas trop, je préfère la vision de Andy Matuschak sur le sujet, vidéo que je vous invite fortement à  regarder, très intéressante)
      • Soit on fait de la composition, avec une classe commune interne qui se charge du parsing des champs communs
    • Bref c'est un point d'amélioration qui soulève des questions d'archi intéressantes et qu'il pourrait être sympa de creuser

     




     


    Oui c'est très intéressant de voir comment font les autres. Je me vois mal séparé de l'aliasing et utiliser que des structures par valeur. 


     


    Dans ton cas  je trouve pas logique l'utilisation de la composition, par exemple la structure Bicycle qui contient une variable de type Vehicule, par contre l'héritage à  du sens. 


     


    Bon c'est juste pour dire que personnellement je vais avoir du mal à  oublier les concepts objets et appliquer les concepts fonctionnels ou bien mélanger entre les deux. 

  • CéroceCéroce Membre, Modérateur


    Bon c'est juste pour dire que personnellement je vais avoir du mal à  oublier les concepts objets et appliquer les concepts fonctionnels ou bien mélanger entre les deux. 




    Tant mieux, quand on sort de sa zone de confort, ça veut dire qu'on progresse !

  • AliGatorAliGator Membre, Modérateur
    février 2015 modifié #30
    Bon j'ai posé la question sur Twitter à  Andy et sa vision c'est de faire de la composition pure, exposée publiquement (et pas encapsulé pour masquer ce détail d'implémentation).

    (D'ailleurs samir je vois que tu parles de mes exemples de "Bicycle" alors que je n'ai évoqué cet exemple que dans mon GIST et sur Twitter... je suppose que tu as suivi les tweets en question ? ^^)


    Autrement dit en général sa vision des choses quand il a un tel besoin c'est juste d'avoir (pour reprendre mon exemple Star Wars plutôt que l'exemple sur mon GIST et sur Twitter) :
    • Une struct "Transportation" qui contient les champs communs
    • La struct "Vehicle" qui contient (composition) un champ de type Transportation + ses champs à  lui
    • La struct "Starship" idem
    • Et du coup pour accéder au champ "manufacturer" d'un vaisseau il faut faire non plus "starship.manufacturer" mais "starship.transportation.manufacturer" (alors que moi j'aurais bien aimé garder l'abstraction et l'encapsulation)
    C'est une vision qui se défend et elle a l'avantage d'être pas prise de tête à  coder, par opposition à  ma solution 3 où une fois qu'on a passé les champs dans la struct intermédiaire faut tout recopier un par un... mais bon, ça expose la composition publiquement... kif kif.


    Notez que c'est déjà  ce que je fais dans ma démo SWPromisesDemo avec la struct ResourceInfo (pour grouper les champs url, createdAt, editedAt). Dans ce cadre ça me choque pas. Bizarrement dans le cadre StarShip/Vehicle ça m'embête un peu +...


  • (D'ailleurs samir je vois que tu parles de mes exemples de "Bicycle" alors que je n'ai évoqué cet exemple que dans mon GIST et sur Twitter... je suppose que tu as suivi les tweets en question ? ^^)




     Oui oui c'est bien sur twitter et j'ai vu ton gist aussi :).  

Connectez-vous ou Inscrivez-vous pour répondre.