Sie können keine reine Funktion namens erstellen random
, die bei jedem Aufruf ein anderes Ergebnis liefert. Tatsächlich können Sie nicht einmal reine Funktionen "aufrufen". Sie wenden sie an. Sie verpassen also nichts, aber das bedeutet nicht, dass Zufallszahlen in der funktionalen Programmierung tabu sind. Lassen Sie mich demonstrieren, dass ich durchgehend die Haskell-Syntax verwende.
Ausgehend von einem zwingenden Hintergrund können Sie zunächst erwarten, dass der Zufallstyp wie folgt lautet:
random :: () -> Integer
Dies wurde jedoch bereits ausgeschlossen, da Zufall keine reine Funktion sein kann.
Betrachten Sie die Idee eines Wertes. Ein Wert ist eine unveränderliche Sache. Es ändert sich nie und jede Beobachtung, die Sie darüber machen können, ist für alle Zeiten konsistent.
Es ist klar, dass random keinen Integer-Wert erzeugen kann. Stattdessen wird eine ganzzahlige Zufallsvariable erzeugt. Der Typ könnte so aussehen:
random :: () -> Random Integer
Abgesehen davon, dass die Übergabe eines Arguments völlig unnötig ist, sind Funktionen rein, sodass eine random ()
so gut ist wie die andere random ()
. Ich gebe ab jetzt zufällig diesen Typ:
random :: Random Integer
Das ist alles in Ordnung, aber nicht sehr nützlich. Sie können erwarten, Ausdrücke wie schreiben zu können random + 42
, aber Sie können nicht, weil es nicht typecheck wird. Mit Zufallsvariablen kann man noch nichts anfangen.
Dies wirft eine interessante Frage auf. Welche Funktionen sollten existieren, um Zufallsvariablen zu manipulieren?
Diese Funktion kann nicht existieren:
bad :: Random a -> a
in irgendeiner nützlichen Weise, denn dann könnte man schreiben:
badRandom :: Integer
badRandom = bad random
Das führt zu einer Inkonsistenz. badRandom ist angeblich ein Wert, aber es ist auch eine Zufallszahl. ein Widerspruch.
Vielleicht sollten wir diese Funktion hinzufügen:
randomAdd :: Integer -> Random Integer -> Random Integer
Dies ist jedoch nur ein Sonderfall eines allgemeineren Musters. Sie sollten in der Lage sein, eine beliebige Funktion auf eine zufällige Sache anzuwenden, um andere zufällige Dinge zu erhalten, wie zum Beispiel:
randomMap :: (a -> b) -> Random a -> Random b
Anstatt zu schreiben random + 42
, können wir jetzt schreiben randomMap (+42) random
.
Wenn Sie nur randomMap hätten, könnten Sie keine Zufallsvariablen miteinander kombinieren. Sie konnten diese Funktion zum Beispiel nicht schreiben:
randomCombine :: Random a -> Random b -> Random (a, b)
Sie könnten versuchen, es so zu schreiben:
randomCombine a b = randomMap (\a' -> randomMap (\b' -> (a', b')) b) a
Aber es hat den falschen Typ. Anstatt mit einem zu enden Random (a, b)
, enden wir mit einemRandom (Random (a, b))
Dies kann durch Hinzufügen einer weiteren Funktion behoben werden:
randomJoin :: Random (Random a) -> Random a
Aber aus Gründen, die irgendwann klar werden könnten, werde ich das nicht tun. Stattdessen füge ich Folgendes hinzu:
randomBind :: Random a -> (a -> Random b) -> Random b
Es ist nicht sofort offensichtlich, dass dies das Problem tatsächlich löst, aber es tut:
randomCombine a b = randomBind a (\a' -> randomMap (\b' -> (a', b')) b)
Tatsächlich ist es möglich, randomBind in Form von randomJoin und randomMap zu schreiben. Es ist auch möglich, randomJoin in Bezug auf randomBind zu schreiben. Aber ich lasse dies als Übung.
Wir könnten das ein wenig vereinfachen. Erlauben Sie mir, diese Funktion zu definieren:
randomUnit :: a -> Random a
randomUnit wandelt einen Wert in eine Zufallsvariable um. Dies bedeutet, dass wir zufällige Variablen haben können, die eigentlich nicht zufällig sind. Dies war jedoch immer der Fall; wir hätten es schon tun können randomMap (const 4) random
. Der Grund für die Definition von randomUnit ist, dass wir jetzt randomMap in Bezug auf randomUnit und randomBind definieren können:
randomMap :: (a -> b) -> Random a -> Random b
randomMap f x = randomBind x (randomUnit . f)
Ok, jetzt kommen wir voran. Wir haben zufällige Variablen, die wir manipulieren können. Jedoch:
- Es ist nicht offensichtlich, wie wir diese Funktionen tatsächlich implementieren könnten,
- Es ist ziemlich umständlich.
Implementierung
Ich werde Pseudozufallszahlen angehen. Es ist möglich, diese Funktionen für echte Zufallszahlen zu implementieren, aber diese Antwort wird bereits ziemlich lang.
Im Wesentlichen wird dies so funktionieren, dass wir überall einen Startwert weitergeben werden. Wann immer wir einen neuen Zufallswert erzeugen, erzeugen wir einen neuen Startwert. Am Ende, wenn wir mit der Erstellung einer Zufallsvariablen fertig sind, wollen wir sie mit dieser Funktion abtasten:
runRandom :: Seed -> Random a -> a
Ich werde den Zufallstyp folgendermaßen definieren:
data Random a = Random (Seed -> (Seed, a))
Dann müssen wir nur noch Implementierungen von randomUnit, randomBind, runRandom und random bereitstellen, was ziemlich einfach ist:
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
Zufällig gehe ich davon aus, dass es bereits eine Funktion vom Typ gibt:
psuedoRandom :: Seed -> (Seed, Integer)
In diesem Fall ist der Zufall gerecht Random psuedoRandom
.
Dinge weniger umständlich machen
Haskell hat syntaktischen Zucker, um Dinge wie diesen auf den Augen schöner zu machen. Es heißt Do-Notation und um alles zu nutzen, müssen wir eine Instanz von Monad for Random erstellen.
instance Monad Random where
return = randomUnit
(>>=) = randomBind
Getan. randomCombine
von vorher konnte nun geschrieben werden:
randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = do
a' <- a
b' <- b
return (a', b')
Wenn ich dies für mich tun würde, würde ich sogar noch einen Schritt weiter gehen und eine Instanz von Applicative erstellen. (Mach dir keine Sorgen, wenn dies keinen Sinn ergibt).
instance Functor Random where
fmap = liftM
instance Applicative Random where
pure = return
(<*>) = ap
Dann könnte randomCombine geschrieben werden:
randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = (,) <$> a <*> b
>>=
Nachdem wir diese Instanzen haben, können wir anstelle von randomBind join anstelle von randomJoin fmap anstelle von randomMap return anstelle von randomUnit verwenden. Wir bekommen auch eine ganze Reihe von Funktionen kostenlos.
Ist es das wert? Man könnte argumentieren, dass es ziemlich schwierig und langwierig war, bis zu diesem Punkt zu gelangen, an dem das Arbeiten mit Zufallszahlen nicht völlig horrend ist. Was haben wir dafür bekommen?
Die unmittelbarste Belohnung ist, dass wir jetzt genau sehen können, welche Teile unseres Programms von der Zufälligkeit abhängen und welche Teile vollständig deterministisch sind. Nach meiner Erfahrung vereinfacht das Erzwingen einer strengen Trennung die Dinge ungemein.
Bisher haben wir angenommen, dass wir nur eine einzige Stichprobe aus jeder Zufallsvariablen benötigen, die wir generieren. Wenn sich jedoch herausstellt, dass wir in Zukunft mehr von der Verteilung sehen möchten, ist dies trivial. Sie können runRandom einfach häufig für dieselbe Zufallsvariable mit verschiedenen Startwerten verwenden. Dies ist natürlich in imperativen Sprachen möglich, aber in diesem Fall können wir sicher sein, dass wir nicht jedes Mal, wenn wir eine Zufallsvariable abtasten, eine unerwartete E / A ausführen und wir müssen nicht vorsichtig mit der Initialisierung des Status sein.