[swift-evolution] Default Generic Arguments

Srđan Rašić srdan.rasic at gmail.com
Wed Jan 25 14:07:32 CST 2017


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/29cafc9d/attachment.html>


More information about the swift-evolution mailing list