Support `try` for default parameters

Consider the following:

func foo(bar: Bar = default()) throws {
}

This is fine if default is nonthrows, but if default throws it's not allowed. This complicates composing throwing functions with default values. I want to use throwing expressions as a default value, like

func foo(bar: Bar = try default()) throws {
}

which would be semantically equivalent to

func foo(bar _bar: Bar? = nil) throws {
    let bar: Bar
    if let _b = _bar { bar = _b }
    else { bar = try default()}
    ...
}

(except for the detail that you can pass nil to this function, whereas you can't pass nil to the other one).

The related case

func nonthrowing(bar: Bar = try default()) {
}

is still not allowed, since the error would be unhandled.

9 Likes

You don't need to do this, you can instead write:

func foo(bar: Bar? = nil) throws {
    let bar = try bar ?? default()
    ...
}

But now bar also has to be unwrapped, adding yet more clutter

+1 on the pitch. there's no reason why it shouldn't be allowed.

Adding syntax needs to meet a high bar, and doing so to avoid one particular scenario where a one-line unwrapping statement will do surely doesn’t meet that bar.

1 Like

A cleaner semantics would be to implicitly define an overload:

func foo(bar: Bar = try default()) throws {}

Being semantically equivalent to the definitions

func foo(bar: Bar) throws {}
func foo() throws {
	let bar: Bar = try default()
}

This makes it more clear that optionals are not involved, and that the exception is thrown inside the function, not inside the caller. It also allows for (if considered desirable) cleanly extending the proposal to cover your related case:

func maybethrowing(bar: Bar = try default()) {}

Being semantically equivalent to the definitions

func maybethrowing(bar: Bar) {}
func maybethrowing() throws {
	let bar: Bar = try default()
}
3 Likes

This is existing syntax, in a new natural place. It's more of an oversight than anything. It's just like how throws isn't allowed on properties and subscripts.

5 Likes

There isn't much of a principled distinction between "existing syntax, in a new natural place" and new syntax. Throwing properties and subscripts would be new syntax in my book. The difference is that, unlike this particular suggestion, throwing properties and subscripts aren't substitutes for a straightforward one-liner.

1 Like

This isn't a oneliner for two reasons. It's undesirable to allow an optional to be passed to that parameter, and there's no solution for that. And @rayhamel points out it may be undesirable to promote a function to throws if the caller intends to pass non-default parameters, and the function only requires to be throws in the case the default value is used.

The reason we don't (yet) have throwing properties is because there is a lot of outstanding design work that needs to get resolved. In this thread we are resolving design issues for this feature.

The only design issue so far is @rayhamel's intriguing idea that we should be implicitly defining overloads, which would let us split out things a bit better for throwing vs nonthrowing clients. My initial question about this is why we aren't doing it already for other kinds of default parameters, because at first glance it makes sense to do it there too.

I thought that might be intended to unify different default-value-patterns under one closure definition, (so one closure could be called in different ways) but in my testing it seems we're implicitly removing the default values when we assign to a closure, so that's not very useful. Implicit overloads would improve that behavior, so one could choose a particular overload to be promoted to the closure. So I wonder if we need to be implying overloads more generally?

1 Like

As to the two reasons here: Why is it undesirable to allow an Optional to be passed to that parameter? And in the specific case where throws vs. non-throws depends on default vs. not default, isn't it far more clear to write out the two overloads manually than to create a design where throws means "not throwing unless defaulted"?

I think it's premature to be trying to do that. In this thread, I'm still questioning whether this feature pulls its own weight such that it's a good fit to be added to the language in the first place.

To provide more color here, I have a services layer that does "stuff" on behalf of a user (think like network requests, etc.) So there is a lot of

func thing(argumentForThing: Int, user: User) { ... }

Now 90% of the time, the appropriate user to pass here is some global variable (e.g. the user we logged in as, or some credentials stored in Keychain, etc). But depending on the state of the application there may be no such user or credentials if you have not logged in, and in that case we want a runtime error if you somehow tried to perform an operation. Additionally, in various testing workflows we might want to use some hardcoded test user, distinct from what username and password you filled out in the application's UX.

What falls out of all this is some kind of composing pattern, such as

//Get some user we're logged in as, or throw an error if we're not logged in
func getCurrentUser() throws -> User { ... }
func delete(child: Child, user: User = try getCurrentUser()) { ... }
func delete(parent: Parent, user: User = try getCurrentUser()) {let _ = parent.children.map{delete(child: $0)}}}}

From UI code this might be called as

@IBAction func deleteParent() {
    do {
        delete(parent: currentParent)
    }
    catch {
        //surface error to user
    }
}

Meanwhile in a unit test we do

func testDeleteParent() throws {
    let p = Parent(forTesting: true)
    delete(parent: p, user: testUser)
}

func testDeleteChild() throws {
    let p = Child(forTesting: true)
    delete(child: p, user: testUser)
}

If we assume we can delete parents, children, aunts, uncles, brothers, and sisters. And beyond deleting we can create, edit, send invoices to, disown, mail birthday cards to and so forth, and these operations compose in strange ways (if we disown a relative, we disown the relatives living in the same household, but not e.g. their adult children). What we have now is less of a one-liner for one function and more of a function prelude, across a very large number of functions.

Why is it undesirable to allow an Optional to be passed to that parameter?

The obvious meaning of operation(argument: p, user: nil) is to perform operation without any user credentials. That is probably nonsensical for most operations in my motivating case (if no credentials are required the operation would not take the argument), but in cases where an operation legitimately sometimes takes credentials and sometimes not, we expect the operation(argument: p, user: nil) to do the operation without credentials but in fact the one-liner will try to find some credentials from the current session to perform the operation. This is not what one expects.

isn’t it far more clear to write out the two overloads manually than to create a design where throws means “not throwing unless defaulted”?

Yes, it is more clear. It is also far more tedious, and Swift lacks a preprocessor to manage this for me across the volume of functions involved.

Ah, there's the clincher. Have you considered Sourcery?

Yes I have, and I think this problem is better solved in the language. I think we may just disagree.

I do hope that we'll make some headway within the next few years on hygienic macros in Swift; ideally, such metaprogramming facilities should be a part of the language itself.

I do disagree that we should take specific use cases well solved by metaprogramming (and especially, as you've agreed with me in this case, where such an approach would actually result in code that's more clear) and make each of them a separate syntactic addition to Swift.

I oppose a clear language at the cost of a useful one. rethrows, or for that matter, throws , or for that matter, default arguments, could be solved by metaprogramming, and we got language features. This situation is far less heavyweight than they, because it's an obvious evolution of those pre-existing concepts.

1 Like

Not at all. rethrows, unless I'm mistaken, has semantics that would not be suited for metaprogramming. Likewise, throws requires deep compiler support for even reasonable performance. On the other hand, what we're discussing here is pure sugar--it's more or less equivalent to a textual transformation--and for such a feature, lack of clarity is a nonstarter.

I disagree that the idea lacks clarity, and unless I'm mistaken, throws rethrows and default arguments are semantic sugar. Unfortunately we aren't making useful progress, but I do appreciate the time you've taken to evaluate the pitch.

@rayhamel brought up one of the key issues with clarity involved in this pitch; namely, that it causes (at least visually on inspection) confusion as to whether the error is thrown inside the function or the caller.

This is much the same confusion that was pervasive with var parameters: var meant mutability inside the function not observable by the caller. To alleviate the confusion, var parameters were entirely removed in SE-0003. Instead, you write var i = i inside the function to replace the functionality of var parameters, roughly in the same way that you'd use @nick.keets's Optional unwrapping line today.

I do think you're mistaken here; "sugar" is not an apt descriptor for Swift's entire error handling design.

Well, I appreciate your taking the time to outline your motivation for the pitch; this certainly provides clarity as to what we're accomplishing here. I think you've hit the nail on the head when you remark that "Swift lacks a preprocess to manage this for me," and my goal would be to convince you that in fact that's the solution to the motivating problem.

At the risk of repeating myself, there is no one-liner that works here. So the analogy to SE-0003 isn’t.

I agree there is a metaprogramming solution that involves emitting multiple overloads. I understand your perspective to be that I have too much of a corner case to expect a syntax for Swift to emit overloads here when I can use a third-party preprocessor to emit them. We have plenty of features in the language now that could have been solved in third-party preprocessors, but ultimately, whether a usecase is “useful enough” to be in the language is subjective and there are going to be irreconcilable points of view among people who use the language in different ways.

I understand that you feel this isn’t useful enough to be a feature. That is certainly a valid point of view, but I don’t know what we can say about it other than that we disagree.

Certainly, there is some subjectivity involved and we could disagree in the end as to just how useful it is.

That said, the bar for syntactic additions is very high and not merely "useful enough": it must be "extremely well-motivated" and demonstrate "widespread [...] positive impact," and I think that within any margin of reasonable subjectivity we can agree that this idea doesn't quite get there.

Well no, we do not agree. But I do appreciate your perspective.