[swift-evolution] Default Generic Arguments
Xiaodi Wu
xiaodi.wu at gmail.com
Wed Jan 25 19:15:11 CST 2017
Srdan, I'm afraid I don't understand your discussion. Can you simplify it
for me by explaining your proposed solution in terms of Alexis's examples
below?
```
// Example 1: user supplied default is IntegerLiteralConvertible
func foo<T=Int64>(t: T) { ... }
foo(22)
// ^
// |
// What type gets inferred here?
```
I believe that it is essential that the answer here be `Int` and not
`Int64`.
My reasoning is: a user's code *must not* change because a library *adds* a
default in a newer version. (As mentioned in several design docs, most
recently the new ABI manifesto, defaults in Swift are safe to add without
breaking source compatibility.)
Here, if version 1 of a library has `func foo<T>(t: T) { ... }`, then
`foo(22)` must infer `T` to be `Int`. That's just the rule in Swift, and it
would be severely source-breaking to change that. Therefore, if version 2
of that library has `func foo<T=Int64>(t: T) { ... }`, then `foo(22)` must
still infer `T` to be `Int`.
Does your proposed solution have the same effect?
```
// Example 2: user supplied default isn't IntegerLiteralConvertible
func bar<T=Character>(t: T) { ... }
bar(22)
// ^
// |
// What type gets inferred here?
```
By the same reasoning as above, this ought to be `Int`. What would the
answer be in your proposed solution?
On Wed, Jan 25, 2017 at 2:07 PM, Srđan Rašić <srdan.rasic at gmail.com> wrote:
> That's a good example Alexis. I do agree that generic arguments are
> inferred in a lot of cases, my point was that they should not be inferred
> in "type declarations". Not sure what's the right terminology here, but I
> mean following places:
>
> (I) Variable/Constant declaration
>
> ```
> let x: X
> ```
>
> (II) Property declaration
>
> ```
> struct T {
> let x: X
> }
> ```
>
> (III) Function declaration
>
> ```
> func a(x: X) -> X
> ```
>
> (IV) Enumeration case declaration
>
> ```
> enum E {
> case x(X)
> }
> ```
>
> (V) Where clauses
>
> ```
> extensions E where A == X {}
> ```
>
> In those cases `X` should always mean `X<Int>` if it was defined as
> `struct X<T = Int>`. That's all my rule says. Sorry for not being clear in
> the last email :)
>
> As for the other cases, mostly those where an instance is created,
> inference should be applied.
>
> Let's go through your examples. Given
>
> struct BigInt: Integer {
> var storage: Array<Int> = []
> }
>
> func process<T: BinaryInteger>(_ input: BigInt<T>) -> BigInt<T> { ... }
>
> what happens with `let val1 = process(BigInt())`? I think this is
> actually the same problem as what happens in case of `let x = BigInt()`.
>
> In such case my rule does not apply as we don't have full type
> declaration. In `let x = BigInt()` type is not defined at all, while in `func
> process<T: BinaryInteger>(_ input: BigInt<T>) -> BigInt<T> { ... }` type
> is explicitly weakened or "undefaulted" if you will.
>
> We should introduce new rule for such cases and allowing `Storage=Int`
> default to participate in such expressions would make sense. As you said,
> it also solves second example: let val2 = process(0).
>
> I guess this would be the problem we thought we were solving initially and
> in that case I think the solution should be what Doug suggested: if you
> can’t infer a particular type, fill in a default.
>
> Of course, if the default conflicts with the generic constraint, it would
> not be filled in and it would throw an error.
>
> For the sake of completeness,
>
> func fastProcess(_ input: BigInt<Int64>) -> BigInt<Int64> { ... }
> let val3 = fastProcess(BigInt())
>
> would certainly infer the type from context as my rule does not apply to
> initializers. It would infer BigInt<Int64>.
>
> As for your last example, I guess we can't do anything about that and
> that's ok.
>
>
> On Wed, Jan 25, 2017 at 7:50 PM, Alexis <abeingessner at apple.com> wrote:
>
>> Yes, I agree with Xiaodi here. I don’t think this particular example is
>> particularly compelling. Especially because it’s not following the full
>> evolution of the APIs and usage, which is critical for understanding how
>> defaults should work.
>>
>>
>> Let's look at the evolution of an API and its consumers with the example
>> of a BigInt:
>>
>>
>> struct BigInt: Integer {
>> var storage: Array<Int> = []
>> }
>>
>>
>> which a consumer is using like:
>>
>>
>> func process(_ input: BigInt) -> BigInt { ... }
>> let val1 = process(BigInt())
>> let val2 = process(0)
>>
>>
>> Ok that's all fairly straightforward. Now we decide that BigInt should
>> expose its storage type for power-users:
>>
>>
>> struct BigInt<Storage: BinaryInteger = Int>: Integer {
>> var storage: Array<Storage> = []
>> }
>>
>>
>> Let's make sure our consumer still works:
>>
>>
>> func process(_ input: BigInt) -> BigInt { ... }
>> let val1 = process(BigInt())
>> let val2 = process(0)
>>
>>
>> Ok BigInt in process’s definition now means BigInt<Int>, so this still
>> all works fine. Perfect!
>>
>>
>> But then the developer of the process function catches wind of this new
>> power user feature, and wants to support it.
>> So they too become generic:
>>
>>
>> func process<T: BinaryInteger>(_ input: BigInt<T>) -> BigInt<T> { ... }
>>
>>
>> The usage sites are now more complicated, and whether they should compile
>> is unclear:
>>
>>
>> let val1 = process(BigInt())
>> let val2 = process(0)
>>
>>
>> For val1 you can take a hard stance with your rule: BigInt() means
>> BigInt<Int>(), and that will work. But for val2 this rule doesn't work,
>> because no one has written BigInt unqualified. However if you say that the
>> `Storage=Int` default is allowed to participate in this expression, then we
>> can still find the old behaviour by defaulting to it when we discover
>> Storage is ambiguous.
>>
>> We can also consider another power-user function:
>>
>>
>> func fastProcess(_ input: BigInt<Int64>) -> BigInt<Int64> { ... }
>> let val3 = fastProcess(BigInt())
>>
>>
>> Again, we must decide the interpretation of this. If we take the
>> interpretation that BigInt() has an inferred type, then the type checker
>> should discover that BigInt<Int64> is the correct result. If however we
>> take stance that BigInt() means BigInt<Int>(), then we'll get a type
>> checking error which our users will consider ridiculous: *of course* they
>> wanted a BigInt<Int64> here!
>>
>> We do however have the problem that this won’t work:
>>
>>
>> let temp = BigInt()
>> fastProcess(temp) // ERROR — expected BigInt<Int64>, found BigInt<Int>
>>
>>
>> But that’s just as true for normal ints:
>>
>>
>> let temp = 0
>> takesAnInt64(temp) // ERROR — expected Int64, found Int
>>
>>
>> Such is the limit of Swift’s inference scheme.
>>
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170125/e6b884dd/attachment.html>
More information about the swift-evolution
mailing list