[Discussion] enable `some` in closure parameter position

Current Situation

A few weeks ago, SE-0328: Structural opaque result types was accepted with modification :tada:

As the quote, one of the modifications is relating to some in parameter position. In this post, I'd like to share my thought about this point.

In the discussion of SE-0328, a conflict was found as a problem. Roughly speaking, it is the conflict between structural opaque result types and 'generalized some syntax' which has been suggested for years.

// structural opaque result type
let closure: (some Numeric) -> () = { (value: Double) in print(value) }

// generalized some syntax (generics)
func function(value: some Numeric) { print(value) }

The behavior of closure can be naturally inferred from the behaviors of other structural opaque result types. closure and function have the same type at a grance. However, their behaviors are exactly opposite; function works as generic function.
The modification is that Swift forbids using, for example, (some Numeric) -> () for a while. But there seems no appropriate solution making both closure and function possible.

Simpler problem appears in the next example. How can one understand why the first some Numeric is generic and the second some Numeric is reverse generic? Generalized some syntax has confusing problem from the beginning.

// generalized some syntax (generics) and opaque result type
func function(value: some Numeric) -> some Numeric { value }

It is not good to keep the situation as pending. The limitation of some usage in parameter position is very artificial considering 'future direction' which most of Swift developers do not know. We should lift it as soon as possible.

Proposed Solution

Currently, we are trying to introduce three things:

  1. Structural opaque result types with some syntax (SE-0328)
  2. Generalized some syntax (shorthand for generics)
  3. Existential any syntax (explicit marker for exsitentials, SE-0335)

It was ideal that they are compatible with each other. However, 1 and 2 are making collision. There is no way to solve the problem rather than using a different marker. Since it is difficult to consider using some for generics and not use for the whole structural opaque result types, generics should use other different marker.

  1. Structural opaque result types with some syntax
  2. Generic ??? syntax (shorthand for generics)
  3. Existential any syntax (explicit marker for existentials)

This is maybe the simplest and the most consistent solution. There are no artificial limitation and double meaning. With new marker, there is clearer distinction.

// closure and function behave equally
let closure: (some Numeric) -> () = { (value: Double) in print(value) }
func function(value: Double as some Numeric) { print(value) } // pseudo syntax

// closure and function behave equally
let closure: (??? Numeric) -> () = { print(value) }
func function<T: Numeric>(value: T) { print(value) }
func function(value: ??? Numeric) { print(value) }

// clear distinction between generics and reverse generics
func function(value: ??? Numeric) -> some Numeric { value }

// short hand for generic result types
func function() -> ??? Numeric { 42 }

There is room for bikeshedding about ??? marker. The marker should be carefully selected considering relationship among any,some, and ???.

Summary

In summary, I suggested two points in this thread.

  • Using some for generics causes conflict with structural opaque result types. It is better to use some exclusively for reverse generics.
  • For generics shorthand, other marker should be come up.

I strongly believe this is the almost only possible solution. I'd like to hear the evaluation of this solution, and what is appropriate for ??? marker. Any thoughts?

References

I'm not sure what has changed since the prior discussion of SE-0328 that you link to. You've summarized your viewpoints; I will not bother rewriting mine but incorporate them here by referring readers to that link.

Briefly, you and I disagree, as I believe that some on the left-hand side of the function arrow should refer to generic parameters and not "reverse" generic parameters, both because the alternative you propose is opposite to how subclassing [edit: and protocol requirement fulfillment] works (inconsistency argument) and dramatically less useful (practicality argument).

The discussion in the Rust community regarding this issue (where the analogous feature is spelled impl T rather than some T) is illuminating and should be referred to:


If, on the other hand, feedback from the community is that using some in the same way that Rust uses impl is too confusing, then I would argue:

  • some should not be allowed in parameter position at all (the status quo), because a shorthand for reverse generics in parameter position will be of low use (practicality argument) and it would be confusing since others will expect it to be a shorthand for "non-reverse" generics (inconsistency argument); instead, we should build out a longhand version of reverse generics by allowing named parameters in angle brackets on the right side of the function arrow;

  • and similarly, nothing should be the shorthand for generics (the status quo) because requiring a different word ("???") would result in treating POP differently from OOP (and protocol constraints from protocol requirement fulfillment)—unnecessarily so—with respect to the function arrow (inconsistency argument).

3 Likes

The main reason to create this thread was to have the place for discussion and the problem being more known. SE-0328 thread was not appropriate place to discuss this specific issue.

I added it to the post!

I almost agree with your argument of practicality. It is impossible for me to consider practical usages of such functions. But practicality doesn't support exclusion of a feature. The function in the following code doesn't have practicality, but there is no reason to ban it.

// there is no way to call the closure
func foo<T>(closure: (T) -> ())

If there is no other way rather than using some for generics, then we have enough reason to ban (some P) -> (). However, since there are alternative ways to achieve shorthands for generics, banning (some P) -> () doesn't seem to have sufficient reason to introduce such an artificial limitation.


Are you mentioning this? About it, I think this is just the difference in the way of considering.

some P doesn't work how subclassing works even now.

// subclassing (Dog: Animal)
var da: (Dog) -> Animal = { $0 }
var dd: (Dog) -> Dog = { $0 }
da = dd

// some syntax (T: P)
var ts: (T) -> some P = { $0 }
var tt: (T) -> T = { $0 }
ts = tt // error, because some P is not always T

In the code below, the behavioral type of argument and result of foo is the same, but in bar it's not. Even in generalized some syntax there is a behavioral inconsistency.

// the two 'C' is 'the same type' at least being C
func foo(value: C) -> C { value }
var c: C = C0()
c = foo(value: c)

// generalized `some` syntax
// the two 'some P' is not the same type
func bar(value: some P) -> some P { value }
var p: some P = P0()
p = bar(value: p)  // error

I'm not sure if it is valid to assume behavioral consistency between the subclassing/any types and some/??? types. In many aspects, some syntax behaves differently to how subclassing works. I cannot understand why consistency in subclassing and some/??? syntax is required.

From the beginning, I think (C) -> C is just taking C and returning C, not caller/callee-decided subclass of C. The callee decided to just take C and return C. Likewise, (any P) -> (any P) is just taking any P and returning any P. The callee can also use some to tell 'callee-decided opaque type' and ??? to request 'caller-decided generic type'. Am I missing something?

Of course some P doesn't work like subclassing; they're two distinct features! One feature isn't inconsistent because it's not identical to another feature; rather, a feature is inconsistent if it behaves differently in aspects that are not required to serve its purpose or justification for existing as a distinct feature.

My argument that you quote is that who chooses is dictated by position relative to the function arrow (in other words, parameter versus return type), and it would be inconsistent if that weren't the case for some P also, since it is absolutely the case for classes.

On a more basic level, moreover, everyone knows that the caller chooses the parameter value (i.e., argument) and the callee chooses the return value. This is never marked with distinct syntax, but based on position relative to the function arrow. In fact, when the callee can modify a parameter value, that's when we add the notation inout; in other languages where there is the notion of out parameters, those similarly require special notation, not the ordinary in parameters.

Likewise, when it comes to inheritance or protocol conformance, Swift allows contravariant parameter types and covariant return types. This is not achieved by distinct spelling, but once again simply by their position relative to the function arrow. Swift does not support covariant parameter types (yet), but in languages such as Dart where covariant parameter types are allowed, it's those that are marked differently with a keyword, not the contravariant parameter types.

Stated simply, given any unadorned (Foo) -> Bar, I expect that as the caller I get to choose the value and subtype (if any) of Foo and the callee gets to choose the value and subtype (if any) of Bar. I would be surprised if this did not hold in Swift or any similar language. However, if some P is used as a shorthand for "reverse generics," then that would break this basic understanding of how function arrows work. That I regard as inconsistent, because the operator -> shouldn't change meaning based on what Foo and Bar are.

5 Likes

a feature is inconsistent if it behaves differently in aspects that are not required to serve its purpose or justification for existing as a distinct feature.

If some is a shorthand of generics in parameter position, then the decider inconsitency can be a problem. But I'm proposing some as the shorthand of reverse generics. Being chosen by the callee is the fundamental and necessary part of 'reverse generics', isn't it? It doesn't unnecessarily behave differently.

Stated simply, given any unadorned (Foo) -> Bar, I expect that as the caller I get to choose the value and subtype (if any) of Foo and the callee gets to choose the value and subtype (if any) of Bar.

I'm not sure what will the some/??? syntax break practically after all. some/??? in parameter position is still only in consuming position, and some/??? in result position is only in producing position too. The meaning of function arrow does not change. I think the source of your confusion is mixing up the idea of substituting a type parameter with concrete type and choosing underlying type of a supertype. They are two distinct concept. Basically, type parameters are decided at compile time, while underlying types can be even random at runtime. I admit some as generics can be explained in your way, but it's not an essential part.


FWIW, using some only for reverse generics, some P behaves as if it is the subtype of any P. This in itself does not support either argument, but I found it interesting.

To be clear, there is no actual feature named “reverse generics.” The feature spelled some P is an opaque type, and the question to be answered is: opaque to whom?

I was one of the first folks here who pointed out that, when used in return position, an opaque type is like a generic constraint but “reverse” in terms of who chooses the underlying type (caller versus callee). The term “reverse generics” was meant as a way of describing what an opaque type is when used in return position—that is, relative to the function arrow.

In a scenario such as let x: some Numeric = 42 as Int (when supported), it’s still an opaque type that’s involved but there is no sense in which there is anything “reverse” about it.

I’m no more confused about the distinction between type parameters and subclassing any more than I am about the distinction between types and values. What I’m saying is that the topic here is the question of who chooses relative to a function arrow, and in all of these scenarios the caller chooses on the left-hand side and the callee chooses on the right-hand side.

I don't know if this is the correct thread to post this, but the Motivation section of the Rust RFC that accepts impl Trait (more or less our some P) as an argument (after it was already used as a return type), is really well written. The critique rebuttals are quite interesting as well.

I also learned today, in The Evolutions of Lambdas in C++14, C++17 and C++20, that C++ accepts auto for both caller-supplied types and callee-supplied types.

Reasoning by analogy is full of traps, so I wouldn't personally rush to conclusion. Yet, right in front of my eyes, I see two major languages that tend to agree that programmers have a very deep intuition around the difference between arguments and return values, and "who" provides which (amongst caller and callee) (quoted from the Rust RFC).

7 Likes