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


68

私はそれについて意味することにということですほとんど、すべてのチュートリアル私は関数型言語について読んだ機能の優れた点の一つは、あなたが二度同じパラメータを持つ関数を呼び出す場合、あなたはだろうということであるということである常にで終わります同じ結果。

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

これは、機能について非常に良いことの1つに反するように見えることを意味しますよね?または、私はここで何かが完全に欠けていますか?

回答:


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を定義するのが良い理由は、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はただRandom psuedoRandomです。

物事を面倒にしない

Haskellには、このようなことを目に見えるようにするための構文糖衣があります。これはdo記法と呼ばれ、それをすべて使用するために、ランダムに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の代わりにjoin、randomMapの代わりにfmap、randomUnitの代わりにreturnを使用できます。また、すべての関数を無料で入手できます。

その価値はありますか?乱数を扱うのが完全に恐ろしいわけではないこの段階にたどり着くのは、非常に難しく、時間がかかりました。この努力と引き換えに何を得ましたか?

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

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


6
Applicative functor / Monadsの実用的な使用例の+1。
-jozefg

9
いい答えですが、いくつかのステップで少し速すぎます。たとえば、なぜbad :: Random a -> a矛盾が生じるのでしょうか?何が悪いの?特に最初のステップについては、説明にゆっくりと進んでください:)「有用な」機能がなぜ役立つのかを説明できれば、これは1000ポイントの答えかもしれません。:)
アンドレスF.

@AndresF。わかりました、私はそれを少し修正します。
dan_waterworth

1
@AndresF。私は答えを修正しましたが、これがどのように実践されるかを十分に説明したとは思わないので、後でまた戻ってくるかもしれません。
dan_waterworth

3
驚くべき答え。私は機能的なプログラマーではありませんが、ほとんどの概念を理解しており、Haskellで「遊んだ」ことがあります。これは、質問者に情報を提供すると同時に、他の人がトピックをより深く掘り下げてトピックについてより多くのことを学ぶように促す回答のタイプです。私の投票で10点を超える追加ポイントを提供できるといいのですが。
RLH

10

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

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

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


9
真のRNGは、純粋(機能的)であるかどうかに関係なく、コンピュータープログラムにはなりません。私たちは皆、ランダムな数字を生成する算術的方法についてのフォン・ノイマンの引用を知っています。いくつかの非決定的なハードウェアとやり取りする必要がありますが、もちろんこれも不純です。しかし、それは単なるI / Oであり、非常に異なるWANで何回か純粋さと調和しています。何らかの方法で使用可能な言語は、I / Oを完全に禁止しません。それ以外の場合は、プログラムの結果を見ることさえできません。

反対票とは何ですか?
l0b0

6
外部の真にランダムなソースがシステムの機能的性質を損なうのはなぜですか?まだ「同じ入力->同じ出力」です。外部ソースをシステムの一部と見なさない限り、「外部」ではないでしょうか?
アンドレスF.

4
これは、PRNG対TRNGとは関係ありません。typeの非定数関数を持つことはできません() -> Integer。typeの純粋に機能的なPRNGを使用PRNG_State -> (PRNG_State, Integer)できますが、不純な手段で初期化する必要があります)。
ジル 'SO-悪であるのをやめる'

4
@Brian Agreed、しかし文言(「真にランダムなものに結びつける」)は、ランダムなソースがシステムの外部にあることを示唆しています。したがって、システム自体は純粋に機能し続けます。そうではない入力ソースです。
アンドレスF.

6

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

機能的です。


乱数の無限リストを作成することは、次のような機能よりも簡単に操作できるとは思いませんpseudoRandom :: Seed -> (Seed, Integer)。このタイプの関数を書くことになるかもしれません[Integer] -> ([Integer], Integer)
dan_waterworth

2
@dan_waterworthは実際、それは非常に理にかなっています。整数はランダムとは言えません。数字のリストにこのプロパティを含めることができます。真実は、ランダムジェネレーターはint-> [int]型、つまりシードを取り、整数のランダムリストを返す関数を持つことができるということです。確かに、ハスケルのdo記法を取得するために、これの周りに状態モナドを持つことができます。しかし、質問に対する一般的な答えとして、これは本当に役立つと思います。
サイモンベルゴ

5

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.