Un Goloiste peut il ĂȘtre plus fonctionnel qu’un Scalafiste?

CĂ©dric Beust, dans le dernier podcast des CastCodeurs, explique le point de vue de la programmation fonctionnelle de cette façon: quand une fonction lance une exception elle ne retourne pas une valeur et ça casse le raisonnement fonctionnel, donc il faut pouvoir toujours retourner “quelque chose”, par exemple retourner un type T si tout va bien ou un type E (ou une variante du type T) si il y a une erreur. Ce qui permet de continuer le “raisonnement” fonctionnel dans votre code. Tout ça est mon interprĂ©tation des paroles de CĂ©dric Beust bien sĂ»r.

Ces concepts sont faciles Ă  mettre en oeuvre avec le langage Golo. Pour rappel, Golo est un langage dynamique pour la JVM, il est tout petit (530kb) mais il peut dĂ©jĂ  jouer dans la cour des grands. Ses multiples influences lui apportent de nombreuses possibilitĂ©s et notamment dans sa capacitĂ© Ă  ĂȘtre “augmentĂ©â€.

Nous allons donc voir comment mettre en oeuvre des fonctions qui en cas d’erreur retournent quand mĂȘme quelque chose quoiqu’il se passe (je traiterais aussi la notion de valeur null).

Traiter “fontionnellement” mes exceptions, partie 1

Ma premiĂšre spĂ©cification va donc ĂȘtre la suivante:

  • Permettre Ă  mes fonctions de pouvoir retourner un type T dont l’instance peut avoir une valeur ou une erreur.

En Golo, on a des structures, du coup mon type sera le suivant:

struct T = { 
  value,
  error
}

Et je peux ensuite l’instancier de cette maniùre:

let a = T()
a: value(5): error(null)
let b = T(42, null)
#  etc. ...

GĂ©rer les valeurs null

Il se trouve qu’en Golo, il est possible de greffer des mĂ©thodes aux structures avec le mot clĂ© augment. Je vais donc ajouter une mĂ©thode getOrElse Ă  ma structure pour gĂ©rer les valeurs null:

augment T {
  function getOrElse = |this, replacementIfNull| {
    if this: value() is null {
      if replacementIfNull oftype java.lang.invoke.DirectMethodHandle.class {
        return replacementIfNull(this)
      }
      return replacementIfNull
    }
    return this: value()  
  } 
}

Cela signifie que je vais pouvoir Ă©crire ceci:

let a = T(null, null)
let b = a: getOrElse("n/a") #  b == "n/a"

ou ceci en passant une fonction anonyme dont le paramĂštre est la rĂ©fĂ©rence Ă  l’instance de type:

let a = T(null, null)
let b = a: getOrElse(|instanceOfT| {
  println("Argh! b is null")
  return 42
}) #  b == 42

Remarque: en Golo il existe le mot clĂ© orIfNull qui permet de gĂ©rer ça facilement, mais j’ai fait comme cela pour introduire la suite.

GĂ©rer les exceptions

Il est tout Ă  fait possible de surcharger une mĂ©thode d’augmentation, donc on va augmenter encore plus notre structure en gĂ©rant un comportement en cas d’exception.

augment T {
  function getOrElse = |this, replacementIfNull| {
    if this: value() is null {
      if replacementIfNull oftype java.lang.invoke.DirectMethodHandle.class {
        return replacementIfNull(this)
      }
      return replacementIfNull
    }
    return this: value()  
  } 

  #  surcharge de getOrElse
  function getOrElse = |this, replacementIfNull, replacementIfError| {    
    if this: error() isnt null {
      if replacementIfError oftype java.lang.invoke.DirectMethodHandle.class {
        return replacementIfError(this)
      }
      return replacementIfError
    } else {
      if this: value() is null {
        if replacementIfNull oftype java.lang.invoke.DirectMethodHandle.class {
          return replacementIfNull(this)
        }
        return replacementIfNull
      }
      return this: value()
    }
  }
}

Cela s’utilisera dans le mĂȘme principe que la gestion des valeurs null. Codons une mĂ©thode divide qui va faire une division:

function divide = |a, b| {
  let ret = T(null, null)
  try {
    ret: value(a/b)
  } catch (e) {
    ret: error(e)
  } finally {
    return ret
  }
}

Et je pourrais utiliser ma fonction de cette maniĂšre:

let d = divide(5,0): getOrElse("n/a", "Huston? We've got a problem!")
#  d == "Huston? We've got a problem!"

J’ai gĂ©nĂ©rĂ© une exception car je fais une division par 0, mais je n’arrĂȘte pas mon programme et d a tout de mĂȘme une valeur.

Comme précédemment je peux aussi faire ceci:

let d = divide(5,0): getOrElse("n/a", |instanceOfT| {
  println(instanceOfT: error()) #  display "java.lang.ArithmeticException: / by zero"
  return 42
}) #  d == 42

LĂ  je commence Ă  ĂȘtre fier de moi (un peu d’autosatisfaction ne fait pas de mal), mais je trouve que le try catch finally c’est laid Ă  lire, je vais donc utiliser une nouveautĂ© de Golo pour faire encore mieux.

Traiter “fontionnellement” mes exceptions, partie 2

Depuis peu, on trouve la notion de decorator (façon Python) dans Golo. Cela correspond Ă  “dĂ©corer” une fonction avec une autre fonction, ce qui permet d’agir avant et aprĂšs l’exĂ©cution d’une fonction (pas clair ? Avec un exemple ça ira (peut-ĂȘtre) mieux).

Mon objectif est de “virer” le try catch finally du corps de mes mĂ©thodes. Je vais donc faire un decorator:

function withGoloType = |decoratorArgs...| {
  return |func| { #  return decorated function
    return |functionArgs...| {
      let ret = T(null, null)
      try {
        ret: value(func: invokeWithArguments(functionArgs))
      } catch (e) {
        ret: error(e)
      } finally {
        return ret
      } 
    }
  }
}

Alors:

  • withGoloType c’est ma fonction decorator
  • funcdans return |func| c’est la fonction retournĂ©e

Globalement je fais des traitements et je peux invoquer ma fonction comme ceci: func: invokeWithArguments(functionArgs)

Dans mon cas,

  • j’instancie un type T: let ret = T(null, null)
  • ensuite “j’essaye” de renseigner la propriĂ©tĂ© value de ret avec le rĂ©sultat de la fonction dĂ©corĂ©e: ret: value(func: invokeWithArguments(functionArgs))
  • si ça se passe mal, je “stocke” l’exception: ret: error(e)
  • et au final je retourne mon instance de type

Et cela s’utilise comme ceci:

#  implémentation de la méthode divide
#  décoration de la méthode divide

@withGoloType()
function divide = |a, b| {
  return a/b
}

function main = |args| {

  let d = divide(5,0): getOrElse("n/a", |instanceOfT| {
    println(instanceOfT: error())
    return 42
  })

}

Une fois décorée, divide ne retournera plus de valeur numérique, mais un type T qui permettra de fournir une valeur avac sa méthode getOrElse.

Voilà, vous venez de voir comment il est trùs facile d’apporter de nouveaux comportements à Golo et de faire du fonctionnel avec (c’est un moyen, il y en a beaucoup d’autres, mais ce sera pour de prochains articles).

@+ P.

blog comments powered by Disqus

Related posts