https://d226lax1qjow5r.cloudfront.net/blog/blogposts/the-functions-that-arent-pure/functional-programing_1200x600.png

Les fonctions qui ne sont pas pures

Temps de lecture : 4 minutes

De plus en plus de développeurs sont sensibilisés au paradigme de la programmation fonctionnelle. Ce paradigme promet un code efficace et sans bogues, car les fonctions pures sont plus faciles à tester et à paralléliser.

Dans la pratique, les applications fonctionnelles à part entière restent encore abstraites. Cependant, certains concepts du paradigme de la programmation fonctionnelle sont de plus en plus souvent appliqués à des langages non fonctionnels. En outre, cette approche "partiellement fonctionnelle" permet de mieux résoudre de nombreux "problèmes" courants.

Aujourd'hui, nous allons examiner de plus près l'un de ces concepts fonctionnels - les fonctions pures/impures.

NOTE : Nous utiliserons du pseudo-code basé sur Kotlin pour définir nos fonctions, en gardant les exemples basiques pour qu'ils soient plus faciles à suivre.

Fonctions pures

La fonction pure est une fonction qui n'a pas d'effets de bord - en d'autres termes, cette fonction ne doit pas récupérer ni modifier d'autres valeurs que les valeurs passées en tant que paramètres. De cette manière, chaque appel de fonction avec les mêmes arguments aboutira toujours à la même sortie (valeur retournée). Commençons par définir une fonction pure :

fun max(a: Int, b: Int) {
if (a > b) 
    return a
else
    return b	
}

La fonction max prend deux arguments, deux nombres, et renvoie le plus grand d'entre eux. Notez que cette fonction n'accède ni ne modifie aucune valeur en dehors de la portée de la fonction, il s'agit donc d'une fonction pure. Nous pouvons être sûrs que l'appel de cette fonction avec les arguments 2 et 7 renverra TOUJOURS 7.

Pour mieux comprendre ce concept, examinons de plus près le revers de la médaille - les différentes manières de rompre la pureté de la fonction.

Fonctions impures

Une fonction fonction impure est une fonction qui a des effets secondaires - elle modifie ou accède à des valeurs en dehors de la portée de la fonction (en dehors du corps de la fonction).

Des effets secondaires évidents

L'exemple le plus simple est une fonction qui modifie une propriété externe pour stocker un état.

val loalScore = 0

val getScore(score: Int): Int {
    loalScore = score
    return loalScore
}

Dans ce cas, l'impureté ne se manifeste pas fortement car les appels de méthode ultérieurs renverront la même valeur :

getScore(12) // returns 12
getScore(6) // returns 6
getScore(3) // returns 3

Cette fonction modifie une valeur externe mais renvoie toujours la même valeur à chaque invocation en raison de l'affectation de la valeur. Cependant, ce n'est pas toujours le cas. Considérons une autre fonction impure :

val addScore = 0

val addScore(score: Int): Int {
    loalScore + score
    return loalScore
}

Cette fonction présente une "impureté plus forte" car elle modifie une valeur externe et renvoie un résultat différent à chaque invocation - l'état est stocké dans la variable, en dehors de la portée de la fonction :

addScore(12) // returns 12
addScore(6) // returns 18
addScore(3) // returns 21

L'inconvénient de conserver l'état est que les tests sont parfois plus complexes ; cependant, nous ne pouvons pas l'éviter dans de nombreuses Applications.

La fonction suivante accède à une valeur en dehors de la portée de la fonction, ce qui la rend impure :

fun getString(length: Int): String {
    return Random().nextString(length)
}

Cette fois, chaque appel de fonction avec le même argument se traduira par une valeur différente :

getString(2) // returns "ab"
getString(2) // returns "hh"
getString(2) // returns "zk"

Si les effets secondaires présentés précédemment sont assez faciles à repérer, les effets secondaires peuvent souvent être plus subtils :

Effets secondaires moins évidents

Un scénario d'effet secondaire intéressant est la modification de l'objet transmis en tant qu'argument de la fonction :

fun increaseHeight(person: Person) {
    person.height++
}

Le fait d'appeler cette fonction plusieurs fois avec la même instance Person donnera des résultats différents car la valeur en dehors de la fonction (stockée dans l'instance Personne) est modifiée.

L'exception lancée par une fonction est un excellent exemple d'effets secondaires plus difficiles à repérer :

fun addDistance (a:Int, b:Int): Int {
    if(a < 0) {
        throw IllegalAccessException("a must be >= 0")
    }
     
    return a + b
}

Une autre façon intéressante de créer des effets de bord consiste simplement à appeler une autre fonction ayant des effets de bord :

fun firstFunction() {
    addDistance(-5, 7)
}

fun addDistance (a:Int, b:Int): Int {
    if(a < 0) {
        throw IllegalAccessException("a must be >= 0")
    }
     
    return a + b
}

Un autre effet secondaire peu évident est la journalisation. Jetons un coup d'œil à cet exemple concret de notre système de Base Video Chat de Base :

private PublisherKit.PublisherListener publisherListener = new PublisherKit.PublisherListener() {
    @Override
    public void onStreamCreated(PublisherKit publisherKit, Stream stream) {
        Log.d(TAG, "onStreamCreated: Publisher Stream Created. Own stream " + stream.getStreamId());
    }

    @Override
    public void onStreamDestroyed(PublisherKit publisherKit, Stream stream) {
        Log.d(TAG, "onStreamDestroyed: Publisher Stream Destroyed. Own stream " + stream.getStreamId());
    }

    @Override
    public void onError(PublisherKit publisherKit, OpentokError opentokError) {
        Log.d(TAG, "PublisherKit onError: " + opentokError.getMessage());
    }
};

Dans le code ci-dessus, la journalisation en tant qu'effet de bord n'a pas d'impact sur la logique de l'application, mais elle nous aide à comprendre ce qui se passe dans l'application. Par la suite, le développeur pourra facilement utiliser les données renvoyées par les rappels et introduire d'autres effets secondaires.

Déterminer si la fonction est pure ou impure

Il existe deux indices indiquant qu'une fonction est impure : elle ne prend aucun argument et ne renvoie aucune valeur. Examinons le premier cas :

list.getItem(): String

Dans l'exemple ci-dessus, la fonction ne prend aucun paramètre, mais elle renvoie la valeur. Cela signifie que, très probablement, la valeur est extraite de l'état de la classe. Examinons ce qui se passe lorsqu'une fonction ne renvoie aucune valeur :

list.setItem("item")

En regardant le nom de la fonction, nous pouvons dire que le paramètre sera probablement utilisé pour modifier l'état de la classe.

Enfin, nous pouvons avoir un combo où il n'y a pas d'argument et pas de valeur renvoyée :

list.sort()

Il ne s'agit que d'indices. Ce n'est pas toujours le cas, mais ces indices sont souvent de bons indicateurs de pureté.

Résumé

Dans le paradigme de la programmation fonctionnelle, toutes les fonctions sont idéalement pures.

Cependant, dans de nombreuses Applications du monde réel, les choses ne sont pas aussi binaires. Il est parfois impossible d'éviter les fonctions impures, en particulier si une application nécessite des ressources externes telles que la persistance, l'entrée utilisateur ou l'accès aux données du réseau. La présence de ces ressources rompt la pureté de la fonction et de l'application dans son ensemble, ce qui n'est pas plus mal.

En règle générale, une même application comporte un mélange de fonctions pures et impures. C'est une bonne pratique d'être conscient de la pureté/impureté car cela facilite les tests d'application et nous aide à éviter les bogues.

Partager:

https://a.storyblok.com/f/270183/384x384/8ae5af43bb/igor-wojda.png
Igor WojdaAnciens de Vonage