Mikey T.K.
Mikey T.K.

Reputation: 1160

Swift nil coalescing when using array contents

I'm just wondering if there's a more readable/less verbose way to write the following:

let rankMap: Dictionary<Int,String> = [1:"Ace", 11:"Jack", 12:"Queen", 13:"King"]

func getCardImage(suit: String, rank: Int) -> NSImage {
  var imgRank: String
  if rankMap[rank] == nil {
    imgRank = rank.description
  } else {
    imgRank = (rankMap[rank]! as NSString).substringToIndex(1)
  }   
  let imgSuit = (suit as NSString).substringToIndex(1)
  //let imgRank: String = rankMap[rank] ?? rank.description

  return NSImage(named: imgRank + imgSuit)
}

In particular, I'm interested in the part where rankMap is checked vs nil. Whatever is returned from the array needs to have every character except the first chopped off. I'm unable to write something like this:

let imgRank: String = (rankMap[rank] as NSString).substringToIndex(1) ?? rank.description

..due to the semantics around nil coalescing. Is there anything else I can do aside from the "hard way" of just doing an "if nil" check?

Upvotes: 0

Views: 432

Answers (2)

rickster
rickster

Reputation: 126137

You don't need substringToIndex to get the first character of a Swift string. String is a CollectionType, to which this generic function from the standard library applies:

func first<C : CollectionType>(x: C) -> C.Generator.Element?

So you might start by trying something like this:

let imgRank = first(rankMap[rank]) ?? rank.description // error

That doesn't work, for two reasons:

  • String.Generator.Element is a Character, not a String, so if you're going to put it in a ?? expression, the right operand of ?? needs to also be a Character. You can construct a Character from a String (with Character(rank.description)), but that'll crash if that string has more than one character in it.
  • rankMap[rank] is an optional, so you can't pass it directly to first. But if you try to force-unwrap it immediately (with first(rankMap[rank]!), you'll crash upon looking up something not in the dictionary.

You could try solving these problems together by putting the ?? inside the first call, but you probably want a two-character string for rank 10.

It'd be nice if we could treat String like an Array, which provides var first for getting its first element. (And have a version of first for strings that returns a String.) Then we could take advantage of optional chaining to put a whole expression to the left of the ??, and just write:

let imgRank = rankMap[rank]?.first ?? rank.description

Well, let's write it in an extension!

extension String {
    var first : String? {
        // we're overloading the name of the global first() function,
        // so access it with its module name
        switch Swift.first(self) {
        case let .Some(char):
            return String(char)
        case .None:
            return nil
        }
    }
}

Now you can be nice and concise about it:

let imgRank = rankMap[rank]?.first ?? String(rank)

(Notice I'm also changing rank.description to String(rank). It's good to be clear -- description gives you a string representation of a value that's appropriate for printing in the debugger; String() converts a value to a string. The former is not guaranteed to always match the latter, so use the latter if that's what you really want.)


Anyhow, @RobNapier's advice stands: you're probably better off using enums for these. That gets you a lot less uncertainty about optionals and invalid lookups. But seeing what you can do with the standard library is always a useful exercise.

Upvotes: 0

Rob Napier
Rob Napier

Reputation: 299355

I removed the NSImage part, just so it's easier to test as a filename, but this should behave similarly.

func cardImage(suit: String, rank: Int) -> String {
  let imgRankFull = rankMap[rank] ?? rank.description
  let imgRank = first(imgRankFull) ?? " "

  let imgSuit = first(suit) ?? " "

  return imgRank + imgSuit
}

This shows how to use ??, but it still has lots of little problems IMO. I would recommend replacing suit and rank with enums. The current scheme has lots of ways it can lead to an error, and you don't do any good error checking. I've dodged by returning " ", but really these should be either fatalError or an optional, or a Result (an object that carries a value or an error).

Upvotes: 2

Related Questions