Система типов: абстрактные и конкретные

Как устроена иерархия типов Julia: абстрактные «категории» и конкретные типы, которые можно создавать.

Абстрактный тип задаёт категорию (например, «любое число») и не имеет экземпляров; конкретный тип описывает реальное представление данных в памяти и может быть создан.

Иерархия типов

Все типы Julia образуют дерево с корнем Any. Числовые типы, например, выстроены так: NumberRealIntegerInt64. Верхние уровни — абстрактные, листья — конкретные. Проверить отношение можно оператором <: («подтип»):

println(Int64 <: Integer)
println(Integer <: Number)
println(Float64 <: Integer)

Вывод:

true
true
false

Зачем нужны абстрактные типы

Абстрактные типы позволяют писать методы «для всех чисел» сразу. Метод f(x::Number) сработает и для Int, и для Float64, и для рационального, и для комплексного — для всего, что является подтипом Number. Это делает код общим, но при этом компилятор всё равно специализирует его под конкретный тип в момент вызова.

describe(x::Integer) = "$x — целое"
describe(x::AbstractFloat) = "$x — дробное"

println(describe(5))
println(describe(2.5))

Вывод:

5 — целое
2.5 — дробное

Свои типы: struct

Собственный конкретный тип создают через struct. По умолчанию он неизменяем (поля нельзя менять после создания), для изменяемого пишут mutable struct:

struct Point
    x::Float64
    y::Float64
end

p = Point(3.0, 4.0)
println(p.x, ", ", p.y)

Вывод:

3.0, 4.0

Свой тип можно объявить подтипом абстрактного: struct Dog <: Animal — и тогда он унаследует все методы, написанные для Animal.

Как работает под капотом

Ключевая идея: наследуются методы, а не поля. Абстрактный тип не имеет полей и не задаёт структуру данных — он лишь объединяет конкретные типы в категорию для диспетчеризации. Это сознательное отличие от классического ООП, где наследуют и поля, и поведение. Конкретные типы с явно указанными типами полей (как x::Float64 в Point) компилируются в плотные структуры памяти, как struct в C, — отсюда скорость. Если же тип поля не указан или абстрактен (x без аннотации = Any), компилятор не знает размер заранее и код замедляется.

Частые ошибки

Распространённая ошибка новичков — делать поля структур абстрактного типа, например x::Number или x::Real вместо x::Float64. Абстрактный тип поля лишает компилятор знания о размере и представлении данных, и быстрый код не генерируется. В «горячих» структурах указывайте конкретные типы полей или используйте параметрические типы (следующий урок).

Итоги

  • Типы образуют дерево с корнем Any: абстрактные сверху, конкретные — листья.
  • <: проверяет и задаёт отношение «подтип».
  • Методы для абстрактного типа (x::Number) работают для всех его подтипов.
  • Свои типы создаются через struct (неизменяемый) или mutable struct.
  • Наследуются методы, а не поля; конкретные типы полей дают скорость.
Проверьте себя
1. Что вернёт выражение Int64 <: Integer в Julia?
Afalse
Btrue, потому что Int64 — подтип Integer
CОшибку
D0
2. Что наследуется при объявлении struct Dog <: Animal?
AПоля абстрактного типа Animal
BМетоды, написанные для Animal, но не поля
CНичего не наследуется
DТолько имя типа
3. Почему в «горячей» структуре лучше писать x::Float64, а не x::Number?
AFloat64 короче пишется
BКонкретный тип поля позволяет компилятору знать размер и генерировать быстрый код
CNumber вызывает ошибку
DРазницы в скорости нет