함수형 언어는 난수를 어떻게 처리합니까?


68

내가 그것에 대해 말은에 있다는 것입니다 거의 모든 튜토리얼 내가 함수형 언어에 대해 읽은 기능의 장점 중 하나는, 당신은 두 번 같은 매개 변수로 함수를 호출하는 경우, 당신은거야 때문이다 항상 와 끝까지 같은 결과.

그러면 어떻게 시드를 매개 변수로 사용하고 그 시드를 기반으로 난수를 반환하는 함수를 만들 수 있습니까?

나는 이것이 함수에 대해 좋은 것들 중 하나에 위배되는 것처럼 보입니다. 아니면 여기에 뭔가 빠졌습니까?

답변:


89

호출 random될 때마다 다른 결과를 제공 하는 순수한 함수를 작성할 수 없습니다 . 실제로 순수한 함수를 "호출"할 수도 없습니다. 당신은 그들을 적용합니다. 따라서 아무것도 빠진 것은 아니지만 기능 프로그래밍에서 임의의 숫자가 제한을 벗어난 것은 아닙니다. 내가 보여줄 수 있도록, 나는 전체적으로 Haskell 구문을 사용할 것이다.

명령적인 배경에서 온다면, 처음에는 무작위로 다음과 같은 유형을 기대할 수 있습니다.

random :: () -> Integer

그러나 이것은 무작위가 순수한 기능이 될 수 없기 때문에 이미 배제되었습니다.

가치의 아이디어를 고려하십시오. 가치는 불변의 것입니다. 그것은 결코 변하지 않으며 그것에 대해 당신이 할 수있는 모든 관찰은 항상 일관됩니다.

분명히 random은 정수 값을 생성 할 수 없습니다. 대신 정수 임의 변수를 생성합니다. 유형은 다음과 같습니다.

random :: () -> Random Integer

인수를 전달하는 것이 완전히 불필요하다는 것을 제외하고 함수는 순수하므로 하나 random ()는 다른 것만 큼 좋습니다 random (). 나는 여기서부터이 유형을 무작위로 줄 것이다 :

random :: Random Integer

어느 것이나 좋고 훌륭하지만 그다지 유용하지는 않습니다. 과 같은 표현식을 작성할 random + 42수는 있지만 유형 검사를 수행 할 수 없으므로 사용할 수 없습니다. 무작위 변수로는 아무것도 할 수 없습니다.

이것은 흥미로운 질문을 제기합니다. 랜덤 변수를 조작하려면 어떤 기능이 있어야합니까?

이 기능은 존재할 수 없습니다 :

bad :: Random a -> a

유용한 방법으로 다음과 같이 쓸 수 있습니다.

badRandom :: Integer
badRandom = bad random

불일치가 발생합니다. badRandom은 아마도 값이지만 임의의 숫자이기도합니다. 모순.

이 함수를 추가해야 할 수도 있습니다.

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

그러나 이것은 더 일반적인 패턴의 특별한 경우입니다. 다른 임의의 것을 얻으려면 임의의 함수를 임의의 것에 적용 할 수 있어야합니다.

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

random + 42우리는 이제 글을 쓰는 대신 글을 쓸 수 있습니다 randomMap (+42) random.

당신이 가진 모든 것이 randomMap이라면, 임의의 변수를 함께 결합 할 수 없습니다. 예를 들어이 함수를 작성할 수 없습니다.

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

다음과 같이 작성하려고 할 수 있습니다.

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

그러나 유형이 잘못되었습니다. 대신에 결말의 Random (a, b), 우리는 끝낼Random (Random (a, b))

다른 기능을 추가하면이 문제를 해결할 수 있습니다.

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

그러나 결국 분명해질 수있는 이유로 저는 그렇게하지 않을 것입니다. 대신 나는 이것을 추가 할 것입니다 :

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

이것이 실제로 문제를 해결한다는 것은 분명하지 않지만

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

실제로 randomJoin 및 randomMap의 관점에서 randomBind를 작성할 수 있습니다. randomBind의 관점에서 randomJoin을 작성할 수도 있습니다. 그러나 나는 이것을 운동으로 남겨 두겠습니다.

우리는 이것을 조금 단순화 할 수있었습니다. 이 함수를 정의 할 수 있습니다 :

randomUnit :: a -> Random a

randomUnit은 값을 임의의 변수로 바꿉니다. 이것은 실제로 무작위가 아닌 임의의 변수를 가질 수 있음을 의미합니다. 이것은 항상 그렇습니다. 우리는 randomMap (const 4) random전에 할 수 있었다 . randomUnit을 정의하는 것이 좋은 이유는 이제 randomMap과 randomBind의 관점에서 randomMap을 정의 할 수 있기 때문입니다.

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

좋아, 이제 어딘가로 가고있다. 우리는 조작 할 수있는 임의의 변수를 가지고 있습니다. 하나:

  • 실제로 이러한 기능을 어떻게 구현할 수 있는지는 확실하지 않습니다.
  • 꽤 번거 롭습니다.

이행

나는 의사 난수를 다룰 것입니다. 실제 난수에 대해 이러한 기능을 구현할 수 있지만이 답변은 이미 꽤 길어지고 있습니다.

기본적으로 이것이 작동하는 방식은 모든 곳에서 시드 값을 전달하는 것입니다. 새로운 임의의 값을 생성 할 때마다 새로운 시드가 생성됩니다. 마지막으로, 임의의 변수 생성을 마치면 다음 함수를 사용하여 샘플링합니다.

runRandom :: Seed -> Random a -> a

다음과 같이 Random 유형을 정의하겠습니다.

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

그런 다음 randomUnit, randomBind, runRandom 및 random 구현을 제공하면됩니다.

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

무작위로, 나는 이미 유형의 기능이 있다고 가정합니다.

psuedoRandom :: Seed -> (Seed, Integer)

이 경우 random은 단지 Random psuedoRandom입니다.

덜 성가신 일 만들기

하스켈은 이와 같은 것들을 눈에 더 좋게 만들기 위해 구문 설탕을 가지고 있습니다. 이를 do-notation이라고하며이를 사용하기 위해 Monad for Random의 인스턴스를 만듭니다.

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

끝난. randomCombine이전부터 다음과 같이 쓸 수 있습니다.

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

내가 이것을 위해 이것을하고 있다면, 이것보다 한 단계 더 나아가서 Applicative의 인스턴스를 만들 것입니다. (이것이 말이되지 않더라도 걱정하지 마십시오).

instance Functor Random where
  fmap = liftM

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

그런 다음 randomCombine을 작성할 수 있습니다.

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

이제 이러한 인스턴스가 있으므로 >>=randomBind 대신 randomJoin 대신 join, randomMap 대신 fmap, randomUnit 대신 return을 사용할 수 있습니다. 우리는 또한 많은 기능을 무료로 얻습니다.

그만한 가치가 있습니까? 임의의 숫자로 작업하는 것이 완전히 끔찍하지 않은이 단계에 도달하는 것은 상당히 어렵고 오래되었습니다. 이 노력에 대한 대가로 무엇을 얻었습니까?

가장 즉각적인 보상은 이제 프로그램의 어느 부분이 무작위성에 의존하고 있으며 어떤 부분이 전적으로 결정적인지를 정확히 볼 수 있다는 것입니다. 내 경험상 이와 같이 엄격한 분리를 강요하면 일이 엄청나게 단순화됩니다.

우리는 지금까지 생성 한 각 랜덤 변수에서 하나의 표본을 원한다고 가정했지만, 향후에 더 많은 분포를보고자한다면 이것은 사소한 것입니다. 다른 시드가있는 동일한 임의의 변수에서 runRandom을 여러 번 사용할 수 있습니다. 물론 이것은 명령형 언어에서 가능하지만,이 경우 임의의 변수를 샘플링 할 때마다 예기치 않은 IO를 수행하지 않을 것이라고 확신 할 수 있으며 상태 초기화에주의 할 필요가 없습니다.


6
어플리 케이 티브 펑터 / 모나드 실제 사용법의 좋은 예는 +1
jozefg

9
좋은 대답이지만 몇 단계를 거치면 너무 빠릅니다. 예를 들어 왜 bad :: Random a -> a불일치가 발생합니까? 뭐가 나쁜거야? 설명, 특히 첫 번째 단계에 대해 천천히 설명하십시오. "유용한"기능이 유용한 이유를 설명 할 수 있다면 이것은 1000 점 답변 일 수 있습니다! :)
Andres F.

@AndresF. 좋아, 조금 수정하겠습니다.
dan_waterworth

1
@AndresF. 나는 내 대답을 수정했지만, 이것이 당신이 이것을 어떻게 사용할 수 있는지 충분히 설명하지 않았다고 생각하므로 나중에 다시 올 수 있습니다.
dan_waterworth 2016 년

3
놀라운 답변. 저는 기능적인 프로그래머는 아니지만 대부분의 개념을 이해하고 Haskell과 "플레이"했습니다. 이것은 질문자에게 정보를 제공하고 다른 사람들이 주제에 대해 더 깊이 파고 배우도록 고무시키는 답변 유형입니다. 본인의 투표에서 10 점 이상으로 몇 가지 추가 포인트를 줄 수 있기를 바랍니다.
RLH

10

당신은 틀리지 않습니다. 동일한 시드를 RNG에 두 번 제공하면 반환되는 첫 번째 의사 난수는 동일합니다. 이것은 기능적 대 부작용 프로그래밍과는 아무런 관련이 없습니다. 시드 의 정의 는 특정 입력이 잘 분산되었지만 결정적으로 비 랜덤 값의 특정 출력을 유발한다는 것입니다. 이것이 바로 의사 난수 (pseudo-random)라고하는 이유이며, 예를 들어 예측 가능한 단위 테스트 작성과 같은 문제에 대해 서로 다른 최적화 방법을 안정적으로 비교하는 것이 좋은 경우가 많습니다.

컴퓨터에서 의사 난수 이외의 숫자를 실제로 원한다면 입자 붕괴 소스, 컴퓨터가있는 네트워크 내에서 발생하는 예측할 수없는 이벤트 등과 같이 임의의 숫자로 연결해야합니다. 그것이 효과가 있더라도 옳고 비싸지 만 의사 난수 값을 얻지 못하는 유일한 방법입니다 (일반적으로 프로그래밍 언어에서받은 값은 명시 적으로 제공하지 않더라도 일부 시드를 기반으로 합니다).

이것 만이 시스템의 기능적 특성을 손상시킵니다. 의사 난수 생성기가 드물기 때문에 자주 발생하지는 않지만, 실제로 실제 난수를 생성하는 방법이 있다면 최소한 프로그래밍 언어의 일부가 100 % 순수하게 작동 할 수는 없습니다. 언어가 예외를 만들지 여부는 언어 구현자가 얼마나 실용적인 지에 대한 문제 일뿐입니다.


9
진정한 RNG는 순수한 기능에 관계없이 컴퓨터 프로그램이 될 수 없습니다. 우리는 폰 노이만 (Bon Neumann)이 임의의 숫자를 생성하는 산술적 방법에 대한 인용문을 알고있다. 비 결정적 하드웨어와 상호 작용해야합니다. 그러나 그것은 단지 I / O입니다. 이것은 매우 다른 완 (wan)에서 여러 차례 순도로 조정되었습니다. 아니 어떤 식 으로든 가능한 것을 허용하지 I에 언어 / O 완전히 - 당신도 프로그램의 결과를 다르게 볼 수 없었다.

다운 투표와 함께 무엇입니까?
l0b0

6
왜 외부의 무작위 소스가 시스템의 기능적 특성을 손상 시키는가? 여전히 "동일한 입력-> 동일한 출력"입니다. 외부 소스를 시스템의 일부로 고려하지 않는 한 "외부"가 아닐까요?
Andres F.

4
PRNG와 TRNG는 아무 관련이 없습니다. 유형이 일정하지 않은 함수를 가질 수 없습니다 () -> Integer. 순전히 기능적인 PRNG 유형을 PRNG_State -> (PRNG_State, Integer)가질 수 있지만 불순한 방법으로 초기화해야합니다.
Gilles

4
@Brian Agreed, 그러나 단어 ( "정말로 무작위로 연결")는 무작위 소스가 시스템 외부에 있음 을 나타냅니다. 따라서 시스템 자체는 순수한 기능을 유지합니다. 그렇지 않은 입력 소스입니다.
Andres F.

6

한 가지 방법은 무한 난수 시퀀스로 생각하는 것입니다.

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

즉, Stack호출 할 수 있는 곳 과 같이 밑이없는 데이터 구조로 생각 하면 Pop되지만 영원히 호출 할 수 있습니다. 일반적인 불변 스택처럼, 하나를 맨 위로 가져 가면 다른 (다른) 스택이 생깁니다.

따라서 불변의 (지연 평가 포함) 난수 생성기는 다음과 같습니다.

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

기능적입니다.


다음과 같은 함수보다 무한 난수 목록을 작성하는 것이 더 쉬운 방법을 모르겠습니다 pseudoRandom :: Seed -> (Seed, Integer). 이 유형의 함수를 작성할 수도 있습니다.[Integer] -> ([Integer], Integer)
dan_waterworth

2
@dan_waterworth 실제로 많은 의미가 있습니다. 정수는 임의라고 말할 수 없습니다. 숫자 목록에는이 속성이있을 수 있습니다. 따라서 임의의 생성기는 int-> [int] 형식을 가질 수 있습니다. 즉, 시드를 취하고 임의의 정수 목록을 반환하는 함수입니다. 물론, 당신은 haskell의 표기법을 얻기 위해이 주위에 상태 모나드를 가질 수 있습니다. 그러나 질문에 대한 일반적인 대답으로 이것이 실제로 도움이된다고 생각합니다.
Simon Bergot 2016 년

5

작동하지 않는 언어와 동일합니다. 여기서는 실제로 난수의 약간 분리 된 문제를 무시합니다.

난수 생성기는 항상 시드 값을 사용하며 동일한 시드에 대해 동일한 난수 시퀀스를 반환합니다 (난수를 사용하는 프로그램을 테스트해야하는 경우 매우 유용). 기본적으로 선택한 시드로 시작한 다음 마지막 결과를 다음 반복의 시드로 사용합니다. 따라서 대부분의 구현은 설명 할 때 "순수한"함수입니다. 값을 가져오고 동일한 값에 대해 항상 동일한 결과를 반환합니다.

Licensed under cc by-sa 3.0 with attribution required.