More consistent function types

Spun off at Single-element labeled tuples?

1 Like

Now I’m thinking of something an old math teacher told me about an answer to a math-competition question. The answer was the empty set. The student wrote that as {∅}. The judges rejected it as a set of one element (which was itself a set, the empty set). I guess you’re siding with the student, while I agree with the judges.

3 Likes

FWIW, I know the difference between an empty set and a set containing an empty set. (Being married to a peano player, ahem.) It’s just that for a casual language user like me it’s sort of non-intuitive that I can’t use generics to arrive from (T) -> () to () -> () by using Void for T. Even though it’s obvious now that I know that Void is a typealias for an empty tuple. I guess the moral of the story is to be a bit more careful before posting to Evolution as a casual language user :–)

1 Like

After re-reading the proposal which suggested removing tuple splat and looking at your example here:

I have to agree with @xwu. This pitch would definitely seem like a request to reinstate tuple splatting. In addition, I think that it would make it more difficult to reason about generic functions as well. For example, if I have a functions () -> U and (T) -> U, I don't know why I should expect to be able to use T in a function that has no arguments, and conversely , why I should expect not to use T in a function that I have defined as taking it in as an argument (T) -> U. Isn't this part of why we have type safety in Swift? So that we may easily reason which types are where and not reach a point where we have been handed a type that is unexpected? It seems like allowing () -> U to "equal" (T) -> U is an opening for complexity just for the sake of not having to explicitly write () -> U and (T) -> U, but instead be allowed to write (T) -> U alone, intend to mean both, and even intend to mean (A, B, ... ) -> U as well. I'm just a hobbyist when it comes to programming so please forgive me if I have said anything to offend, certainly not intended. I recall there being a lot of heated discussion on this topic in the past. I am just trying to understand how this would help the language beyond syntactic sugar.

1 Like

No. I don't know why I can't properly communicate what I am advocating.

It appears that functions have a minimum of zero and a maximum of one return value. But in reality functions have a minimum of one and maximum of one (exactly one) return value.

On the other hand, functions can have zero or more arguments; a mix of simple arguments, inout argument and/or vararg argument. This proposal is about changing the rule for arguments so that each functions have one or more arguments.

"No return value" for a function actually means a return type of Void and a fixed return value of () (the sole value of type Void).

This proposal want to make "No input argument" to actually mean one input argument of type Void with the argument value of ().

The above two are considered more consistent (both starting at one, rather than permitting really no input argument, but not permitting really no returns argument).

I quote Mark again:

This has nothing to do with splatting.

A signature like this:

func f<T>() -> () -> U

would be sugar for:

func f<T>(_: Void = ()/*[1]*/ ) -> (Void) -> T
// [1]: Look at this as the sole value of type Void. It just happens to be an empty tuple

Just as:

func f<T>(_x: T)

is a sugar for:

func f<T>(_ x: T) -> Void

Is there any tuple splats here?

Yes, it's precisely equivalent to splatting: you make a single argument that's a tuple of n elements equivalent to a list of n arguments. In this case, it's about specifically n = 0.

OK, I admit it is one way of looking at it and I guessed someone would bring that up. That is why I put this note:

But what you say is also valid and a different way of looking at it.

Yes, and in the same vein, the current convention for result types is precisely equivalent to unsplatting. You declare a function with no return value, and we return a tuple of zero elements, which can be passed to a function that accepts such a type.

1 Like

Would this remove the zero-vs-one-argument duplication from here?

If so, big +1 from me. If not, normal-sized +1 from me.

Unless I am missing something, this is a one-vs-two-argument difference, so no.

(I won't hijack the thread by giving details on how I think with extra runtime overhead you can remove the duplication, but we can discuss in Users or somewhere else if you'd like).

I agree that swift functions are unbalanced: we are permitted to have zero arguments, but we are required to always have at least one return (even if it is of type Void). This is indeed an inconsistency that I have even noted myself. However, my argument is from the other angle: why can we not truly have a no return function: func noArgNoReturn() should not return even void. To me this would be the better option as far as symmetry is concerned. Both sides would then have the ability to contain 0 to n elements. I am curious to your's and @hooman's thoughts are on this.

The other problem I see, is that you are asking for the type Void be allowed to equal whatever type T becomes at compile time. Again, this also goes completely against the type safety that has progressively been put into place over each version. Great lengths have been made to limit the use of Any and AnyObject in favor of explicit types. I believe that could even be said regarding the generic types. While they don't have an actual inherent type when you're writing your code, they do expect T to not be A to not be U or however many generics are available in that signature. For Void to allow T or A, B, etc. is certainly very C, but it is not very Swift.

func noArg() -> ()
func oneArg<T>(T) -> ()
func takesNoArgFunc(() -> ()) -> ()

takesNoArgFunc(noArg) /* the types matches, so this is allowed */
takesNoArgFunc(oneArg) /* the types do no match here, and should not be allowed */
/* Even if it did accept the function, the type signature is not expecting `T`, so what should we expect the function to do with `T`? */

Removing Void implicit return type is out of the question. It will be seriously source breaking without a tangible benefit.

The reason this proposal is put forward is to reduce the source breaking effect of enforcing a change in Swift arguments model that will show up in Swift 4.1. Up to Swift 4.0 no-arg functions have been compatible with Void taking functions.

@xwu opposes this because this proposal itself is source breaking, although it reduces the source breaking effect of the newly enforced parameter model. Mark and I believe that this change reduces overall amount of actual source break and is desirable for generic programming.

Concerning T matching Void: T is an unconstrained generic type, and Void is a type. I acknowledge that this is surprising if you consider Void to be the absence of argument or return value. This does not cause a problem in generic code. If your generic code really doesn't have any requirement for a type (protocol constraint, etc) then Void is as good a type as any.

I am not sure if I understand what you mean in your sample code:

It does not match now and will not match as a result of this proposal.

The following example will match as a result of this proposal which may be surprising:

func takeFunc<T,U>(_ f: (T)->U) { print("Worked!") }
func noArg() -> Int { return 0 }

takeFunc(noArg) // Will accept it and print "Worked!"
// Actually this works now with the shipping 4.0.3 compiler, but
// will stop working without the proposal when Swift 4.1 ships. 

But again if we look for surprises, the other surprise will remain. Given the same takeFunc above:

func noResult(_ x: Int) { /*...*/ }
takeFunc(noResult) // Will accept it and print "Worked!"
// This is working now and will keep working after 4.1 release.

If you want to avoid such cases, you need to add explicit overloads of the function to handle these cases differently if you want to treat no-arg or no-result functions differently:

func takeFunc<T>(_ f: (T)->Void) { /* No Result */ }
func takeFunc<T>(_ f: ()->T) { /* No Argument */ }
func takeFunc(_ f: ()->Void) { /* No Argument and No Result */ }
1 Like

A small correction here. I added a hack to swift-4.1-branch to keep allowing this for -swift-version 4.

The general point is correct, though - This once worked by accident and no longer works on master as a result of improvements that were made to the way functions types are represented. Those improvements are ongoing. Once those improvements are complete we should see far less of this sort of unintended behavior than we've seen in the past.

This pitch is all about recognizing that the particular allowance for inferring T to be Void in:

  func f(_ fn: () -> ()) {...}
  func g<T>() -> (T) -> () {...}
  f(g())

much like we infer U to Void in:

func apply<T, U>(_ x: T, _ fn: (T) -> U) -> U { return fn(x) }
func i(_ i: Int) { ... }

apply(3, i)

is useful to avoid code duplication and/or odd work-arounds.

3 Likes

Its a corner case, but it would be nice if it worked. You could easily imagine that this would be a real pain with some generic algorithms.

I thought that might be the case, but I also thought it was worth mentioning as an alternative. I also see that by allowing the return list to be zero, “no return”, that when working with genetics you no longer could infer ‘Void’ to be ‘T’ anymore.

I believe this example is to be at least one of the major use cases you are concerned about in regard to keeping this feature. I see the convenience here, but I also think it allows for “unsplatting” (as @rudkx pointed out). This may be a happy side effect, but it still allows, as you have shown above, use of function types where one may not expect them to work, at first glance. I won’t push this issue further as I see why this is a “certainly cannot do”. It was just a thought.

By this I meant non-matching types () -> () and (T) -> () should not be expected to be used where the other is requested. In my example I accidentally reversed them from your examples and created confusion. My apologies.

I see what you mean here. However, how do we prevent splatting, but still allow for the inference of () -> () in a pace where (T) -> () is expected? If we allow T to be () -> () we also allow T to be anything as @MutatingFunk described above. Or is this also something that you would like to allow for generic functions.

Definitely not all of these things.

Let's look at each example specifically.

This is the only thing proposed. Effectively redefine ( ) -> U to be synonymous with (()) -> U.

We don't have single element tuples as a result of the syntactic ambiguity. We could have unambiguous single-element labeled tuples, but do not as mentioned here.

So no, this doesn't happen and this proposal doesn't change that in any way.

This happens today, and is completely legitimate. You can infer T to be a single argument, of tuple type. However that does not imply:

Which is splatting.

1 Like

I just replied to @MutatingFunk. There is no splatting being proposed here. It's perfectly legitimate to infer a single argument T to be anything, including tuples, function types, etc. This happens today. It's not legitimate to take a T that has been inferred to a N-tuple and use it in a location where N independent arguments are expected. That is tuple splatting and would not be a natural consequence of this proposal.

1 Like

Agreed

Also agreed, hence my example of your suggestion was simply:

  • '(T) -> U where T == (A, B)''((A, B)) -> U' (multiple elements)

Understood, I wasn't meaning to suggest this proposal changed the status quo here. My reasoning is that there is already an equivalency in the language between single-element tuples and raw types, to the point that removing single-value tuples as their own entity was deemed acceptable. To my knowledge, (T) and T have always had the exact same meaning in Swift.

My understanding of your suggestion is that you are satisfied with using tuples as tuples (no splatting), so my examples translated the generics to tuple-taking functions, then added your suggested sugar to the empty-tuple case.

My intent was to illustrate the subtle difference between the essence of your proposal (an indirect equivalency between (T) and ()), and that of tuple-splat (a direct equivalency).

Your suggestion uses Void similar to how C uses it, where a function taking no arguments is spelled f(void). Introducing this spelling to Swift, as you suggest, would make generics just that bit nicer.

In Swift, however, Void == (). A call to f(_: Void) can be spelled f(()), making the effect of your suggestion a subset of tuple splat, when the reasoning behind the suggestion is in fact quite different. You have to consider that, however different your reasoning is, existing Swift syntax means that you are in fact proposing a subset of tuple splat, and you'll get the pushback that implies.

Yes, and that meaning is T because the parens here are superfluous, as they are in (42).

1 Like

I'll point out that SE-0066 directly touches on the ambiguity of () -> T.

This is the proposal that introduced the requirement of explicit parens around function arguments in function types.

1 Like