🔥

Тред (Глеб Лукьянец)


Дженерики в свифте: радость через боль и страдания. С одной стороны, всё классно: generic-код получается красивым, типобезопасным и (относительно) эффективным. С другой: чуть шаг в сторону и всё – провал.

Проблема 1: вариантность поддержана только для некоторых типов из стандартной библиотеки, которые свифт знает "в лицо". Например, вот это работает:
notion image

А вот это – нет:
notion image

Обидно видеть подобное упущение. Тем обиднее что даже ObjC __covariant и __contravariant были с самого начала.

Проблема 2: Отсутствие вариадиков и не-типовых аргументов.

Если бы дженерик мог принимать переменное количество аргументов, некоторые задачи решались бы куда лаконичнее. В группе в SwiftUI помещалось бы больше 10 вьюх, а работать с функциями можно было бы без миллиарда перегрузок для каррирования/композиции и.т.п.

Сам язык, местами, тоже мог бы стать проще и лучше: например, dynamicCallable (возможно) мог бы быть протоколом, а тапплы как отдельная сущность могли бы вообще не понадобится.

Как это могло бы выглядеть: вот маленькая функция на С++, которая выполняет аналог операции map над таплами (очень упрощенный пример)
notion image

Сам std::tuple, кстати, в C++ – просто библиотечный тип. Определен он мог бы быть как-то так:
notion image

Проблема 3: PAT-ы и отсутствие хороших способов с ними совладать.

Суть: когда вы добавляете associated type constraint к протоколу (или используете Self в сигнатуре), он перестает быть типом и становится тем, что называют "protocol with associated types" (PAT). Главное применение для такой штуки – быть generic type constraint-ом.

То есть вы больше не можете, например, создать переменную с таким протоколом в качестве типа, или передать его как дженерик-аргумент. Из-за этого обычно добавить ассоциированный тип к уже существующему в вашем проекте протоколу зачастую крайне проблематично

Из-за этого торжествует generic poisoning, проекты полнятся всякими AnyProtocol (пр. AnyHashable, AnyView), а местами вместо хорошо типизированных абстракций – какая-то срамота с кастами.

Помочь ситуации можно было бы сразу несколькими способами. Во-первых, не вижу ни одной причины, почему нельзя разрешать заворачивать значения под PAT-ом в обычные протокольные existential containers с доступом только к тем элементам интерфейса, которые не содержат AT в сигнатурах

То есть, грубо говоря, могло бы быть можно делать как на скриншоте. Для Hashable, на самом деле, нет нужны быть PAT-ом, он просто унаследовал это от Equatable, в котором есть констреинт на Self
notion image

В качестве альтернативы, можно было бы разрешить специфицировать PAT-ы как дженерики (и вызовы дженерик-функций, заодно, тоже, раз такая пляска): например, let foo: Sequence<Element: Int> = [1, 2, 3]

Примечательно что PAT-ы это не совсем то-же самое, что и дженерик-протоколы/интерфейсы (которых в свифте нет). Дженерик-протокол с разными аргументами это два разных протокола. PAT с разными ассоциированными типами это все-еще один и тот-же PAT.

Из этого следует, что вы могли бы законформить один и тот-же дженерик протокол дважды. Крайне полезно для протоколов в духе ConvertibleFrom или ComparableTo. К сожалению, разработчики языка заявляли, что не намерены поддерживать эту фичу.

А что бы вы пофиксили/улучшили в дженериках в первую очередь?
🤔 25.8% Ко[нтра]вариантность
🤔 12.9% Вариадики
🤔 32.3% PAT-ы
🤔 29.0% Что-то ещё