Closures & method/property syntax are the obvious problem. Let's say we hand-wave some problems away:
- no more ad-hoc overloads
- introduce multiparameter typeclasses to handle operators
- create unidirectional inference similar to Hindley-Milner
Now consider
struct MyStruct {
var property: Int
}
func withMyStruct<R>(_ operation: (MyStruct) -> R) -> R {
operation(MyStruct(property: 3))
}
// and a call to it
let x = withMyStruct { $0.property }
This is well-typed in current Swift — x
is Int
and $0
is MyStruct
.
With unidirectional inference, we should start at $0.property
, and we're already in trouble — we have no idea what type $0
is. If we're in Haskell, the code looks like property $0
and we're fine — property :: MyStruct -> Int
is a global function, we can just look it up. With member syntax, we need to have a type declaration of the receiver before we can resolve any member references. So we're forced to rewrite:
let x = withMyStruct { (s: MyStruct) in s.property }
(Instantly wiping out all closure argument shorthands including $0
and untyped x, y in
).
With that out of the way, the rest of the expression typechecks here — the closure is inferred as (MyStruct) -> Int
, R
is inferred as Int
, and x
is inferred as Int
. But if we instead write:
let x: UInt64 = withMyStruct { (s: MyStruct) in 3 }
we're back in problem territory. Once again, in Haskell we're OK — the closure is inferred as Num a => MyStruct -> a
, the call to withMyStruct
is inferred as Num a => a
and it's fine to bind that to x :: UInt64
. But in Swift, we don't have the concept of the "most general type" (in fact, closures must have concrete types†), and we'd be forced to infer the closure as (MyStruct) -> Int
, R
as Int
, and then error on the binding to x
. This might be surmountable by inferring the closure expression as (MyStruct) -> some ExpressibleAsIntegerLiteral
, then concretizing in a second (but still polynomial) pass back down the tree, and erroring only if we're left with a non-concrete closure type.
You could almost consider a world where the additional pass back down is the solution to the first problem too — infer { $0.property }
as something like @_structural protocol π0 { associatedtype τ0; var property: τ0 { get } }; (some π0) -> π0.τ0
. Then the call to withStruct
would match π0
against MyStruct
successfully, and substitute π0.τ0=Int
, and infer the call to withMyStruct
as Int
. What I don't know is whether one pass "up" and another "down" wouldn't just fail in a slightly more complex situation, whether @_structural protocol
could reasonably be created for the full complexity of method calls, etc.
† except the mysterious & magical _openExistential(:do:)
…