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

Consider the following example:

var number: Int? = 42
// TODO: print number, if it contains a value.

Currently, Swift provides different ways to deal with Optional values.
The first technique that might come to mind is Optional Binding:

if let number = number {
   print(number)
}

However, one could say that using a control structure is quite a verbose pick for such a simple task. This can be seen in the re-introduction of a now unwrapped number variable, merely for calling a single function on it on the next line.

Another technique, Optional Chaining, is irrelevant in this case, as we're not accessing properties/methods on the number variable.

The Nil-Coalescing Operator is yet another technique, perhaps in the form of:

print(number ?? _)

However, this is not currently possible, as there is no way to "bail out" without providing a default value.

A final option that might come to mind is taking advantage of Swift's higher-order functions:

number.map { print($0) }

The map(_:) function definitely has the shape required for the task:

func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?

In this example, The transform closure has a signature of (Int) -> Void, and the entire function is evaluated concretely to be ((Int) -> Void) -> Void?

However, going back to our original intention it's clear that nor are we mapping number to a different value, neither we're interested in the outer return value - an Optional Void - which on itself seems unusual.

A transformation of this shape, i.e one that returns nothing, is not uncommon in our day-to-day tasks, and in my opinion deserves a name which clearly and conspicuously translates the writer's intent in call-site.

In this respect, another Container type in Swift that has been added a dedicated method to handle such "transformations" is the Array type. Consider the following functions:

  • func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
  • func forEach(_ body: (Element) throws -> Void) rethrows

While we would all agree that forEach(_:) is not a necessity, we can also agree that for the task of printing all elements in an array, this:
[4, 2].forEach { print($0) }
reads much nicer than this:
_ = [4, 2].map { print($0) }

Thus I propose adding a parallel function to Array's forEach(_:), taking advantage of a similar naming for convenience:

public extension Optional {
    func forValue(_ body: (Wrapped) throws -> Void) rethrows {
        guard let value = self else { return }
        try body(value)
    }
}
IMPORTANT NOTE:

A similar pitch was raised a couple of years ago, suggesting to add an ifPresent(_:) function with a similar signature:
func ifPresent(@noescape f: (Wrapped) throws -> Void) rethrows

Unfortunately, the discussion quickly turned into a debate of whether to make Optional into a CollectionType.
Towards the end the misleading nature of map(_:) was actually raised, but discussion didn't make any progress, and more specifically - didn't make the case for handling a particular kind of closure shape passed to a higher order function.

PS:

I'd like to thank my fellow iOS team members @Vimeo, Amir Aharon and Liron Yehoshua, for their wise input on this issue.

9 Likes

While it's abuse of a function, you can use map for that:

number.map { print($0) }

or, when without using a closure

number.map(someFunctionTakingInt)

I like to think about Optional as a Sequence of zero or one elements, and there should be forEach method on it too, that way we won't abuse map, but I imagine a lot of people would dislike the name.

Edit:
After you finished your post I see that we're thinking in exactly the same way. +1!

1 Like

Hi and welcome to the SE forums. This idea was pitched quite a lot, if you intend to push it forward I would love if you would sum up and link previous threads. It is always good if you could provide new insights about the problem, because otherwise we would simply re-hashing everything that has been already said. :wink:

1 Like

Hi,

Please take a look at my revised pitch. I've addressed both.

Hi,

Thanks for the warm welcome :slight_smile:
I've added a link to the only thread I've found discussing this, and indicated my why the point wasn't made clear enough IMHO.

I hope I've provided valuable information and thoughts to make this discussion more fruitful and possibly to get a review on this.

You can do something like

extension Optional {
  func `let`(_ unwrapped: (Wrapped) -> Void) {
	switch self {
	  case let .some(value):
		unwrapped(value)
	  case .none:
		break
	}
  }
}

let number: Int? = 1
number.let {
  print($0) // 1
}

let anotherNumber: Int? = nil
anotherNumber.let { // Won't be called
  print($0)
}

but this is basically like a map, but with a different name (you can call it whatever you want - ifLet, unwrap, etc)

This is so similar to map that I would rather implement it like this:

extension Optional {
  func `let`(_ unwrapped: (Wrapped) -> Void) {
	map(unwrapped)
  }
}

Yep.

Actually I really like the idea of a dedicated function for Optionals for using their unwrapped value.
It always feels wrong to me to use the map(_:) function for just using the unwrapping it provides.

I like @suyashsrijan idea with the let(_:) function, it is Kotlin-like :slight_smile:.

Great pitch! :clap:

I'm not sure being kotlin-like is a convincing argument to reuse let in this context.
In Kotlin, let is not a keyword used elsewhere, which is obviously not the case in Swift.

4 Likes

This seems cosmetic and anyone who doesn’t like the look of map could just write an extension to use a more palatable name for them.

3 Likes

In Swift, it should obviously be named val. :sweat_smile:

2 Likes

Would you say the same about Array's forEach(_:) ?

Since forEach made sense to me long before I could grok map there, I will concede there is value there for general readability and code comprehension. I’ll change my vote from “but, why?” to “+1” accordingly.

I find "forValue" not intuitive since "for" reminds me a loop, while in this case the Optional is not

Also "let" is not self explanatory.

I think "evaluateIfNotNil" is self explanatory and precise.
maybe simple "evaluate" is more swiftly...

for example:

number.evaluateIfNotNil { print($0) }

or

number.evaluate { print($0) }

I think personally I would be okay with it being named 'let', since it basically sugar around an if let value = value check.

Just to throw in one more: number.ifLet { print($0) }

2 Likes

I agree that evaluateIfNotNil(_:) is precise, but it's a bit too verbose IMHO. We should take advantage of the fact that in call-site it's clear that number is an optional.

evaluate(_:) actually seems even less appropriate considering the history of eval:

Eval is understood to be the step of converting a quoted string into a callable function and its arguments

I went with forValue(_:) to be symmetrical with Array's forEach(_:), and don't think that the for prefix necessarily associated itself with a for loop, when read in one breath, the same way that synonyms like for example don't.

However, if this is an issue perhaps alternative such as withValue(_:) or usingValue(_:) might suffice.

I think ifLet corresponds quite well to if let in the same way that forEach corresponds to a for loop.

1 Like

Meaning you support an ifLet(_:) naming?