Interface sans effet secondaire au-dessus d'une bibliothèque avec état

16

Dans une interview avec John Hughes où il parle d'Erlang et Haskell, il a dit ce qui suit sur l'utilisation des bibliothèques avec état à Erlang:

Si je veux utiliser une bibliothèque avec état, je construis généralement une interface sans effet secondaire par-dessus pour pouvoir ensuite l'utiliser en toute sécurité dans le reste de mon code.

Que veut-il dire par là? J'essaie de penser à un exemple de ce à quoi cela ressemblerait, mais mon imagination et / ou mes connaissances me manquent.

bêta
la source
Eh bien, si l'Etat existe, il ne disparaîtra pas. L'astuce consiste à créer quelque chose qui gardera une trace de la dépendance. La réponse standard de Haskell est "monades" ou "flèches" plus avancées . Ils sont un peu difficiles à comprendre et je ne l'ai jamais vraiment fait, donc quelqu'un d'autre devrait essayer de les expliquer.
Jan Hudec

Réponses:

12

(Je ne connais pas Erlang, et je ne peux pas écrire Haskell, mais je pense que je peux quand même répondre)

Eh bien, dans cette interview, l'exemple d'une bibliothèque de génération de nombres aléatoires est donné. Voici une interface avec état possible:

# create a new RNG
var rng = RNG(seed)

# every time we call the next(ceil) method, we get a new random number
print rng.next(10)
print rng.next(10)
print rng.next(10)

La sortie peut être 5 2 7. Pour quelqu'un qui aime l'immuabilité, c'est tout simplement faux! Cela devrait l'être 5 5 5, car nous avons appelé la méthode sur le même objet.

Alors, quelle serait une interface sans état? Nous pouvons voir la séquence de nombres aléatoires comme une liste évaluée paresseusement, où nextrécupère en fait la tête:

let rng = RNG(seed)
let n : rng = rng in
  print n
  let n : rng = rng in
    print n
    let n : rng in
      print n

Avec une telle interface, nous pouvons toujours revenir à un état précédent. Si deux morceaux de votre code font référence au même RNG, ils obtiendront en fait la même séquence de chiffres. Dans un état d'esprit fonctionnel, cela est hautement souhaitable.

L'implémenter dans un langage avec état n'est pas si compliqué. Par exemple:

import scala.util.Random
import scala.collection.immutable.LinearSeq

class StatelessRNG (private val statefulRNG: Random, bound: Int) extends LinearSeq[Int] {
  private lazy val next = (statefulRNG.nextInt(bound), new StatelessRNG(statefulRNG, bound))

  // the rest is just there to satisfy the LinearSeq trait
  override def head = next._1
  override def tail = next._2
  override def isEmpty = false
  override def apply(i: Int): Int = throw new UnsupportedOperationException()
  override def length = throw new UnsupportedOperationException()
}

// print out three nums
val rng = new StatelessRNG(new Random(), 10)
rng.take(3) foreach (n => println(n))

Une fois que vous avez ajouté un peu de sucre syntaxique pour que cela ressemble à une liste, c'est en fait assez agréable.

amon
la source
1
Quant à votre premier exemple: rnd.next (10) produire des valeurs différentes à chaque fois n'a pas autant à voir avec l'immuabilité qu'avec la définition d'une fonction: les fonctions doivent être de 1 à 1. (+1 cependant, bonnes choses)
Steven Evers
Merci! Ce fut une explication et un exemple vraiment sympa et perspicace.
bêta
1

Un concept clé ici est celui de l'état mutable externe . Une bibliothèque qui n'a pas d'état mutable externe est une bibliothèque sans effets secondaires. Chaque fonction d'une telle bibliothèque ne dépend que des arguments qui lui sont passés.

  • Si votre fonction accède à une ressource qui n'a pas été créée par elle, qui lui est donnée (c'est-à-dire en tant que paramètre), alors cela dépend de l'état externe .
  • Si votre fonction crée quelque chose qu'il ne renvoie pas à l'appelant (et ne le détruit pas), alors votre fonction crée un état externe.
  • Lorsque l'état externe ci-dessus peut avoir différentes valeurs à différents moments, il peut alors être modifié .

Tests décisifs pratiques que j'utilise:

  • si la fonction A doit être exécutée avant la fonction B, alors A crée un état externe dont dépend B.
  • si une fonction que j'écris ne peut pas être mémorisée, cela dépend de l'état mutable externe. (La mémorisation n'est peut-être pas une bonne idée à cause de la pression de la mémoire, mais cela devrait quand même être possible)
Steven Evers
la source