web-dev-qa-db-ja.com

関数型言語は乱数をどのように処理しますか?

つまり、ほぼ関数型言語について読んだすべてのチュートリアルでは、関数の優れた点の1つは、同じパラメーターで関数を2回呼び出すと、alwaysは同じ結果になります。

では、いったいどのようにして、シードをパラメーターとして受け取り、そのシードに基づいて乱数を返す関数を作成しますか?

これは、機能に関して非常に優れている点の1つに反するように思えるかもしれませんね。それとも、ここで何かが完全に欠けていますか?

69
Electric Coffee

呼び出されるたびに異なる結果が得られるrandomという純粋な関数を作成することはできません。 実際には、純粋な関数を「呼び出す」こともできません。それらを適用します。したがって、何も不足していませんが、これは、関数型プログラミングで乱数が制限されていることを意味しません。デモンストレーションを許可します。私はHaskell構文を全体的に使用します。

命令的な背景から来て、最初はランダムに次のようなタイプがあると期待するかもしれません:

_random :: () -> Integer
_

しかし、ランダムは純粋な関数ではあり得ないため、これはすでに除外されています。

値の考えを検討してください。値は不変のものです。それは決して変わらず、あなたがそれについて行うことができるすべての観察はいつでも一貫しています。

明らかに、ランダムは整数値を生成できません。代わりに、整数の確率変数を生成します。タイプは次のようになります。

_random :: () -> Random Integer
_

引数を渡す必要がまったくないことを除いて、関数は純粋なので、1つの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を定義するのが良い考えである理由は、今ではrandomUnitとrandomBindの観点からrandomMapを定義できるからです。

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

わかりました、今、私たちはどこかに着いています。操作できるランダム変数があります。しかしながら:

  • これらの関数を実際に実装する方法は明らかではありませんが、
  • それはかなり面倒です。

実装

疑似乱数に取り組みます。実際の乱数に対してこれらの関数を実装することは可能ですが、この答えはすでにかなり長くなっています。

基本的に、これが機能する方法は、あらゆる場所でシード値を渡すことです。新しいランダム値を生成するたびに、新しいシードを生成します。最後に、ランダム変数の作成が完了したら、次の関数を使用してランダム変数をサンプリングします。

_runRandom :: Seed -> Random a -> a
_

次のようにランダムタイプを定義します。

_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 psuedoRandom_です。

煩わしさを軽減

Haskellには、このようなものを目に優しくする構文糖があります。それはdo-notationと呼ばれ、それを使用するには、ランダム用のMonadのインスタンスを作成する必要があります。

_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の代わりに結合、randomMapの代わりにfmap、randomUnitの代わりにreturnを使用できます。また、関数の全負荷を無料で取得します。

その価値はありますか?あなたは、乱数を扱うことが完全に恐ろしいわけではないこの段階に到達することは非常に困難で長続きしないと主張することができます。この努力と引き換えに何を得ましたか?

最も直接的な報酬は、プログラムのどの部分がランダム性に依存していて、どの部分が完全に決定論的であるかを正確に確認できることです。私の経験では、このように厳密な分離を強制すると、非常に簡単になります。

これまでは、生成する各ランダム変数から単一のサンプルが必要であると想定してきましたが、将来的に実際に分布をもっと見たい場合は、これは簡単なことです。異なるシードを持つ同じ確率変数に対して、runRandomを何度も使用できます。もちろん、これは命令型言語で可能ですが、この場合、ランダム変数をサンプリングするたびに予期しないIOを実行するつもりはなく、状態の初期化には注意が必要です。

89
dan_waterworth

あなたは間違っていません。同じシードをRNGに2回与えると、それが返す最初の疑似乱数は同じになります。これは、関数型プログラミングと副作用プログラミングでは関係ありません。シードのdefinitionは、特定の入力が、よく分散されているが明らかにランダムでない値の特定の出力を引き起こすことです。これが疑似ランダムと呼ばれる理由であり、多くの場合、これは良いことです。予測可能な単体テストを記述したり、同じ問題について異なる最適化手法を確実に比較したりするためなど。

実際にコンピューターから非疑似乱数が必要な場合は、粒子減衰源、コンピューターが接続されているネットワーク内で発生する予測できないイベントなど、真にランダムなものに接続する必要があります。これは困難です。正しく機能し、通常は機能しますが、それがnotの疑似ランダム値を取得する唯一の方法です(通常、プログラミング言語から受け取る値はsomeに基づいています) =シード(明示的に指定しなかった場合でも)

これは、これだけで、システムの機能的な性質を損なうことになります。非疑似乱数ジェネレーターはまれであるため、これは頻繁には発生しませんが、実際に真の乱数を生成するメソッドがある場合は、少なくともプログラミング言語のその少しのビットが100%純粋に機能することはできません。言語がその例外を作るかどうかは、言語の実装者がどれほど実用的であるかという問題にすぎません。

10
Kilian Foth

1つの方法は、それを乱数の無限シーケンスと考えることです。

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

つまり、Stackだけを呼び出すことができるPopのように、それを永久に呼び出すことができる、ボトムレスデータ構造と考えてください。通常の不変スタックと同様に、1つを上から取得すると、別の(異なる)スタックが得られます。

したがって、不変(遅延評価付き)の乱数ジェネレーターは次のようになります。

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

それは機能的です。

6
Scott Whitlock

非関数型言語でも同じです。ここでは真に乱数のわずかに別の問題を無視します。

乱数ジェネレーターは常にシード値を取り、同じシードに対して同じ乱数シーケンスを返します(乱数を使用するプログラムをテストする必要がある場合に非常に役立ちます)。基本的には、選択したシードから開始し、最後の結果を次の反復のシードとして使用します。したがって、ほとんどの実装は、説明するとおり「純粋な」関数です。値を取得すると、同じ値に対して常に同じ結果が返されます。

5