Created
July 4, 2026 16:39
-
-
Save gistya/cc40cd4133e77c5df899b9d0908cf47d to your computer and use it in GitHub Desktop.
Implicit member syntax through a function type — adversarial playground
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ============================================================================= | |
| // Implicit member syntax through a function type — adversarial playground | |
| // ============================================================================= | |
| // | |
| // Feature: a leading dot whose contextual type is a function type `(P...) -> R` | |
| // also considers static members of the return type `R` (enum case constructors, | |
| // static factories). i.e. `.b` may mean `R.b` when `R.b : (P...) -> R`. | |
| // | |
| // Build (feature ON): | |
| // swiftc -typecheck -enable-experimental-feature ImplicitMemberOnFunctionType \ | |
| // implicit-member-function-type-playground.swift | |
| // | |
| // This file compiles clean with the feature on. Every case below is one we (or | |
| // reviewers) worried might be a gotcha — an ambiguity trap, a silent | |
| // mis-resolution, a swallowed diagnostic, or a look-through that shouldn't | |
| // happen. None of them break. Cases that are *correctly rejected* are shown in | |
| // comment blocks with the exact diagnostic the compiler produces. | |
| // | |
| // The leading dot here resolves to exactly the choice you'd get by writing | |
| // `R.member` in current Swift, and only allows conversions the language already | |
| // performs in those existing cases. No doors are opened to additional conversion | |
| // or generalization due to self-referential pinning. | |
| // ============================================================================= | |
| // ----------------------------------------------------------------------------- | |
| // 1. What it enables (the happy path) | |
| // ----------------------------------------------------------------------------- | |
| enum Move: Equatable { case idle; case step(Int); case jump(Int, Int) } | |
| struct Transition { | |
| init(on: Move, to: Move) {} | |
| init<P>(on: @escaping (P) -> Move, to: Move) {} | |
| init<P, Q>(on: @escaping (P, Q) -> Move, to: Move) {} | |
| } | |
| func demoHappyPath() { | |
| _ = Transition(on: .idle, to: .idle) // value case -> init(on: Move, ...) | |
| _ = Transition(on: .step, to: .idle) // case ctor -> init<P>(on:), .step: (Int) -> Move | |
| _ = Transition(on: .jump, to: .idle) // 2-payload -> init<P,Q>, .jump: (Int, Int) -> Move | |
| // (no auto-splat: a 2-payload case needs a 2-arg param) | |
| // A static factory works the same way. | |
| _ = Transition(on: Widget.make, to: .idle) | |
| } | |
| struct Widget { static func make(_ x: Int) -> Move { .step(x) } } | |
| // ----------------------------------------------------------------------------- | |
| // 2. Ambiguity traps that resolve correctly (NOT ambiguous) | |
| // ----------------------------------------------------------------------------- | |
| // 2a. Static factory vs. same-named instance method. The instance method's | |
| // unapplied type is `(Theme) -> (String) -> Theme` (it carries `self`), so it | |
| // can never be `(String) -> Theme`... it's not even a candidate. | |
| struct Theme { | |
| static var custom: (String) -> Theme { { _ in Theme() } } | |
| func custom(name: String) -> Theme { self } | |
| } | |
| let themeMaker: (String) -> Theme = .custom // unambiguous: the static var | |
| // 2b. A property is preferred over an unapplied function of the same name | |
| // (pre-existing rule), so this is unambiguous too. | |
| struct Palette { | |
| static var custom: (String) -> Palette { { _ in Palette() } } | |
| static func custom(_ name: String) -> Palette { Palette() } | |
| } | |
| let paletteMaker: (String) -> Palette = .custom | |
| // 2c. The look-through is strictly lower priority than a direct member with | |
| // no bespoke score; the solver already prefers the direct member. Here the | |
| // value overload wins and `picked` is Int (if the look-through could win, | |
| // this would be ambiguous or `picked` would be String). | |
| struct Both { | |
| static var thing: Both { Both() } | |
| static func thing(_: Int) -> Both { Both() } | |
| } | |
| func pick(_: Both) -> Int { 0 } | |
| func pick<P>(_: (P) -> Both) -> String { "" } | |
| let picked: Int = pick(.thing) | |
| // 2d. And when it is genuinely ambiguous across the return type's members, you | |
| // get a real "ambiguous use" error, not a silent pick. | |
| // | |
| // struct A { static func make(_: Int) -> A { A() } } | |
| // struct B { static func make(_: Double) -> B { B() } } | |
| // func build(_: (Int) -> A) {} | |
| // func build(_: (Double) -> B) {} | |
| // build(.make) | |
| // // error: ambiguous use of 'make' | |
| // 2e. The load-bearing case for the lower-priority rule: two NON-generic | |
| // overloads, one taking a value (direct member `S.b`) and one taking a | |
| // function (look-through to `E.b`). The direct member wins, so this still | |
| // resolves to Int exactly as it did before the feature existed. Without the | |
| // strict-lower-priority rule the two would tie and `g(.b)` would newly | |
| // become "ambiguous use of 'b'" — a source break. This is why the feature | |
| // carries a dedicated ranking score and is not decoration. | |
| enum Payload { case a; case b(Int) } | |
| struct Direct { static let b = Direct() } | |
| func g(_ x: Direct) -> Int { 0 } | |
| func g(_ h: (Int) -> Payload) -> String { "" } | |
| let gResult: Int = g(.b) // Int — the direct `Direct.b` outranks `Payload.b` | |
| // ----------------------------------------------------------------------------- | |
| // 3. Instance methods never masquerade as constructors (currying) | |
| // ----------------------------------------------------------------------------- | |
| // An unapplied instance method carries `self`, so its type never matches a | |
| // plain `(Args) -> R` slot — the return-type lookup simply doesn't surface it. | |
| // | |
| // class Logger { func log(message: String) {} } | |
| // func configure(_ action: (Logger, String) -> Void) {} | |
| // configure(.log) | |
| // // error: type '(Logger, String) -> Void' has no member 'log' | |
| // | |
| // Same story with a generic sink and an overloaded instance method: | |
| // | |
| // struct MultiTool { func execute(mode: Int) -> Bool { true } } | |
| // func run<T>(_ handler: (MultiTool) -> T) {} | |
| // run(.execute) | |
| // // error: type '(MultiTool) -> T' has no member 'execute' | |
| // // error: generic parameter 'T' could not be inferred | |
| // ----------------------------------------------------------------------------- | |
| // 4. Structural types it correctly does NOT look through | |
| // ----------------------------------------------------------------------------- | |
| // 4a. Nested / curried function type: the direct result `(String) -> Move` is | |
| // itself a function (no members), so there is no second-level look-through. | |
| // | |
| // let n: (Int) -> (String) -> Move = .step | |
| // // error: type '(Int) -> (String) -> Move' has no member 'step' | |
| // 4b. A generic wrapper is NOT a function type, so its type argument is never | |
| // searched. `Deferred<Move>` is nominal — the dot looks on Deferred, not Move. | |
| // (Same reasoning rules out `Task<Success, Failure>`, `Optional`, etc.) | |
| // | |
| // struct Deferred<T> {} | |
| // func schedule(_ t: Deferred<Move>) {} | |
| // schedule(.step) | |
| // // error: type 'Deferred<Move>' has no member 'step' | |
| // 4c. Variadic and inout parameters make a different function type, so an | |
| // ordinary case constructor can't fit and is cleanly rejected. | |
| // | |
| // func variadic(_ g: (Int...) -> Move) {} | |
| // variadic(.step) // error: member 'step' expects argument of type 'Int' | |
| // func writeback(_ g: (inout Int) -> Move) {} | |
| // writeback(.step) // error: member 'step' expects argument of type 'Int' | |
| // ----------------------------------------------------------------------------- | |
| // 5. It only uses conversions valid in current swift. | |
| // ----------------------------------------------------------------------------- | |
| // 5a. Result covariance / existential erasure: `(Int) -> Bold` fits | |
| // `(Int) -> any TextStyle` because that conversion already exists today | |
| // (the explicit `Bold.bold` form uses the very same one). | |
| protocol TextStyle { var name: String { get } } | |
| struct Bold: TextStyle { let name = "bold" } | |
| extension TextStyle where Self == Bold { | |
| static var bold: (Int) -> Bold { { _ in Bold() } } | |
| } | |
| func render(_ make: (Int) -> any TextStyle) { _ = make(12) } | |
| func demoCovariance() { | |
| let widened: (Int) -> any TextStyle = Bold.bold // legal in plain Swift today | |
| _ = widened | |
| render(.bold) // the feature: drop the type name | |
| } | |
| // 5b. `async` / `throws` contexts work only because a sync, non-throwing function | |
| // already converts to an async/throwing one. No new conversion is invented. | |
| func runAsync(_ g: () async -> Move) {} | |
| func runThrows(_ g: () throws -> Move) {} | |
| extension Move { static func makeIdle() -> Move { .idle } } | |
| func demoEffects() { | |
| runAsync(.makeIdle) // () -> Move <: () async -> Move | |
| runThrows(.makeIdle) // () -> Move <: () throws -> Move | |
| } | |
| // 5c. Optional's own payload case is repaired the same way: `.some` is | |
| // `Optional.some: (Wrapped) -> Wrapped?`, a *direct* member of the result | |
| // type — so it's found. (Note: the feature does NOT take a second hop: a | |
| // payload case of the *wrapped* type, e.g. `.step` against `(P) -> Move?`, | |
| // is deliberately not surfaced — only plain, first-level members of R are.) | |
| let someCtor: (Move) -> Move? = .some | |
| // ----------------------------------------------------------------------------- | |
| // 6. Chained implicit members (SE-0287) still compose | |
| // ----------------------------------------------------------------------------- | |
| enum Chainable { | |
| case a | |
| case b(Int) | |
| var asFunction: (Int) -> Chainable { { _ in self } } | |
| } | |
| let chained: (Int) -> Chainable = .a.asFunction // leading dot via return type, then chained | |
| let chainedSelf: (Int) -> Chainable = .b.self // '.self' off the function value | |
| // ----------------------------------------------------------------------------- | |
| // 7. Existing dot syntax is undisturbed (parity, feature on or off) | |
| // ----------------------------------------------------------------------------- | |
| enum Val { case a, b } | |
| struct Ref { static let a = Ref() } | |
| // 7a. Subscripts | |
| struct Bag { subscript(_ v: Val) -> Int { 0 }; subscript(_ r: Ref) -> String { "" } } | |
| func demoSubscript(_ bag: Bag) { let _: Int = bag[.a]; let _: String = bag[.a] as String } | |
| // 7b. Ternary / autoclosure / array & dictionary literals / try? | |
| func f7(_ v: Val) -> Int { 0 } | |
| func f7(_ r: Ref) -> String { "" } | |
| func autoc(_ v: @autoclosure () -> Val) -> Int { 0 } | |
| func arr(_ xs: [Val]) -> Int { 0 } | |
| func demoParity(_ cond: Bool) { | |
| let _: Int = f7(cond ? .a : .b) | |
| let _: Int = autoc(.a) | |
| let _: Int = arr([.a, .b]) | |
| } | |
| // 7c. `.none` / `.some` against an Optional param, and an enum with its own | |
| // `none` case — both keep their existing (parity) behavior. | |
| func opt(_ x: Val?) -> Int { 0 } | |
| func demoOptionalNames() { _ = opt(.none); _ = opt(.some(.a)) } | |
| // ----------------------------------------------------------------------------- | |
| // 8. Generic contexts — the return-type member must actually satisfy the bound | |
| // ----------------------------------------------------------------------------- | |
| enum Tap { case tap(Int); case idle } | |
| struct Event { static func make(_ i: Int) -> Event { Event() } } | |
| func mix<T>(_ f: (T) -> Event) -> Int { 0 } // T inferred from Event.make -> Int overload | |
| func mix(_ f: (Int) -> Tap) -> String { "" } // Tap.tap -> String overload | |
| func demoGeneric() { | |
| let _: Int = mix(.make) // only Event has 'make' | |
| let _: String = mix(.tap) // only Tap has 'tap' | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment