JavaRush /Курси /Swift SELF /Неявне відкриття екзистенціалів

Неявне відкриття екзистенціалів

Swift SELF
Рівень 46 , Лекція 1
Відкрита

1. Межа між any і generic-кодом

Коли ви вперше починаєте активно використовувати any, зазвичай усе йде добре… аж до моменту, коли ви намагаєтеся передати значення any P у generic-функцію func f<T: P>(_ x: T). І тут Swift раптово вмикає режим «суворий викладач математики» та видає повідомлення, що звучить майже як філософський парадокс.

Чому зʼявляється помилка «cannot conform to itself»

Класичний приклад — дуже показовий і водночас цілком життєвий:

import Foundation

protocol P {
    associatedtype A
    func getA() -> A
}

func takeP<T: P>(_ value: T) {
    _ = value.getA()
}

func test(p: any P) {
    // takeP(p) // ❌ Раніше: "protocol 'P' as a type cannot conform to itself"
}

Чому це взагалі відбувається? Тому що any P — це не «якийсь конкретний тип, що відповідає P». Це коробка, у якій лежить «щось, що відповідає P», і всередині може бути різне в різний час. У Swift-специфікації цю ситуацію прямо описують як пастку: легко перейти від generics до екзистенціалів, але складно повернутися назад.

І саме тут зʼявляється ідея implicit opening: «а давайте компілятор зможе тимчасово зазирнути всередину коробки й підставити конкретний тип у generic-параметр, хоча б на час одного виклику».

Що означає «відкрити екзистенціал» по-людськи

Перш ніж радіти «ура, компілятор розумнішає», важливо зрозуміти, що саме він робить і чому це не перетворює any P назад на «нормальний тип» назавжди.

Уявіть, що any P — це посилка без наліпки з описом вмісту. Ви знаєте лише те, що всередині лежить предмет, який відповідає контракту P. Цей предмет — underlying type, тобто конкретний тип, захований усередині existential-значення.

Implicit opening existentials — це коли під час конкретного виклику generic-функції компілятор робить приблизно таке:

  1. бере значення p: any P,
  2. «відкриває коробку» й бачить, що всередині лежить, скажімо, IntRepository або FancyCostume,
  3. тимчасово підставляє цей underlying type у generic-параметр T,
  4. виконує виклик так, ніби ви написали takeP(ось_цей_конкретний_тип),
  5. і після виклику не дає цьому «відкритому типу» втекти назовні, знову повертаючись до безпечного «стертого» світу.

У Swift-специфікації важливу думку формулюють так: відкриття роблять, щоб «зсунути» нас від динамічно типізованого значення (existential) назад до статично типізованого generic-контексту — але локально, лише всередині виклику.

2. Практика: як викликати generic-функцію на [any Repository]

Тепер зробімо приклад, який дуже нагадує стиль нашого курсового проєкту: є контракт, є кілька реалізацій, і ми хочемо зберігати їх разом, але обробляти через generic-функцію.

Протокол Repository з associatedtype

import Foundation

protocol Repository {
    associatedtype Item
    func allItems() -> [Item]
}

struct IntRepository: Repository {
    func allItems() -> [Int] { [1, 2, 3] }
}

struct StringRepository: Repository {
    func allItems() -> [String] { ["Swift", "6.2"] }
}

Generic-функція, для якої важливий звʼязок RR.Item

Зробімо generic-функцію, яка друкує вміст репозиторію. Зверніть увагу: всередині функції нам важливо, що R.Item повʼязаний саме з R.

import Foundation

func dumpRepository<R: Repository>(_ repo: R) {
    for item in repo.allItems() {
        print("•", item)
    }
}

А тепер — сховище різнорідних реалізацій:

import Foundation

let repos: [any Repository] = [
    IntRepository(),
    StringRepository()
]

І ось ключовий момент лекції. Ми хочемо:

import Foundation

for repo in repos {
    dumpRepository(repo) // implicit opening: відкриваємо базовий тип на час виклику
}

З погляду концепції, на кожній ітерації циклу компілятор міркує так: «у цій ітерації repo усередині коробки — IntRepository, отже R == IntRepository; на наступній — StringRepository, отже R == StringRepository». Саме цей сценарій часто наводять як один із мотивів implicit opening: обробку масиву [any P] через generic-функцію.

Чому repo не стає «нормальним типом»

Важливо: змінна repo у циклі не стає IntRepository або StringRepository «назавжди». Ззовні вона й далі any Repository. Відкриття живе лише в межах конкретного виклику dumpRepository(repo).

Це ніби ви на мить зазирнули в коробку, подивилися, що там усередині, зробили дію — і знову її закрили. Інакше коробка перестане бути коробкою, а мова — типобезпечною.

3. Чому відкриття локальне

Тут часто виникає спокуса сказати: «Окей, компілятор уміє відкривати коробку. Отже, я можу… ну… усе?» І саме тут Swift акуратно забирає в нас зайві надії.

Відкритий тип не повинен “витікати” в типову систему користувача — інакше почнеться залежна типізація, коли тип залежить від значення, а це вже зовсім інший рівень складності. У пропозиції окремо обговорюють, чому це ламається під час мутацій і розгалужень.

Дві коробки — не один T

Приклад, чому не можна порівняти два any Equatable через один T:

import Foundation

func areSame<T: Equatable>(_ a: T, _ b: T) -> Bool {
    a == b
}

func demo(a: any Equatable, b: any Equatable) {
    // areSame(a, b) // ❌ не можна: компілятор не може гарантувати один і той самий underlying type для a і b
}

Інтуїтивно: areSame вимагає, щоб обидва аргументи були одного конкретного типу T. А any Equatable каже: «усередині лежить щось Equatable», але для a і b це «щось» може бути різним.

У Swift-специфікації схожий приклад наводять через identity, а потім — через порівняння результатів: навіть якщо ви викликаєте одну й ту саму generic-функцію на одному й тому самому existential-значенні, результат після виклику знову проходить type erasure, і компілятор не вважає два результати статично одним типом.

«Але ж вони обидва Int усередині!» — так, але компілятор не зобовʼязаний вірити

Навіть якщо ви як людина знаєте, що всередині a і b лежить Int, компілятор не може «офіційно» прийняти це як факт без доказу на рівні типів.

Це одна з найважливіших навичок, яку треба тренувати на цьому етапі: відрізняти «я знаю» від «типова система знає».

4. Діагностика: перекладаємо помилки компілятора

Ви натрапите на помилки, що звучать так:

  • protocol 'P' as a type cannot conform to itself
  • any P does not conform to P
  • cannot convert value of type 'any P' to expected argument type …
  • та інші варіації.

І це нормально: компілятор намагається пояснити типову проблему, а ми поки мислимо сюжетом («ну це ж репозиторій, чого ти…»).

Таблиця-перекладач

Нижче — невелика «таблиця-перекладач», яку зручно тримати в нотатках.

Діагностика компілятора Людський переклад Що перевірити в коді
protocol 'P' as a type cannot conform to the protocol itself «Ви намагаєтеся використати any P як T: P, але коробка сама не є конкретною реалізацією контракту» Чи є associatedtype/Self у P? Чи не передаєте ви any P у func f<T: P>?
any P does not conform to P «Ваш узагальнений код чекає на конкретний тип, а ви дали екзистенціал» Чи потрібні тут generics замість any — або навпаки?
не вдається вивести T «Для одного T обидва значення мають мати один і той самий concrete type» Чи не використовуєте ви один і той самий T одразу в кількох параметрах?
помилка навколо методу з параметром Self «Через any не можна гарантувати збіг concrete type для self та аргумента» Чи є в протоколі вимога на кшталт func f(_ x: Self)?

Звідки логічно береться «cannot conform to itself»

Swift-специфікація пояснює це прямо: під час виклику generic-функції компілятор раніше намагався підставити T == any P (тобто «нехай T буде самою коробкою»), але коробка не може відповідати вимогам, де потрібен збіг конкретного типу. Тому й виходило «P як тип не може відповідати P».

Implicit opening existentials якраз і зʼявився для того, щоб у таких ситуаціях компілятор міг підставляти не коробку, а underlying type.

Як читати помилку: шукаємо «де потрібен один конкретний тип»

Коли ви бачите страшну діагностику, корисно на мить зупинитися і поставити собі два запитання.

Перше запитання: «у сигнатурі є T (або R, або будь-який generic-параметр) і він використовується один раз чи кілька разів?». Якщо один раз, то «відкриття» часто можливе: компілятору потрібно лише один раз привʼязати T до underlying type конкретного значення.

Друге запитання: «я передаю одне значення any P чи два?». Якщо два, то дуже часто проблема в тому, що два existential-значення не зобовʼязані мати один concrete type.

5. Swift 6: обмеження implicit opening

Ця частина здається дрібницею, доки ви не зустрінете її в реальному коді — а потім вона перетворюється на «чому перестановка аргументів зламала компіляцію». Тому краще дізнатися про це зараз, у спокійній обстановці.

У Swift важливо, що аргументи функції обчислюються зліва направо. А щоб «відкрити» екзистенціал, компілятору іноді потрібно спочатку обчислити аргумент (отримати коробку), зазирнути всередину й лише потім типізувати інші аргументи. Якщо це ламає порядок обчислення, Swift забороняє таке відкриття. У пропозиції цей момент розбирають на прикладі, де відкриття вимагало б виконати getP() раніше за hello(), що порушило б звичний порядок.

Чому перестановка параметрів може ламати компіляцію

Суть не в конкретному прикладі, а в тому, що generic-параметр, який «привʼязується» до opened existential, не має бути потрібним для аргументів, що стоять раніше в списку.

Якщо ви бачите помилку, яка зникає після перестановки аргументів, це часто саме воно: компілятор не може одночасно зберегти left-to-right і зробити implicit opening там, де йому це потрібно.

Як вимкнути opening через as any P

Іноді розробнику потрібно заборонити implicit opening (наприклад, щоб зберегти стару семантику або щоб явно визнати: «я втрачаю обмеження, роблячи type erasure»). Для цього використовують прийом: зробити явне приведення до екзистенціалу прямо в аргументі, наприклад p as any P.

Але є нюанс: додаткові дужки можуть змінювати поведінку придушення. Тобто getP((x as any P)) може поводитися інакше, ніж getP(x as any P). У тексті пропозиції прямо наводять приклад, де дужки допомагають обійти конфлікт: з одного боку, потрібне as any P через втрату обмеження, а з іншого — це саме придушує opening.

На рівні курсу вам не потрібно запамʼятовувати всі ці нюанси як заклинання. Достатньо зрозуміти ідею: «implicit opening — це не “магія без правил”; у нього є обмеження заради передбачуваності мови».

6. Типові помилки

Помилка №1: очікувати, що implicit opening “полагодить” усі проблеми any.
Implicit opening розвʼязує досить конкретну проблему: «у мене є екзистенціал, я хочу викликати generic-функцію, і компілятору достатньо знати underlying type лише всередині виклику». Але якщо ви хочете, щоб цей тип зберігався між викликами, щоб два значення гарантовано були одного underlying type, або щоб можна було зберігати щось і потім безпечно діставати це типізовано, — це вже інше завдання. Тут implicit opening не всесильний, і це нормально.

Помилка №2: не помічати, що один generic-параметр використовують кілька разів.
Дуже багато «страшних» помилок насправді зводяться до однієї простої речі: T трапляється двічі, отже обидва місця мають бути одним конкретним типом. Щойно ви бачите сигнатуру на кшталт func f<T>(_: T, _: T), подумки вмикайте лампочку: «один і той самий T». З any це часто означає конфлікт, бо дві коробки не зобовʼязані містити однакові типи.

Помилка №3: намагатися лікувати діагностику примусовими приведеннями as!.
Коли код не компілюється, рука тягнеться до «ну я ж знаю, що там Int!». Але as! тут майже завжди перетворює типову проблему на потенційне падіння в рантаймі. А ще ви закріплюєте невдалий дизайн: виходить, що ваш API вимагає одних гарантій, а ви проштовхуєте інші силою. Краще зупинитися і спитати: «мені тут потрібен any чи generic-параметр?».

Помилка №4: плутати «коробка відповідає протоколу» і «значення в коробці відповідає протоколу».
Це тонка думка, але вона — серцевина теми. Усередині any P лежить значення, яке відповідає P. Але сам any P як тип не зобовʼязаний відповідати P, особливо якщо в протоколі є Self/associatedtype-звʼязки. Саме звідси й беруться повідомлення «cannot conform to itself», і саме це частково лікує implicit opening, відкриваючи коробку всередині виклику.

1
Задача
Swift SELF, 46 рівень, 1 лекція
Недоступна
Відкритий провайдер
Відкритий провайдер
1
Задача
Swift SELF, 46 рівень, 1 лекція
Недоступна
Друк репозиторіїв
Друк репозиторіїв
1
Задача
Swift SELF, 46 рівень, 1 лекція
Недоступна
Порівняння коробок
Порівняння коробок
1
Задача
Swift SELF, 46 рівень, 1 лекція
Недоступна
Пригнічення відкриття
Пригнічення відкриття
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ