Spec-Zone.ru › Crystal

Вывод типов

Философия Crystal заключается в том, чтобы требовать как можно меньше ограничений типов. Однако некоторые ограничения необходимы.

Рассмотрим такое определение класса:

class Person
  def initialize(@name)
    @age = 0
  end
end

Мы сразу видим, что @age — целое число, но тип @name нам неизвестен. Компилятор мог бы вывести его тип на основе всех использований класса Person. Однако у такого подхода есть несколько проблем:

  • Тип не очевиден человеку, читающему код: ему также пришлось бы проверить все использования Person, чтобы это выяснить.
  • Некоторые оптимизации компилятора, например необходимость анализировать метод только один раз, а также инкрементальная компиляция, становятся практически невозможными.

По мере роста кодовой базы эти проблемы становятся всё актуальнее: разобраться в проекте всё труднее, а время компиляции становится невыносимым.

По этой причине Crystal должен очевидным образом (настолько очевидным, чтобы это было понятно человеку) знать типы переменных экземпляра и переменных класса.

Есть несколько способов сообщить Crystal эти типы.

С ограничениями типов

Самый простой, но, вероятно, и самый утомительный способ — использовать явные ограничения типов.

class Person
  @name : String
  @age : Int32

  def initialize(@name)
    @age = 0
  end
end

Без ограничений типов

Если не указывать явное ограничение типа, компилятор попытается вывести тип переменных экземпляра и класса, используя набор синтаксических правил.

Для данной переменной экземпляра/класса, если можно применить правило и предположить тип, этот тип добавляется в множество. Когда больше нельзя применить ни одного правила, выведенным типом становится объединение этих типов. Кроме того, если компилятор выводит, что переменная экземпляра инициализируется не всегда, он также включит тип Nil.

Правил много, но обычно чаще всего используются первые три. Запоминать их все необязательно. Если компилятор сообщает об ошибке, что тип переменной экземпляра не удаётся вывести, всегда можно добавить явное ограничение типа.

В следующих правилах упоминаются только переменные экземпляра, но они также применимы к переменным класса. Правила:

1. Присваивание литерального значения

Когда переменной экземпляра присваивается литерал, тип литерала добавляется в множество. Все литералы имеют соответствующий тип.

В следующем примере для @name выводится тип String, а для @age — тип Int32.

class Person
  def initialize
    @name = "John Doe"
    @age = 0
  end
end

Это правило, как и все последующие, применяется также в методах, отличных от initialize. Например:

class SomeObject
  def lucky_number
    @lucky_number = 42
  end
end

В приведённом выше случае для @lucky_number выводится тип Int32 | Nil: Int32, поскольку ей присвоено значение 42, и Nil, поскольку ей не присваивается значение во всех методах initialize класса.

2. Присваивание результата вызова метода класса new

Когда переменной экземпляра присваивается выражение вроде Type.new(...), тип Type добавляется в множество.

В следующем примере для @address выводится тип Address.

class Person
  def initialize
    @address = Address.new("somewhere")
  end
end

Это правило также применяется к обобщённым типам. Здесь для @values выводится тип Array(Int32).

class Something
  def initialize
    @values = Array(Int32).new
  end
end

Примечание: метод new может быть переопределён типом. В этом случае выведенным типом будет тип, возвращаемый new, если его можно вывести с помощью одного из следующих правил.

3. Присваивание переменной, являющейся параметром метода с ограничением типа

В следующем примере для @name выводится тип String, поскольку параметр метода name имеет ограничение типа String и присваивается переменной @name.

class Person
  def initialize(name : String)
    @name = name
  end
end

Обратите внимание, что имя параметра метода не имеет значения; это работает и так:

class Person
  def initialize(obj : String)
    @name = obj
  end
end

Сокращённый синтаксис присваивания переменной экземпляра из параметра метода даёт тот же результат:

class Person
  def initialize(@name : String)
  end
end

Также обратите внимание, что компилятор не проверяет, присваивается ли параметру метода другое значение:

class Person
  def initialize(name : String)
    name = 1
    @name = name
  end
end

В приведённом выше случае компилятор всё равно выведет для @name тип String, а позже, при полной типизации метода, выдаст ошибку компиляции о том, что Int32 нельзя присвоить переменной типа String. Используйте явное ограничение типа, если @name не должна иметь тип String.

4. Присваивание результата метода класса с ограничением возвращаемого типа

В следующем примере для @address выводится тип Address, поскольку метод класса Address.unknown имеет ограничение возвращаемого типа Address.

class Person
  def initialize
    @address = Address.unknown
  end
end

class Address
  def self.unknown : Address
    new("unknown")
  end

  def initialize(@name : String)
  end
end

На самом деле в приведённом выше коде ограничение возвращаемого типа в self.unknown не требуется. Дело в том, что компилятор также анализирует тело метода класса и, если может применить одно из предыдущих правил (это метод new, литерал и т. д.), выводит тип из этого выражения. Поэтому приведённый выше код можно записать проще:

class Person
  def initialize
    @address = Address.unknown
  end
end

class Address
  # No need for a return type restriction here
  def self.unknown
    new("unknown")
  end

  def initialize(@name : String)
  end
end

Это дополнительное правило очень удобно, поскольку помимо new часто используются методы класса, подобные конструкторам.

5. Присваивание переменной, являющейся параметром метода со значением по умолчанию

В следующем примере значение по умолчанию для name — строковый литерал, который затем присваивается @name, поэтому String будет добавлен в множество выводимых типов.

class Person
  def initialize(name = "John Doe")
    @name = name
  end
end

Разумеется, это также работает с сокращённым синтаксисом:

class Person
  def initialize(@name = "John Doe")
  end
end

Значением параметра по умолчанию также может быть метод Type.new(...) или метод класса с ограничением возвращаемого типа.

6. Присваивание результата вызова функции lib

Поскольку у функции библиотеки должны быть явно указаны типы, при присваивании её результата переменной экземпляра компилятор может использовать тип возвращаемого значения.

В следующем примере для @age выводится тип Int32.

class Person
  def initialize
    @age = LibPerson.compute_default_age
  end
end

lib LibPerson
  fun compute_default_age : Int32
end

7. Использование выражения библиотеки out

Поскольку у функции библиотеки должны быть явно указаны типы, компилятор может использовать тип аргумента out, который должен быть типом указателя, и предположить, что тип разыменованного значения подходит.

В следующем примере для @age выводится тип Int32.

class Person
  def initialize
    LibPerson.compute_default_age(out @age)
  end
end

lib LibPerson
  fun compute_default_age(age_ptr : Int32*)
end

Другие правила

Компилятор будет стараться работать как можно умнее, чтобы реже требовать явных ограничений типов. Например, если присваивается выражение if, тип будет выведен на основе ветвей then и else:

class Person
  def initialize
    @age = some_condition ? 1 : 2
  end
end

Поскольку приведённое выше if (точнее, тернарный оператор, но он похож на if) содержит целочисленные литералы, для @age успешно выводится тип Int32 без необходимости в избыточном ограничении типа.

Другой пример — || и ||=:

class SomeObject
  def lucky_number
    @lucky_number ||= 42
  end
end

В приведённом выше примере для @lucky_number будет выведен тип Int32 | Nil. Это очень полезно для переменных с отложенной инициализацией.

Компилятор также учитывает константы, поскольку сделать это довольно просто и для него, и для человека.

class SomeObject
  DEFAULT_LUCKY_NUMBER = 42

  def initialize(@lucky_number = DEFAULT_LUCKY_NUMBER)
  end
end

Здесь применяется правило 5 (значение параметра по умолчанию), и поскольку константа раскрывается в целочисленный литерал, для @lucky_number выводится тип Int32.

To the extent possible under law, the persons who contributed to this workhave waived
all copyright and related or neighboring rights to this workby associating CC0 with it.
https://crystal-lang.org/reference/1.2/syntax_and_semantics/type_inference.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API