Unwrapped Arguments for Functions

I have an idea which would solve an annoyance that I run into frequently. Imagine I have a function with this signature:

func complementaryColor (to color: Color) -> Color

It returns the color which is opposite to the given color on the color wheel. I can of course call it like this:

let complement = complementaryColor(to: user.favoriteColor)
print("The complement to your favorite color is \(complement)")

This assumes, however, that the property favoriteColor on my User type is a non-optional Color - but not everyone has a favorite color, or at least not everyone is comfortable disclosing such information to just anyone. I often find myself in a situation like this, where the parameter I want to pass in is optional, and my desired behavior is that if the parameter is nil the function will return nil. To handle this I sometimes create an overload for the function that I want to use this way:

func complementaryColor (to color: Color?) -> Color? {
    guard let color = color else { return nil }
    return complementaryColor(to: color)
}

The second line in this function will invoke the original function, because the value is non-optional and the original function accepts a non-optional parameter, which causes it to take precedence (therefore, no infinite recursion). Instead of having to use this overload hack, I would love to be able to invoke the original (non-optional) function like this:

guard let complement = complementaryColor(to: user.favoriteColor?) else { return }
print("The complement to your favorite color is \(complement)")

where user.favoriteColor is now of the type Color?, which is more reasonable than the rather demanding Color which was previously its type. The language feature I'm proposing is that an optional value can be passed into a non-optional parameter position in a function or closure call if it is post-fixed by ? - the result is that the return type becomes optional, and the whole function evaluation is skipped and nil is returned if any of the unwrapped parameters are nil.

I think this is in line with the current behavior of optional chaining, where optionals can be used rather freely as long as one is aware that certain parts of the code may be skipped if nil is found (e.g., optionalValue?.doSomething(input: thisMayNeverBeEvaluated()).

I think that if the return type of a function is already optional, then rather than turn it into a double optional it should be flattened (i.e., remain the same even when used with an unwrapped argument). Meaning that if the function is:

func returnsAnOptional (input: Bool) -> Bool?

then invoking it like this:

guard let nonOptionalBool = returnsAnOptional(input: optionalBool?) else { return }

will result in nonOptionalBool being of type Bool, rather than type Bool?. If one needs to distinguish between "nil because the unwrapped parameter was nil" and "nil because the function legitimately returned nil" then that can still of course be done by explicitly unwrapping the value first:

guard let unwrappedInput = optionalBool else { handleInputBeingNil(); return }
guard let nonOptionalBool = returnsAnOptional(input: unwrappedInput) else { handleFunctionReturningNil(); return }

One way in which this is an improvement over my overload solution is that it becomes apparent at the call site that there is the possibility of skipped evaluation.

Looking forward to hearing what people think about this.

5 Likes

You seem to be reaching for Optional's map function here.

guard let complement = user.favoriteColor.map(complementaryColor(to:)) else { return }

This achieves the same semantics of optional chaining, but using well-defined and currently-existing language features. Do you feel that the cruft of using map in this way is undesirable?

12 Likes

@schrismartin To me this definitely seems far less readable, but more importantly it's also not the same, because while this works for the one argument case, how would I use map if I have two optional values that I would like to do this with simultaneously? With my proposal it would look like this:

let sum: Int? = addTwoInts(value1?, value2?)

Interesting. Would value1 and value2 both required to be suffixed with ?, or could they be a mix of unwrapped and non-unwrapped?

I admit that the map solution gets a little messy with multiple parameters.

let sum: Int? = value1.map { v1 in 
    value2.map { v2 in 
        addTwoInts(v1, v2)
    }
}

This has been proposed several times before. Here are some of the prior discussions:

https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160711/024201.html

4 Likes

@mayoff Thank you. Each thread mentions that this has been discussed in the past, sometimes linking to the more senior thread, sometimes letting the trail go cold... I'd love to know the actual first time this was posted about. Perhaps it's lost history.

I of course have not read all of the discussion on all of the threads, but @xwu's comment here jumped out to me as an understandable grievance. Here's my thinking on this:

We have a couple situations in which we are already accustomed to having to reason about possibly skipped code:

optionalValue?.functionWithSideEffects() // May not be run

and

guard let value = optionalValue else { return }
functionWithSideEffects(input: value) // May not be run

for example. In both of these cases there is a simple reason that code is skipped - it's because it can't be run. It is dependent on a state which did not come to pass. But arguments of functions are siblings in their space - by definition they aren't dependent on one another since they are in parallel. Therefore, if one happens to fail there's no reason whatsoever that the rest of them can't run, and therefore they should. Yes, when typed out, the horizontal layout of function arguments combined with ? makes them somewhat similar in appearance to an optional chain, in which things to the right are skipped if things to the left are nil. But fundamentally they are not at all the same and there is no actual reason that anyone should expect function arguments to the right to be skipped due to a failure in a function argument to the left. My vote would be that all arguments are first evaluated, then if one of the unwrapped arguments is nil the function evaluation is skipped.

Although it is true and occasionally relevant that function arguments are evaluated left to right, it is more common that arguments passed to functions have no side-effects and therefore that the order of evaluation is irrelevant. Passing the result of a side-effect function to another function is a very dubious practice that I don't think should be especially catered to, merely tolerated. If the decided behavior, as is my vote, is evaluation of every parameter regardless of failures, one can still of course abort due to the failure of a specific argument with a separate line of code:

guard let mustNotBeBil = mightBeNil1 else { return }
doSomething(a: mustNotBeNil, b: mightBeNil2?)

There is already a construct in the language similar to the one you propose: a throwing expression. We should look at how it interacts with the evaluation rules.

For example, here's an expression using your proposed syntax:

f(a, maybe0()?, b, maybe1()?, c)

and here is the analogous expression with throwing subexpressions:

try? f(a, try mightThrow0(), b, try mightThrow1(), c)

Note that any of the identifiers in either expression could refer to a computed property or in general could be replaced by a function call or an arbitrarily complex subexpression.

Your proposed rule is, I guess, to evaluate f(a, maybe0()?, b, maybe1()?, c) as follows:

  1. f
  2. a
  3. maybe0
  4. maybe0(), using the result of step 3
  5. b
  6. maybe1
  7. maybe1(), using the result of step 6
  8. c
  9. f(...) using the above results, if steps 4 and 7 evaluated to .some

But Swift requires the analogous expression try? f(a, mightThrow0(), b, mightThrow1(), c) to be evaluated as follows:

  1. f
  2. a
  3. mightThrow0
  4. mightThrow0(), using the result of step 3, and stopping if this step throws
  5. b
  6. mightThrow1
  7. mightThrow1(), using the result of step 6, and stopping if this step throws
  8. c
  9. f(...)

I would prefer both expressions to follow the same rules, and we can't change the rules for the throwing expression without breaking source compatibility.

6 Likes

I like the analogy to throwing subexpressions, and the guaranteed left-to-right evaluation order of function arguments in Swift makes this a well-defined evaluation order. I agree that copying the behavior of throwing functions make sense.

1 Like

It occurs to me that you can prototype this right now using a higher-order function. Well, several higher-order functions, since we don't have variadic generics.

Suppose we have this function:

func returnsNonOptional(_ a: Int, _ b: Int) -> Int {
    return a + b
}

With your proposal, we could call it like this:

let a: Int? = ...
let b: Int? = ...
let c: Int? = returnsNonOptional(a?, b?)

Instead, we can write a helper function that we use like this:

let c: Int? = ignoringNils(returnsNonOptional)(a, b)

Here's the helper function:

@inlinable @inline(__always)
public func ignoringNils<A0, A1, Answer>(_ f: @escaping (A0, A1) -> Answer)
    -> (A0?, A1?) -> Answer?
{
    return { a0, a1 in
        guard
            let a0 = a0,
            let a1 = a1
            else { return nil }
        return f(a0, a1)
    }
}

It should be obvious how to extend this to more (or fewer) arguments.

If we start with a function that already returns an Optional, we probably want another helper that avoids adding another layer of Optional to the return value. It can have the same name. Thus:

func returnsOptional(_ a: Int, _ b: Int) -> Int? {
    return a > 0 ? a + b : nil
}

@inlinable @inline(__always)
public func ignoringNils<A0, A1, Answer>(_ f: @escaping (A0, A1) -> Answer?)
    -> (A0?, A1?) -> Answer?
{
    return { a0, a1 in
        guard
            let a0 = a0,
            let a1 = a1
            else { return nil }
        return f(a0, a1)
    }
}

let c: Int? = ignoringNils(returnsOptional)(-1, 2)

These helpers use your proposed rule of evaluating all arguments regardless of which are nil. Example:

let c: Int? = ignoringNils(returnsOptional)(nil,
    (print("hello"), 1).1)
// prints "hello"

But we can tweak the helpers to act more like the throwing expression rules by using @autoclosure:

@inlinable @inline(__always)
public func ignoringNils<A0, A1, Answer>(_ f: @escaping (A0, A1) -> Answer?)
    -> (@autoclosure @escaping () -> A0?, @autoclosure @escaping () -> A1?) -> Answer?
{
    return { a0, a1 in
        guard
            let a0 = a0(),
            let a1 = a1()
            else { return nil }
        return f(a0, a1)
    }
}

let c: Int? = ignoringNils(returnsOptional)(nil,
    (print("hello"), 1).1)
// doesn't print "hello"
3 Likes

Throwing expressions are a branch of the language that I've used exceptionally little (pun only retroactively intended), so the idea of wanting to match unwrapped argument behavior to throwing behavior hadn't occurred to me. Makes sense.

Personally I think that when I use this feature I will very rarely use it to pass the result of a non-pure-function directly into another function, so for me the official rule about evaluation won't matter most of the time anyway, and I think I can handle reasoning with the official rule in mind on the few occasions when I might need to.

My question then is, if we have already built essentially this same mechanism into the language and have managed to agree upon how it will work, why can't we just copy the same design decisions over to the error-handling's younger brother, Optional? Is there some fundamental difference between this and throwing expressions? In this context, is it wrong to think of optionals as simply a less verbose error handling mechanism, which makes up for its bluntness with ergonomics?

Essentially I'm asking, how is it that we are held up by not being able to agree on the rules if we have already agreed upon and implemented the rules for an identical situation?

2 Likes

Another thought - I don't know where the notion of compiler enforced pure functions currently stands, but if that were possible then perhaps allowing this unwrapped argument feature only with pure functions and properties as the arguments would be a good compromise

I wrote one of those previous posts that @mayoff refers to. For what it's worth, I still think this is a good idea and I think the fact that (at least) three posts have landed on the same suggested syntax (a '?' suffixed to any parameters you want unwrapped) is a positive sign for its usability.

I agree that it does allow you to create confusing, complex structures if you really want to but I see this as a non-issue.

On evaluation order, I see the point that there isn't a 'right' or 'intuitive' answer to the evaluation order or short-circuiting. I see this too as a non-issue. Relying on evaluation order of function arguments at all should be seen as questionable so I agree that as long as it's well-defined and documented then this is not a serious concern.

2 Likes

I can't speak for anyone else, but I was never particularly fond of this proposal during prior discussions because I didn't see any good arguments for any particular evaluation rule. (Not that my opinion of the proposal carries any weight.)

But I also don't recall anyone previously drawing the analogy between unwrapping-arguments-in-place (this proposal) and throwing expressions, and (at the risk of tooting my own horn) I think the analogy is a pretty good argument for a matching evaluation rule. So that takes me, personally, from mildly opposed to neutral on this proposal.

Aside from that, your best bet is to find or become someone capable of and willing to implement at least a prototype of this idea in the compiler.

1 Like

Just spent some time looking around the Swift source code for the first time since I downloaded it some time ago. It sounds like it would be a ton of fun to try this. The idea has taken root, so I'll see what happens...

Rules for throwing expressions were designed by the Core Team before Swift Evolution process. That is why that design was completed and this one keeps going in circles...

I hope the remaining key features of the language (e.g. language-level concurrency) will follow a path similar to property wrappers: Bring them into Evolution process only after the design is mostly completed and implemented. Then the Evolution process can refine and polish that design; just like what happened with property wrappers.

Excellent observation Rob.

Past discussions on the topic were bogged down with bikeshedding what happens when multiple optional values are passed to the same function, but you are entirely correct that Swift has already answered this question vis à vis throwing.

To the best of my recollection, that point was not previously recognized, and now that it has been the correct behavior is apparent.

The spelling had already achieved broad agreement, with postfix “?” after each argument that needs to be unwrapped, so I think this pitch is finally ready to move forward.

1 Like

I would have expected left to right evaluation and early exit ( short-circuit evaluation ) like for boolean evaluations. I haven’t thought through or been exposed to the compound throwing expressions Rob brought up but it seems like they behave similarly. If all three follow the same convention that would feel “right” to me and I’d find this useful addition to the language. I do wonder how the type inference aspects of the compiler will react to this. If it makes compiles slower or the compiler more likely to throw up its hands because expressions are too complicated then this feature isn’t worth that.

I don't know if this is the right solution for this problem or not as I'm not an expert, but in my experience I've also faced this problem multiple times on a regular basis and I feel the problem is significant enough to warrant a coordinated effort towards solving it.

This problem stems from the need to not handle optionality of function parameters in its implementation unless nil value of a parameter itself specifies a special situation which the function wants to handle in a specific way according to its stated contract.

In most cases I've wanted to handle optionality of arguments at the caller, taking a recourse as desirable at the call site. This allows you to keep your function signature clean and and its functionality unambiguous and promotes use of non optional variables throughout the code.

Definitely a problem worth solving, big outstanding pain point of optionals.

I think we need special syntax for this case that appears similar to optional chaining.

func doThing(with s: String) -> Int {
    ...
}

var str: String? = nil

obj?.doThing?(with: str).map { } // preferred syntax
obj?.doThing(with: str)?.map { } // alternative

doThing?(...) is already used for when doThing is optional function, or optional closure variable.

2 Likes
Terms of Service

Privacy Policy

Cookie Policy