Future: On value-based generic arguments for functions

For generic types we can explicitly mention the generic arguments:

Array<Int>

Can we do the same for generic functions? As far as I know, the answer is no.

If I'm correct here, is that going to change when/if value-based generic parameters are added? Or are we going to be stuck with requiring every generic argument having to be able to be reversed-engineered from the call-arguments' types?

If support is added, what are everyone's ideas for the syntax? Could we use C++ syntax, which it applies to both the declarations of templated items and their usage (where Swift here uses the syntax for both type and function declarations but only for type usages)?

1 Like

We can already do this, no?

print(((Int) -> Int).self)
// (Int) -> Int

print(Array<Int>.self)
// [Int]

But in order to use the same syntax we would need functions to be nominal types, which they currently aren't. Then you'd get something like Function<Int, Int>.

I mean if we have

func test<T>(_ x: T) { /* ... */ }

we can only call it with

test(6)

AFAIK we can't use

test<Int>(6)

Or am I wrong here? If not, should this be changed, especially before value-based generic parameters are added? This is a required step before using arguments that aren't implied by the call's arguments.

I think this would be a good addition to the language eventually, but I've never personally been held back by the lack of the feature. Even when/if value-based generics are added, I suspect their primary use-case will be in the generic parameter lists of types, not functions. In a lot of cases, even with functions parameterized by values, those values will be able to be inferred, like so:

struct FixedSizedArray<Element, Count: Int> {
    //...
}
func foo<Count: Int>(_ array: FixedSizedArray<Foo, Count>) {}

This is getting a little ahead, I think. If value generic parameters are added, then we will need some way to specify them, but we don't need to fix that problem before we have value generic parameters, or at least a serious proposal for them (although maybe I've missed it?). Pre-emptively solving a problem before fully understanding what the motivation will look like seems like a recipe for not completely covering every aspect.

In any case, currently, if a parameter can't be inferred from the arguments, the recommended fix is to pass a type argument, e.g.:

// instead of:
func foo<T>() {}
foo() // what's T?

// use:
func bar<T>(_: T.Type) {}
bar(Int.self)

I'd guess this style could be extended to value generic parameters too.

Do you happen to know why it's like that? i.e. why can't we just write foo<Int>() directly?

I always wondered about this. It feels like a bit of an arbitrary limitation.

2 Likes

The reason could be that it would read like an init for a type named foo:

typealias foo = Optional

print(foo<Int>(0))
// Optional(0)

By the way, @huon's example made me of a use case why we would want some like this. (Because the whole concept didn't really click with me before.)

I currently make heavy use of two extensions on Mirror for a DSL:

public extension Mirror {
  func children <T> (_ type: T.Type) -> [T] {
    return self.children.fmap({$0.value}).filter({$0 is T}) as! [T]
  }

  func descendants <T> (_ type: T.Type) -> [T] {
    return self.children.fmap({$0.value}).bind({(element: Any) -> [T] in
      let mirror = Mirror(reflecting: element)

      if element is T {
        return [T].pure(element).mappend(mirror.descendants(type))
      }

      return mirror.descendants(type)
    }
  }
}

Calling children(_:) would then be:

struct Struct {
  let integer1 = 1
  let boolean1 = true
  let string1 = "a"
  let integer2 = 2
  let boolean2 = false
  let integer3 = 3
}

print(Mirror(reflecting: Struct()).children(Int.self))
// [1, 2, 3]

(All of the following is currently non-working.)

This could then indeed be rewritten by:

print(Mirror(reflecting: Struct()).children<Int>())
// [1, 2, 3]

But then I'd personally even go a step further; allow generics on computed properties as well and extend the syntax:

public extension Mirror {
  var children <T>: [T] {
    return self.children.fmap({$0.value}).filter({$0 is T}) as! [T]
  }
}
print(Mirror(reflecting: Struct()).children<Int>)
// [1, 2, 3]

I think once upon a time we thought it would make parsing and type-checking simpler if we could assume that attaching angle brackets to something always meant it was a type, but I'm not sure that's true any more. (That same line of thought led to [Foo.Bar]() not working for a long time even though Array<Foo.Bar>() was fine.) It's entirely possible that today's compiler could support this pretty easily, but I haven't looked into it recently.

One interesting note is that not having this sometimes makes source-compatible changes easier. For instance, a second generic parameter can be added to a function as long as all existing calls continue to work. Generic parameters in functions can also be reordered freely, which they can't be in classes. (I'm not sure why someone would go out of their way to do this, but maybe a bunch of different functions were added separately and now someone is tidying them up to be consistent.)

@Douglas_Gregor, do you remember any more history here?

7 Likes

I feel like there are design options that could reduce friction here as much as possible. E.g., we could allow inference of some generic parameters (as well as default types) and have a rule that partial list match the earliest parameters first). Concretely, you could define a function as

func foo<T>(t: T) { ... }

and call it as

foo<String>(t: "bar")

Then, if you need to add a generic parameter later, you could write it as:

func foo<T, U = Int>(t: T, u: U = 0) { ... }

or even just

func foo<T, U>(t: T, u: U = 0) { ... }

which, when called as

foo<String>(t: "bar")

would be equivalent to both of the following

foo<String, Int>(t: "bar", u: 0)
foo(t: "bar", u: 0)

EDIT: On second thought, I'm a bit confused about how you could add such a parameter... the code below fails to compile:

func foo<T, U: ExpressibleByIntegerLiteral>(t: T, u: U = 0)
{
    print(t)
    print(u)
}

foo(t: 5) // Generic parameter 'U' could not be inferred

How is it possible right now to add a generic parameter and have existing calls function properly?

I was thinking of generalization:

// Before:
func repeatingThrice<Result: RangeReplaceableCollection>(
  _ str: String
) -> Result
where Result.Element == String {
  var result = Result()
  result.append(str)
  result.append(str)
  result.append(str)
  return result
}
// After:
func repeatingThrice<Result: RangeReplaceableCollection, Value>(
  _ val: Value
) -> Result
where Result.Element == String {
  var result = Result()
  result.append(String(describing: val))
  result.append(String(describing: val))
  result.append(String(describing: val))
  return result
}

@jrose is certainly correct that this was originally a compiler limitation; we didn't get around to implementing support for this syntax in the type checker in the early days.

Later on, the additional of the f<T, U>() syntax for specifying generic arguments of a generic function was discussed in some of the Swift language design meetings. We decided at that time not to add the feature, because we felt it unnecessary:

  • Type inference can usually get the right answer. When it can't, one can often use as to fix it. This works well for, e.g., numericCast(_:).

  • Sometimes a function wants one of the types to be explicitly specified. For that, we felt that a an explicit (possibly-labeled) argument that takes a metatype (e.g., T.self) was a better solution. unsafeBitCast is a good example here, because type inference here can be a bit... unsafe.... Explicit metatype arguments can also handle dynamic types. For example, I could have: var foo: Superclass.Type = Subclass.self; unsafeBitCast(something, to: foo) and that will pass through (dynamically) the subclass's metatype. Explicit generic arguments would likely not handle that, because they deal in static types.

    Doug

1 Like