Дженерики в свифте: радость через боль и страдания.
С одной стороны, всё классно: generic-код получается красивым, типобезопасным и (относительно) эффективным.
С другой: чуть шаг в сторону и всё – провал.
Проблема 1: вариантность поддержана только для некоторых типов из стандартной библиотеки, которые свифт знает "в лицо". Например, вот это работает:
А вот это – нет:
Обидно видеть подобное упущение. Тем обиднее что даже ObjC __covariant и __contravariant были с самого начала.
Проблема 2: Отсутствие вариадиков и не-типовых аргументов.
Если бы дженерик мог принимать переменное количество аргументов, некоторые задачи решались бы куда лаконичнее. В группе в SwiftUI помещалось бы больше 10 вьюх, а работать с функциями можно было бы без миллиарда перегрузок для каррирования/композиции и.т.п.
Сам язык, местами, тоже мог бы стать проще и лучше: например, dynamicCallable (возможно) мог бы быть протоколом, а тапплы как отдельная сущность могли бы вообще не понадобится.
Как это могло бы выглядеть: вот маленькая функция на С++, которая выполняет аналог операции map над таплами (очень упрощенный пример)
Сам std::tuple, кстати, в C++ – просто библиотечный тип. Определен он мог бы быть как-то так:
Проблема 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
В качестве альтернативы, можно было бы разрешить специфицировать PAT-ы как дженерики (и вызовы дженерик-функций, заодно, тоже, раз такая пляска): например, let foo: Sequence<Element: Int> = [1, 2, 3]
Примечательно что PAT-ы это не совсем то-же самое, что и дженерик-протоколы/интерфейсы (которых в свифте нет). Дженерик-протокол с разными аргументами это два разных протокола. PAT с разными ассоциированными типами это все-еще один и тот-же PAT.
Из этого следует, что вы могли бы законформить один и тот-же дженерик протокол дважды. Крайне полезно для протоколов в духе ConvertibleFrom или ComparableTo. К сожалению, разработчики языка заявляли, что не намерены поддерживать эту фичу.
А что бы вы пофиксили/улучшили в дженериках в первую очередь?
🤔
25.8%
Ко[нтра]вариантность🤔
12.9%
Вариадики🤔
32.3%
PAT-ы🤔
29.0%
Что-то ещё