Grantismo
Grantismo

Reputation: 3819

Optional chaining in swift with arbitrary operations?

Apple supplies an example of succinct optional chaining

class Person {
  var residence: Residence?
}

class Residence {
  var numberOfRooms = 1
}

let john = Person()

if let roomCount = john.residence?.numberOfRooms {
  println("John's residence has \(roomCount) room(s).")
} else {
  println("Unable to retrieve the number of rooms.")
}

Imagine trying to adjust the condition with some arithmetic operations. This results in a compiler error as the modulo operator doesn't support optionals.

if john.residence?.numberOfRooms % 2 == 0 { 
  // compiler error: Value of optional type Int? not unwrapped
  println("John has an even number of rooms")
} else {
  println("John has an odd number of rooms")
}

Of course you could always do something like the following, but it lacks the simplicity and succinctness of optional chaining.

if let residence = john.residence {
  if residence.numberOfRooms % 2 == 0  {
    println("John has an even number of rooms")
  }else{
    println("John has an odd number of rooms")
  }
} else {
  println("John has an odd number of rooms")
}

Are there any Swift language features which might provide a better solution?

Upvotes: 5

Views: 756

Answers (7)

Jean-Pierre
Jean-Pierre

Reputation: 570

To make the following line work (i.e. make the operator directly accessing the value behind the optional left operand):

if john.residence?.numberOfRooms % 2 == 0

the operator must be in a precedence-group with assignment: true. The modulo operator is in the MultiplicationPrecedence precedence-group:

precedencegroup MultiplicationPrecedence {
  associativity:left
  higherThan: AdditionPrecedence
}

You can make your own prededence-group with assignment: true

precedencegroup AssignmentTruePrecedence {
    assignment: true
    associativity:left
    higherThan:AdditionPrecedence
    lowerThan:MultiplicationPrecedence
}

Now you can declare your own operator, assign it to the above precedence-group, and define the associated function to do the modulo operation.

infix operator %| : AssignmentTruePrecedence

extension Int {

    static func %| (left: Int, right: Int)->Int {
        return left % right
    }

}

The line will work without error with our new modulo operator:

if john.residence?.numberOfRooms %| 2 == 0

It turns out that you don't even have to declare a new operator, as redeclaring the modulo operator and assigning it to our new precedence-group seems to work!

infix operator % : AssignmentTruePrecedence

Upvotes: 0

angusc
angusc

Reputation: 411

I've been trying to do the same thing for the last 10 minutes, using both optional chaining and the nil coalescing operator; the compiler doesn't like either of them. Can be done with an tf/then to get it working, but it feels ugly.

Should be able to do with an extension to Double, though I agree it feels like swift could (should?) support optional chaining in the standard operators. The following works in a playground:

extension Double {
  func divide(by: Double) -> Double {
    return (self/by)
  }
}

var optDouble: Double? = nil
var result = optDouble?.divide(by: 100.0)   // nil

optDouble = 20.25
result = optDouble?.divide(by: 100.0)       // 0.2025

Upvotes: 0

William Kinaan
William Kinaan

Reputation: 28819

A possible solution would be to use Guard

func testOddEven(){
    guard let number = john.residence?.numberOfRooms else {print("Doesn't even have a residence loool"); return }
    switch number % 2 == 0 {
    case true:
        print("event")
        break;
    case false:
        print("odd")
        break;
    }
}

Upvotes: 0

Wes Campaigne
Wes Campaigne

Reputation: 4170

Pattern matching is quite powerful, and can work in situations like this:

switch john.residence?.numberOfRooms {
case .Some(let x) where x % 2 == 0:
    println("Even number of rooms")
default:
    println("Odd number of rooms")
}

Upvotes: 2

Grantismo
Grantismo

Reputation: 3819

Best I could come up with is an extension on Optionals called omap (optional map).

extension Optional{
  func omap<K:Any>(optionalBlock:(T)->(K?))->K?{
    if self{
      return optionalBlock(self!)
    }
    return nil
  }
}

Similar to map, omap simply returns an instance of the optional to a supplied closure, but handles the nil case appropriately.

This allows you to chain an optional with arbitrary operations like so:

if (john.residence.omap{r in r.numberOfRooms % 2 == 0}){
  println("John has an even number of rooms")
} else {
  println("John has an odd number of rooms")
}

See: https://gist.github.com/Grantismo/3e1ba0412e911dfd7cfe

Upvotes: 0

powerpete
powerpete

Reputation: 160

I think what you are looking for is generally called a monad in functional programming.

It is not directly available in swift, but by using some of the language features, you can implement monads yourself in a generic way. (And also define a nice looking infix operator that makes it look like the monads in Haskell)

A quick google search for "monad swift" turned up some promising looking code at https://gist.github.com/cobbal/7562875ab5bfc6f0aed6

Upvotes: 3

Lou Franco
Lou Franco

Reputation: 89232

If I understand, you want it to say it's even if it's definitely even and odd if it's either odd or any of the optional values aren't set. If so:

if john.residence?.numberOfRooms && (john.residence!.numberOfRooms! % 2) == 0 {
    println("John has an even number of rooms")
} else {
    println("John has an odd number of rooms")
}

is the best I could do without a helper function. The issue is that you need methods to call on optional values to chain.

There are either bugs with if and Bool when optional chaining is being used, or I don't understand it well enough, but this works for me:

extension Int{
    func isEven() -> Bool { return self % 2 == 0 }
}

if (john.residence?.numberOfRooms?.isEven() == true){
    println("John has an even number of rooms")
} else {
    println("John has an odd number of rooms")
}

If you write

if (john.residence?.numberOfRooms?.isEven())

That resolves as true as long as numberOfRooms has some value, even if isEven() returns false. As if the expression was Bool?, but it's not, it's Bool.

Upvotes: 0

Related Questions