Як функціональні мови обробляють випадкові числа?


68

Що я маю на увазі з цього приводу, що майже в кожному підручнику, який я читав про функціональні мови, є те, що одна з найважливіших речей щодо функцій - це те, що якщо ви двічі викличете функцію з тими ж параметрами, ви завжди будете мати такий же результат.

Як на цьому місці ви робите функцію, яка приймає насіння як параметр, а потім повертає випадкове число на основі цього насіння?

Я маю на увазі, що це, здається, суперечить одній із речей, які так добре стосуються функцій, правда? Або мені зовсім чогось тут не вистачає?

Відповіді:


89

Ви не можете створити чисту функцію, яка називається, randomяка даватиме інший результат кожного разу, коли вона буде викликана. Насправді, ви навіть не можете "викликати" чисті функції. Ви їх застосовуєте. Отже, ви нічого не пропускаєте, але це не означає, що випадкові числа є поза межами функціонального програмування. Дозвольте мені демонструвати, я буду використовувати синтаксис Haskell протягом усього часу.

Виходячи з обов'язкового фону, ви можете спочатку очікувати випадкового типу такого типу:

random :: () -> Integer

Але це вже виключено, тому що випадковий не може бути чистою функцією.

Розглянемо ідею цінності. Цінність - річ непорушна. Це ніколи не змінюється, і кожне спостереження, яке ви можете зробити про нього, є послідовним на весь час.

Зрозуміло, що випадковий вибір не може створити ціле число. Натомість вона створює випадкову змінну Integer. Цей тип може виглядати приблизно так:

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

Але він має неправильний тип. Замість того, щоб закінчити з a Random (a, b), ми закінчимо з aRandom (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)

Насправді, можна записати randomBind в термінах randomJoin та randomMap. Також можна записати randomJoin в терміні randomBind. Але я залишу це робити як вправу.

Ми могли б трохи спростити це. Дозвольте мені визначити цю функцію:

randomUnit :: a -> Random a

randomUnit перетворює значення у випадкову змінну. Це означає, що ми можемо мати випадкові величини, які насправді не є випадковими. Це завжди було так; ми могли це зробити randomMap (const 4) randomраніше. Причина визначення randomUnit є хорошою ідеєю в тому, що тепер ми можемо визначити randomMap в термінах randomUnit і randomBind:

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 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, fmap замість randomMap, повернути замість randomUnit. Також ми отримуємо ціле навантаження функцій безкоштовно.

Чи варто того? Ви можете стверджувати, що потрапити на цей етап, коли робота з випадковими числами не є зовсім жахливим, було досить складно і довго. Що ми отримали в обмін на ці зусилля?

Найбільш безпосередня винагорода полягає в тому, що ми зараз можемо точно бачити, які частини нашої програми залежать від випадковості, а які - цілком детерміновані. На моєму досвіді, примушування до такого жорсткого розлучення дуже спрощує речі.

Ми до цього часу вважали, що просто хочемо поодиноку вибірку з кожної генерованої випадкової величини, але якщо виявиться, що в майбутньому ми хотіли б побачити більше розподілу, це тривіально. Ви можете просто використовувати runRandom багато разів на одній випадковій змінній з різними насінням. Це, звичайно, можливо в імперативних мовах, але в цьому випадку ми можемо бути впевнені, що ми не збираємось виконувати непередбачуваний IO кожен раз, коли вибираємо випадкову змінну і нам не потрібно бути обережними щодо ініціалізації стану.


6
+1 - хороший приклад практичного використання функціональних функторів / монадів.
jozefg

9
Приємна відповідь, але з деякими кроками йде трохи занадто швидко. Наприклад, навіщо bad :: Random a -> aвводити невідповідності? Що в цьому поганого? Будь ласка, поволі йдіть у поясненні, особливо до перших кроків :) Якщо ви могли б пояснити, чому корисні функції корисні, це може бути відповідь на 1000 балів! :)
Андрес Ф.

@AndresF. Гаразд, я трохи перегляну це.
dan_waterworth

1
@AndresF. Я переглянув свою відповідь, але не думаю, що достатньо пояснив, як ви можете використовувати це є практикою, тому я можу повернутися до неї пізніше.
dan_waterworth

3
Чудова відповідь. Я не функціональний програміст, але я розумію більшість концепцій, і я "грав" з Haskell. Це тип відповіді, який і інформує запитувача, і надихає інших копати глибше та дізнаватися більше про тему. Я хотів би, щоб я міг дати вам кілька додаткових балів вище за 10 від мого голосування.
RLH

10

Ви не помиляєтесь. Якщо ви дасте одне і те ж насіння в RNG двічі, то перше псевдовипадкове число, яке воно поверне, буде таким самим. Це не має нічого спільного з функціональним та побічним програмуванням; визначення насіння є те , що конкретний вхід викликає певний висновок добре розподілених , але безумовно не-випадкових значень. Ось чому його називають псевдовипадковим, і це часто добре мати, наприклад, писати передбачувані тестові одиниці, надійно порівнювати різні методи оптимізації з однієї проблеми тощо.

Якщо ви насправді хочете непсихо-випадкових чисел з комп'ютера, вам доведеться підключити його до чогось справді випадкового, наприклад, джерела занепаду частинок, непередбачуваних подій, що відбуваються в мережі, на якій працює комп'ютер, тощо. Це важко отримати правильно і зазвичай дорого, навіть якщо це працює, але це єдиний спосіб не отримати псевдовипадкових значень (зазвичай значення, які ви отримуєте від вашої мови програмування, базуються на деякому насінні, навіть якщо ви прямо не вказали його).

Це, і тільки це, може поставити під загрозу функціональний характер системи. Оскільки непсевдовипадкові генератори рідкісні, це не часто зустрічається, але так, якщо у вас дійсно є метод, що генерує справжні випадкові числа, то принаймні цей трохи вашого мови програмування не може бути на 100% чистим функціоналом. Будь мова зробить виняток для неї чи ні - це лише питання наскільки прагматичним є мовний реалізатор.


9
Справжня RNG взагалі не може бути комп'ютерною програмою, незалежно від того, чи вона чиста (чи функціональна) чи ні. Всі ми знаємо цитату фон Неймана про арифметичні методи отримання випадкових цифр (ті, хто цього не робить, шукають це - бажано, вся справа, а не лише перше речення). Вам потрібно буде взаємодіяти з деяким недетермінованим обладнанням, що, звичайно, теж нечисто. Але це лише введення-виведення, яке декілька разів узгоджувалося з чистотою в дуже різних підходах. Жодна мова, яка жодним чином корисна, не дозволяє повністю ввімкнути введення-виведення - ви навіть не могли бачити результат програми в іншому випадку.

Що з голосом "за"?
l0b0

6
Чому зовнішнє та справді випадкове джерело загрожує функціональній природі системи? Це все ще "той самий вхід -> той же вихід". Якщо ви не розглядаєте зовнішнє джерело як частину системи, але тоді воно не було б "зовнішнім", чи не так?
Андрес Ф.

4
Це не має нічого спільного з PRNG проти TRNG. Ви не можете мати непостійну функцію типу () -> Integer. Ви можете мати чисто функціональний PRNG типу PRNG_State -> (PRNG_State, Integer), але вам доведеться ініціалізувати його нечистими способами).
Жиль

4
@Brian Погодився, але формулювання ("підключіть його до чогось справді випадкового") говорить про те, що випадкове джерело є зовнішнім для системи. Тому сама система залишається чисто функціональною; це джерело введення, яке не є.
Андрес Ф.

6

Один із способів - мислити це як нескінченну послідовність випадкових чисел:

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)
дописати

2
@dan_waterworth насправді це має багато сенсу. Не можна сказати, що ціле число є випадковим. Список номерів може мати цю властивість. Тож правда полягає в тому, що випадковий генератор може мати тип int -> [int], тобто функцію, яка бере насіння і повертає випадковий список цілих чисел. Звичайно, ви можете мати державну монаду навколо цього, щоб отримати позначення haskell. Але як загальна відповідь на питання, я думаю, що це справді корисно.
Саймон Бергот

5

Це те саме для нефункціональних мов. Ігноруючи тут трохи окрему проблему справді випадкових чисел.

Генератор випадкових чисел завжди приймає значення насіння і для того ж насіння повертає ту саму послідовність випадкових чисел (дуже корисно, якщо вам потрібно перевірити програму, яка використовує випадкові числа). В основному він починається з вибраного насіння, а потім використовує останній результат як насіння для наступної ітерації. Тож більшість реалізацій - це "чисті" функції, як ви їх описуєте: приймайте значення і для одного і того ж значення завжди повертайте той же результат.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.