Ви не можете створити чисту функцію, яка називається, 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 кожен раз, коли вибираємо випадкову змінну і нам не потрібно бути обережними щодо ініціалізації стану.