Чи

Числа у Julia

7 вправ

Про концепцію Числа

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

Цілі числа

Ціле число - це «кругле» число без десяткової крапки.

У концепції Основи ми бачили, що ціле значення можна присвоїти змінній, не вказуючи тип.

Для читабельності можна використовувати знаки підкреслення як роздільники цифр. Компілятор їх ігнорує.

julia> x = 3
3

julia> typeof(x)
Int64

julia> large_number = 1_234_567_890
1234567890

Усередині компілятор використовуватиме той тип цілого числа зі знаком, який найкраще підходить для конкретного процесора. На сучасних ПК це зазвичай Int64, цілком достатній для більшості завдань.

Про типи докладніше йтиметься в одній із наступних концепцій. А поки що сподіваймося, що числові типи достатньо інтуїтивні, щоб цей документ був зрозумілим.

Як ми побачимо в одній із наступних концепцій, Julia нативно підтримує дуже великі багатовимірні масиви. Вони можуть випробовувати як обсяг доступної памʼяті, так і пропускну здатність для передавання даних.

Щоб дати програмісту тонший контроль, можна й досить часто варто вказати бажаний тип цілого числа зі знаком або без нього. Багатьом, хто вивчає Julia і раніше працював із Python, буде корисно порівняти це з NumPy, а не з базовим Python.

julia> y = Int8(42)
42

julia> typeof(y)
Int8

julia> z = UInt32(1024)
0x00000400

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

Цілі числа також можна вводити у двійковому, вісімковому або шістнадцятковому вигляді, з префіксами 0b, 0o та 0x відповідно.

julia> a = 0x10
0x10

julia> Int(a)
16

julia> b = 0b1010 # displays as hexadecimal
0x0a

Переповнення цілих чисел і BigInt

Кожен тип цілого числа має максимальне й мінімальне значення, яке може зберігати:

julia> typemax(Int8)
127

julia> typemin(Int8)
-128

Вихід за межі цього допустимого діапазону спричинить переповнення цілого числа, а результати здаватимуться дуже дивними.

julia> c = Int8(126)
126

julia> c * c
4

Мабуть, ми погодимося, що 126 * 126 не повинно дорівнювати 4!

Докладне пояснення вимагає знання того, як регістри процесора зберігають цілі числа, а це виходить за межі цієї концепції.

Проблему можна зменшити, використовуючи «ширший» тип, аж до Int128 або UInt128 (що означає «ширший», розглянемо нижче). Однак усі вони мають жорсткі межі, перш ніж почнуться проблеми з переповненням.

Одне з рішень - скористатися типом BigInt, обмеженим лише памʼяттю компʼютера. Це реалізація арифметики довільної точності у Julia.

julia> 2 ^ 70
0

julia> big = BigInt(2) ^ 70
1180591620717411303424

julia> UInt128(big)
0x00000000000000400000000000000000

Оскільки 2 у степені 70 не вміщується в Int64, перше обчислення в наведеному прикладі зазнає невдачі, хоч зауважмо: повідомлення про помилку відсутнє. Будьмо пильними!

Приведення 2 до BigInt дає правильну відповідь, а показ у шістнадцятковому форматі ясніше дає зрозуміти, що це степінь двійки.

Числа з плаваючою комою

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

julia> f = 3.45
3.45

julia> typeof(f)
Float64

Звісно, науковий запис підтримується.

julia> avogadro = 6.02e23
6.02e23

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

Як скорочення, значення Float32 можна створити за допомогою суфікса f0.

julia> f32 = 4.56f0
4.56f0

julia> typeof(f32)
Float32

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

Максимальне й мінімальне значення можуть здивувати:

julia> typemax(Float64)
Inf

julia> typemin(Float64)
-Inf

Нескінченність - це допустиме значення!

Однак корисний діапазон чисел із плаваючою комою обмежений: усе дуже велике просто отримує значення Inf, а все дуже мале округлюється до 0.0.

Щоб побачити ці межі, можна скористатися іншою парою функцій: приблизно ± 10 ^ 308.

julia> floatmax(Float64)
1.7976931348623157e308

julia> floatmin(Float64)
2.2250738585072014e-308

Крім того, точність обмежена: подається велика, але скінченна кількість значущих цифр (приблизно 15 для Float64).

Арифметичні оператори

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

2 + 3  # 5 (addition)
2 - 3  # -1 (subtraction)
2 * 3  # 6 (multiplication)
8 / 2  # 4.0 (division)
8 % 3  # 2 (remainder)
2 ^ 3  # 8 (exponentiation)

Однак кілька деталей, специфічних для Julia, варті обговорення.

Множення

julia> x = 4.2
4.2

julia> 2 * x
8.4

julia> 2x
8.4

julia> 2.4x
10.08

Це може здивувати.

Завжди можна використати * як інфіксний оператор, як і в більшості інших мов програмування.

Однак Julia створювали люди, які вірять, що код має бути якнайбільше схожим на математичні рівняння.

Оскільки назви змінних мають починатися з літери, число (ціле або з плаваючою комою) перед назвою вважається неявним множенням.

Наприклад, якщо нам потрібна площа поверхні сфери, замість 4 * pi * r * r можна зробити так:

julia> surface(r) = 4π * r^2
surface (generic function with 1 method)

julia> surface(3)
113.09733552923255

Хоча π - вбудована константа, це також (грецька) літера. Тому парсеру все одно потрібен один явний *, щоб відокремити π від r.

Ділення

Використання / як інфіксного оператора завжди дає результат із плаваючою комою, навіть для цілих вхідних даних.

Для ділення цілих чисел є більше варіантів:

julia> 10 / 3
3.3333333333333335

julia> div(10, 3)
3

julia> 10 ÷ 3
3

julia> 10 // 3
10//3

Функція div() призначена для ділення цілих чисел; результат відкидається в бік нуля: вниз для додатних чисел, вгору для відʼємних.

Як синонім можна використати інфіксний оператор ÷, знову ж таки прагнучи зробити запис математичнішим. Якщо ми користуємося редактором, який знає Julia, введімо \div і натиснімо клавішу <Tab>.

Оператор // потребуватиме окремої концепції пізніше в силабусі.

Поки що скажімо лише, що результат // - це «раціональне» число, формальна назва того, що більшість людей називає дробом.

Спільні множники скорочуються в чисельнику й знаменнику, щоб отримати відношення двох цілих чисел у тому, що називають «найпростішим виглядом».

julia> rationalnum = 22 // 6
11//3

julia> typeof(rationalnum)
Rational{Int64}

Маємо раціональні числа. А що щодо «ірраціональних» чисел?

julia> π
π = 3.1415926535897...

julia> typeof(π)
Irrational{:π}

Ірраціональне число - те, яке не можна звести до відношення цілих чисел. Поширені приклади: π, e (число Ейлера) та багато коренів, як-от √2 (квадратний корінь із 2).

Julia намагається робити математику як слід.

Перетворення числових типів

Часто це відбувається автоматично:

julia> x = 2 + 3.5
5.5

julia> typeof(x)
Float64

Ми додали Int64 до Float64 й отримали результат Float64.

Насправді ціле число було непомітно перетворене на Float64 ще до додавання.

У Julia є поняття «ширини» числових типів.

  • Для цілих чисел і для чисел із плаваючою комою це просто кількість бітів, потрібних для зберігання. Отже, Int64 ширший за Int16.
  • Числа з плаваючою комою вважаються ширшими за цілі, бо можуть зберігати будь-яку дробову частину.

Якщо у виразі використовується суміш типів, кожен із них за потреби «підвищується» до найширшого використаного типу.

Щоб змусити перетворення, ми можемо привести ціле число до конкретного типу, як у Float64(5).

Або ж просто скористаймося float(5) і дозвольмо компілятору вибрати відповідний тип.

Перетворення з чисел із плаваючою комою на цілі неминуче складніші. Що робити з тим, що стоїть після десяткової крапки?

  • Функція round() перетворює до найближчого цілого; у разі рівновіддаленості, як-от 4.5, округлення йде до найближчого парного цілого.
  • floor() округлює вниз, ceil() округлює вгору, trunc() округлює в бік нуля.
  • Спроба прямого приведення, наприклад за допомогою Int32(), завершиться невдачею з InexactError.

Однак типово ці функції повертають не той тип цілого числа, який міг бути потрібен. Бажаний тип результату можна вказати.

julia> round(4.5)
4.0

julia> round(Int64, 4.5)
4

julia> round(Int, 4.5)  # => default integer type
4

julia> ceil(Int16, 4.3)
5

Округлення до заданої кількості цифр після десяткової крапки теж можливе за допомогою ключового слова digits.

julia> round(π, digits=10)
3.1415926536

Докладніше див. у документації.

Ділення на нуль

Невже це просто спричиняє помилку? Насправді все не так просто.

Ділення цілих чисел за допомогою ÷ або // спричинить помилку, як і слід було очікувати.

Ділення чисел із плаваючою комою за допомогою / дотримується радше інженерного підходу, ніж стандартного підходу інформатики:

julia> 2 / 0
Inf

julia> 0 / 0
NaN

Як уже йшлося в попередньому розділі, нескінченність - це допустиме число з плаваючою комою в Julia, позначуване як Inf.

Коли чисельник теж дорівнює нулю, результат математично не визначений. Тоді Julia трактує його як «не число», позначуване як NaN.

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

Нескінченна ручна перевірка значень була б стомливою для програмування і неодмінно зашкодила б продуктивності під час виконання.

Зупинка з повідомленням про помилку за кожного найменшого збою зробила б нашу програму вкрай непопулярною серед користувачів!

Порівняння значень із плаваючою комою

Як описано в концепції Conditionals, перевірку на рівність зазвичай виконують оператором ==.

Це добре працює для цілих чисел, символів, рядків тексту (англ. string) тощо. Однак значення з плаваючою комою мають обмежену точність, і різні способи обчислення того самого результату можуть давати трохи різні значення. Для Float64 це зазвичай десь на 15-й значущій цифрі: невелика різниця, але не «рівність».

Традиційно програмістам радять ніколи не використовувати == зі значеннями з плаваючою комою: результати непередбачувані.

Широко вживана альтернатива - перевіряти абсолютне значення різниці проти деякого допустимого відхилення (часто його називають епсилон, або ϵ). Тож замість a == b використовуймо abs(a - b) < epsilon.

Julia пропонує чистішу альтернативу, функцію isapprox().

Для абсолютного відхилення синтаксис такий: isapprox(a, b, atol=epsilon). У цьому випадку ключове слово atol обовʼязкове.

Відносне відхилення часто корисніше. Воно типове, тож isapprox(a, b) спробує вибрати якесь розумне значення для rtol (правила досить складні).

Відносне відхилення теж можна вказати як частку від значень, що порівнюються. Тож isapprox(a, b, rtol=0.01 перевіряє, що значення відрізняються не більш ніж на 1%.

У звичному для Julia стилі є математичний оператор, синонім типового випадку: a ≈ b (оператор вводять як \approx, а потім <tab>).

Повʼязані майбутні концепції

Крім раціональних чисел, у наступних частинах силабусу йтиметься про:

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці

Вивчити концепцію Числа

Практика заблокована

Розблокуйте ще 2 вправи, щоб практикувати концепцію Числа