The "ExpressibleBy-" Protocols in Swift

Swift gives us many interesting features to write cleaner and more obvious code. This code is more readable, and it helps both SDK consumers and code maintainers.

One such feature Swift has is the ExpressibleBy- family of protocols. This is a set of protocols that allow you to instantiate objects by providing some native Swift object. For example, we can instantiate an object providing a Boolean, or a String.

This family of protocols consist of the following protocols (this is not a complete list):

  • ExpressibleByNilLiteral
  • ExpressibleByStringLiteral
  • ExpressibleByIntegerLiteral
  • ExpressibleByFloatLiteral
  • ExpressibleByBooleanLiteral
  • ExpressibleByArrayLiteral
  • ExpressibleByDictionaryLiteral

We can use these, and a few others, to create neater code for certain initializers.

Using the ExpressibleBy- Protocols

The different variations of these protocols have different requirements. We will explore a few of them so you can get up to speed and know what to do when you find a situation when you can use them.

ExpressibleByNilLiteral

Suppose you have a requirement that requires that, when an object gets initialized with nil, you don’t want the whole object to be nil. You may have a custom requirement in which you need to consider nil something different.

For example, suppose you want to treat the existence of an object that actually does exist, but has all its properties set to nil.

To use ExpressibleByNilLiteral, you need to implement the print("New doll: \(doll.name)") method.

Consider the following example:

public class Doll: ExpressibleByNilLiteral {
  var name: String?
  var maker: String?
  
  public required init(nilLiteral: ()) {
    self.name = nil
    self.maker = nil
  }
}

When we create a Doll object and assign it to nil, we will create a doll object whose name and maker properties point to nil.

let doll: Doll = nil
print("New doll: \(doll.name)") // Prints "New doll: nil"

Make sure you only use this when it really make sense to, as new programmers to your codebase may be confused when they see a non-optional being assigned nil.

ExpressibleByStringLiteral

We can instantiate our objects using strings by using ExpressibleByStringLiteral. When using this protocol, make sure you implement at least the public required init(stringLiteral:) method:

public class Doll: ExpressibleByStringLiteral {
  var name: String
  var maker: String
  
  public required init(stringLiteral value: StringLiteralType) {
    let splat = value.split(separator: "|")
    self.name = String(splat.first ?? "")
    self.maker = String(splat.last ?? "")
  }
}

With this, we can instantiate a new Doll with a string with the format DOLL_NAME|DOLL_MAKER, as so:

let aliceDoll: Doll = "Classical Alice|Pullip"

print("\(aliceDoll.name) was made by \(aliceDoll.maker)") // Prints "Classical Alice was made by Pullip

This is one of my personal favorites, as it can help you create nice initializers for complex data.

ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral

These two are very similar, and as such they share the same section.

It is very easy to use a number to instantiate our objects. The following example declares MultipliedNumber, which takes a number and multiplies it by itself:

public class MultipliedNumber: ExpressibleByIntegerLiteral {
  let number: Int
  
  public required init(integerLiteral value: IntegerLiteralType) {
    self.number = value * value
  }
}
let myNumber: MultipliedNumber = 8

print("myNumber is \(myNumber.number)")

ExpressibleByBooleanLiteral

I really like this one, because if you have an object that simply keeps track of different boolean states, you can use this to initialize them all to the same value.

public class DollFlags: ExpressibleByBooleanLiteral {
  var hasWig: Bool
  var hasStockOutfit: Bool
  var hasExtraAccessories: Bool
  
  public required init(booleanLiteral value: BooleanLiteralType) {
    self.hasWig = value
    self.hasStockOutfit = value
    self.hasExtraAccessories = value
  }
}

Now we can initialize them all to the same value by initializing it as so:

let flags: DollFlags = true

You can naturally do much more with it, but this is one of my favorite use cases.

ExpressibleByArrayLiteral

Now we will see two of the most interesting ones due to their additional constraints. because Arrays and Dictionaries are typed in Swift, we need to keep that in mind when using ExpressibleByArrayLiteral and ExpressibleByDictionaryLiteral.

In the following example, we will create an object that takes an array of numbers, multiplies them by themselves, and stores that result:

public class ArrayNumberMultipler: ExpressibleByArrayLiteral {
  public typealias ArrayLiteralElement = Int
  
  let numbers: [ArrayLiteralElement]
  
  public required init(arrayLiteral elements: ArrayLiteralElement...) {
    self.numbers = elements.map { $0 * $0 }
  }
}

These protocols use associated types to assign the data type of the elements. In our case, our object can be initialized with an array of integers, so we assign ArrayLiteralElement to Int.

let myNumbers: ArrayNumberMultipler = [2, 4, 6]

print(myNumbers.numbers) // Prints "[4, 16, 36]"

ExpressibleByDictionaryLiteral

Finally, the last ExpressibleBy- protocol we will explore will allow us to instantiate objects with a dictionary. This can be very cool and handy in certain cases.

public class Doll: ExpressibleByDictionaryLiteral {
  public typealias Key = String
  public typealias Value = String
  
  let name: String?
  let maker: String?
  
  public required init(dictionaryLiteral elements: (Key, Value)...) {
    self.name = elements.filter { $0.0 == "name" }.first?.1 ?? ""
    self.maker = elements.filter { $0.0 == "maker" }.first?.1 ?? ""
  }
}
let doll: Doll = ["name": "Classical Alice", "maker": "Pullip"]

Once again, we have associated types, this time for the Key and Value of the dictionary.

Conclusion

The ExpressibleBy- protocols are very helpful and they can help us write very expressive code. We shouldn’t abuse them as they can be shocking for someone looking at a codebase the first time, but when used in moderation, they are one of my favorite features of Swift.