Como as linguagens funcionais lidam com números aleatórios?


68

O que quero dizer sobre isso é que, em quase todos os tutoriais que li sobre linguagens funcionais, uma das grandes coisas sobre funções é que, se você chamar uma função com os mesmos parâmetros duas vezes, sempre terminará com o mesmo resultado.

Como diabos você cria uma função que usa uma semente como parâmetro e depois retorna um número aleatório com base nessa semente?

Quero dizer, isso parece ir contra uma das coisas que são tão boas em funções, certo? Ou estou perdendo completamente alguma coisa aqui?

Respostas:


89

Você não pode criar uma função pura chamada randomque fornecerá um resultado diferente toda vez que for chamada. De fato, você não pode nem "chamar" funções puras. Você os aplica. Então você não está perdendo nada, mas isso não significa que números aleatórios estejam fora dos limites na programação funcional. Permita-me demonstrar, usarei a sintaxe Haskell por toda parte.

Vindo de um contexto imperativo, você pode inicialmente esperar que aleatório tenha um tipo como este:

random :: () -> Integer

Mas isso já foi descartado porque o aleatório não pode ser uma função pura.

Considere a ideia de um valor. Um valor é uma coisa imutável. Isso nunca muda e todas as observações que você pode fazer sobre isso são consistentes o tempo todo.

Claramente, aleatório não pode produzir um valor inteiro. Em vez disso, produz uma variável aleatória Inteiro. Seu tipo pode ficar assim:

random :: () -> Random Integer

Exceto que passar um argumento é completamente desnecessário, as funções são puras, então uma random ()é tão boa quanto a outra random (). Vou dar aleatoriamente, a partir daqui, este tipo:

random :: Random Integer

Tudo bem, mas não muito útil. Você pode escrever expressões como random + 42, mas não pode, porque não será verificado. Você ainda não pode fazer nada com variáveis ​​aleatórias.

Isso levanta uma questão interessante. Quais funções devem existir para manipular variáveis ​​aleatórias?

Esta função não pode existir:

bad :: Random a -> a

de qualquer maneira útil, porque então você pode escrever:

badRandom :: Integer
badRandom = bad random

O que introduz uma inconsistência. badRandom é supostamente um valor, mas também é um número aleatório; uma contradição.

Talvez devêssemos adicionar esta função:

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

Mas este é apenas um caso especial de um padrão mais geral. Você deve poder aplicar qualquer função à coisa aleatória para obter outras coisas aleatórias como:

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

Em vez de escrever random + 42, agora podemos escrever randomMap (+42) random.

Se tudo o que você tinha fosse randomMap, não seria possível combinar variáveis ​​aleatórias. Você não pôde escrever esta função, por exemplo:

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

Você pode tentar escrever assim:

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

Mas tem o tipo errado. Em vez de terminar com um Random (a, b), terminamos com umRandom (Random (a, b))

Isso pode ser corrigido adicionando outra função:

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

Mas, por razões que podem eventualmente ficar claras, não vou fazer isso. Em vez disso, vou adicionar isso:

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

Não é imediatamente óbvio que isso realmente resolve o problema, mas sim:

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

De fato, é possível escrever randomBind em termos de randomJoin e randomMap. Também é possível escrever randomJoin em termos de randomBind. Mas vou deixar de fazer isso como um exercício.

Poderíamos simplificar um pouco isso. Permita-me definir esta função:

randomUnit :: a -> Random a

randomUnit transforma um valor em uma variável aleatória. Isso significa que podemos ter variáveis ​​aleatórias que não são realmente aleatórias. Esse sempre foi o caso; nós poderíamos ter feito randomMap (const 4) randomantes. O motivo para definir randomUnit é uma boa ideia é que agora podemos definir randomMap em termos de randomUnit e randomBind:

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

Ok, agora estamos chegando a algum lugar. Temos variáveis ​​aleatórias que podemos manipular. Contudo:

  • Não é óbvio como podemos realmente implementar essas funções,
  • É bastante complicado.

Implementação

Vou abordar números pseudo-aleatórios. É possível implementar essas funções para números aleatórios reais, mas essa resposta já está ficando muito longa.

Essencialmente, a maneira como isso vai funcionar é que vamos repassar um valor inicial em todo lugar. Sempre que gerarmos um novo valor aleatório, produziremos uma nova semente. No final, quando terminarmos de construir uma variável aleatória, desejaremos fazer uma amostra dela usando esta função:

runRandom :: Seed -> Random a -> a

Vou definir o tipo aleatório assim:

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

Então, precisamos fornecer implementações de randomUnit, randomBind, runRandom e random, o que é bastante direto:

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

Por acaso, vou assumir que já existe uma função do tipo:

psuedoRandom :: Seed -> (Seed, Integer)

Nesse caso, aleatório é justo Random psuedoRandom.

Tornando as coisas menos complicadas

Haskell tem açúcar sintático para tornar as coisas mais agradáveis ​​aos olhos. Isso se chama do-notation e, para usar tudo o que temos, criar uma instância do Monad for Random.

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

Feito. randomCombinede antes agora poderia ser escrito:

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

Se eu estivesse fazendo isso por mim mesmo, daria um passo além e criaria uma instância de Applicative. (Não se preocupe se isso não faz sentido).

instance Functor Random where
  fmap = liftM

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

Então randomCombine poderia ser escrito:

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

Agora que temos essas instâncias, podemos usar em >>=vez de randomBind, ingressar em vez de randomJoin, fmap em vez de randomMap, retornar em vez de randomUnit. Também recebemos uma grande quantidade de funções gratuitamente.

Vale a pena? Você poderia argumentar que chegar a esse estágio em que trabalhar com números aleatórios não é completamente horrendo foi bastante difícil e demorado. O que recebemos em troca desse esforço?

A recompensa mais imediata é que agora podemos ver exatamente quais partes do nosso programa dependem da aleatoriedade e quais são inteiramente determinísticas. Na minha experiência, forçar uma separação estrita como essa simplifica imensamente as coisas.

Supomos até agora que queremos apenas uma amostra de cada variável aleatória que geramos, mas se acontecer que, no futuro, gostaríamos de ver mais da distribuição, isso é trivial. Você pode usar runRandom muitas vezes na mesma variável aleatória com sementes diferentes. É claro que isso é possível em linguagens imperativas, mas, neste caso, podemos ter certeza de que não executaremos IO imprevisíveis toda vez que amostramos uma variável aleatória e não precisamos ter cuidado com a inicialização do estado.


6
+1 para um bom exemplo de uso prático de funcores aplicáveis ​​/ mônadas.
27413 Jozefg

9
Boa resposta, mas vai um pouco rápido demais com alguns passos. Por exemplo, por que bad :: Random a -> aintroduzir inconsistências? O que há de ruim nisso? Por favor, vá devagar na explicação, especialmente nos primeiros passos :) Se você puder explicar por que as funções "úteis" são úteis, isso pode ser uma resposta de 1000 pontos! :)
Andres F.

@AndresF. Ok, vou revisar um pouco.
dan_waterworth

11
@AndresF. Revisei minha resposta, mas acho que não expliquei suficientemente como você pode usar essa prática, para que eu possa voltar mais tarde.
dan_waterworth

3
Resposta notável. Não sou um programador funcional, mas entendo a maioria dos conceitos e "brinquei" com Haskell. Esse é o tipo de resposta que informa o interlocutor e inspira outras pessoas a aprofundar e aprender mais sobre o tópico. Eu gostaria de poder lhe dar alguns pontos extras acima dos 10 da minha votação inicial.
RLH 28/06

10

Você não está errado. Se você der a mesma semente a um RNG duas vezes, o primeiro número pseudo-aleatório que ele retornar será o mesmo. Isso não tem nada a ver com programação funcional versus efeitos colaterais; a definição de uma semente é que uma entrada específica causa uma saída específica de valores bem distribuídos, mas decididamente não aleatórios. É por isso que é chamado de pseudo-aleatório, e geralmente é bom ter, por exemplo, escrever testes de unidade previsíveis, comparar de forma confiável diferentes métodos de otimização no mesmo problema, etc.

Se você realmente deseja números não pseudoaleatórios de um computador, é necessário conectá-lo a algo genuinamente aleatório, como uma fonte de decaimento de partículas, eventos imprevisíveis que ocorrem na rede em que o computador está ligado etc. Isso é difícil de acerte e geralmente seja caro, mesmo que funcione, mas é a única maneira de não obter valores pseudo-aleatórios (geralmente os valores que você recebe da sua linguagem de programação são baseados em algumas sementes, mesmo que você não tenha fornecido explicitamente).

Isso, e somente isso, comprometeria a natureza funcional de um sistema. Como geradores não pseudoaleatórios são raros, isso não ocorre com frequência, mas sim, se você realmente tem um método para gerar números aleatórios verdadeiros, pelo menos esse pouco da sua linguagem de programação não pode ser 100% funcional. Se uma linguagem abriria uma exceção ou não, é apenas uma questão de quão pragmático o implementador de linguagem é.


9
Um verdadeiro RNG não pode ser um programa de computador, independentemente de ser puro (funcionalmente) ou não. Todos conhecemos a citação de von Neumann sobre métodos aritméticos de produção de dígitos aleatórios (aqueles que não o consultam - de preferência a coisa toda, não apenas a primeira frase). Você precisaria interagir com algum hardware não determinístico, o que obviamente também é impuro. Mas isso é apenas E / S, que foi reconciliado com pureza várias vezes em wans muito diferentes. Nenhum idioma que seja de alguma forma utilizável desabilita completamente a E / S - você não poderia ver o resultado do programa de outra maneira.

O que há com o voto negativo?
l0b0

6
Por que uma fonte externa e verdadeiramente aleatória comprometeria a natureza funcional do sistema? Ainda é "mesma entrada -> mesma saída". A menos que você considere a fonte externa como parte do sistema, mas não seria "externa", seria?
precisa

4
Isso não tem nada a ver com PRNG vs TRNG. Você não pode ter uma função não constante do tipo () -> Integer. Você pode ter um PRNG puramente funcional do tipo PRNG_State -> (PRNG_State, Integer), mas precisará inicializá-lo por meios impuros).
Gilles 'SO- stop be evil'

4
@ Brian Concordou, mas o texto ("conecte-o a algo genuinamente aleatório") sugere que a fonte aleatória é externa ao sistema. Portanto, o próprio sistema permanece puramente funcional; é a fonte de entrada que não é.
Andres F.

6

Uma maneira é pensar nisso como uma sequência infinita de números aleatórios:

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

Ou seja, pense nisso como uma estrutura de dados sem fundo, como uma Stackonde você só pode ligar Pop, mas pode chamá-lo para sempre. Como uma pilha imutável normal, tirar uma do topo dá a você outra pilha (diferente).

Portanto, um gerador de números aleatórios imutável (com avaliação lenta) pode se parecer com:

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)
    {
        //...
    }
}

Isso é funcional.


Eu não vejo como a criação de uma lista infinita de números aleatórios é mais fácil de trabalhar do que função como: pseudoRandom :: Seed -> (Seed, Integer). Você pode até acabar escrevendo uma função deste tipo[Integer] -> ([Integer], Integer)
dan_waterworth

2
@dan_waterworth, na verdade, faz muito sentido. Não se pode dizer que um número inteiro seja aleatório. Uma lista de números pode ter essa propriedade. Portanto, a verdade é que um gerador aleatório pode ter o tipo int -> [int], ou seja, uma função que pega uma semente e retorna uma lista aleatória de números inteiros. Claro, você pode ter uma mônada estadual ao redor para obter a notação de haskell. Mas, como resposta genérica à pergunta, acho que isso é realmente útil.
Simon Bergot

5

É o mesmo para idiomas não funcionais. Ignorando o problema ligeiramente separado de números verdadeiramente aleatórios aqui.

Um gerador de números aleatórios sempre pega um valor de semente e, para a mesma semente, retorna a mesma sequência de números aleatórios (muito útil se você precisar testar um programa que usa números aleatórios). Basicamente, começa com a semente que você escolhe e, em seguida, usa o último resultado como semente para a próxima iteração. Portanto, a maioria das implementações são funções "puras" conforme as descreve: Pegue um valor e, para o mesmo valor, sempre retorne o mesmo resultado.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.