Багатьом програмам потрібні (приблизно) випадкові значення, щоб імітувати події реального світу.
До звичних і знайомих прикладів належать:
('H', 'T').Генерування справді випадкових значень на компʼютері - напрочуд складна технічна задача, тож ці результати можуть називати «псевдовипадковими».
Важливо: ця концепція не охоплює криптографічно стійкі випадкові числа, які становлять значно складнішу задачу.
Утім, добре спроєктовані бібліотеки, як-от модуль Random зі стандартної бібліотеки Julia, швидкі, гнучкі й дають результати, цілком достатні для більшості застосунків у моделюванні, симуляції та іграх.
Julia розділяє функціональність випадкових чисел між кількома місцями:
Base, які доступні завжди.Random.Random входить до складу стандартної бібліотеки й, імовірно, уже встановлений, але нам потрібно додати using Random на початку програми, щоб його вміст потрапив у простір імен.
rand()
Що робить ця функція, залежить від аргументів, які ми їй передаємо. Варіантів дуже багато.
Без аргументів вона генерує число з плаваючою комою між 0 (включно) і 1.
Це uniform-розподіл, де всі значення однаково ймовірні, як ми обговоримо в розділі «Робота з розподілами» нижче.
Один аргумент-ціле число генерує вектор такої довжини.
julia> rand()
0.10261774967264703
julia> rand(5)
5-element Vector{Float64}:
0.24134501977563894
0.5664193284851202
0.9804412082089355
0.6229551330613335
0.47589221741904664
Для іншого діапазону достатньо лише відповідно зсунути й масштабувати результат.
У наведеному нижче прикладі для віднімання використано трансляцію, про яку йдеться в концепції «Операції з векторами».
Оператор .- просто застосовує цю арифметику до кожного елемента вектора.
# numbers between -1.0 and +1.0
julia> (rand(5) .- 0.5) * 2
5-element Vector{Float64}:
-0.5303906759076336
0.9635682226775855
-0.048823697086981754
0.465842804648374
0.9880834344780736
Якщо єдиний аргумент - це тип, rand використає typemin і typemax як межі.
Імовірно, це не те, що нам потрібно!
Для випадкових цілих чисел ми можемо вказати діапазон і, за бажанням, скільки значень згенерувати.
julia> rand(Int64)
-9159538335234594326 # not very useful
julia> rand(1:10, 5)
5-element Vector{Int64}:
1
1
1
4
7
У прикладі rand(1:10, 5) вище звернімо увагу, що там (випадково) є повторювані значення, бо кожен вибір незалежний.
Це «вибірка з поверненням», про яку докладніше йдеться нижче.
Для чисел з плаваючою комою в діапазоні зазвичай потрібно вказати крок. Інакше крок типово дорівнюватиме 1.0, що рідко буває корисним.
julia> rand(2.4:0.01:3.2, 4)
4-element Vector{Float64}:
3.19
2.53
3.14
3.13
Або ж передаймо масив чи кортеж, і rand поверне випадковий елемент:
julia> rand([4, 9, 16, 25])
16
# coin flip
julia> rand(['H', 'T'])
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)
# mixed types in tuple
julia> rand( (1, 3.2, "name"), 2 )
2-element Vector{Any}:
1
"name"
Уявімо, що в нас є торбина з 3 червоними та 4 зеленими кульками, і ми випадково витягуємо з неї одну кульку. Щоб дістати другу кульку, є два варіанти:
Сценарій 1 - це вибірка з поверненням, сценарій 2 - без повернення, і вони дають різні результати.
Щоб змоделювати вибірку без повернення в Julia, є кілька варіантів.
Найпростіший (і в межах Exercism єдиний) - скористатися Random.shuffle(), щоб розташувати елементи у випадковому порядку, а потім узяти перші n елементів.
Це годиться для невеликих задач, але може погано масштабуватися на великі колекції: shuffle має згенерувати весь масив, навіть якщо нам потрібна лише його мала частка.
Щоб робити вибірку з поверненням «як слід», установімо пакет StatsBase.jl.
Він надає функцію sample() з повним набором можливостей.
Цілком можна сподіватися, що подібну функціональність додадуть до Random у майбутньому випуску й зроблять її частиною стандартної бібліотеки (приклади коду в цьому документі перевірено на Julia 1.11).
Досі ми розглядали випадки, де всі результати рівноймовірні.
Наприклад, rand(1:100) з однаковою ймовірністю дає будь-яке ціле число від 1 до 100.
Багато реальних ситуацій значно складніші за це.
Тому статистики створили безліч distributions, щоб математично описати результати з «реального світу».
Функцію rand(), описану вище, використовують, коли всі ймовірності рівні.
Такий розподіл називають uniform.
Його також називають «нормальним» розподілом або «дзвоноподібною» кривою; це дуже поширений спосіб описати неточність виміряних значень.
Наприклад, уявімо, що завод, де ми працюємо, щойно купив 10 000 болтів, які мали б бути однаковими.
Нам потрібно налаштувати заводського робота, щоб він із ними працював, тож ми зважуємо вибірку зі 100 болтів і бачимо, що їхня середня (або mean) вага становить 4,731 г.
Це аж ніяк не означає, що всі вони важать рівно 4,731 г.
Можливо, ми виявимо, що значення лежать у межах від 4,627 до 4,794 г, але гуртуються навколо 4,731 г.
Це Gaussian distribution, у якому ймовірності сягають піку на середньому значенні й симетрично спадають з обох боків (звідси й «дзвоноподібна» форма).
Щоб змоделювати його програмно, нам потрібен спосіб задати ширину кривої (зазвичай дорогі болти гуртуються щільніше навколо середнього, ніж дешеві!).
За домовленістю для цього використовують standard deviation: малі значення для гострої, вузької кривої, великі - для низької, широкої.
Математики люблять грецькі літери, тож середнє значення позначають μ («мю»), а стандартне відхилення - σ («сигма»).
Тож якщо ми читаємо, що «95% значень лежать у межах 2σ від μ» або що «бозон Гігса виявлено з достовірністю 5 сигм», ці зауваження стосуються стандартного відхилення.
Докладніше про це йтиметься в концепції Statistics.
randn()
Скорочення від «random normal»; ця функція подібна до варіанта rand() для чисел з плаваючою комою, але її значення розподілені за Гаусом із середнім 0 і стандартним відхиленням 1.
Знову ж таки, нам може знадобитися масштабувати необроблені вихідні дані randn під потрібне стандартне відхилення й зсунути їх під потрібне середнє.
Приклад нижче приводить їх до середнього 30 і стандартного відхилення 5.
julia> raw = randn(5)
5-element Vector{Float64}:
3.0762588867281475
1.5101100620253902
-0.5914858221637778
0.684175554069735
-0.8416433926114673
julia> raw * 5 .+ 30
5-element Vector{Float64}:
45.38129443364074
37.55055031012695
27.04257088918111
33.420877770348675
25.791783036942665
Дивлячись на вихідні дані, важко помітити, що необроблені значення гуртуються ближче до нуля, ніж за рівномірного розподілу. Якщо сумніваємося, згенеруймо 1000 або більше значень і побудуймо їхній графік, щоб це стало очевидніше.
Random
Цей модуль містить наступний рівень функціональності, який не включили до Base, щоб зменшити розмір типової конфігурації Julia.
Random доповнює rand і randn з Base версіями з мутацією: rand! і randn!.
Корисне доповнення - це randstring, яка генерує рядок тексту (англ. string) заданої довжини.
Типово вона використовує великі та малі літери, а також цифри від 0 до 9, але можна вказати й інші набори символів.
julia> using Random
julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"
Крім того, є функція bitrand, яка генерує випадковий BitArray заданої довжини.
julia> bitrand(5)
julia> bitrand(5)
5-element BitVector:
1
1
0
0
1
Щоб випадково перемішати елементи Vector, є shuffle, а також shuffle!, яка змінює вхідний вектор на місці.
julia> v = ['A', '1', '2', 'J', 'Q', 'K'];
julia> shuffle(v)
6-element Vector{Char}:
'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
'1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
'2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)
# shuffles are random:
julia> shuffle(v)
6-element Vector{Char}:
'2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)
'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
'1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
Іноді замість цього корисно мати перемішані індекси.
Для цього використовуймо randperm(n), де n - довжина послідовності.
julia> randperm(6)
6-element Vector{Int64}:
6
2
4
1
3
5
По суті, приклад вище дає ті самі результати, що й shuffle(1:6).
Мері дуже любить телесеріал Star Trek: The Next Generation. Вона часто грає в настільні рольові ігри, де вони з друзями уявляють себе екіпажем Starship Enterprise. Серед її персонажів є капітан Пікар, а отже, вона має вести журнал капітана. Вона любить творчу частину гри, але не любить генерувати випадкові дані нашвидкуруч.
Допоможімо Мері, створивши генератори випадкових даних для того, що часто трапляється в журналі капітана.
У своїх подорожах Starship Enterprise зустрічає багато планет. Планети у всесвіті Star Trek поділяються на категорії за своїми властивостями. Наприклад, Земля належить до планет класу M. Усі можливі класи планет: D, H, J, K, L, M, N, R, T і Y.
Реалізуйте функцію random_planet. Вона має повертати один із класів планет випадковим чином.
julia> random_planet_class()
'K'
У космосі літає не лише Enterprise (реєстраційний номер NCC-1701)! Коли він зустрічається з іншим зорельотом, Мері потрібно занотувати реєстраційний номер цього зорельота.
Реєстраційні номери починаються з префікса «NCC-», після якого йде число від 1000 до 9999 (включно).
Реалізуйте функцію random_ship_registry_number, яка повертає випадковий реєстраційний номер зорельота.
julia> random_ship_registry_number()
"NCC-1947"
Яка користь із журналу, якщо в ньому немає дат?
Зоряна дата - це число з плаваючою комою. Пригоди Starship Enterprise з першого сезону The Next Generation відбуваються між зоряними датами 41000.0 і 42000.0. «4» означає 24 століття, а «1» - перший сезон.
Реалізуйте функцію random_stardate, яка повертає число з плаваючою комою між 41000.0 (включно) і 42000.0.
julia> random_stardate()
41458.15721310934
Були скарги, що в зоряних датах забагато десяткових знаків, тож Мері вирішує округлювати записи до одного десяткового знака.
Хоча це можна було б зробити, округливши значення з random_stardate(), ми розуміємо, що є значно простіший підхід.
Реалізуйте функцію random_stardate_v2, яка повертає число з плаваючою комою між 41000.0 і 42000.0 (включно), з одним десятковим знаком.
julia> random_stardate_v2()
41732.6
Одна з версій гри передбачає взаємодію з іншими зорельотами, і Мері потрібен якийсь вибір.
Існує повний масив можливих зорельотів, але в кожній грі потрібно лише кілька з них, і вибирати їх слід випадковим чином.
Реалізуйте функцію pick_starships(starships, number_needed), яка приймає вектор унікальних реєстраційних номерів зорельотів і повертає вектор довжиною number_needed. Значення результату мають братися з starships, без повторів.
julia> pick_starships(["NCC-5011", "NCC-1228", "NCC-7039", "NCC-3978", "NCC-1476"], 2)
2-element Vector{String}:
"NCC-1228"
"NCC-5011"
Ми обмежили аргумент starships так, щоб він був вектором.
Як реалізувати pick_starships(), щоб вона працювала з іншими ітерованими обʼєктами, як-от кортежі чи множини?
Для цієї частини тестів немає.
Зареєструйтеся на Exercism, щоб вивчати й опановувати Julia, а також 35 концепцій128 вправ та справжнє наставництво від людей, і все це безкоштовно.