답변:
호출 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를 수행하지 않을 것이라고 확신 할 수 있으며 상태 초기화에주의 할 필요가 없습니다.
bad :: Random a -> a
불일치가 발생합니까? 뭐가 나쁜거야? 설명, 특히 첫 번째 단계에 대해 천천히 설명하십시오. "유용한"기능이 유용한 이유를 설명 할 수 있다면 이것은 1000 점 답변 일 수 있습니다! :)
당신은 틀리지 않습니다. 동일한 시드를 RNG에 두 번 제공하면 반환되는 첫 번째 의사 난수는 동일합니다. 이것은 기능적 대 부작용 프로그래밍과는 아무런 관련이 없습니다. 시드 의 정의 는 특정 입력이 잘 분산되었지만 결정적으로 비 랜덤 값의 특정 출력을 유발한다는 것입니다. 이것이 바로 의사 난수 (pseudo-random)라고하는 이유이며, 예를 들어 예측 가능한 단위 테스트 작성과 같은 문제에 대해 서로 다른 최적화 방법을 안정적으로 비교하는 것이 좋은 경우가 많습니다.
컴퓨터에서 의사 난수 이외의 숫자를 실제로 원한다면 입자 붕괴 소스, 컴퓨터가있는 네트워크 내에서 발생하는 예측할 수없는 이벤트 등과 같이 임의의 숫자로 연결해야합니다. 그것이 효과가 있더라도 옳고 비싸지 만 의사 난수 값을 얻지 못하는 유일한 방법입니다 (일반적으로 프로그래밍 언어에서받은 값은 명시 적으로 제공하지 않더라도 일부 시드를 기반으로 합니다).
이것 만이 시스템의 기능적 특성을 손상시킵니다. 의사 난수 생성기가 드물기 때문에 자주 발생하지는 않지만, 실제로 실제 난수를 생성하는 방법이 있다면 최소한 프로그래밍 언어의 일부가 100 % 순수하게 작동 할 수는 없습니다. 언어가 예외를 만들지 여부는 언어 구현자가 얼마나 실용적인 지에 대한 문제 일뿐입니다.
() -> Integer
. 순전히 기능적인 PRNG 유형을 PRNG_State -> (PRNG_State, Integer)
가질 수 있지만 불순한 방법으로 초기화해야합니다.
한 가지 방법은 무한 난수 시퀀스로 생각하는 것입니다.
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)