Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoid consecutive "if let" declarations in Swift [duplicate]

Tags:

swift

In Swift I used if let declarations to check if my object is not nil

if let obj = optionalObj
{
}

But sometimes, I have to face with consecutive if let declarations

if let obj = optionalObj
{
    if let a = obj.a
    {
        if let b = a.b
        {
            // do stuff
        }
    }
}

I'm looking for a way to avoid consecutive if let declarations.

I would try something like :

if let obj = optionalObj && if let a = obj.a && if let b = a.b
{
    // do stuff
}

But the swift compiler do not allow this.

Any suggestion ?

like image 226
Jean Lebrument Avatar asked Oct 31 '14 09:10

Jean Lebrument


4 Answers

I wrote a little essay on the alternatives some time ago: https://gist.github.com/pyrtsa/77978129090f6114e9fb

One approach not yet mentioned in the other answers, which I kinda like, is to add a bunch of overloaded every functions:

func every<A, B>(a: A?, b: B?) -> (A, B)? {
    switch (a, b) {
        case let (.Some(a), .Some(b)): return .Some((a, b))
        default:                       return .None
    }
}

func every<A, B, C>(a: A?, b: B?, c: C?) -> (A, B, C)? {
    switch (a, b, c) {
        case let (.Some(a), .Some(b), .Some(c)): return .Some((a, b, c))
        default:                                 return .None
    }
}

// and so on...

These can be used in if let statements, case expressions, as well as optional.map(...) chains:

// 1.
var foo: Foo?
if let (name, phone) = every(parsedName, parsedPhone) {
    foo = ...
}

// 2.
switch every(parsedName, parsedPhone) {
    case let (name, phone): foo = ...
    default: foo = nil
}

// 3.
foo = every(parsedName, parsedPhone).map{name, phone in ...}

Having to add the overloads for every is boilerplate'y but only has to be done in a library once. Similarly, with the Applicative Functor approach (i.e. using the <^> and <*> operators), you'd need to create the curried functions somehow, which causes a bit of boilerplate somewhere too.

like image 25
Pyry Jahkola Avatar answered Nov 05 '22 01:11

Pyry Jahkola


Update

In swift 1.2 you can do

if let a = optA, let b = optB {
  doStuff(a, b)
}

Original answer

In your specific case, you can use optional chaining:

if let b = optionaObj?.a?.b {
  // do stuff
}

Now, if you instead need to do something like

if let a = optA {
  if let b = optB {
    doStuff(a, b)
  }
}

you're out of luck, since you can't use optional chaining.

tl; dr

Would you prefer a cool one-liner instead?

doStuff <^> optA <*> optB

Keep reading. For how scaring it might look, this is really powerful and not so crazy to use as it seems.

Fortunately, this is a problem easily solved using a functional programming approach. You can use the Applicative abstraction and provide an apply method for composing multiple options together.

Here's an example, taken from http://robots.thoughtbot.com/functional-swift-for-dealing-with-optional-values

First we need a function to apply a function to an optional value only only when it contains something

// this function is usually called fmap, and it's represented by a <$> operator
// in many functional languages, but <$> is not allowed by swift syntax, so we'll
// use <^> instead
infix operator <^> { associativity left }

func <^><A, B>(f: A -> B, a: A?) -> B? {
    switch a {
    case .Some(let x): return f(x)
    case .None: return .None
    }
}

Then we can compose multiple options together using apply, which we'll call <*> because we're cool (and we know some Haskell)

// <*> is the commonly-accepted symbol for apply
infix operator <*> { associativity left }

func <*><A, B>(f: (A -> B)?, a: A?) -> B? {
    switch f {
    case .Some(let value): return value <^> a
    case .None: return .None
    }
}

Now we can rewrite our example

doStuff <^> optA <*> optB

This will work, provided that doStuff is in curried form (see below), i.e.

func doStuff(a: A)(b: B) -> C { ... }

The result of the whole thing is an optional value, either nil or the result of doStuff

Here's a complete example that you can try in the playground

func sum(a: Int)(b: Int) -> Int { return a + b }

let optA: Int? = 1
let optB: Int? = nil
let optC: Int? = 2

sum <^> optA <*> optB // nil
sum <^> optA <*> optC // Some 3

As a final note, it's really straightforward to convert a function to its curried form. For instance if you have a function taking two parameters:

func curry<A, B, C>(f: (A, B) -> C) -> A -> B -> C {
    return { a in { b in f(a,b) } }
}

Now you can curry any two-parameter function, like + for example

curry(+) <^> optA <*> optC // Some 3
like image 60
Gabriele Petronella Avatar answered Nov 05 '22 03:11

Gabriele Petronella


In some cases you can use optional chaining. For your simple example:

if let b = optionalObj?.a?.b {
    // do stuff
}

To keep your nesting down and to give yourself the same variable assignments, you could also do this:

if optionalObj?.a?.b != nil {
    let obj = optionalObj!
    let a = obj.a!
    let b = a.b!
}
like image 32
vacawama Avatar answered Nov 05 '22 01:11

vacawama


After some lecture thanks to Martin R, I found an interesting workaround: https://stackoverflow.com/a/26012746/2754218

func unwrap<T, U>(a:T?, b:U?, handler:((T, U) -> ())?) -> Bool {
    switch (a, b) {
    case let (.Some(a), .Some(b)):
        if handler != nil {
            handler!(a, b)
        }
        return true
    default:
        return false
    }
}

The solution is interesting, but it would be better if the method uses variadic parameters.

I naively started to create such a method:

extension Array
{
    func find(includedElement: T -> Bool) -> Int?
    {
        for (idx, element) in enumerate(self)
        {
            if includedElement(element)
            {
                return idx
            }
        }

        return nil
    }
}

func unwrap<T>(handler:((T...) -> Void)?, a:T?...) -> Bool
{

    let b : [T!] = a.map { $0 ?? nil}

    if b.find({ $0 == nil }) == nil
    {
        handler(b)
    }
}

But I've this error with the compiler: Cannot convert the expression's type '[T!]' to type '((T...) -> Void)?'

Any suggestion for a workaround ?

like image 1
Jean Lebrument Avatar answered Nov 05 '22 01:11

Jean Lebrument