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.
bad :: Random a -> a
introduirait des incohérences? Quel est le problème? S'il vous plaît allez lentement dans l'explication, en particulier pour les premières étapes :) Si vous pouviez expliquer pourquoi les fonctions "utiles" sont utiles, cela pourrait être une réponse en 1000 points! :)Tu n'as pas tort. Si vous donnez la même graine à un groupe de ressources nucléaires deux fois, le premier nombre pseudo-aléatoire renvoyé sera le même. Cela n'a rien à voir avec la programmation fonctionnelle par rapport aux effets secondaires; la définition d'une graine est qu'une entrée spécifique provoque une sortie spécifique de valeurs bien distribuées mais décidément non aléatoires. C'est pourquoi on l'appelle pseudo-aléatoire, et c'est souvent une bonne chose d'avoir, par exemple, d'écrire des tests unitaires prévisibles, de comparer de manière fiable différentes méthodes d'optimisation sur le même problème, etc.
Si vous voulez réellement des nombres non pseudo-aléatoires d'un ordinateur, vous devez le brancher à quelque chose de vraiment aléatoire, comme une source de désintégration de particules, des événements imprévisibles survenant dans le réseau sur lequel l'ordinateur est allumé, etc. bien faire et généralement coûteux même si cela fonctionne, mais c'est le seul moyen de ne pas obtenir de valeurs pseudo-aléatoires (généralement, les valeurs que vous recevez de votre langage de programmation sont basées sur une graine, même si vous n'en avez pas explicitement fournie.)
Ceci, et seulement cela, compromettrait la nature fonctionnelle d'un système. Comme les générateurs non pseudo-aléatoires sont rares, cela ne se produit pas souvent, mais si vous avez vraiment une méthode générant de vrais nombres aléatoires, au moins ce petit bout de votre langage de programmation ne peut pas être 100% fonctionnel. Le fait qu'une langue fasse une exception ou non est simplement une question de pragmatisme de la part du développeur de la langue.
la source
() -> Integer
. Vous pouvez avoir un type de PRNG purement fonctionnelPRNG_State -> (PRNG_State, Integer)
, mais vous devrez l’initialiser par des moyens impurs).Une solution consiste à la considérer comme une suite infinie de nombres aléatoires:
C’est-à-dire qu’il suffit de penser à une structure de données sans fond, comme
Stack
si vous ne pouviez appeler quePop
, mais vous pouvez toujours l’appeler. Comme une pile normale immuable, en enlever une du haut vous donne une autre pile (différente).Ainsi, un générateur de nombre aléatoire immuable (avec évaluation paresseuse) pourrait ressembler à ceci:
C'est fonctionnel.
la source
pseudoRandom :: Seed -> (Seed, Integer)
. Vous pourriez même finir par écrire une fonction de ce type[Integer] -> ([Integer], Integer)
C'est la même chose pour les langages non fonctionnels. Ignorer le problème légèrement distinct des nombres vraiment aléatoires ici.
Un générateur de nombres aléatoires prend toujours une valeur de départ et renvoie pour la même valeur de départ la même séquence de nombres aléatoires (très utile si vous devez tester un programme utilisant des nombres aléatoires). Fondamentalement, cela commence par la graine que vous choisissez, puis utilise le dernier résultat comme graine pour la prochaine itération. Ainsi, la plupart des implémentations sont des fonctions "pures" telles que vous les décrivez: Prenez une valeur et pour la même valeur, vous obtenez toujours le même résultat.
la source