Vous ne pouvez pas créer une fonction pure appelée random
qui donnera un résultat différent à chaque appel. En fait, vous ne pouvez même pas "appeler" des fonctions pures. Vous les appliquez. Donc, vous ne manquez rien, mais cela ne signifie pas que les nombres aléatoires sont interdits dans la programmation fonctionnelle. Permettez-moi de démontrer que je vais utiliser la syntaxe Haskell.
Venant d’un arrière-plan impératif, vous pouvez vous attendre initialement à ce que random ait un type comme celui-ci:
random :: () -> Integer
Mais cela a déjà été exclu parce que le hasard ne peut pas être une fonction pure.
Considérons l'idée d'une valeur. Une valeur est une chose immuable. Cela ne change jamais et toutes les observations que vous pouvez faire à ce sujet sont cohérentes pour tous les temps.
Clairement, aléatoire ne peut pas produire une valeur Integer. Au lieu de cela, il produit une variable aléatoire Integer. Son type pourrait ressembler à ceci:
random :: () -> Random Integer
Sauf que passer un argument est totalement inutile, les fonctions sont pures, donc l'une random ()
vaut l'autre random ()
. Je vais donner au hasard, à partir de maintenant, ce type:
random :: Random Integer
Ce qui est très bien, mais pas très utile. Vous pouvez vous attendre à pouvoir écrire des expressions comme random + 42
, mais vous ne pouvez pas le faire, car cela ne vérifiera pas le texte. Vous ne pouvez rien faire avec des variables aléatoires pour le moment.
Cela soulève une question intéressante. Quelles fonctions devraient exister pour manipuler des variables aléatoires?
Cette fonction ne peut pas exister:
bad :: Random a -> a
de toute façon utile, car alors vous pourriez écrire:
badRandom :: Integer
badRandom = bad random
Ce qui introduit une incohérence. badRandom est censé être une valeur, mais c'est aussi un nombre aléatoire; une contradiction.
Peut-être devrions-nous ajouter cette fonction:
randomAdd :: Integer -> Random Integer -> Random Integer
Mais ceci n’est qu’un cas particulier d’un modèle plus général. Vous devriez pouvoir appliquer n'importe quelle fonction à une chose aléatoire afin d'obtenir d'autres choses aléatoires comme ceci:
randomMap :: (a -> b) -> Random a -> Random b
Au lieu d'écrire random + 42
, nous pouvons maintenant écrire randomMap (+42) random
.
Si vous n'aviez que randomMap, vous ne pourriez pas combiner des variables aléatoires ensemble. Vous ne pouvez pas écrire cette fonction par exemple:
randomCombine :: Random a -> Random b -> Random (a, b)
Vous pourriez essayer de l'écrire comme ceci:
randomCombine a b = randomMap (\a' -> randomMap (\b' -> (a', b')) b) a
Mais c'est du mauvais type. Au lieu de finir avec un Random (a, b)
, on se retrouve avec unRandom (Random (a, b))
Cela peut être corrigé en ajoutant une autre fonction:
randomJoin :: Random (Random a) -> Random a
Mais, pour des raisons qui pourraient éventuellement devenir claires, je ne le ferai pas. Au lieu de cela, je vais ajouter ceci:
randomBind :: Random a -> (a -> Random b) -> Random b
Il n'est pas immédiatement évident que cela résout le problème, mais cela le fait:
randomCombine a b = randomBind a (\a' -> randomMap (\b' -> (a', b')) b)
En fait, il est possible d'écrire randomBind en termes randomJoin et randomMap. Il est également possible d'écrire randomJoin en termes de randomBind. Mais, je vais laisser faire cela comme un exercice.
Nous pourrions simplifier un peu cela. Permettez-moi de définir cette fonction:
randomUnit :: a -> Random a
randomUnit transforme une valeur en une variable aléatoire. Cela signifie que nous pouvons avoir des variables aléatoires qui ne sont pas réellement aléatoires. Ce fut toujours le cas cependant; nous aurions pu faire randomMap (const 4) random
avant. La raison pour laquelle randomUnit est une bonne idée est que nous pouvons maintenant définir randomMap en termes de randomUnit et randomBind:
randomMap :: (a -> b) -> Random a -> Random b
randomMap f x = randomBind x (randomUnit . f)
Ok, maintenant nous allons quelque part. Nous avons des variables aléatoires que nous pouvons manipuler. Pourtant:
- Il n'est pas évident de savoir comment nous pourrions réellement implémenter ces fonctions,
- C'est assez encombrant.
la mise en oeuvre
Je vais aborder des nombres pseudo aléatoires. Il est possible d'implémenter ces fonctions pour de vrais nombres aléatoires, mais cette réponse est déjà assez longue.
Cela fonctionnera essentiellement en transmettant une valeur de départ partout. Chaque fois que nous générons une nouvelle valeur aléatoire, nous produirons une nouvelle graine. A la fin, lorsque nous aurons fini de construire une variable aléatoire, nous voudrons en échantillonner à l'aide de cette fonction:
runRandom :: Seed -> Random a -> a
Je vais définir le type aléatoire de la manière suivante:
data Random a = Random (Seed -> (Seed, a))
Ensuite, nous devons juste fournir les implémentations de randomUnit, randomBind, runRandom et random, ce qui est assez simple:
randomUnit :: a -> Random a
randomUnit x = Random (\seed -> (seed, x))
randomBind :: Random a -> (a -> Random b) -> Random b
randomBind (Random f) g =
Random (\seed ->
let (seed', x) = f seed
Random g' = g x in
g' seed')
runRandom :: Seed -> Random a -> a
runRandom seed (Random f) = (snd . f) seed
Pour le hasard, je suppose qu'il existe déjà une fonction du type:
psuedoRandom :: Seed -> (Seed, Integer)
Dans ce cas, le hasard est juste Random psuedoRandom
.
Rendre les choses moins lourdes
Haskell utilise du sucre syntaxique pour rendre les choses plus agréables aux yeux. Cela s'appelle do-notation et pour tout utiliser, il faut créer une instance de Monad pour Random.
instance Monad Random where
return = randomUnit
(>>=) = randomBind
Terminé. randomCombine
d'avant pourrait maintenant être écrit:
randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = do
a' <- a
b' <- b
return (a', b')
Si je le faisais moi-même, j'irais même plus loin et créerais un exemple d'application. (Ne vous inquiétez pas si cela n'a aucun sens).
instance Functor Random where
fmap = liftM
instance Applicative Random where
pure = return
(<*>) = ap
RandomCombine pourrait alors être écrit:
randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = (,) <$> a <*> b
Maintenant que nous avons ces instances, nous pouvons utiliser à la >>=
place de randomBind, join à la place de randomJoin, fmap au lieu de randomMap, return à la place de randomUnit. Nous recevons également tout un tas de fonctions gratuitement.
Est-ce que ça vaut le coup? Vous pourriez argumenter qu'arriver à cette étape, où travailler avec des nombres aléatoires n'était pas complètement horrible, était assez difficile et long. Qu'avons-nous eu en échange de cet effort?
La récompense la plus immédiate est que nous pouvons maintenant voir exactement quelles parties de notre programme dépendent de l’aléatoire et lesquelles sont entièrement déterministes. D'après mon expérience, forcer une séparation aussi stricte que celle-ci simplifie énormément les choses.
Jusqu'à présent, nous avons supposé que nous voulions seulement un échantillon de chaque variable aléatoire que nous générons, mais s'il s'avère qu'à l'avenir, nous aimerions voir plus de la distribution, c'est trivial. Vous pouvez simplement utiliser runRandom de nombreuses fois sur la même variable aléatoire avec des graines différentes. Ceci est bien sûr possible dans les langages impératifs, mais dans ce cas, nous pouvons être certains que nous n'allons pas effectuer d'E / S inattendues à chaque fois que nous échantillonnons une variable aléatoire et que nous ne devons pas faire attention à l'initialisation de l'état.