Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save gistya/cc40cd4133e77c5df899b9d0908cf47d to your computer and use it in GitHub Desktop.

Select an option

Save gistya/cc40cd4133e77c5df899b9d0908cf47d to your computer and use it in GitHub Desktop.
Implicit member syntax through a function type — adversarial playground
// =============================================================================
// 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