Les Monades (suite): Le Functor Maybe..

Il y a deux containers

Nous avons vue dans le précédent post un pseudo-container qui nous permet d’emballer nos valeurs. Nous allons muscler un peu notre container mais partons d’un exemple.

Je souhaite récupérer le mail du client “bob” ou afficher “pas de mail”

1
2
3
4
5
6
7
function getMail($name) {
$mail = getUserByName($name)->getAddress()->getMail();
if (null === $mail) {
   return "pas de mail";
}
return $mail;
}

Facile non ?

Si getAdress() renvoie null, Outch …

1
PHP Fatal error: Call to a member function getMail() on a non-object..

L’utilisateur n’existe pas forcement et puis l’adresse est peut-être vide.. Une implémentation naïve

1
2
3
4
5
6
7
8
9
10
11
12
function getMail($name) {
    $user = getUserByName($name);
    if ($user) {
        $address = $user->getAddress();
        if ($address) {
            //etc ...
            return $adresse->getMail();
        }
   }
   return "pas de mail";

}

Ce code vous le connaissez, vous l’avez probablement déjà écris, il y a moyen d’optimiser de faire plus propre.

Deux containers pour le prix d’un.

Le Maybe à la rescousse..

Voici le Maybe en dessin.

Il y a deux containers

J’ai un container Some et un Container Nothing.

Le container Nothing est un container qui n’a aucune valeurs. La méthode map renvoie toujours un container Nothing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Nothing extends Container{
    public function map($function)
    {
       return static::of(null);
    }
    public static function of($value)
    {
        return new static($value);
    }

    public function bind($transformation)
    {
        return static::of(null);
    }

    public function getOrElse($default)
    {
        return $default;
    }
}

Le container Some le résultats de map est un nouveau container Some s’il y a un résultat non-null sinon c’est un container Nothing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Some extends Container{
    public function map($function)
    {
        $result = $this->bind($function);
        if ($result === null) {
            return Nothing::of(null);
        }
        return static::of($result);
    }
    public static function of($value)
    {
        return new static($value);
    }

    public function bind($transformation)
    {
        return call_user_func($transformation, $this->value);
    }

    public function getOrElse($default)
    {
        return $this->value;
    }
}

Enfin j’ai besoin d’un helper

1
2
3
4
5
function maybeFromValue($value) {
 if ($value === null)
   return Nothing::of(null);
 return Some::of($value);
}

Notons que j’ai une méthode qui me permet de sortir avec une valeurs par défaut

Quelques exemples:

1
2
3
4
5
6
7
8
echo maybeFromValue(null)->map("ucfirst")->getOrElse("non!!");
// non!!
echo maybeFromValue("oui!!")->map("ucfirst")->getOrElse("non!!");
// Oui!!
echo Some::of("oui")->map("ucfirst")
   ->map(function($value) {return null;})
   ->getOrElse("Non!!");
// Non !!

Nous pouvons simplifier notre problème

En le refactorisant ainsi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// example
// method("name") return function($obj) {return $obj->getName()};

function method($name)
{
    return function ($obj) use ($name) {
        return $obj->$name();
    };
}

$mail = maybeFromValue(getUserByName($name))
    ->map(method("getAddress")) // $value->getAdress()
    ->map(method("getMail"))
    ->getOrElse("pas de mail");

Quelques dessins Le cas ou tout marche

chainage tout va bien

Le cas ou getUser() renvoie null

getAdress renvoie null, on prend la valeur par défaults

Sympa la refactorisation. On peux supprimer ainsi une partie de la logique (la plupart des if, les nulls ont tous disparus).

Une librairie toute faite

Je vais parler de php-option. Si vous faite du symfony2 vous l’avez déja dans votre /vendor et vous ne le saviez pas.

La syntaxe est un peu près le même

Mais il y a plein de fonctionnalités

1
2
3
$entity = $this->findSomeEntity()->getOrElse(new Entity());
$entity = $this->findSomeEntity()->getOrCall('createAnNewAddress');
$entity = $this->findSomeEntity()->getOrThrow(new \Exception('ha!!!!!'));

Il y a aussi des possibilité de chainer les réponses si pas de résultats

1
2
$entity = $this->findSomeEntity()->orElse($this->findSomeOtherEntity())
            ->orElse($this->createEntity());

Nous n’utilisons que l’instruction map pour le moment. Donc nous n’utilisons pas le container en tant que monade mais plutôt en tant que functor. Nous verrons cela dans le troisième post.

Conclusion

Je suis désolé si certain termes sont inexacts comme le container. Je ne suis pas un expert, mais j’admets bien volontiers mon erreur.

Si vous avez un code ou vous vérifiez tout le temps si les valeurs sont nulles. Il y a probablement moyen que cette structure vous aide.

Dans le prochain Post nous utiliserons le Maybe avec l’instruction bind.