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

In a pure naming sense, yes.

I'm neutral on the pitch, but from a naming perspective I would prefer ifSome instead of ifLet.

7 Likes

I’m fine with either of those, but definitely like for less than if. I think I like ifLet for this.

This should be unwrap, to match the existing terminology used for optionals. ifLet and the like make no sense from a Swift naming perspective.

Additionally, we should have get, which can throw, to match Result.

4 Likes

I think this has merit as a standard language / library inclusion vs either use of map or piecemeal wrappers thereupon.

map is not logically appropriate in my mind - it is a perversion of its intent to use it for this purpose, and though I have seen it used this way in the wild, every time I do it adds cognitive burden to me as the reader while I 'handle the exception' that it's not actually being used to map anything. This is entirely subjective and not a consensus perspective, but it is not an uncommon one either. There is the semantic interpretation that you're mapping the lambda to the values, not values to other values, but for whatever reason that doesn't fit as well for me. I also think it enhances readability, in any case, to make clearer your intent between "I am transforming this collection of values into another collection of values" vs "I am doing something with all the values in this collection". i.e. map vs forEach on Array.

The clearer alternative (if let …) is unduly verbose for such a frequent and conceptually simple operation. If nothing else I feel like it warrants some syntactic sugar.

I do find ifLet a little unattractive, though, as it seems to be making an explicit reference to the if let … syntax - while not actually following any pattern of said syntax - syntax which is incidental to what you're trying to do anyway, and understanding any parallels or history from the if let … syntax is not useful to understanding what you're trying to do.

ifPresent, ifSome, ifSet, etc are much more to the point. But, some are verbose:

number.ifPresent { print(number) }

…is barely shorter than the existing:

if let number = number { print(number) }

Though the former is clearly cleaner (fewer repetitions of 'number', and no introduction of technically a distinct, possibly shadowing variable).

Just exploring the solution space, there's also the option of more deliberately syntactic sugar / compression for the if let … syntax, something essentially like:

if number? { print(number) }

(exact syntax might need some fiddling to fit in with existing family of & use of '?' operators…)

I also mulled over variations that use some kind of operator on 'number' to indicate that the enclosing code (e.g. 'print') should not be invoked if 'number' is nil, but every syntax I've come up with is a bit too 'spooky action at a distance'. It seems only natural to have the condition precede the action and be stated clearly (especially if you have non-trivial examples where the 'skip this whole statement if this value is optional' might be visually buried deep in the statement).

It does feel like an awkward asymmetry that we have the very nice '?.' operator for member functions yet free functions have nothing.

Or perhaps the way optionals are fundamentally treated should be changed, such that you can do:

assert(number != nil)
print(number)  // 'number' is implicitly unwrapped, by virtue of the compiler knowing it cannot be nil at this point.

And:

if number != nil and otherNumber != nil {
    // 'number' and 'otherNumber' are implicitly unwrapped in this context.
}

And:

guard number != nil and otherNumber != nil else {
    // Bewm.
}

Optionals from the compiler's perspective are just a contract that it needs to enforce, and it currently requires a lot more boilerplate from the humans than strictly necessary. IMO the only significant question is whether humans will be unreasonably confused by a less explicit system. I for one don't think so - the above reads much like most other C-family languages, merely with the distinction that the Swift compiler is more careful that we humans don't screw up by missing an assertion, conditional, guard, or similar.

Or maybe reuse of the try syntax family? e.g.:

func doAllTheThings(_ a: Int, _ b: String) { ... }

let maybeString: String? = ...

doAllTheThings(5, maybeString) // Compiler raises error as normal.
try? doAllTheThings(5, maybeString) // Compiler allows this.

I feel like the practical concern with that is that, by adding more things that try sort of does, you could end up with individual statements where that try is covering for many different things - exceptions, unwraps, etc. Having them conflated isn't necessarily always your intent (though it might be - sometimes you really do just want to say "try to do this thing and if it doesn't happen for any reason, that's fine", for which this is great). Though you always could decouple them if you wanted, using existing if let ... syntax to handle optional unwrapping separately.

2 Likes

OK. I must disagree. Optional Binding is itself syntactic sugar, not a logical concept, thus not really serving as a valid reason to follow it.
Furthermore, we aren't really binding anything are we?
And lastly, if ifLet(_:) why not ifVar(_:) - Both are considered bindings. Preferring the former over the latter simply makes no sense.

1 Like

Although I don’t mind ifSome and ifLet for their similarity to forEach I feel equally good about unwrap as the spelling.

I disagree about needing a get which throws on Optional, though. I think a reasonable way to semantically differentiate Optional and Result is to talk about “exists or not” vs. “right or wrong.” If you need to represent “exists or throws” then it makes more sense to me to create a Result from your Optional and use Result’s throwing accessor.

2 Likes

We shouldn’t need to create an intermediate Result just to immediately call get.

Syntax is syntax, whether sugared or salted. Syntactic sugar is the reason in the first place that optionals in Swift aren't as cumbersome as it would have been if Swift would have been a strictly logical language.

In Kotlin, the spelling is simply .let{…}, but since let is already a keyword in Swift, we may want to stay away from that.

It cannot know that it cannot be nil, at most it can assume that it's the case

var _number: Int? = 5
var number: Int? {
        defer { _number = nil }
        return _number
}

assert(number != nil)
print(number) // will print nil in the current swift version
3 Likes

I don't really see the point in a throwing .get method, since it would conceivably only ever throw one error. In such cases, Optional is exactly what we need rather than do … try … catch … evaluate any kind of error.

Mind you, a throwing accessor would compose better with other parts of a do block, just like Optional.map does with map and flatMap chains now.

1 Like

This feels like a feasible route to me. Switch cases already have similar sugar for optionals, e.g.:

switch optional {
case value?: print("The unwrapped value is: \(value)")
default: fatalError("No value!")
}

So we could have if number? { print(number) }
Or, alternatively: if case number? { print($0) }

1 Like

Precisely, a throwing accessor makes using options in an error context painless, and it’s something I eventually add to every project. We can customize the error is a variety of ways, as this has been discussed before.

It is not much shorter, but it would all be supplied by code completion, where the existing alternative would not.

1 Like

Looks nice, and is very consistent with Swift's switch syntax but:

  • We aren't gaining much here in terms of characters won
  • Moving to a conditional statement kind of defeats the point of keeping it lightweight
  • Who really writes an if-statement on a single line?
1 Like

I like this idea, people definitely shouldn’t be using map for this. The purpose of map is the exact opposite of this method. Map takes a function that should ha no side effects, and you use it because you want to access the transformed value. This function here would be used for its side effects, and would not return anything. Completely different in other words, just like forEach.

When it comes to naming, let or ifLet make no sense at all, since let is used for binding things and nothing is bound here.

ForEach is not a bad idea, nor is forValue or forSome.

1 Like

This is not actually how it would be used though, it would be
number.ifPresent { print($0) }

Also, I’m not sure the number of characters you save is necessarily the point here, isn’t it more about clarity? And avoiding having to optionally bind ‘number’ just in order to use it could be considered clearer.

3 Likes

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