День 1 Летней школы Julia 2025
Целочисленные типы
Познакомившись с тем, что такое переменные, перейдём к последовательному изучению того, на что же эти переменные могут ссылаться. А начнём мы с целочисленных переменных.
typeof(123)
В 64-битной системе Int будет означать то, что число занимает 64 бита или же 8 байт.
Т.е. последовательность 64 двоичных значений: 0 или 1.
Для примитивных типов можно вывести их представление в памяти в удобном виде при помощи функции bitstring.
А что такое примитивный тип, мы с вами узнаем на следующих занятиях
bitstring(12)
Как видим, имеется последовательность 64 битов. Единицы стоят на 4 и 3 позиции, что соответствует 8 и 4 соответственно. 2^3 и 2^2 соответственно.
bitstring(Int8(12))
Самый левый бит соответствует знаку числа. Т.е. максимальное число, которое мы можем получить будет состоять из 7 единиц. Подробнее о том, как устроено целочисленное число, можно прочитав, про "дополнительный код".
Мы же обратим внимание на то, что если к 8-битному знаковому целочисленному числу добавить единицу того же типа, то мы получим не 128, а -128.
x::Int8 = 127
x + Int8(1)
Но если мы сложим 8-битное число с 64-битным, то произойдёт продвижение типов, где конечный результат расширяется до наибольшего из типов, по особым правилам, которые в Julia пользователь может также задавать сам.
x + 1
Чтобы узнать, какое максимальное значение может хранить тип - воспользуемся функцией typemax.
typemax(Int8)
И перед тем, как дальше работать с числами,обратите внимание, на синтаксический сахар, который позволяет использовать постфиксную запись вызова функции. То есть не "напечатай 1 + 2, а 1 + 2 напечатай". Для этого вместо print(1+2) используем 1+2 |> print
В Julia встроен и тип Int128, который позволяет хранить числа крайне больших значений.
100_200_300_400_500_600_700_800_900_100_200_300_400 |> typeof
Применив log10 к максимальному значению, которое может хранить Int128, можем понять, что этот тип применим для чисел до 10^38.
Т.е. мы можем хранить значения целых чисел до 10^38.
log10(typemax(UInt128))
Int128(10^38)
Но и такие числа тоже подвержены проблемы переполнения.
Int128(10)^39
Для работы с большими целыми значениями можно воспользоваться типом BigInt.
BigInt(10)^40
Он не подвержен проблеме переполнения. Но его недостатком является то, что это уже не встроенный во многие процессоры тип данных, в отличие от Int64 или Int128.
Рациональные числа
Теперь перейдём к другому числовому типу.
Рассмотрим сначала деление числа 1 на число 2.
В результате мы получим значение 0.5, представляющее собой число с плавающей запятой.
1 / 2
двойная косая черта означает создание рационального числа.
2 // 3
Обратим внимание, что рациональное 1//2 равна 0.5 с типом плавающая запятая, который мы будем называть Float
1//2 == 0.5
Но рациональное 2//3 не равно Float(2/3).
2 // 3 == 2 / 3
Однако числа, кратные двойке, равны между собой. Это связано с представлением Float в памяти компьютера и того, что основанием в них является двойка.
Если кратко, то вещественные числа представляется как некоторое число A, называемое мантиссой, умноженное на 2 в степени В, где В - это порядок
333 / 128 == 333 // 128
rational_i8 = Int8(7) // Int8(9)
В отличие от целочисленных и float типов, рациональные числа представляют собой структуру, у которой есть два поля. Числитель - numerator и знаменатель - denumenator.
Обращение к этим свойствам (полям структуры) происходит с использованием точки после названия объекта.
rational_i8.num
rational_i8.den
Функция sizeof покажет сколько байт занимает тип. Так, для 8битного Int8 это значение равно 1 байт
sizeof(Int8)
А поскольку мы определили нашу переменную как отношение двух чисел типа Int8, то и наше число займёт два байта.
sizeof(rational_i8)
Числа с плавающей запятой
Числа с плавающей запятой близки по смыслу к вещественным. Обычно они занимают 8 байт, но могут быть и других размеров
typeof(2/3)
sizeof(2/3) # 8 * 8 = 64
Но нужно понимать, что несмотря на то, что для некоторых прикладных задач они могут казаться близкими к вещественными. Не все числа получится представить в виде Float. Ведь у нас имеется ограниченное количество бит.
123_456_789.1 + 0.75
Так, например, для 16-битного Float-числа 2048 можем получить неожиданный результат, если прибавим к нему единицу того же типа. После добавления 1 наше число так и не поменялось и осталось равным 2048. Это связано с тем, что у Float-чисел есть своя точность, с которой они могут представлять числа.
Для того чтобы её узнать - воспользуемся функцией eps(Float16(2048)). Получив результат - 2, можно сделать вывод, что мы не сможем выразить число 2049. Но сможем получить 2050
Float16(2048.)
print(Float16(2048. + 1))
eps(Float16(2048.))
На самом деле Float16 есть смысл применять только при работе с GPU или процессорами, которые прямо их поддерживают.
NaN, Inf, +0 -0
Рассмотрим некоторые особые Float числа.
Если мы получаем слишком большое число, то для таких чисел имеется специальное обозначение и представление в памяти, называемое Inf (бесконечность).
π^3000
Бесконечность может быть как положительной, так и отрицательной.
-ℯ^3000
И также как и в математике, в некоторых случаях возникает неопределённость. Например, когда мы вычитаем из бесконечность бесконечность. В таких ситуациях результатом является число NaN - not a number.
n = π^3000 - ℯ^3000
Но само число при этом остаётся типа Float64
typeof(n)
Так же эта неопределённость возникает при делении 0 на 0.
0/0
Но интересным соглашением является то, что 0 в степени NaN это NaN, а не 0.
0^NaN
Но вот 1^NaN уже 1.
1^NaN
И NaN в степени 0 также 1.
NaN^0
Помимо знаковых бесконечностей, есть и знаковые нули, которые можно получить деление числа на соответствующую знаковую бесконечность.
1 / Inf
-1 / Inf
Комплексные числа
Наконец, рассмотрим комплексные числа.
Они задаются при помощи символов im и соответствующего числа.
z = 3 + 5im
Так же как и рациональные, комплексные числа представляют собой структуру с 2 полями (назовём их частями) - вещественная - real и мнимая - imaginary.
z.re
z.im
И так как наше комплексное число задавалось типами Int64, то и занимать она будет 2 по 8 байт.
sizeof(z)
Для примера зададим некоторое комплексное число z. где вещественная часть - -1/2, а мнимая - -√3/2
z = -1/2 - √3/2*im
round(z^3)
Но если мы возьмём квадратный корень из -1, то выведется ошибка. Так как если мы передаём в неё вещественный аргумент, то взятие корня для отрицательных вещественных чисел не определено.
sqrt(-1)
Но если мы передадим в качестве аргумента - комплексное число, то тут уже функция выдаст корректный комплексный результат - число i.
sqrt(-1 + 0im)