In che modo i linguaggi funzionali gestiscono numeri casuali?


68

Quello che voglio dire al riguardo è che in quasi tutti i tutorial che ho letto sui linguaggi funzionali, è che una delle grandi cose sulle funzioni è che se chiamate una funzione con gli stessi parametri due volte, finirete sempre con il stesso risultato.

Come mai fai una funzione che prende un seme come parametro e quindi restituisce un numero casuale basato su quel seme?

Voglio dire, questo sembrerebbe andare contro una delle cose che sono così buone con le funzioni, giusto? O mi manca completamente qualcosa qui?

Risposte:


89

Non è possibile creare una funzione pura chiamata randomche darà un risultato diverso ogni volta che viene chiamata. In effetti, non puoi nemmeno "chiamare" funzioni pure. Li applichi. Quindi non ti manca nulla, ma ciò non significa che i numeri casuali siano vietati nella programmazione funzionale. Permettetemi di dimostrare, userò la sintassi di Haskell per tutto.

Proveniente da un background imperativo, inizialmente potresti aspettarti che un tipo casuale abbia questo tipo:

random :: () -> Integer

Ma questo è già stato escluso perché random non può essere una pura funzione.

Considera l'idea di un valore. Un valore è una cosa immutabile. Non cambia mai e ogni osservazione che puoi fare al riguardo è coerente per tutti i tempi.

Chiaramente, random non può produrre un valore intero. Invece, produce una variabile casuale Integer. Il suo tipo potrebbe apparire così:

random :: () -> Random Integer

Tranne il fatto che passare un argomento è del tutto inutile, le funzioni sono pure, quindi uno random ()è buono come un altro random (). Darò a caso, da qui in poi, questo tipo:

random :: Random Integer

Tutto bene e bene, ma non molto utile. Potresti aspettarti di essere in grado di scrivere espressioni come random + 42, ma non puoi, perché non controllerà i caratteri. Non puoi ancora fare nulla con variabili casuali.

Ciò solleva una domanda interessante. Quali funzioni dovrebbero esistere per manipolare variabili casuali?

Questa funzione non può esistere:

bad :: Random a -> a

in alcun modo utile, perché allora potresti scrivere:

badRandom :: Integer
badRandom = bad random

Il che introduce un'incoerenza. badRandom è presumibilmente un valore, ma è anche un numero casuale; una contraddizione.

Forse dovremmo aggiungere questa funzione:

randomAdd :: Integer -> Random Integer -> Random Integer

Ma questo è solo un caso speciale di un modello più generale. Dovresti essere in grado di applicare qualsiasi funzione a cose casuali per ottenere altre cose casuali in questo modo:

randomMap :: (a -> b) -> Random a -> Random b

Invece di scrivere random + 42, ora possiamo scrivere randomMap (+42) random.

Se tutto ciò che avevi fosse randomMap, non saresti in grado di combinare variabili casuali insieme. Non è possibile scrivere questa funzione per esempio:

randomCombine :: Random a -> Random b -> Random (a, b)

Potresti provare a scriverlo in questo modo:

randomCombine a b = randomMap (\a' -> randomMap (\b' -> (a', b')) b) a

Ma ha il tipo sbagliato. Invece di finire con a Random (a, b), finiamo con aRandom (Random (a, b))

Questo può essere risolto aggiungendo un'altra funzione:

randomJoin :: Random (Random a) -> Random a

Ma, per ragioni che alla fine potrebbero diventare chiare, non lo farò. Invece ho intenzione di aggiungere questo:

randomBind :: Random a -> (a -> Random b) -> Random b

Non è immediatamente ovvio che ciò risolva effettivamente il problema, ma lo fa:

randomCombine a b = randomBind a (\a' -> randomMap (\b' -> (a', b')) b)

In effetti, è possibile scrivere randomBind in termini di randomJoin e randomMap. È anche possibile scrivere randomJoin in termini di randomBind. Ma lascerò fare questo come esercizio.

Potremmo semplificarlo un po '. Mi permetta di definire questa funzione:

randomUnit :: a -> Random a

randomUnit trasforma un valore in una variabile casuale. Ciò significa che possiamo avere variabili casuali che in realtà non sono casuali. Questo è sempre stato il caso però; avremmo potuto fare randomMap (const 4) randomprima. Il motivo che definisce randomUnit è una buona idea è che ora possiamo definire randomMap in termini di randomUnit e randomBind:

randomMap :: (a -> b) -> Random a -> Random b
randomMap f x = randomBind x (randomUnit . f)

Ok, ora stiamo arrivando da qualche parte. Abbiamo variabili casuali che possiamo manipolare. Però:

  • Non è ovvio come potremmo effettivamente implementare queste funzioni,
  • È piuttosto ingombrante.

Implementazione

Affronterò numeri pseudo casuali. È possibile implementare queste funzioni per numeri casuali reali, ma questa risposta sta già diventando piuttosto lunga.

In sostanza, il modo in cui funzionerà è che passeremo un valore di seed ovunque. Ogni volta che generiamo un nuovo valore casuale, produrremo un nuovo seme. Alla fine, quando avremo finito di costruire una variabile casuale, vorremmo campionarlo usando questa funzione:

runRandom :: Seed -> Random a -> a

Definirò il tipo casuale in questo modo:

data Random a = Random (Seed -> (Seed, a))

Quindi, dobbiamo solo fornire implementazioni di randomUnit, randomBind, runRandom e random che è abbastanza semplice:

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

Per caso, suppongo che ci sia già una funzione del tipo:

psuedoRandom :: Seed -> (Seed, Integer)

Nel qual caso random è giusto Random psuedoRandom.

Rendere le cose meno ingombranti

Haskell ha zucchero sintattico per rendere le cose più piacevoli per gli occhi. Si chiama notazione e per usarlo tutto ciò che dobbiamo fare è creare un'istanza di Monad for Random.

instance Monad Random where
  return = randomUnit
  (>>=) = randomBind

Fatto. randomCombineda prima si poteva ora scrivere:

randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = do
  a' <- a
  b' <- b
  return (a', b')

Se lo facessi da solo, farei un ulteriore passo avanti e creerei un'istanza di Applicativo. (Non preoccuparti se questo non ha senso).

instance Functor Random where
  fmap = liftM

instance Applicative Random where
  pure = return
  (<*>) = ap

Quindi randomCombine potrebbe essere scritto:

randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = (,) <$> a <*> b

Ora che abbiamo queste istanze, possiamo usare al >>=posto di randomBind, join invece di randomJoin, fmap anziché randomMap, return invece di randomUnit. Inoltre riceviamo un intero carico di funzioni gratuitamente.

Ne vale la pena? Si potrebbe obiettare che arrivare a questo stadio, dove lavorare con numeri casuali non è del tutto orribile, è stato piuttosto difficile e prolisso. Cosa abbiamo ottenuto in cambio di questo sforzo?

La ricompensa più immediata è che ora possiamo vedere esattamente quali parti del nostro programma dipendono dalla casualità e quali parti sono interamente deterministiche. Nella mia esperienza, forzare una separazione rigorosa come questa semplifica immensamente le cose.

Abbiamo ipotizzato così lontano che desideriamo solo un singolo campione da ogni variabile casuale che generiamo, ma se si scopre che in futuro vorremmo davvero vedere più della distribuzione, questo è banale. Puoi semplicemente usare runRandom molte volte sulla stessa variabile casuale con semi diversi. Questo è ovviamente possibile nei linguaggi imperativi, ma in questo caso possiamo essere certi che non eseguiremo IO imprevisti ogni volta che campioneremo una variabile casuale e non dovremo stare attenti a inizializzare lo stato.


6
+1 per un buon esempio di utilizzo pratico di funzioni / monadi.
jozefg,

9
Bella risposta, ma va un po 'troppo veloce con alcuni passaggi. Ad esempio, perché bad :: Random a -> aintrodurre incoerenze? Cosa c'è di male? Per favore, vai lentamente nella spiegazione, specialmente per i primi passi :) Se potessi spiegare perché le funzioni "utili" sono utili, questa potrebbe essere una risposta da 1000 punti! :)
Andres F.

@AndresF. Ok, lo rivedrò un po '.
dan_waterworth,

1
@AndresF. Ho rivisto la mia risposta, ma non credo di aver spiegato a sufficienza come potresti usare questa è pratica, quindi potrei tornarci più tardi.
dan_waterworth,

3
Risposta notevole. Non sono un programmatore funzionale ma capisco la maggior parte dei concetti e ho "giocato" con Haskell. Questo è il tipo di risposta che informa l'interrogatore e ispira gli altri a scavare più a fondo e a saperne di più sull'argomento. Vorrei poterti dare qualche punto in più sopra i 10 dal mio voto positivo.
RLH,

10

Tu non hai torto. Se dai lo stesso seme a un RNG due volte, il primo numero pseudo-casuale che restituisce sarà lo stesso. Questo non ha nulla a che fare con la programmazione funzionale vs. effetti collaterali; la definizione di un seme è che un input specifico provoca un output specifico di valori ben distribuiti ma decisamente non casuali. Ecco perché si chiama pseudo-casuale, ed è spesso una buona cosa avere, ad esempio scrivere test unit prevedibili, confrontare in modo affidabile diversi metodi di ottimizzazione sullo stesso problema, ecc.

Se in realtà vuoi numeri non pseudo-casuali da un computer, devi collegarlo a qualcosa di veramente casuale, come una fonte di decadimento delle particelle, eventi imprevedibili che si verificano all'interno della rete su cui si trova il computer, ecc. È difficile diventa giusto e di solito costoso anche se funziona, ma è l'unico modo per non ottenere valori pseudo-casuali (di solito i valori che ricevi dal tuo linguaggio di programmazione sono basati su alcuni seed, anche se non ne hai fornito esplicitamente uno).

Questo, e solo questo, comprometterebbe la natura funzionale di un sistema. Poiché i generatori non pseudo-casuali sono rari, ciò non si presenta spesso, ma sì, se hai davvero un metodo che genera veri numeri casuali, almeno quel pezzettino del tuo linguaggio di programmazione non può essere puro al 100%. Se una lingua ne farebbe o meno un'eccezione è solo una questione di quanto sia pragmatico l'implementazione della lingua.


9
Un vero RNG non può essere affatto un programma per computer, indipendentemente dal fatto che sia puro (funzionante) o meno. Conosciamo tutti la citazione di von Neumann sui metodi aritmetici di produzione di cifre casuali (quelli che non lo fanno, guardano in alto - preferibilmente il tutto, non solo la prima frase). Dovresti interagire con un hardware non deterministico, che ovviamente è anche impuro. Ma questo è solo I / O, che è stato riconciliato con la purezza più volte in wans molto diversi. Nessun linguaggio utilizzabile in alcun modo non consente l'I / O completo, altrimenti non si potrebbe nemmeno vedere il risultato del programma.

Cosa c'è con il voto negativo?
l0b0,

6
Perché una fonte esterna e veramente casuale comprometterebbe la natura funzionale del sistema? È ancora "stesso input -> stesso output". A meno che non consideri la fonte esterna come parte del sistema, ma non sarebbe "esterna", vero?
Andres F.

4
Questo non ha nulla a che fare con PRNG vs TRNG. Non è possibile avere una funzione di tipo non costante () -> Integer. Puoi avere un PRNG di tipo puramente funzionale PRNG_State -> (PRNG_State, Integer), ma dovrai inizializzarlo con mezzi impuri).
Gilles 'SO- smetti di essere malvagio' il

4
@Brian Concorda, ma il testo ("collegalo a qualcosa di veramente casuale") suggerisce che la fonte casuale è esterna al sistema. Pertanto, il sistema stesso rimane puramente funzionale; è la fonte di input che non lo è.
Andres F.

6

Un modo è considerarlo come una sequenza infinita di numeri casuali:

IEnumerable<int> randomNumberGenerator = new RandomNumberGenerator(seed);

Cioè, pensalo come una struttura di dati senza fondo, come un Stackdove puoi solo chiamare Pop, ma puoi chiamarlo per sempre. Come un normale stack immutabile, toglierne uno dalla parte superiore ti dà un altro (diverso) stack.

Quindi un generatore di numeri casuali immutabile (con valutazione pigra) potrebbe apparire come:

class RandomNumberGenerator
{
    private readonly int nextSeed;
    private RandomNumberGenerator next;

    public RandomNumberGenerator(int seed)
    {
        this.nextSeed = this.generateNewSeed(seed);
        this.RandomNumber = this.generateRandomNumberBasedOnSeed(seed);
    }

    public int RandomNumber { get; private set; }

    public RandomNumberGenerator Next
    {
        get
        {
            if(this.next == null) this.next = new RandomNumberGenerator(this.nextSeed);
            return this.next;
        }
    }

    private static int generateNewSeed(int seed)
    {
        //...
    }

    private static int generateRandomNumberBasedOnSeed(int seed)
    {
        //...
    }
}

Funzionale


Io non vedo come la creazione di una lista infinita di numeri casuali è più facile da lavorare rispetto funzione come: pseudoRandom :: Seed -> (Seed, Integer). Potresti anche finire per scrivere una funzione di questo tipo[Integer] -> ([Integer], Integer)
dan_waterworth,

2
@dan_waterworth in realtà ha molto senso. Non si può dire che un numero intero sia casuale. Un elenco di numeri può avere questa protezione. Quindi la verità è che un generatore casuale può avere il tipo int -> [int] cioè una funzione che prende un seme e restituisce un elenco casuale di numeri interi. Certo, puoi avere una monade di stato attorno a questo per ottenere la notazione di haskell. Ma come risposta generica alla domanda, penso che questo sia davvero utile.
Simon Bergot,

5

È lo stesso per i linguaggi non funzionali. Ignorando qui il problema leggermente separato di numeri veramente casuali.

Un generatore di numeri casuali prende sempre un valore seed e per lo stesso seed restituisce la stessa sequenza di numeri casuali (molto utile se è necessario testare un programma che utilizza numeri casuali). Fondamentalmente inizia con il seme scelto e quindi utilizza l'ultimo risultato come seme per la successiva iterazione. Quindi la maggior parte delle implementazioni sono funzioni "pure" mentre le descrivi: Prendi un valore e per lo stesso valore restituisci sempre lo stesso risultato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.