Update: This post was written in 2014 when Swift first came on to the scene. As such it may be out of date and not applicable to Swift 2. Proceed with caution!
We’ve said it before and we’ll say it again: Make everything as simple as possible, but not simpler. Wait, no, Albert Einstein said that. We said more descriptive code is better code, and that’s one reason we fell in love with programming in Objective-C. This
1 |
convertDegrees(50, 'C', 'R') |
is much less readable than
1 |
[temperature convertDegrees:50 fromScale:"Celsius" toScale:"Rankine"] |
Yes, you have to type more, but the resulting code is easier to read and comprehend what is going on. Enough already with the trying to make code as compact or as short as possible!
The good folks at Apple must have also appreciated this feature of Objective-C enough to include it in Swift. Using named parameters you can declare functions that require argument labels in the function call. For example:
1 |
func convertDegrees(degrees:Double, fromScale s1:TemperatureScale, toScale s2:TemperatureScale) -> Double |
allows you to invoke the function as:
1 |
convertDegrees(50, fromScale:.Celsius, toScale:.Fahrenheit) |
Failure to include the labels in the call will result in a missing argument labels
error from the Swift compiler.
This is fine and all, but, it seems a bit annoying that when using an argument label that could be also be used as the variable name itself to have to provide some other name like s1
or s2
. What would be nice is if we can require the argument label but then reuse that label as the variable name as well. Of course this post would come to an abrupt and unfulfilling end if there weren’t a way to do that. Fortunately there is!
1 |
func convertDegrees(degrees:Double, #fromScale:TemperatureScale, #toScale:TemperatureScale) |
Using the #
symbol you tell Swift, “I want to use this as the argument label and the variable name.” Now rather than using s1
and s2
in your convertDegrees
function, you can use fromScale
and toScale
.
The full source code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import Foundation enum TemperatureScale { case Fahrenheit, Celsius, Kelvin, Rankine } func convertDegrees(degrees:Double, #fromScale:TemperatureScale, #toScale:TemperatureScale) -> Double { // If fromScale and toScale are the same just return degrees if (fromScale == toScale) { return degrees } // Convert to Kelvin then to our target var kelvin:Double switch fromScale { case .Fahrenheit: kelvin = (degrees + 459.67) * (5/9) case .Celsius: kelvin = degrees + 273.15 case .Kelvin: kelvin = degrees case .Rankine: kelvin = (degrees) * (5/9) } var result:Double switch toScale { case .Fahrenheit: result = (kelvin * (9/5)) - 459.67 case .Celsius: result = kelvin - 273.15 case .Kelvin: result = kelvin case .Rankine: result = (kelvin * (9/5)) } return result } var temperature = 120.0 var converted:Double converted = convertDegrees(temperature, fromScale:.Celsius, toScale:.Fahrenheit) println("\(temperature) Celsius is \(converted) Fahrenheit") |
Astute readers will notice that our enumeration could have used the String
type, and we can improve on things further and take advantage of named elements in Swift tuples as well:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import Foundation enum TemperatureScale:String { case Fahrenheit = "Fahrenheit", Celsius = "Celsius", Kelvin = "Kelvin", Rankine = "Rankine" } ... println("\(temperature.degrees) \(temperature.scale.toRaw()) is \(converted.degrees) \(converted.scale.toRaw()).") |
Throwing in named element tuples, we can refactor our entire function to:
1 |
func convertTemperature((degrees:Double, scale:TemperatureScale), #toScale:TemperatureScale) |
A final look:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
import Foundation enum TemperatureScale:String { case Fahrenheit = "Fahrenheit", Celsius = "Celsius", Kelvin = "Kelvin", Rankine = "Rankine" } func convertTemperature(temperature:(degrees:Double, scale:TemperatureScale), #toScale:TemperatureScale) -> (degrees:Double,scale:TemperatureScale) { // If fromScale and toScale are the same just return degrees if (temperature.scale == toScale) { return temperature } // Convert to Kelvin then to our target var kelvin:Double let degrees = temperature.degrees switch temperature.scale { case .Fahrenheit: kelvin = (degrees + 459.67) * (5/9) case .Celsius: kelvin = degrees + 273.15 case .Kelvin: kelvin = degrees case .Rankine: kelvin = (degrees) * (5/9) } var converted:Double switch toScale { case .Fahrenheit: converted = (kelvin * (9/5)) - 459.67 case .Celsius: converted = kelvin - 273.15 case .Kelvin: converted = kelvin case .Rankine: converted = (kelvin * (9/5)) } return (converted, toScale) } var temperature:(degrees:Double, scale:TemperatureScale) = (120.0, .Celsius) var converted = convertTemperature(temperature, toScale:.Fahrenheit) println("\(temperature.degrees) \(temperature.scale.toRaw()) is \(converted.degrees) \(converted.scale.toRaw()).") |
Thanks for the good explanation. I hope I learn to use that feature judiciously.
One trouble with the blogging software you have is that the line width available to the code means we have to do lots of scrolling to see the whole lines. Is there a setting or a theme that would make the code sections expand indefinitely with the browser?
TIA
Mark
Mark, thanks for the feedback. I’ve noticed the same thing and it’s beginning to annoy me. I’ll see what I can do to get a template that doesn’t require all that scrolling.
I wanted to know if we could define the tuple and changed the function header to this:
typealias Temperature = (degrees: Double, scale: TemperatureScale)
func convertTemperature(temperature: Temperature, #toScale: TemperatureScale) -> Temperature
That is a very satisfying thing to do.
Also, this blog disabused me of a misconception: that if you didn’t assign a value to a var in the declaration, it would need to be an optional. With the move toward functional programming concepts, I thought that would be the thing to do.
But we could have had that restriction in your example if the switch statement could be used like the ternary operator, and return a value. Not sure what the syntax would be, since he use of return would imply return from the function, but that would be a good step in the direction of functional programming.
Mark, thanks for the question. You can indeed use typealias and the code is much tidier with it!