İşlevsel diller rasgele sayıları nasıl ele alır?


68

Ne bu konuda demek de olduğunu neredeyse her öğretici ben fonksiyonel diller hakkında okudum işlevler güzel yanlarından biri, iki kez aynı parametrelerle bir işlevi çağırmak varsa, edeceğiz olmasıdır hep ile bitirmek Aynı sonuç.

O zaman nasıl yeryüzünde bir tohumu parametre olarak alan ve o tohumu temel alarak rastgele bir sayı döndüren bir işlev yaparsınız?

Demek istediğim, bu fonksiyonlar hakkında çok iyi olan şeylerden birine karşı geliyor gibi görünüyor, değil mi? Yoksa burada tamamen bir şeyi mi özlüyorum?

Yanıtlar:


89

Her çağrıldığında randomfarklı bir sonuç verecek olan saf bir fonksiyon yaratamazsınız . Aslında, saf fonksiyonları “çağıramazsınız” bile. Onları uygula. Yani hiçbir şeyi kaçırmıyorsunuz, ancak bu, fonksiyonel programlamada rasgele sayıların sınırsız olduğu anlamına gelmez. Göstermeme izin ver, boyunca Haskell sözdizimini kullanacağım.

Zorunlu bir arka plandan geliyorsa, başlangıçta böyle bir türe sahip olmasını rasgele bekleyebilirsiniz:

random :: () -> Integer

Ancak bu zaten ekarte edildi çünkü rastgele saf bir işlev olamaz.

Bir değer fikrini düşünün. Bir değer değişmez bir şeydir. Asla değişmez ve bu konuda yapabileceğiniz her gözlem her zaman için tutarlıdır.

Açıkçası, rastgele bir Tamsayı değeri üretemez. Bunun yerine, bir Tamsayı rasgele değişken üretir. Bu tür şöyle görünebilir:

random :: () -> Random Integer

Bir argüman iletmenin tamamen gereksiz olması dışında, işlevler saf, bu yüzden biri diğeri random ()kadar iyidir random (). Bundan sonra, bu tür rastgele vereceğim:

random :: Random Integer

Her şey iyi ve iyi, ama çok kullanışlı değil. Gibi ifadeler yazmayı bekleyebilirsiniz random + 42, ancak yazamazsınız , çünkü o yazmaz. Henüz rastgele değişkenlerle bir şey yapamazsınız.

Bu ilginç bir soru ortaya çıkarır. Rasgele değişkenleri manipüle etmek için hangi fonksiyonlar bulunmalıdır?

Bu işlev var olamaz:

bad :: Random a -> a

Herhangi bir yararlı şekilde, çünkü o zaman yazabilirsiniz:

badRandom :: Integer
badRandom = bad random

Bu bir tutarsızlık getiriyor. badRandom sözde bir değer, ancak aynı zamanda rastgele bir sayıdır; bir çelişki.

Belki bu işlevi eklemeliyiz:

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

Ama bu sadece daha genel bir model için özel bir durum. Bunun gibi diğer rastgele şeyleri almak için herhangi bir işlevi rastgele şeye uygulayabilmelisiniz:

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

random + 42Yazmak yerine şimdi yazabiliriz randomMap (+42) random.

Sahip olduğunuz her şey randomMap olsaydı, rastgele değişkenleri bir araya getiremezdiniz. Örneğin, bu işlevi yazamazsınız:

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

Böyle yazmayı deneyebilirsin:

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

Ancak yanlış tiptedir. Bunun yerine bir ile biten Random (a, b), bir ile bitirmekRandom (Random (a, b))

Bu, başka bir işlev ekleyerek düzeltilebilir:

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

Ancak, sonunda netleşebilecek nedenlerden dolayı, bunu yapmayacağım. Bunun yerine şunu ekleyeceğim:

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

Bunun aslında sorunu çözdüğü hemen belli değil, ama çözüyor:

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

Aslında, randomBind'i randomJoin ve randomMap cinsinden yazmak mümkündür. RandomJoin'i randomBind cinsinden yazmak da mümkündür. Ama bunu bir egzersiz olarak yapmaktan ayrılacağım.

Bunu biraz basitleştirebiliriz. Bu işlevi tanımlamama izin ver:

randomUnit :: a -> Random a

randomUnit, bir değeri rastgele bir değişkene dönüştürür. Bu, aslında rastgele olmayan rastgele değişkenlere sahip olabileceğimiz anlamına gelir. Bu her zaman olsa da oldu; Daha randomMap (const 4) randomönce de yapabilirdik. RandomUnit'i tanımlamanın nedeni iyi bir fikirdir, şimdi randomMap'i randomUnit ve randomBind olarak tanımlayabiliriz:

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

Tamam, şimdi bir yerlere geliyoruz. Değiştirebileceğimiz rastgele değişkenler var. Ancak:

  • Bu fonksiyonları gerçekten nasıl uygulayabileceğimiz belli değil,
  • Oldukça hantal.

uygulama

Sözde rasgele sayıları ele alacağım. Bu işlevleri gerçek rasgele sayılar için uygulamak mümkündür, ancak bu cevap oldukça uzadı.

Temel olarak, bunun işe yarayacağı yol her yerde bir tohum değeri geçireceğimizdir. Ne zaman yeni bir rastgele değer üretsek, yeni bir tohum üreteceğiz. Sonunda, rastgele bir değişken oluşturmayı tamamladığımızda, bu işlevi kullanarak ondan örnek almak isteyeceğiz:

runRandom :: Seed -> Random a -> a

Random türünü şöyle tanımlayacağım:

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

O zaman, sadece oldukça basit olan randomUnit, randomBind, runRandom ve random uygulamalarını sunmamız gerekiyor:

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

Rastgele olarak, zaten türün bir işlevi olduğunu kabul edeceğim:

psuedoRandom :: Seed -> (Seed, Integer)

Bu durumda rastgele adil Random psuedoRandom.

İşleri daha az hantal hale getirmek

Haskell, gözlerinde bu kadar güzel şeyler yapmak için sözdizimsel şekere sahiptir. Buna no-notasyon denir ve hepsini kullanmamız gerekir, bunu Rastgele için bir Monad örneği yaratırız.

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

Bitti. randomCombinedaha önce şimdi yazılmış olabilir:

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

Bunu kendim için yapsaydım, bundan daha bir adım öteye gider ve bir Uygulayıcı örneği yaratırdım. (Bu hiç mantıklı değilse endişelenmeyin).

instance Functor Random where
  fmap = liftM

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

Sonra randomCombine yazılabilir:

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

Şimdi bu örneklere sahip olduğumuz için >>=, randomBind yerine kullanabilir, randomJoin yerine join, randomMap yerine fmap, randomUnit yerine return. Ayrıca tüm fonksiyon yüklerini ücretsiz olarak alıyoruz.

Buna değer mi? Rasgele sayılarla çalışmanın tamamen korkunç olmadığı bu aşamaya gelmenin zor ve uzun soluklu olduğunu iddia edebilirsiniz. Bu çaba karşılığında ne aldık?

En acil ödül, programımızın hangi bölümlerinin rasgeleliğe bağlı olduğunu ve hangi bölümlerin tamamen belirleyici olduğunu tam olarak görebilmemizdir. Tecrübelerime göre, böyle bir ayrılığı zorlamak, işleri çok basitleştirir.

Şimdiye kadar ürettiğimiz her rastgele değişkenden tek bir örnek istediğimizi varsaydık, ancak gelecekte daha fazla dağılım görmek istediğimizi ortaya çıkarırsak, bu önemsizdir. RunRandom'u farklı tohumlarla aynı rastgele değişkende birçok kez kullanabilirsiniz. Bu, elbette, zorunlu dillerde mümkündür, ancak bu durumda, rastgele bir değişkeni örneklediğimiz her seferde beklenmeyen G / Ç gerçekleştirmeyeceğimizden emin olabiliriz ve durumu başlatmak için dikkatli olmak zorunda değiliz.


6
Uygulayıcı işlevlerin iyi bir örneği için +1 / Monads'ın pratik kullanımı.
jozefg

9
Güzel cevap, ama bazı adımlarla biraz hızlı gider. Örneğin, neden bad :: Random a -> atutarsızlıklar ortaya çıksın? Bunun nesi kötü? Lütfen açıklamada yavaşça gidin, özellikle ilk adımlar için :) Eğer "kullanışlı" işlevlerin neden işe yaradığını açıklayabilirseniz, bu 1000 puanlık bir cevap olabilir! :)
Andres F.

@AndresF. Tamam, biraz revize edeceğim.
dan_waterworth

1
@AndresF. Cevabımı revize ettim, ancak bunun pratikte nasıl kullanılacağını yeterince açıkladığımı sanmıyorum, bu yüzden daha sonra tekrar gelebilirim.
dan_waterworth

3
Olağanüstü cevap. İşlevsel bir programcı değilim ama kavramların çoğunu anlıyorum ve Haskell ile "oynadım". Bu, hem sorgulayıcıyı bilgilendiren hem de başkalarına daha derine inmeleri ve konu hakkında daha fazla bilgi edinmeleri için ilham veren bir cevap türüdür. Oy kullanmamın 10'dan daha fazla üzerine birkaç puan verebilmeyi isterdim.
RLH,

10

Hatalı değilsin. Aynı çekirdeği bir RNG'ye iki kez verirseniz, döndürdüğü ilk sözde rasgele sayı aynı olacaktır. Bu, işlevsel ve yan etki programlama ile ilgisi yoktur; tanımı , bir tohum spesifik bir giriş iyi dağıtılmış ama kesinlikle rastgele olmayan, belirli bir değer çıkış neden olmasıdır. Bu yüzden sözde rastgele olarak adlandırılır ve genellikle aynı problem üzerinde farklı optimizasyon metotlarını güvenilir bir şekilde karşılaştırmak için, örneğin öngörülebilir birim testleri yazmak, olması iyi bir şeydir.

Gerçekten bir bilgisayardan sözde rasgele olmayan numaralar istiyorsanız, onu bir parçacık çürüme kaynağı, bilgisayarın içinde olduğu ağda meydana gelen tahmin edilemeyen olaylar, vb. Gibi gerçekten rastgele bir şeye bağlamanız gerekir. hatta eğer çalışırsa doğru ve genellikle pahalı olsun ama bunun tek yolu değil sözde rastgele değerler alma (eğer dayanmaktadır programlama dilinden almak genellikle değerleri bazı açıkça birini vermedi bile tohum.)

Bu ve sadece bu, bir sistemin işlevsel doğasını tehlikeye atar. Sözde rastgele olmayan jeneratörler nadir olduğundan, bu sık sık ortaya çıkmaz, ancak evet, gerçek rastgele sayılar üreten bir yönteminiz varsa, o zaman en azından programlama dilinizin küçük kısmı% 100 saf işlevsel olamaz. Bir dilin bunun için bir istisna yapıp yapmaması, sadece dilin uygulayıcısının ne kadar pragmatik olduğu sorusudur.


9
Gerçek bir RNG, saf olmasına (işlevsel değil) bakılmaksızın hiç bir bilgisayar programı olamaz. Von Neumann'ın rastgele rakamlar üretme konusundaki aritmetik yöntemleriyle ilgili söylediklerini hepimiz biliyoruz (bunu görmeyenler, yukarı bakanlar - tercihen her şey, sadece ilk cümle değil). Tabii ki de belirleyici olmayan bazı donanımlarla etkileşime girmeniz gerekiyor. Ama bu sadece birkaç kez saflıkta birkaç kez saflıkla bağdaştırılan G / Ç'dir. Herhangi bir şekilde kullanılabilir hiçbir dil G / Ç'ye tamamen izin vermez - aksi takdirde programın sonucunu bile göremezsiniz.

Aşağı oyla neyin nesi?
l0b0

6
Harici ve gerçekten rastgele bir kaynak neden sistemin işlevsel yapısını tehlikeye atar? Hala "aynı girdi -> aynı çıktı". Dış kaynağı sistemin bir parçası olarak düşünmezseniz, ancak o zaman “dış” olmazdı, değil mi?
Andres F.

4
Bunun TRNG ile PRNG ile ilgisi yok. Sabit olmayan bir fonksiyon türüne sahip olamazsınız () -> Integer. Tamamen işlevsel bir PRNG türüne sahip olabilirsiniz PRNG_State -> (PRNG_State, Integer), ancak bunu saf olmayan yollarla başlatmanız gerekir).
Gilles

4
@ Briç Anlaşıldı, ancak ifadeler ("onu gerçekten rastgele bir şeye bağla"), rastgele kaynağın sistemin dışsal olduğunu gösteriyor. Bu nedenle, sistemin kendisi tamamen işlevsel kalır; Bu giriş kaynak değil.
Andres F.

6

Bunun bir yolu, onu sonsuz rastgele sayı dizisi olarak düşünmektir:

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

Yani, sadece onu Stackarayabileceğiniz bir yer gibi, dipsiz bir veri yapısı olarak düşünün, Poponu sonsuza kadar çağırabilirsiniz. Normal bir değişmez yığın gibi, üstünden bir tane alarak size başka (farklı) bir yığın verir.

Bu yüzden değişmez (tembel bir değerlendirme ile) rasgele sayı üreteci şöyle görünebilir:

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

Bu işlevsel.


Sonsuz bir rasgele sayılar listesi oluşturmanın, bunun gibi bir işlevle çalışmaktan daha kolay olduğunu görmüyorum pseudoRandom :: Seed -> (Seed, Integer). Hatta bu tür bir fonksiyon yazmanız bile mümkün olabilir[Integer] -> ([Integer], Integer)
dan_waterworth

2
@ dan_waterworth aslında çok mantıklı. Bir tamsayı rastgele olduğu söylenemez. Bir numara listesinin bu özelliği olabilir. Yani gerçek şu ki, rastgele bir üretici int -> [int] türüne sahip olabilir, yani bir tohum alan ve rastgele bir tamsayı listesi döndüren bir işleve sahip olabilir. Tabii, haskell'in not almasını sağlamak için buralarda bir devlet monadınız olabilir. Ancak bu sorunun genel bir cevabı olarak, bunun gerçekten yararlı olduğunu düşünüyorum.
Simon Bergot

5

İşlevsel olmayan diller için aynıdır. Burada gerçekten rastgele sayılardan biraz ayrı sorunu görmezden gelmek.

Bir rastgele sayı üreticisi her zaman bir tohum değeri alır ve aynı tohum için aynı rasgele sayı dizisini döndürür (rastgele sayılar kullanan bir programı test etmeniz gerekirse çok yararlıdır). Temel olarak seçtiğiniz tohumla başlar ve daha sonra bir sonraki yineleme için son sonucu tohum olarak kullanır. Bu nedenle çoğu uygulama, tanımladığınız şekilde "saf" işlevlerdir: Bir değer alın ve aynı değer için her zaman aynı sonucu döndürün.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.