Какие функции вызывает Swift? Часть 5: Range против Interval.

Это русский перевод статьи airspeedvelocity Which function does Swift call? Part 5: Range vs Interval

Это часть 5 из серии постов о том, как Swift разрешает “неоднозначность” в “перегруженных” функциях. Часть 1 можно найти  здесь (русский перевод – здесь). В предыдущей статье (русский перевод – здесь), мы рассматривали как управлять  generics.

И наконец …

Теперь, когда мы рассмотрели как подбираются  generics, мы можем наконец ответить на первоначальный вопрос – как мы получаем тип  Range по умолчанию для выражения 1...5 ? Вот образец кода:

// r будет иметь тип Range<Int>
let r = 1...5

// если вы хотите такой тип ClosedInterval<Int>, то вам
// нужно задать тип явно:
let integer_interval: ClosedInterval = 1...5

// если вам нужен тип Doubles, то нет необходимости
// декларировать тип явно.
// floatingpoint_interval is a ClosedInterval<Double>
let floatingpoint_interval = 1.0...5.0

Функция, соответствующая infix оператору  ...  , определяется трижды в стандартной библиотеке со следующими сигнатурами:

func ...<T : Comparable>
  (start: T, end: T) -> ClosedInterval<T>

func ...<Pos : ForwardIndexType>
  (minimum: Pos, maximum: Pos) -> Range<Pos>

func ...<Pos : ForwardIndexType where Pos : Comparable>;
  (start: Pos, end: Pos) -> Range<Pos>

Первые две – очень простые. Они обе generic, у них единственная generic метка-заполнитель (placeholder), которая ограничена единственным протоколом. Нет преимущества у одного ограничения перед другим.

Оба протокола Comparable и ForwardIndexType происходят из протокола Equatable, но ни один из них не происходит от другого. Если мы оставим только эти две функции, то мы получим ошибку “неоднозначности” –ambiguous call error.

Но есть третья функция, которая и заставляет Swift выбрать Range , а не  ClosedInterval. В этой функции, Pos ограничивается не только ForwardIndexType, но и Comparable. Ограничение двумя протоколами выигрывает у ограничения с одним, так что при “перегрузке” будет выбрана эта функция.

После 4-х длинных “подводящих” постов, мы наконец можем немного расслабиться?

Давайте зададим другой вопрос – почему присутствует эта последняя функция?

Одно из возможных объяснений: версия с ограничением в виде двух протоколов выполнена чисто для того, чтобы “развязать” узел, так как ошибка “неоднозначности” (ambiguous call errors) становится надоедливой. Создание Range в действительности не требует сравнения, но добавления требования “сравниваемости” (comparable) имеет эффект предпочтения диапазонов (ranges) над интервалами (intervals). Если бы это был желаемый результат, то следовало бы все дополнительные функции для ... снабдить этим протоколом.

Но, возможно, что это не так. Есть другая причина, по которой требуется протокол Comparable для “перегрузки”.

Фабрика функций

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

Например, функция lazy определена 4 раза для  4-х различных типов аргументов : один раз для случайного доступа (random-access), для двунаправленного доступа (bidirectional), и возрастающей коллекции ( forward collections); и один раз для последовательности (sequences). Каждая функция возвращает различные подходящие  lazy типы (e.g. LazyRandomAccessCollection). Когда вы вызываете  lazy, компилятор выбирает наиболее подходящую (в заданном порядке). Если вы комбинируете это с type inference (выводом типа из контекста), вы получите самый мощный доступный тип, при этом все определяется на этапе компиляции.

// a будет иметь тип LazyRandomAccessCollection
// так как массивы - это случайный доступ (random access)
let a = lazy([1,2,3,4])

// s будет иметь тип LazyBidirectionalCollection,
// так как строки не могут индексироваться случайно
let s = lazy(&quot;hello&quot;)

// r будет иметь тип LazySequence, так как StrideTo
// не является коллекцией, а просто последовательность
let r = lazy(stride(from: 1, to: 8, by: 2))

// ничего не мешает вам декларировать эти
// lazy объекты вручную и напрямую:
let l = LazyRandomAccessCollection([1,2,3,4])

Каждая версия lazy , возможно, вообще ничего не делает, кроме инициализации и возвращения надлежащего типа  – это просто делает декларирование различных  lazy типов более удобным по сравнению с тем, чтобы давать им полное классовое соответствующее имя. Все это становится возможным благодаря приоритетам “перегрузки”, описанным в серии этих постов. Большинству каждодневных пользователей lazy не нужно знать ничего из вышеупомянутого – они просто используют функцию и она делает то, что с интуитивной точки зрения “правильно”.

В некоторых случаях, какая “фабричная” функция будет вызвана зависит непосредственно от вызывающей стороны. Например, функция stride создается либо как StrideTo, либо как  StrideThrough, в зависимости от имени среднего аргумента  to: или through:.

// x будет иметь тип StrideThrough
let x = stride(from: 1, through: 10, by: 5)

// y будет иметь тип StrideTo
let y = stride(from: 1, to: 10, by: 5)

В случае со stride, это единственный способ, каким вы можете конструировать эти объекты, та как их инициализаторы являются private (что означает, что  stride может вызвать их, так как они задекларированы внутри Swift, а вы – нет).

Использование “Фабричных” функций для выполнения проверки на правильность

Для типа Range, оператор ... две дополнительные задачи, помимо того, что конструирует возвращаемое значение. Во-первых, если ввод поддерживает comparators, он может установить, что аргументы “начала” и “конца” не перевернуты. Если да, то вы получаете runtime assertion (утверждение на этапе runtime). Вы можете выполнить такую проверку, только если используете оператор ... . Если вы декларируете диапазон как: Range(start: 5, end: 1), то это будет работать без проверки.

Между прочим, вы получите эту проверку для  String диапазонов. Индексы для строк в Swift не являются random-access (из-за  переменной длины символов ), но они являются “сравниваемыми” (comparable). Хотя вы не можете узнать, сколько символов между двумя индексами, вы можете знать, что один индекс предшествует другому.

Другая цель функции ... – это нормализовать “закрытые диапазоны” (closed ranges) в “полу-закрытые диапазоны” (half-open ranges). Range это своего рода противоположность  ClosedInterval и HalfOpenInterval. Не существует “закрытой” версии Range, диапазоны всегда являются  “полу-закрытыми”. Когда вы вызываете  ..., то добавляется единица ко второму параметру, чтобы получить эквивалентный “полу-закрытый диапазон”. Если вы напечаете  1...5 на playground, то увидите , что он в действительности преобразовался в  1..<6, который и отображается на playground.

Именно поэтому мы получим сообщение об ошибке на этапе runtime, если вы попытаемся сконструировать “закрытый диапазон” из конечного индекс строки, хотя теоретически это могло бы быть правильно:

let s = "hello"
let start = s.startIndex
let end = s.endIndex

// fatal error: не можем увеличить endIndex
let r = start...end

// тоже самое, если вы будете делать так:
end.successor()

Вы могли бы вложить эту логику в Range.init? В случае “полу-закрытых” преобразований, вы, возможно, могли бы иметь две версии initwith to: и through: аргументами. Но более понятно делать это с использованием операторов.

(Вы могли бы сказать тоже самое для stride, но вам понадобятся пользовательские триадные операторы). Если вы хотите узнать, как возможно построить триадные пользовательские операторы, посмотрите замечательную статью  Nate Cook.

Но в случае проверки инверсии параметров, которая использует comparable, вы могли бы сделать generic версию init, может быть что-то типа такого:

extension Range {
    init<C: Comparable>(start: C, end: C) {
        assert(start <= end, "Can't form Range with end < start")
        self.init(start: start, end: end)
    }
}

Да, это будет компилироваться, но не будет делать то, что нам нужно. В действительности, ваш init никогда не будет вызван, даже если вы передадите comparable типы. Почему? Потому что этот init generic, а обычная версия  Range.init – нет. Как мы видели в предыдущих материалах, generic функции никогда не становятся вызываемыми, если есть возможность “перегрузить” не-generic версию.

Вы можете возразить: “Но подождите, текущая версия Range.init является такой generic!”  “Смотрите:”

// Приводится из Swift определения Range
struct Range : Equatable ...etc {
    /// Construct a range with `startIndex == start` and `endIndex ==
    /// end`.
    init(start: T, end: T)
}

“Видите? T – это generic метка-заполнитель (placeholder)! А мы наложили больше ограничений на нашу метку-заполнитель функции, так что она и должна вызываться.”

Да, T – это generic placeholder. Но это не placeholder для функции. Это placeholder для struct. В контексте функции , T фиксируется по месту как специфический тип. Для того, чтобы понять это, попробуем следующий код:

struct S<T> {
    func f(i: Int) { print("f(Int)") }
    func f(t: T) { print("f(T)") }

    func g(i: Int) { print("g(Int)") }

    // заметим, что правила области действия означает, что
    //placeholder T это  другой_ T по отношению к  struct T
    func <T: IntegerType>(t: T) { print("g(T)") }
}

// зададим T как Int
let s = S<Int>()

// error: Неоднозначное использование f
s.f(1)

// печатается "g(Int)", а не "g(T)"
// потому что  generics теряется...
s.g(1)

Между прочим, такие мелочи как потенциальное замешательство относительно области действия  приведенного выше  T  (или эти placeholders могут быть случайно переименованы) заставляют меня нервничать относительно повторного использования placeholders для структур struct, когда мы расширяем эту структуру. Не чувствуется, что это правильно. Обычно, хорошо написанные generic классы используют  специальные typealias для этих placeholders. Например, Range мог бы использовать typealiases T как Index. Может быть так лучше.

В любом случае, для типа Range, декларирование generic функции ..., которая не должна конкурировать с любой не-generic альтернативами, убирает эти проблемы. Плюс операторы выглядят лучше.

Следующая тема: что, если мы не хотим, чтобы по умолчанию выдавался тип Range ? Что мы должны для этого сделать?

Leave a Reply

Your email address will not be published. Required fields are marked *