Strange optional resolution

Strange optional resolution. Knowing the Swift philosophy I would expect a within if block to be non optional.

let a: String? = "a"
let b: String? = "b"

if let a = [a,b].first(where: {$0 == "a" }) {
    print("\(a)") // a is optional
}

.first() itself returns an optional (String??). So your if let a = ... only binds the wrapped value of that first optional. The collection itself still has optional elements.

4 Likes

Then you won't be able getting nil in this example (which may be desired):

if let x = [nil as String?, "b" as String?].first(where: { _ in true }) {
    print("\(x)") // nil
}
2 Likes

What's less than obvious here is that your "a" is being promoted to Optional<String>, triggering this overload:

If you want the overload that doesn't rely on optionals, I suggest compacted.

if let a = [a, b].compacted().first(where: { $0 == "a" }) {
4 Likes

@anon9791410 I understand about compacted() but imho the original example yet it is breaking the philosophy as what is the point of even having that expression in if.
@tera I havent played with it for a long time. But I remember that in one of the swift evolutions there were quite an issue with double optionals and it was resolved in a way that I described. So I was surprised to run into them again.

I am surprised, too. I was under the impression that optional binding removed an arbitrary number of levels of optionality, such that both

let x: Int? = 0
if let unwrapped = x {
 print(type(of: unwrapped))
}

and

let y: Int??? = 0
if let unwrapped = y {
 print(type(of: unwrapped))
}

should both print Int, but I'm apparently misremembering. (Because when I did this in a Playground the second one sure printed Optional<Optional<Int>>.)

This is an interesting note. I found this post:

So in your example you could do the optional binding as if let a = [a,b].first(where: {$0 == "a" }) as? String { to get a non-optional String out of it.

The problem is not that I dont know how to avoid it. The problem is that it creates some kind of witchcraft in a place you dont expect it imho. if let for Multiple Optionals to get rid of just one optional has no sense. It is more philosophical question.

if let first = [nil, "a"].first has no sense to proceed to the if block

This reply in another thread goes into a great explanation of why the distinction is important:

2 Likes

The other provides some rationale for it.

When you put optionals in an array, the array is elevated to the highest level present. E.g.

let a: String?? = "a"
let b: String?????????? = "b"
[a, b] // is [String??????????], not [String??]

first adds one more ?, and if let takes one away.

if let a {
  a // String?
}

if case let a?? = a {
  a // String
}

You have this idea of "Swift philosophy" not matching that; that is only your current interpretation, and you can deprogram it.


Why would you have written the example code in the first place, though? There's not a reason to extract a match, when you have a non-optional version of the match to begin with.

let match = "a"
if [a, b].contains(match) {
  print("\(match)")
}
2 Likes

@anon9791410 You are right in that I interpret it myself and call it a philosophy. I might be wrong but i think in swift 3 conversion to 4 there was an attempt to get rid of optionals of optionals. But it could also be that my memory is failing me.
Also, poptentially I do not understand what other wrote me.

I personally find that let first = [nil, "a"].first by itself has absolute sense.
but

if let first = [nil, "a"].first {
  // Should not appear here
}

this how I (and personally I) would expect it to work.

Sorry, my bad.

I think you're thinking of SE- 0230 Flatten nested optionals resulting from 'try?'.

There is no general direction of trying to reduce the occurrences of optional optionals. The compositional nature of Optional is an advantage over the conventional notion of null as single, flat value. It's a feature, not a bug.

2 Likes

In this Array example, it's less obvious what would be preferable, but I think this dictionary example makes a very strong case for why "it should only unwrap one layer of optionality" is the right way to go:

let d: [String: Int?] = ["key": nil, "other key": 123]

if let existingValue = d["key"] {
  print(#"The dictionary contains the value "\#(existingValue as Any)" for the key "key"#)
} else {
  print(#"The dictionary doesn't contain any values for the key "key"#)
}

You would want to hit the if case, not the else case, because the dictionary does contain a value for "key", which is nil. This is semantically distinct from having no value at all.

Consider C# by comparison. Their null is just one single flat value. There's no way to encode the difference between:

  1. "I got null because the value is not there"
  2. "I got null because the value is there and is actually null"

Checking value == null is not enough, because it can't those two cases apart, so their TryGetValue API needs to return a separate bool:

public bool TryGetValue (TKey key, out TValue value);
1 Like

You may need something like this:

protocol OptionalProtocol {
	static var wrappedType: Any.Type { get }
	// nested optional types unwrapping
	static var fullUnwrappedType: Any.Type { get }
	
	var	isNil			: Bool		{ get }
	var wrappedValue	: Any 		{ get }
}

extension Optional : OptionalProtocol {
	static var wrappedType: Any.Type {
		Wrapped.self
	}
	
	static var fullUnwrappedType: Any.Type {
		var	currentType = wrappedType
		while let actual = currentType as? OptionalProtocol.Type {
			currentType = actual.wrappedType
		}
		return currentType
	}
	
	var isNil: Bool { self == nil }
	
	var wrappedValue: Any { self! }
	
}

extension Optional where Wrapped == Any {
	// nested optionals unwrapping
	init( fullUnwrapping value:Any ) {
		var	currentValue = value
		while let optionalValue = currentValue as? OptionalProtocol {
			if optionalValue.isNil {
				self = .none
				return
			} else {
				currentValue = optionalValue.wrappedValue
			}
		}
		self = .some( currentValue )
	}
}

let y: Int??????????????????????? = 0
if let unwrapped = Optional(fullUnwrapping: y as Any) {
	print( type(of: unwrapped) )	// print Int
}

let unwrappedType = type(of: y).fullUnwrappedType
print( type(of: unwrappedType) )	// print Int.Type