回答:
呼び出される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を実行することはなく、状態の初期化に注意する必要はありません。
bad :: Random a -> a
矛盾が生じるのでしょうか?何が悪いの?特に最初のステップについては、説明にゆっくりと進んでください:)「有用な」機能がなぜ役立つのかを説明できれば、これは1000ポイントの答えかもしれません。:)
あなたは間違っていません。同じシードをRNGに2回与えると、それが返す最初の擬似乱数は同じになります。これは、機能プログラミングと副作用プログラミングとは関係ありません。シードの定義は、特定の入力が、十分に分散されているが明らかに非ランダムな値の特定の出力を引き起こすことです。それが擬似ランダムと呼ばれる理由です。たとえば、予測可能な単体テストを書いたり、同じ問題で異なる最適化方法を確実に比較したりするなど、しばしば良いことです。
実際にコンピューターから非擬似乱数が必要な場合は、粒子崩壊源、コンピューターがオンになっているネットワーク内で発生する予測不可能なイベントなど、真にランダムなものに接続する必要があります。正しく機能していても通常は高価になりますが、擬似乱数値を取得しない唯一の方法です(通常、プログラミング言語から受け取る値は、明示的に指定しなくてもシードに基づいています)。
これ、そしてこれだけが、システムの機能的性質を損なうでしょう。非疑似乱数ジェネレーターはまれなので、これは頻繁には発生しませんが、はい、本当の乱数を生成するメソッドが本当にある場合、プログラミング言語の少なくともその少しは100%純粋に機能することはできません。言語が例外を作成するかどうかは、言語実装者がどれほど実用的であるかという問題にすぎません。
() -> Integer
。typeの純粋に機能的なPRNGを使用PRNG_State -> (PRNG_State, Integer)
できますが、不純な手段で初期化する必要があります)。
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)
非機能言語でも同じです。ここで真に乱数のわずかに別の問題を無視します。
乱数ジェネレーターは常にシード値を取得し、同じシードに対して同じ乱数シーケンスを返します(乱数を使用するプログラムをテストする必要がある場合に非常に役立ちます)。基本的に、選択したシードから開始し、最後の結果を次の反復のシードとして使用します。したがって、ほとんどの実装は、説明したとおり「純粋な」関数です。値を取得し、同じ値に対して常に同じ結果を返します。