The Swift Language User Group meetup held on August 21st had a very special guest: Chris Lattner, the creator of Swift! He was nice enough to answer questions from the crowd once the event was over.
But our main event was welcoming back Austin Zheng for a second talk, generously hosted by Groupon. This time, Austin spoke about Swift enumerations, pattern matching, generics, and more.
Enums (0:40)
Enums are a kind of abstraction that allow you to give a variable one value among several related values. For example, when building a state machine you could have an enum that describes the state of an object from a set number of states. There are three types of enums in Swift.
Basic Enum (1:12)
The most basic Swift enum simply has a bunch of cases and is declared using the enum key word. In this example, the enum is called Direction and can only be one of the four provided cases. To declare an enum, you use the enum name, followed by a dot and then one of the possible values. Unlike in Objective-C or C, enums are not typedefs of aliases for integers.
enum Direction {
case North
case South
case East
case West
}
let myDirection = Direction.North
Raw Value Enum (2:00)
The second type of Swift enum is a “raw value” enum, which is the same as the basic enum but has additional raw values associated with each case. In this enum Title, each case has an associated string that is the name for the title. Important to note is that the raw value and the enum itself are not interchangeable. The toRaw and fromRaw methods help to go between the value and the enum.
enum Title : String {
case CEO = "Chief Executive Officer"
case CTO = "Chief Technical Officer"
case CFO = "Chief Financial Officer"
}
let myTitle = Title.CEO
let myString : String = Title.CEO.toRaw()
let anotherTitle : Title =
Title.fromRaw("Chief Executive Officer")!
For integer type raw enums, integer numbering remains implicit if you do not specify values. Swift automatically counts up from the last provided explicit value. This can be seen in the following example:
enum Planet : Int {
case Mercury = 1
case Venus, Earth, Mars // 2, 3, 4
case Jupiter = 100
case Saturn, Uranus, Neptune // 101, 102, 103
}
Associated Value Enum (3:52)
The third type of enum is one with associated values. Each case in the enum can carry associated data. In this example from the Swift book, the bar code enum allows you to associate different QR codes with different strings.
enum Barcode {
case UPCA(sys: Int, data: Int, check: Int)
case QRCode(String)
}
let myUPC =
Barcode.UPCA(sys: 0, data: 27917_01919, check: 2)
let myQRCode =
Barcode.QRCode("http://example.com")
Associated values are especially useful in handling JSON, because arrays and dictionaries are actually types. Thus, you can create an enum to represent a JSON structure as a tree, with JSON nodes wrapping different types.
enum JSONNode {
case NullNode
case StringNode(String)
case NumberNode(Float)
case BoolNode(Bool)
case ArrayNode([JSONNode])
case ObjectNode([String:JSONNode])
}
let x : JSONNode = .ArrayNode(
[.NumberNode(10.0),
.StringNode("hello"),
.BoolNode(false)])
Switch Statements (Pattern Matching) (6:17)
Switch statements is where most of the pattern matching functionality in Swift comes from. A basic switch statement looks very similar to an Objective-C switch statement, where there is a clause followed by cases. Two things to note: Swift switches don’t have breaks, so for old style fallthrough behaviour, your code must have an explicit use of the keyword fallthrough. Switch statements must also be comprehensive, so default cases are required, which may help prevent errors.
let value = 10
switch value {
case 10: println("ten")
case 20: println("twenty")
case 30: println("thirty")
default: println("another number")
}
Ranges (8:35)
In Swift, you can also pattern match on anything that’s comparable. You can even match strings now to a case (and printout emojis for a fun parlor trick!). Anything that can be compared with the double equals operator can be matched. Swift also allows matching on ranges, where ranges can be the pattern inside a switch state.
let v: UInt = 10
switch v {
case 0...9: println("Single digit")
case 10...99: println("Double digits")
case 100...999: println("Triple digits")
default: println("4 or more digits")
}
Tuples (9:00)
Tuples are composite data types - they contain multiple elements that can all be of different types. A tuple is then represented by the types of its elements. In this tuple-based switch statement, a tuple is the input. Each element in the pattern tuple is actually a sub-pattern, so you can match against patterns in each element. The underscore represents a “I don’t care” value. In this example, the last case functions as a default because the (_, _) says the same thing as a default - none of the other cases match, and we don’t care about other possible values.
let person = ("Helen", 25)
switch person {
case ("Helen", let age):
println("Your name is Helen, and you are \(age)" + " years old")
case (_, 13...19):
println("You are a teenager")
case ("Bob", _):
println("You are not a teenager, but your name" + " is Bob.")
case (_, _):
println("no comment")
}
Binding in Cases (10:56)
If you have case statements, there may be associated data that needs to be used in the case statement body. The let binding is how you can do that. The first two cases in this example take the tuple elements and create the bindings X and Y to represent those elements.
let myTuple = ("abcd", 1234)
switch myTuple {
case let (x, y):
"The string in 'x' is \(x); " + "the integer in 'y' is \(y)"
case (let x, let y):
"Another way to do the exact same thing"
case (_, let y):
"We don't care about the string in 'x', " + "but the integer in 'y' is \(y)"
}
Enums (12:22)
You can also switch on enums, but for enums with values wrapped inside, switch statements are the only way you can access those values. The following switch statement uses let to bind each case and use the value.
enum ParseResult {
case NumericValue(Int)
case Error(String)
}
let a = ParseResult.NumericValue(1)
switch a {
case let .NumericValue(v):
"Success; numeric value is \(v)"
case .Error(let err):
"Failed; error message is \(err)"
}
Types (13:20)
The main type of switch is the type/sub-class pattern. If your switch conditional in this clause is a class, then class will have sub- or superclasses. In this example, the UIView is matched against the different possible types of views: UIImageView, UILabel, and UITableView. The “as” keyword differs from the “is” keyword in that “as” allows you to use the let binding and do something with myView, while “is” is used simply to check the type.
let myView : UIView = getView()
switch myView {
case is UIImageView:
println("It's an image view")
case let lbl as UILabel:
println("It's a label, with text \(lbl.text)")
case let tv as UITableView:
println("It's a table view, with"+ " \(tv.numberOfSections()) sections")
default:
println("It's some other type of view")
}
where clause (14:33)
Another important thing about switch statements is the where cause. This clause can be added to any case and acts as a boolean expression that returns true or false. The case is then taken only if this where clause returns true. This switch example relies not on pattern matching but instead on these where clauses, following through with the case if myView is of a certain size or other characteristic.
let myView : UIView = getView()
switch myView {
case _ where myView.frame.size.height < 50:
println("Your view is shorter than 50 units")
case _ where myView.frame.size.width > 20:
println("Your view is at least 50 units tall," + " and is more than 20 units wide")
case _ where
myView.backgroundColor == UIColor.greenColor():
println("Your view is at least 50 units tall," + " at most 20 units wide, and is green.")
default:
println("I can't describe your view.")
}
Expression Operator (15:28)
A final important feature in Swift is the expression operator.
func ~=(pattern: Type1, value: Type2) -> Bool
This operator takes the pattern and the value, and then uses pattern matching to return true or false. It can be overloaded as well to implement custom matching behaviour. The following piece of code is Swift pseudo-code to demonstrate what you could build. In an array four elements long, you could create cases that matched any combination of the elements.
let myArray = [1, 2, 4, 3]
switch myArray {
case [..., 0, 0, 0]:
doSomething()
case [4, ...]:
doSomething()
case [_, 2, _, 4]:
doSomething()
case [_, _, 3, _]:
doSomething()
case [_, _, _, 3]:
doSomething()
default:
doDefault()
}
Expression Patterns (17:42)
The following extended example is a custom implementation of the pattern match operator. This custom operator turns the elements of an array into the enum values.
enum WCEnum {
case Wildcard
case FromBeginning
case ToEnd
case Literal(Int)
}
func ~=(pattern: [WCEnum], value: [Int]) -> Bool {
var ctr = 0
for currentPattern in pattern {
if ctr >= value.count || ctr < 0 { return false }
let currentValue = value[ctr]
switch currentPattern {
case .Wildcard: ctr++
case .FromBeginning where ctr == 0:
ctr = (value.count - pattern.count + 1)
case .FromBeginning: return false
case .ToEnd: return true
case .Literal(let v):
if v != currentValue { return false }
else { ctr++ }
}
}
return true
}
Protocols (21:06)
In order to understand generics, you have to understand protocols, which are something that existed in Objective-C as well. Protocols are basically a contract that can define nothing at all or any number of methods and properties. However, protocols can’t have any implementation details - they can only have method signatures and property names. MyProtocol in this code defines just one property as requiring a getter and setter, as well as one method.
protocol MyProtocol {
var someProperty : Int { get set }
func barFunc (x: Int, y: Int) -> String
}
Types can conform to none, one, or multiple protocols. Protocols can also inherit from other protocols, thereby getting all the parent protocol’s methods and properties.
Conformance (23:18)
With protocols, you can make classes, structs, and enums conform to them. By making a type conform to a protocol, you are telling the compiler that your type will match the definitions set in the protocol. MyClass conforms to MyProtocol in providing implementations that match.
class MyClass {
// Nothing here...
// This won't compile!
}
class MyClass : MyProtocol {
var myProperty : Int = 0
// Property conformance
func barFunc (x: Int, y: Int) -> String {
return "\(x) + \(y) = \(x + y)"
}
var someProperty : Int {
get {
return myProperty
}
set {
myProperty = newValue
}
}
}
Uses (24:06)
There are a few reasons you may want to use protocols. The first reason is parent-child relationships, where you don’t want all the children to have a specific class of a parent. For example, UITableView comes with Cocoa and is a list of objects that specifies the number and content of cells. For the delegate to provide that information, it just has to implement the protocol which has methods for the cells. This way, you can avoid specifying the delegate as a sub-class of a class.
A second major use for protocols can be seen in the Swift Standard Library, where they add functionality to a type, piece by piece. Equatable, LogicValue, and AbsoluteValuable are all protocols that are implemented by different types in the library. The LogicValue protocol has a method that takes an object and turns it into true or false, so if you create a custom type and implement this protocol, you can use that type in an if statement’s clause.
One final use for protocols is to allow different types to be described by the same generic type parameter. For example, if you wanted to put different types into an array, you could give the objects a protocol and then declare the array with the protocol. You then have an array of equatables and anything that conforms to the equatable in that array can be added.
protocol JSONType { }
extension String : JSONType { }
extension Float : JSONType { }
extension Bool : JSONType { }
extension Array : JSONType { }
extension Dictionary : JSONType { }
let b : Array<JSONType> = [10.1, 10.2, "foo"]
let a : Array<JSONType> = [10.0, "bar", false, b]
Generics (28:36)
Generics did not exist in Objective-C. THey are basically the statically typed languages’ answer to the flexibility of dynamically typed languages. Generics are used to allow you to use the same code for different types. The type parameter
func swapItems<T>(inout this: T, inout that: T) {
let tempThis = this
this = that
that = tempThis
}
With generics, you can also enforce more constraints, as in the following example. The “: Equatable” allows you to constrain T to any type that conforms to Equatable, or is valid with the double equals operator.
func firstAndLastAreEqual<T : Equatable>
(someArray: Array<T>) -> Bool {
let first = someArray[0]
let last = someArray[someArray.count - 1]
return first == last
}
You can also use generics for either functions or type declarations. Type information is available at runtime, which is helpful. The compiler can optimize by creating specific versions for each type that’s being used (like C++ templates), but it can also fall back to the generic implementation.
Extended Example (33:56)
In the extended example, the goal was to create a function that was typesafe and could handle every case that involved the following types: Array, Dictionary, SquareMatrix, TreeNode. The function was to take a collection of one of the above types, take an array, and append the items in that collection to the array.
A protocol was defined to specify that a type can give back all of the elements inside of it. You have containers that can implement “AllElements”. On a generic level, you might need to know the type of the elements inside the collection, and so a wildcard is called “ElementType”. And so, you specify the element type when you implement the function itself.
protocol AllElements {
typealias ElementType
// Return an array containing all the objects
// in the collection
func allElements() -> Array<ElementType>
}
Another thing you can use is the NilLiteral protocol, the point of which is to take nil and turn it into something equivalent to nil in that type. For an int, it would return 0, or maybe for a string, it would return “”. In the following example, you would want it to return something of type self.
protocol NilLiteralConvertible {
class func convertFromNilLiteral() -> Self
}
Extensions then add methods, computed properties, and protocol conformance to existing types. Array and SquareMatrix are both fairly straightforward - you just return the arrays themselves. For the Dictionary, you iterate through all the elements, create a buffer, and return it with the elements inside. Finally, for the tree, you go through the tree in a recursive traversal and again add the elements to a buffer.
Put together, the final function appendToArray calls AllElements and gives “a”, which is an array with all the elements on the source. Then each of those elements is just added to the array in “dest”. This works because the generic argument “T” has to implement AllElements, and U has been constrained to the same type as T. The where clause is included in case you want to do more than have this type implement one protocol. Before the where, you declare type arguments, which can conform to either one or none protocols. If you want T to conform to 2+ protocols, add a “where” clause to constrain. You can then constrain any free type arguments you declared earlier and any associated types associated with the type arguments via protocols (e.g. T.SomeType).
func appendToArray
<T: AllElements, U where U == T.ElementType>
(source: T, inout dest: Array<U>) {
let a = source.allElements()
for element in a {
dest.append(element)
}
}
var buffer = ["a", "b", "c"]
appendToArray([1: "foo", 2: "bar"], &buffer)
Now you’ve created five methods for this one process. In actuality, you would have your containers implement Sequence and use a for-in loop to go through all the elements. This requires knowledge of Generators, EnumerateGenerators, and other material.
Q&A (46:05)
Q: In the extension example, are we extending all arrays of anything?
Austin: Yes, which is why this is a bad idea in practice. You can’t actually say that you only want arrays with JSON type objects to also implement JSON array, which is needed to ensure that you have valid JSON.
Q: Is the type T built-in? Where does it come from?
Austin: T is actually coming from the declaration of array. If you command-click an array in XCode, the declaration of the array shows that it declares a generic argument T that has some constraints.
Q: Is there any kind of trace for generics and the way it works? Is it traceable to an existing language or is it brand-new?
Austin: I don’t know the answer, but I’m sure there are precedents. Generics in Swift kind of de-emphasize object oriented programming because they’re built more around protocols than around class hierarchies. You might get a closer procedent if you looked at Haskell.
Q: What is the exclamation point used for?
Austin: The exclamation mark is for optionals. Everything in Swift is non-nilable by default, so if you want to set something to nil, it must be declared as an optional. The question mark after the type signifies this. Then, the exclamation mark, or bang, takes the value out of the optional so you can use it.
Q: Is there something like value template parameters in Swift? Or should we just type in, type template?
Austin: I don’t think so, I don’t think Swift generics are as powerful.
Q: Can you explain let? It seemed like it would sometimes check the case and othertimes it would set it to the constant.
Austin: A let pattern kind of matches everything, but also takes a value and puts it in the constant for later use. When we use “as”, it both checks the type and also puts it in the constant.
Receive news and updates from Realm straight to your inbox