A dedicated function for evaluating Void returning closures when Optional instance is not nil

Another name to consider could be ifSome, e.g.

value.ifSome(doSomething)

The idea is that this matches the .some(_) case of Optional<Value>.

1 Like

I’d like to see a solution here that handles (or at least considers the future direction of) the case of multiple optional variables. Currently available solutions such as flatMap and if let really start to show their inadequacy when you have to write:

if let foo = foo, let bar = bar, let baz = baz { doSomething(with: foo, bar, baz)

It would be much nicer to write something like (foo, bar, baz).ifAll(doSomething(with:)).

2 Likes

You're touching here on a valuable (yet entirely different) concept of zipping values together.
Similar to Collection's zip(_:, _:), you could introduce a zip operation over Optionals:

zip<A, B>(_ lhs: A?, _ rhs: B?) -> (A, B)? {
    guard let left = lhs, let right = rhs else { return nil } 
    return (left, right)
}

With this in place you can now define other zips that take 3, 4, 5 etc. arguments.

3 Likes

Brainstorming a completely different approach to solve the original problem and bulldoze the bike shed so it doesn’t need painting...

It would be nice to be able to extend Any (or have a temporary, stand‐in, empty AnyProtocol that everything can manually conform to in the meantime). With either, this would be possible:

let number: Int? = 42

// Isn’t this both as clear and as short as can possibly be?
number?.do { print($0) }

// It can be implemented with the following (in Swift 5.0):
protocol AnyProtocol {}
extension Int: AnyProtocol {}
extension AnyProtocol {
    func `do`(_ closure: (Self) throws -> Void) rethrows {
        try closure()
    }
}

Just for kicks (and not very seriously), the do method could be tied into the compiler and provided a yourself keyword to seem more like self, but from an outside context (It is functionally identical to $0.):

number?.do { print(yourself) }
3 Likes

SE-0253 (when implemented) might allow for:

extension Optional {

    @inlinable
    @discardableResult
    public func callAsFunction<U>(
        _ transform: (Wrapped) throws -> U?
    ) rethrows -> U? {
        return try flatMap(transform)
    }
}
let possibleNumber: Int? = Int("42")

let possibleString: String? = possibleNumber { String($0) }

possibleNumber { print($0) } // discardableResult
1 Like

This is precisely what ifSome(_:), ifNone(_:) did in my pitch here: API Additions to Optional (filter, ifSome, ifNone, peek)

However, I haven't had time to learn how to implement it into the standard library, which is a precondition for a formal proposal.

3 Likes

An interesting example. In general concerns around determinism can be addressed through @pure et al, though in this specific case I'm not sure if you could consider that property pure or not… I argue it's not, because it has side effects (mutating itself - and in a way that's non-deterministic from its arguments).

If it were pure, then as per usual for pure functions the compiler could trust that its value would remain the same. In all other cases, the compiler would require the coder to store the value in a local variable for use across validation & subsequent use. This is of course the only safe way to do this irrespective of this particular proposal on possible compiler help with syntax.

An equivalent example that more clearly demonstrates why this sort of thing is not a dissuasion is a queue: once you pop an item, it's no longer in the queue (@cukr's example being a kind of specialised, single-item queue). No one would expect this code to work correctly:

while queue.pop() != nil {
    doStuff(queue.pop())
}

…unless your very unusual intent is to skip every other item.

if let ... handles this elegantly just as saving the value into a local variable does.

try does this naturally, e.g.:

try? doSomething(foo, bar, baz)

As long as it's acceptable to the reader that this could mean:

  • doSomething throws.
  • One or more of the arguments are optionals and must be unwrapped in order to match the parameters of doSomething.
  • Both of the above simultaneously.

I really like the brevity & reuse of existing, clearly related language features for this (more than I like the otherwise interesting .ifSome { $0 }-style approach), but I still don't know how I feel about the heavy load on that one little try?.

Though, again, it doesn't preclude any coder, whom wishes to make it clearer, from decomposing it:

if let bar = bar {
    try? doSomething(foo, bar, baz)
}

Though it does rely on a guess, essentially, that bar is the one argument that needs unwrapping, and not merely that the author forgot about the others (or that the function signature changed at some point).

So it does hinge on the question of whether you ever really need to distinguish between why the try "failed".

It seems to make sense long as you think of try? in broad terms: "do this thing if it's possible, and just don't worry about it if it's not for any reason". I'm curious if others read / interpret try? differently?

I like this [slightly] best amongst the optional.something { … } approaches proposed thus far. It's concise and close to the very similar ?. operator. And could presumably be chained similarly?

number?.do { calculate($0) } ?.do { print($0) }

Not necessarily elegant, but if you find this ugly then perhaps it's fundamentally because of the use of free functions as opposed to member methods?

A counter-argument to such chaining might be that in the 'inner links' of the chain should logically use map instead:

number.map { calculate($0) } ?.do { print($0) }

Even if so, I don't think Swift needs to be so opinionated about it, however, as to explicitly preclude ?.do chaining. It can be a style preference.

In any case, it's interesting to me that this does start to look, if you squint a little, like 'then' syntax (e.g. one of many async libraries implementing that pattern).

…but I worry that yourself isn't a suitably generic keyword. Nonetheless an example of where it wouldn't read well doesn't spring immediately to mind.

It is extra language surface area over the established $0 convention, though, and longer. (though I'm not a particular fan of $0 to begin with… a tangent making a compelling argument for yourself or similar as a general replacement for $0 would be welcome by me)

I feel that going this route sends the message that the functionality proposed is not really intimately related to the Optional type - or any other type, actually - but rather generic, contrary to the current state of affairs in regards to map(_:).

IMHO Optional Chaining a generic function as a solution moves the discussion from how to elegantly handling the content of a Container Type.

This seems to be the crux of your argument, and I don't really agree with it.

Rather than doing this, I would rather have flow-sensitive optional support. So when the compiler sees:

if someOptional != nil

It will secretly transform it in to:

if let __someOptional = someOptional (or some other hidden name)

And within the conditional scope, it would accept using someOptional directly where the underlying type would be expected, rewriting those uses to instead use __someOptional (the unwrapped version). Generic parameter resolution would favour the underlying type over the optional.

So your example would become: if number != nil { print(number) }

Similarly,

if number != nil { 
  let x = Array(number) // favours Array<Int> over Array<Int?>
}
2 Likes

Now that this idea has sat for a few days, I think I agree that using sufficiently concise conditional control flow would be my preference over introducing a map-like function intended to be used when side-effects are desirable.

I would go the other direction (as was proposed upthread) and retain the explicit callout that a new constant is being created for the scope that follows rather than retaining the explicit nil-check.

If we are representing

if let number = number { ... }

with either

if number != nil { ... // number is not optional in this scope } 

or

if let number { ... // number is not optional in this scope }

Then the second option is my preference for a few reasons

  1. if let is established enough to make the leap intuitive.
  2. We don't accidentally eliminate any code from being possible. If we magically turn an optional into a non-optional within the scope of a conditional then there might be interesting (maybe uncommon) situations where we break code that relies on the variable retaining its optional type within the scope. I could try to come up with an example maybe later.
  3. It feels less magical to me, which I guess is really just a corollary to point number 1. Rebinding based on a nil-check is established in Kotlin but it would be entirely new magic for Swift.
1 Like

I want to agree unconditionally - especially because, as you note, the safety proof required for the first form is much higher as a result of it being a much broader and more powerful and thus potentially more edge-casey approach. Therefore it'll be more work to pitch. However, you then mention that Kotlin already does this - I know of Kotlin but am not that familiar with it; can you comment on how Kotlin does this and how well it's worked there?

The first approach implies a compiler mechanism with much broader implications, applications, and appeal (e.g. it probably ties in naturally if not as a prerequisite to constrained types and many other powerful language enhancements). If it does end up being implemented for those reasons, having a redundant if let optional { syntax - while not the end of the world - is, well, redundant.

Bikeshedding: I'm not sure I agree that the let number reads better than number != nil (though I'd get used to either form in practice, I'm sure). It's just that in plain English "if let number" is not a sentence, and it leaves the reader hanging with the natural question "if let number what?".

can you comment on how Kotlin does this and how well it's worked there?

I'll leave this topic for someone else. I've used Kotlin, but I cannot speak to the community sentiment or how the feature is implemented.

It's just that in plain English "if let number" is not a sentence

I'd argue that the intent is not in this case to be conversational but rather extremely concise and still very exacting. Use the same natural language trick on the thing we are trying to shorten and it performs no better: if let number = number: "If let number equal number" means nothing to me, and certainly does not imply that we are only letting "number equal number" if "number" is not nil. The degree to which we are increasing cognitive load is far less than that required to learn the if let pattern in the first place.

I want to stress that my second point is specifically that if we introduce

if let number { ... // number is not optional }

as a companion to the existing

if number != nil { ... // number _is_ still optional }

Then we don't break source compatibility, but more importantly the language remains equally expressive -- if I want to nil-check without changing the type of the thing named "number" I can still do that in the most obvious way possible.

Verbosity is only one motivation for this suggestion.
A much stronger one, in my opinion, is providing a declarative way of plucking out Optional's honest value. And from your solution it seems we agree that the if let dance is imperative overhead.

However, you actually touched on what I dislike about your solution:

This magical transformation unwraps the value implicitly, and assumes it's a let.
Why not var? What if I only want to check that number has an honest value, but without needing to unwrap it?

This secret unwrapping, as you called it, hides intention and holds back on clarity. In other words I'd say it's no longer declarative.

That's why the analogy to Array's forEach(_:) makes a lot of sense to me - To allow to do something with each value in the container, just with a different type of container. And since map(_:) is unclear in callsite, I'm suggesting forValue(_:).

1 Like

I don't understand why this topic is still considered.

You have also Result ( specialized Either ) monad. I would like to do the same ( print a value or an error ).

Should we consider to add forEach or let and forEachError and letError functions with the same functionality to this type?

extension Result {
  func `let`(_ success: (Success) -> Void) {
    map(success)
  }
  func letError(_ failure: (Failure) -> Void) {
    mapError(failure)
  }
}

More bikeshedding: When I write Swift, nil is seldom what I'm interested in. What I need to know is if the variable has a value, and if I can bind it. Expressing that in a negated clause about nil, which I wasn't interested in to begin with, doesn't express the intent of the program.

4 Likes

I think your point about verbosity only being one factor is definitely key. I realized only after my latest post in this thread that there’s a totally separate pitch right now that is proposing adding if let something as shorthand for if let something = something with largely different motivations ([Syntax] Default name for optional binding self and other properties).

I guess where I land in the end is I acknowledge that they address a different set of goals that merely overlap and I happen to be more interested in seeing the if let shorthand but (1) I can’t think of an argument against having the method you are looking for, (2) there’s precedent with forEach, and (3) I’d probably use it on occasion (as has been the case with forEach).

1 Like

I'm just describing a mental model, for illustrative purposes. Of course, if the value is unused, I wouldn't expect any actual unwrapping.

I hate the Kotlin model because it force you to structure your code to workaround limitations of the compiler. There is many places where the compiler is not smart enough or to much conservative and force introduction of intermediary variables or usage of force unwrap to make it accept the code.

The situation improved slightly with Kotlin 1.3 and method annotation, but it remains fragil IMHO.

1 Like
Terms of Service

Privacy Policy

Cookie Policy