Apple decides everything, and we will like it.
-- Charles McCathie Nevile
You can keep writing normal OOP code. But some things are easier when done in a functional way.
func add(x: Int, y: Int) -> Int {
return x + y
}
func add(x: Int, y: Int) -> Int {
return x + y + Int(arc4random())
}
add(1,2)
1176275535
The same input should always give the same output.
A pure function has no side-effects:
- No use of global variables
- No reading of files
- No use of functions that are not pure
Most of your code should be pure, only small bits have side-effects.
Suppose we build a library for describing diagrams:
- Describing a diagram: pure
- Calculating the layout of a diagram: pure
- Executing the actual drawing: side-effect
This will make your code a lot easier to change, reuse and test.
let cities : Dictionary<String,Int> =
[ "Минск": 1834200
, "Борисов": 147100
, "Солигорск": 102300
, "Молодечно": 94200
, "Жодино": 61800
]
let names = Array(cities.keys)
let populations = Array(cities.values)
[Жодино, Минск, Молодечно, Солигорск, Борисов]
[61800, 1834200, 94200, 102300, 147100]
func addCity(s: String) -> String {
return s + "is a city"
}
names.map(addCity)
[Жодиноis a city, Минскis a city, Молодечноis a city, Солигорскis a city, Борисовis a city]
func isMinsk(s: String) -> Bool {
return s == "Минск"
}
names.filter(isMinsk)
[Минск]
names.filter({ (s: String) -> Bool in
return s == "Минск"
})
[Минск]
names.filter({ s in
return s == "Минск"
})
[Минск]
names.filter({
return $0 == "Минск"
})
[Минск]
names.filter { $0 == "Минск" }
[Минск]
populations.filter { $0 > 100000 }
[1834200, 102300, 147100]
func sum(arr: Int[]) -> Int {
var result = 0
for i in arr {
result += i
}
return result
}
sum(Array(1..10))
45
func product(arr: Int[]) -> Int {
var result = 1
for i in arr {
result *= i
}
return result
}
product(Array(1..10))
362880
func reduce(initialValue: Int,
combine: (Int,Int) -> Int,
arr: Int[]) -> Int {
var result = initialValue
for i in arr {
result = combine(result,i)
}
return result
}
reduce(0, +, Array(1..10))
45
reduce(1, *, Array(1..10))
362880
let sum = { reduce(0,+,$0) }
let product = { reduce(1,*,$0) }
func concat(strings: String[]) -> String {
var result = ""
for x in strings {
result += x
}
return result
}
concat(names)
ЖодиноМинскМолодечноСолигорскБорисов
func reduce<A>(initialValue: A,
combine: (A,A) -> A,
arr: A[]) -> A {
var result = initialValue
for i in arr {
result = combine(result,i)
}
return result
}
reduce("", +, names)
ЖодиноМинскМолодечноСолигорскБорисов
reduce("", { $0 + "\n" + $1 }, names)
Жодино
Минск
Молодечно
Солигорск
Борисов
func reduce<A,R>(initialValue: R,
combine: (R,A) -> R,
arr: A[]) -> R {
var result = initialValue
for i in arr {
result = combine(result,i)
}
return result
}
func map<A,B>(array: A[], f: A -> B) -> B[] {
return reduce([], { (var arr: B[], el: A) in
arr += f(el)
return arr
}, array)
}
map(Array(1..10), { $0 * 2})
( 2, 4, 6, 8, 10, 12, 14, 16, 18)
func filter<A>(array: A[], f: A -> Bool) -> A[] {
return reduce([], { (var arr, el) in
if (f(el)) {
arr += el
}
return arr
}, array)
}
let isEven = { $0 % 2 == 0 }
filter(Array(1..10), isEven)
( 2, 4, 6, 8)
func partition(v: Int[], left: Int, right: Int) -> Int {
var i = left
for j in (left + 1)..(right + 1) {
if v[j] < v[left] {
i += 1
(v[i], v[j]) = (v[j], v[i])
}
}
(v[i], v[left]) = (v[left], v[i])
return i
}
func quicksort(v: Int[], left: Int, right: Int) {
if right > left {
let pivotIndex = partition(v, left, right)
quicksort(v, left, pivotIndex - 1)
quicksort(v, pivotIndex + 1, right)
}
}
// Source: https://gist.github.com/fjcaetano/b0c00a889dc2a17efad9
func qsort(var array: Int[]) -> Int[] {
if array.count == 0 { return [] }
let pivot = array.removeAtIndex(0)
let lesser = array.filter { $0 < pivot }
let greater = array.filter { $0 >= pivot }
return qsort(lesser) + [pivot] + qsort(greater)
}
qsort(populations)
[61800, 94200, 102300, 147100, 1834200]
let citiesAndPopulations = cities.keysAndValues
> [ (Жодино, 61800)
> , (Минск, 1834200)
> , (Молодечно, 94200)
> , (Солигорск, 102300)
> , (Борисов, 147100)]
func qsortWith<A>(var array: A[],
compare: (A,A) -> Bool)
-> A[] {
if array.count == 0 { return [] }
let pivot = array.removeAtIndex(0)
let lesser = array.filter { !compare(pivot,$0) }
let greater = array.filter { compare(pivot,$0) }
return qsortWith(lesser,compare) +
[pivot] +
qsortWith(greater,compare)
}
qsortWith(citiesAndPopulations) { (l,r) in l.0 > r.0 }
[(Солигорск, 102300),
(Молодечно, 94200),
(Минск, 1834200),
(Жодино, 61800),
(Борисов, 147100)]
qsortWith(citiesAndPopulations) { (l,r) in l.1 > r.1 }
[(Минск, 1834200),
(Борисов, 147100),
(Солигорск, 102300),
(Молодечно, 94200),
(Жодино, 61800)]
empty: Diagram
func rect(#width: Double, #height: Double) -> Diagram
func circle(#radius: Double) -> Diagram
@infix func ||| (l: Diagram, r: Diagram) -> Diagram
let blueSquare = square(1).fill(NSColor.blueColor())
let redSquare = square(2).fill(NSColor.redColor())
let greenCircle = circle(radius:1).fill(NSColor.greenColor())
let cyanCircle = circle(radius: 1).fill(NSColor.cyanColor())
example = blueSquare ||| cyanCircle ||| redSquare ||| greenCircle
func hcat(d: Diagram[]) -> Diagram {
return reduce(empty, |||, d)
}
example = hcat([blueSquare, cyanCircle, redSquare, greenCircle])
func barGraph(input: (String,Double)[]) -> Diagram {
return hcat(normalize(values).map { x in
return rect(width: 1, height: 3*x).alignBottom()
})
}
func barGraph(input: (String,Double)[]) -> Diagram {
let values : Double[] = input.map { $0.1 }
let bars = hcat(normalize(values).map { x in
return rect(width: 1, height: 3*x).alignBottom()
})
let labels = hcat(input.map { x in
return text(width: 1, height: 0.3, text: x.0).alignTop()
})
return bars --- labels
}
Almost all functions are pure: create a diagram, combine diagrams, color diagrams.
Only the rendering is impure.
Using functional programming to automate testing
func prop_sort(ls: Int[]) -> Bool {
return qsort(ls) == sort(ls)
}
Let's generate a lot of random integer arrays, and run them through our property. Makes it easier to find mistakes.
check(prop_sort)
true
func check<X : Arbitrary>(prop : Array<X> -> Bool) -> Bool {
for _ in 0..numberOfIterations {
let randomLength = Int(arc4random() % 50)
let array : X[] = Array(0..randomLength).map {
_ in return X.arbitrary()
}
if(!prop(array)) {
println("Property doesn't hold: \(array)")
return false
}
}
return true
}
check(prop_sort)
true
check { (x: Int[]) in
qsort(x) == x
}
Property doesn't hold: [459, 9052, 1750, 3572, 3101, 3104, 1675, 148, 4781, 5031,
4769, 6738, 2946, 4396, 3190, 4217, 9975, 5143, 7339, 7180, 2041, 6552, 6780,
3839, 7279, 1060, 375, 1096, 5957, 5484, 2832, 8705, 9202, 2158, 3015, 303, 1911,
9846, 8316, 3300]
false
protocol Smaller {
func smaller() -> Self?
}
extension Array : Smaller {
func smaller() -> Array<T>? {
if self.count == 0 { return nil }
var copy = self
copy.removeAtIndex(0)
return copy
}
}
func check1<X : Arbitrary>(prop : Array<X> -> Bool) -> Bool {
for _ in 0..numberOfIterations {
let randomLength = Int(arc4random() % 50)
let array : X[] = Array(0..randomLength).map {
_ in return X.arbitrary()
}
if(!prop(array)) {
let smallerValue = iterateWhile(array, { !prop($0) }) {
$0.smaller()
}
println("Property doesn't hold: \(smallerValue)")
return false
}
}
return true
}
check1 { (x: Int[]) in
qsort(x) == x
}
Property doesn't hold: [6890, 465]
false
You can start using this in your Swift code.
Swift also allows for much easier OOP, and we can mix and match whatever we want.
- Enums
- Pattern matching
- Optionals
- Applicative Functors and Monads