Add unwrap function to Tuple for unwrapping multiple optionals

Introduction

Commonly, If we need unwrapping multiple properties, we can use if let or guard let.
But, Sometimes, many if let, guard patterns make poor readability.
Or If you avoid poor readability, you can define function parameters to optional.
But, You have to unwrap parameters in function.
So, I will introduce new function to unwrap multiple optionals more effectively.

Motivation

I use that functions to unwrapping multiple optionals

func unwrap<A, B>(a: A?, b: B?) -> (A, B)? {
    return a.flatMap { a -> (A, B)? in
        b.flatMap { b -> (A, B)? in
            (a, b)
        }
    }
}

// swiftlint:disable large_tuple
func unwrap<A, B, C>(a: A?, b: B?, c: C?) -> (A, B, C)? {
    return a.flatMap { a -> (A, B, C)? in
        b.flatMap { b -> (A, B, C)? in
            c.flatMap { c -> (A, B, C)? in
                (a, b, c)
            }
        }
    }
}

// swiftlint:disable large_tuple
func unwrap<A, B, C, D>(a: A?, b: B?, c: C?, d: D?) -> (A, B, C, D)? {
    return a.flatMap { a -> (A, B, C, D)? in
        b.flatMap { b -> (A, B, C, D)? in
            c.flatMap { c -> (A, B, C, D)? in
                d.flatMap { d -> (A, B, C, D)? in
                    (a, b, c, d)
                }
            }
        }
    }
}

That is usage

func fullName(firstName: String, lastName: String) -> String {
    return "\(firstName) \(lastName)"
}
var firstName: String? = "Raynor"
var lastName: String? = "Jim"

let name = unwrap(a: firstName, b: lastName).map(fullName)

If I don't use that function, I have to guard or if let patterns, or make parameters to optional

// That pattern can be a boilerplate code.
if let firstName = self.firstName, let lastName = self.lastName {
    let fullName = fullName(firstName: firstName, lastName: lastName)
}

or

// That function is not definitely. because fullName function don't need optional firstName, lastName. and that function don't need return optional String.
func fullName(firstName: String?, lastName: String?) -> String? {
    guard let firstName = firstName, let lastName = lastName else {
          return nil
    }
    return "\(firstName) \(lastName)"
}

So I think if swift support multiple optional unwrapping function, we can make a function more definitely.
And I think add unwrap function to Tuple is best way to supporting multiple optional unwrapping.

Proposed solution

// If tuple has unwrap method, result of (firstName, lastName).unwrap() is (String, String)? so you can use map or flatMap to transform that tuple. Then, you don't have to make a boilerplate codes, you can make function more definitely.
let result = (firstName, lastName).unwrap().map(fullName) // (firstName, lastName) is (String?, String?)

I think add unwrap function to Tuple if tuple has optional properties.
But If add unwrap function to tuple that don't have optional properties, It does not seem to be a problem.

1 Like

If I understand correctly, you can already do something similar to what you're describing like so:

var firstName: String? = "Raynor"
var lastName: String? = "Jim" 

if let (first, last) = (firstName, lastName) as? (String, String) { ... }

Personally I'm not a huge fan of adding unwrap functions to the language, since currently unwrapping is handled by operators. It seems like it would hurt the clarity of the language.

5 Likes

I definitely have felt the pain of this problem. Though Swift's approach around this is probably my favorite compared to the others I've used, IMO it's currently the most ergonomically frustrating thing when dealing with optional properties/parameters.. I'm wondering if a better way to solve it would be an operator that creates and unwraps an available variable. E.g:

guard let firstName =? , let lastName =?  else {
    return
}

I've seen talk around this kind of thing before, but I've never really been happy with the syntax (including my example).

As a callback to Objective-C, I always liked like the if(firstName) style syntax for nil checks. it's a hard sacrifice of brevity over clarity, but FWIW it's one of the few things I miss from Objective-C.

C# has flow-sensitive optionals ("nullables"). So if you previously checked that the value was non-nil, you can just use it directly without needing to explicitly unwrap it or bind it to a new name.

It would be nice to have something like that in Swift some day, although it might introduce source compatibility problems due to overloading based on parameter type :neutral_face:.

Oh, I didn't think about that way.
I agree your opinion. I'm also think adding unwraping method can hurt the clarity of the language.
Nevertheless, Why I suggest that unwrapping function, swift doesn't have transforming multiple optional way.
If I use operators for unwrap, source code can be

func fullName(firstName: String, lastName: String) -> String {
    return "\(firstName) \(lastName)"
}

let firstName: String? = "Raynor"
let lastName: String? = "Jim"

var fullname: String?
if let (first, last) = (firstName, lastName) as? (String, String) {
    fullname = fullName(firstName: first, lastName: last)
}

I think that pattern looks ugly.
But, We can make that code with optional transfrom.

let fullname = ((firstName, lastName) as? (String, String)).map(fullName)

It is good way to transform multiple optional. But, I think it looks ugly.
Because, we know firstName, lastName are String?, but we must cast (String, String) to use transform.

I want to write code more shortly, and clearly. and I want to decrease unnecesary unwrapping code.
I think optional transform(map/flatMap) helps decreasing unnecessary unwrapping code and writing code shortly and clearly.

That's why I want to add syntactic sugar to easy way to unwrap/transform multiple optionals.

var tuple:(A?, B?)
let unwrapped: (A, B)? = tuple.unwrap()

If you have any good idea for unwrap/transform multiple optionals, please reply.
Thank you.

if case let (firstName?, lastName?) = tuple {
    ...
}

I think that pattern can't support optional transform.
I also want transform multiple optionals like that

Kotlin has more/less the same thing and although I think I prefer Swift's approach, it seems like something that could be added to Swift as well, however Optional being a wrapper around a variable rather than simply a Type decoration does complicate things.

It shouldn't, really. The compiler would know if you already checked for the presence of a value and just unwrap the optional for you.

The problem is that it would affect overloads:

func someFunction(_: String?) { /* ... */ }
func someFunction(_: String)  { /* ... */ }

let myVal: String? = "blah blah blah"

if myVal != nil {
  someFunction(myVal)
}

Without flow-sensitive, automatic unwrapping (like today), this would dispatch to the first function. With it (like in C#), it would dispatch to the second one.

We could potentially solve it though, by only automatically unwrapping if you try to use the optional as a non-optional.

I'm not sure how to verify this, but the meaningful difference is (I think) that with flow-sensitive checking it can be implemented at a static-analysis level. The compiler can basically just omit the warning if a nullable Type has a null check.

In Swift's case it would have to also unwrap the optional, so it would essentially have to translate if foo != nil to if let foo = foo (or to whatever that explodes into under the hood). In addition to your previously mentioned overload corner case, it would also be misleading in the case of a variable that could technically change during execution, so it would only work on constant values.

Again that's not to say it isn't possible...just that it's more complex and so requires more of a reason to be implemented.

Flow-sensitive null checking feels a bit "magical" to me: I'm not a huge fan of implicitly changing the type of a variable based on a conditional check:

var foo: MyType?

 // here foo is Optional<MyType>

if foo != nil {
    foo.bar() // here foo is not MyType
}

// here foo is Optional<MyType> again?  Unless foo were let, then maybe it's still MyType?

It seems like the most likely result of such a feature is longer build times at the cost of code clarity.

1 Like

It depends very much on whether it’s present pervasively in the language or not. TypeScript’s flow-sensitive type-narrowing, limited though it is in certain ways, is pervasive in the language and is incredibly useful. I miss it whenever writing Rust or Swift.

However, when applied to optional types in TS or in C♯, the semantics are quite different than they are with Swift optionals, because in TS’s case they’re representing an untagged union, T | null; and in C♯’s case they’re dealing with a type marked as nullable but not an enumerated type as in the case of Swift/Rust/other ML-influenced languages.

The net is that it would have to implicitly unwrap the type, and it wouldn’t be a very general feature (unless it could be designed as a kind of sugar for which some protocol conformance would supply the type flow, perhaps—at which point we’re well out of my pay grade :joy:).

I believe that such a function should be called zip

You could say the same about optional chaining or if let, guard let, etc. Optionals are a special type with deeper language support than most other parts of the standard library.