[swift-evolution] Default Generic Arguments

Srđan Rašić srdan.rasic at gmail.com
Tue Jan 24 17:26:38 CST 2017


We are probably taking the wrong direction here and trying to solve the
problem that does not need solving. We are discussing how to infer
gereneric arguments in type declarations while we should not do that at
all.

Let me repeat Doug's examples:


struct X<T = Int> { }

func f1() -> X<Double> { return X() }

func f2() -> X<Int> { return X() }
func f2() -> X<Double> { return X() }

func f3<T>(_: T) -> X<T> { return X() }

let x1: X = f1()   // okay: x1 has type X<Double>?
let x2: X = f2()   // ambiguous?
let x3a: X = f3(1.5)   // okay: x3a has type X<Double>?
let x3b: X = f3(1)   // okay: x3a has type X<Int>?

Thinking about what the generic argument of X should be inferred to for x1,
x2 and x3 is pointless. If one omits generic arguments in the variable
declaration, one is accepting the defaults. In other words, doing let x: X
= ... should always be treated as doing let x: X<Int> = ..., regardless of
what we have on the right hand side. No inference should happen in this
case. It would mean inferring already specified type.

Why? Consider what happens if we define x as a property:

struct Test {
  let x: X

  init() {
    x = f()
  }
}

It would make no sense that the initialization in the initializer
specializes the generic argument of the property, so for the sake of
consistency we should not do it for the variables/constants either.

Given that, we can solve Doug's example as:

let x1: X = f1() // error: cannot assign X<Double> to X<Int>

let x2: X = f2()   // ok: using X<Int> overload
let x3a: X = f3(1.5)   // error like in x1
let x3b: X = f3(1)   // ok because rhs is inferred as X<Int>

I think this is the only valid way to go and it really simplifies things,
both the understanding of how the feature works, but also the
implementation.

What do you think?


tir. 24. jan. 2017 kl. 22.16 skrev David Sweeris <davesweeris at mac.com>:

>
> On Jan 24, 2017, at 11:41, Alexis via swift-evolution <
> swift-evolution at swift.org> wrote:
>
> It’s worth noting that the question of “how do these defaults interact
> with other defaults” is an issue that has left this feature dead in the
> water in the Rust language despite being accepted for inclusion two years
> ago. See
> https://internals.rust-lang.org/t/interaction-of-user-defined-and-integral-fallbacks-with-inference/2496 for
> some discussion of the issues at hand.
>
> For those who don’t want to click that link, or are having trouble
> translating the syntax/terms to Swift. The heart of Niko’s post is the
> following (note: functions are used here for expedience; you can imagine
> these are `inits` for a generic type if you wish):
>
> // Example 1: user supplied default is IntegerLiteralConvertible
>
> func foo<T=Int64>(t: T) { ... }
>
> foo<_>(22)
> //  ^
> //  |
> //  What type gets inferred here?
>
>
>
> // Example 2: user supplied default isn't IntegerLiteralConvertible
>
> func bar<T=Character>(t: T) { ... }
>
> bar<_>(22)
> //  ^
> //  |
> //  What type gets inferred here?
>
>
> There are 4 strategies:
>
> (Note: I use “integer literal” here for simplicity; in general it's “any
> kind of literal, and its associated LiteralType”. So this reasoning also
> applies to FloatLiteralType, StringLiteralType, BooleanLiteralType, etc.)
>
> * Unify all: always unify the variables with all defaults. This is the
> conservative choice in that it gives an error if there is any doubt.
>
> * Prefer literal: always prefer IntegerLiteralType (Int). This is the
> maximally backwards compatible choice, but I think it leads to very
> surprising outcomes.
>
> * Prefer user: always the user-defined choice. This is simple from one
> point of view, but does lead to a potentially counterintuitive result for
> example 2.
>
> * Do What I Mean (DWIM): Prefer the user-defined default, except in the
> case where the variable is unified with an integer literal *and* the
> user-defined default isn't IntegerLiteralConvertible. This is complex to
> say but leads to sensible results on both examples. (Basically: prefer
> user, but fallback to IntegerLiteralType if the user default doesn’t
> actually make sense)
>
> | Strategy       | Example 1 | Example 2 |
>
> | -------------- | --------- | --------- |
>
> | Unify all      | Error     | Error     |
>
> | Prefer literal | Int       | Int       |
>
> | Prefer user    | Int64     | Error     |
>
> | DWIM           | Int64     | Int       |
>
>
> Personally, I’ve always favoured DWIM. Especially in Swift where
> IntegerLiteralType inference is so frequently used (you don’t want adding a
> default to cause code to stop compiling!). In practice I don’t expect there
> to be many cases where this ambiguity actually kicks in, as it requires the
> user-specified default to be a LiteralConvertible type that isn't the
> relevant LiteralType, and for the type variable to affect an actual
> Literal. So <T=String>(x: T) never causes problems, but <T=StaticString>(x:
> T) does.
>
> As for the matter of “what if I want the other one” — you clearly know the
> actual type you want; just name it explicitly.
>
>
> We could just remove that parameter from the type inference system... if
> the default value is Int and the user passes in a String, that'd be an
> error, unless the user also sets that parameter to String.
>
> I'd envisioned using default parameters as more "compile-time
> configuration options" than something for the type system to actually have
> deal with. IIRC, I was trying to come up with a way to write just one
> arbitrary-sized integer struct where I *could* specify the width of its
> internal calculations, but would usually just use a default value.
>
> - Dave Sweeris
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170124/2e095349/attachment.html>


More information about the swift-evolution mailing list