The deeper parts of a language are typically hidden away from the user; only accessible to the inner machinations of the linker, lexer and loader. However, The Swift Standard Library exposes language features we take for granted in the form of protocols. Specifically, I’ll be talking about the “ExpressibleBy___” family of protocols. This power is not without great responsibility, as we’ve seen with custom operators, “too much of a good thing” can harm readability and maintenance.
Since 2012, it’s been possible to initialize an NSDictionary, NSArray, NSNumber or NSString with a ‘literal’ expression.
NSNumber *numb = @1;
NSArray *things = @[@3,@45,@65];
NSDictionary *map = @{@"Florida" : @1 , @"Nevada" : @6 };
NSString *greet = @"Hello there!";
The essence of a “literal” value is a human readable, ASCII-art style token representation of a data structure. The literal makes no assumptions about the datatypes contained within, and remains flexible for any type. The advantages of literals are readability and ease of initialization. Don’t make me type more!
Swift exposes the ability to initialize a type within a literal through the “ExpressibleBy _____” class of protocols.
- ExpressibleByArrayLiteral
- ExpressibleByBooleanLiteral
- ExpressibleByDictionaryLiteral
- ExpressibleByFloatLiteral
- ExpressibleByIntegerLiteral
- ExpressibleByStringLiteral
- ExpressibleByUnicodeScalarLiteral
- ExpressibleByExtendedGraphemeClusterLiteral
In this article, we’ll focus on what I think is a good use case for the protocol “ExpressibleByArrayLiteral.”
Array-ish Data Structures
As mentioned, literal values improve code readability by “looking” just like an ASCII-art data structure. They are just like what we may draw on a napkin or whiteboard when trying to solve a problem.
For example, take the Swift array literal:
[1,2,3,4,5,6,7,8]
Depending on the problem, this could potentially represent a linked-list! We could initialize the following data structure with this literal.
Note, code is Swift 3 and will run on Xcode 8.3. Earlier versions may throw an error due to the nested type within a generic class. Code below is for demonstration purposes. Recommend that your linkedlist conform to Sequence protocol. :]
class LinkedList<T> : ExpressibleByArrayLiteral, CustomDebugStringConvertible {
var debugDescription: String {
get {
var accum = ""
guard let startNode = self.head else { return "(empty list)" }
accum += startNode.debugDescription
var currentNode:Node = startNode
while(true) {
guard let node = currentNode.next else { break }
accum += node.debugDescription
currentNode = node
}
return accum
}
}
var head:Node?
var tail:Node?
typealias Element = T
required public init(arrayLiteral elements:Element...) {
for i in elements {
if self.head == nil {
self.head = Node(data:i)
self.tail = self.head
continue
}
let newNode = Node(data:i)
self.tail?.next = newNode
self.tail = newNode
}
}
class Node : CustomDebugStringConvertible {
var debugDescription: String {
get {
var accum = "(\(data))->"
if next == nil {
accum += "*"
}
return accum
}
}
var next:Node?
var data:T
init(data:T) {
self.data = data
}
}
}
let link:LinkedList<Int> = [1,2,3,4,5,6,7,8]
print(link)
// Prints (1)->(2)->(3)->(4)->(5)->(6)->(7)->(8)->*
It’s important to note that the data-structure must be initialized with an array literal. The following will not work:
let someArray = [1,2,3,4]
let list:LinkedList<Int> = someArray //Error!
A Stack and a Queue could also be modeled by an array literal. These types might look like:
struct ConvertibleStack<T> : ExpressibleByArrayLiteral, CustomDebugStringConvertible {
var debugDescription: String {
get {
return "Stack: \(storage)"
}
}
typealias Element = T
private var storage: [T] = []
public init(arrayLiteral elements:Element...) {
self.storage = elements
}
mutating func push(item:T) {
storage.insert(item, at: 0)
}
mutating func pop() -> T? {
guard storage.count > 0 else { return nil }
return storage.remove(at: 0)
}
}
and as a Queue:
struct ConvertibleQueue<T> : ExpressibleByArrayLiteral, CustomDebugStringConvertible {
var debugDescription: String {
get {
return "Queue: \(storage)"
}
}
typealias Element = T
private var storage: [T] = []
public init(arrayLiteral elements:Element...) {
self.storage = elements
}
mutating func push(item:T) {
storage.append(item)
}
mutating func pop() -> T? {
guard storage.count > 0 else { return nil }
return storage.remove(at: 0)
}
}
Considerations
Thanks to ExpressibleByArrayLiteral
our generic linked-list, queue, or stack certainly benefits from increased readability and ease of initialization. There are, however some caveats to consider before adding literal initialization to a data-type.
No failable or throwable initializer
As previously mentioned, the literal value can be of virtually any
type, and should work with array literals full of numbers, strings, or custom-objects. This is where generics are definitely a requirement of any data-structure which conforms to ExpressibleByArrayLiteral
. The initializer also makes no assumption about the structure of the literal. Multi-nested arrays as well as flat ones all work fine given the correct generic type:
var stack:ConvertibleStack<Any> = [1, [1,2,3] , 4, 5]
stack.pop() // 1
stack.pop() // [1,2,3]
A wise rule to consider is if your custom type requires parsing, or validation of any part of the literal, it would be better written as a custom initializer. Consider the following example of a new type “SuperDateFromArray” which conforms to ExpressibleByArrayLiteral
:
Var theDate:SuperDateFromArray = [“4” , “30” , “2016” , “1” , “30”, “PM”]
It certainly is convenient to initialize a date object from an array of Strings! But what if the user puts in numbers instead of strings? Or (like the rest of the world) invert the day and month ? Because ExpressibleByArrayLiteral
does not allow for a failable or throwable initializer, what do we return? In Swift, returning an “empty” object feels like the wrong thing. This functionality would be better written as:
init?(fromArray:[String])
Date strings, currency strings, token sequences, anything that requires special validation is better handled within its own failable or throwable initializer. It’s not the right time to “express” yourself, literally. :)
In closing, initializing from a literal value can improve readability and initialization if used correctly. What other data structures “look” like an array, or a dictionary? Would this approach work for a trie or directed graph? I leave this exploration up to the reader :) Happy hacking!