9 December 2014

Используем замыкания в Swift по полной

Development for iOSDevelopment of mobile applicationsSwift
Tutorial
Несмотря на то, что в Objective-C 2.0 присутствуют замыкания (известные как блоки), ранее эппловский API использовал их неохотно. Возможно, отчасти поэтому многие программисты под iOS с удовольствием эксплуатировали сторонние библиотеки, вроде AFNetworking, где блоки применяются повсеместно. С выходом Swift, а также добавлением новой функциональности в API, работать с замыканиями стало чрезвычайно удобно. Давайте рассмотрим, какими особенностями обладает их синтаксис в Swift, и какие трюки можно с ними «вытворять».



Продвигаться будем от простого к сложному, от скучного к веселому. Заранее приношу извинения за обильное использование мантр «функция», «параметр» и «Double», но из песни слов не выкинешь.

Часть 1. Вводная


1.1. Объекты первого класса


Для начала укрепимся с мыслью, что в Swift функции являются носителями гордого статуса объектов первого класса. Это значит, что функцию можно хранить в переменной, передавать как параметр, возвращать в качестве результата работы другой функции. Вводится понятие «типа функции». Этот тип описывает не только тип возвращаемого значения, но и типы входных аргументов.
Допустим, у нас есть две похожие функции, которые описывают две математические операции сложения и вычитания:
func add(op1: Double, op2: Double) -> Double {
    return op1 + op2
}

func subtract(op1: Double, op2: Double) -> Double {
    return op1 - op2
}

Их тип будет описываться следующим образом:
(Double, Double) -> Double

Прочесть это можно так: «Перед нами тип функции с двумя входными параметрами типа Double и возвращаемым значением типа Double
Мы можем создать переменную такого типа:
// Описываем переменную
var operation: (Double, Double) -> Double

// Смело присваиваем этой переменной значение
// нужной нам функции, в зависимости от каких-либо условий:
for i in 0..<2 {
    if i == 0 {
        operation = add
    } else {
        operation = subtract
    }
    
    let result = operation(1.0, 2.0) // "Вызываем" переменную
    println(result)
}

Код, описанный выше, выведет в консоли:
3.0
-1.0


1.2. Замыкания


Используем еще одну привилегию объекта первого класса. Возвращаясь к предыдущему примеру, мы могли бы создать такую новую функцию, которая бы принимала одну из наших старых функций типа (Double, Double) -> Double в качестве последнего параметра. Вот так она будет выглядеть:
// (1)
func performOperation(op1: Double, op2: Double, operation: (Double, Double) -> Double) -> Double { // (2)
    return operation(op1, op2) // (3)
}

Разберем запутанный синтаксис на составляющие. Функция performOperation принимает три параметра:
  • op1 типа Double (op — сокращенное от «операнд»)
  • op2 типа Double
  • operation типа (Double, Double) -> Double

В своем теле performOperation просто возвращает результат выполнения функции, хранимой в параметре operation, передавая в него первых два своих параметра.
Пока что выглядит запутанно, и, возможно, даже не понятно. Немного терпения, господа.

Давайте теперь передадим в качестве третьего аргумента не переменную, а анонимную функцию, заключив ее в фигурные {} скобки. Переданный таким образом параметр и будет называться замыканием:
let result = performOperation(1.0, 2.0, {(op1: Double, op2: Double) -> Double in
    return op1 + op2 // (5)
}) // (4)
println(result) // Выводит 3.0 в консоли

Отрывок кода (op1: Double, op2: Double) -> Double in — это, так сказать, «заголовок» замыкания. Состоит он из:
  • псевдонимов op1, op2 типа Double для использования внутри замыкания
  • возвращаемого значения замыкания -> Double
  • ключевого слова in

Еще раз о том, что сейчас произошло, по пунктам:
(1) Объявлена функция performOperation
(2) Эта функция принимает три параметра. Два первых — операнды. Последний — функция, которая будет выполнена над этими операндами.
(3) performOperation возвращает результат выполнения операции.
(4) В качестве последнего параметра в performOperation была передана функция, описанная замыканием.
(5) В теле замыкания указывается, какая операция будет выполняться над операндами.

Часть 2. Веселая.
Синтаксический сахар и неожиданные «плюшки»


Авторы Swift приложили немало усилий, чтобы пользователи языка могли писать как можно меньше кода и как можно больше тратить свое драгоценное время на чтение Хабра размышления об архитектуре проекта. Взяв за основу наш пример с арифметическими операциями, посмотрим, до какого состояния мы сможем его «раскрутить».

2.1. Избавляемся от типов при вызове.

Во-первых, можно не указывать типы входных параметров в замыкании явно, так как компилятор уже знает о них. Вызов функции теперь выглядит так:
performOperation(1.0, 2.0, {(op1, op2) -> Double in
    return op1 + op2
})

2.2. Используем синтаксис «хвостового замыкания».

Во-вторых, если замыкание передается в качестве последнего параметра в функцию, то синтаксис позволяет сократить запись, и код замыкания просто прикрепляется к хвосту вызова:
performOperation(1.0, 2.0) {(op1, op2) -> Double in
    return op1 + op2
}

2.3. Не используем ключевое слово «return».

Приятная (в некоторых случаях) особенность языка заключается в том, что если код замыкания умещается в одну строку, то результат выполнения этой строки автоматичеси будет возвращен. Таким образом ключевое слово «return» можно не писать:
performOperation(1.0, 2.0) {(op1, op2) -> Double in
    op1 + op2
}

2.4. Используем стенографические имена для параметров.

Идем дальше. Интересно, что Swift позволяет использовать так называемые стенографические (англ. shorthand) имена для входных параметров в замыкании. Т.е. каждому параметру по умолчанию присваивается псевдоним в формате $n, где n — порядковый номер параметра, начиная с нуля. Таким образом, нам, оказывается, даже не нужно придумывать имена для аргументов. В таком случае весь «заголовок» замыкания уже не несет в себе никакой смысловой нагрузки, и его можно опустить:
performOperation(1.0, 2.0) { $0 + $1 }

Согласитесь, эта запись уже совсем не похожа на ту, которая была в самом начале.

2.5. Ход конем: операторные функции.

Все это были еще цветочки. Сейчас будет ягодка.
Давайте посмотрим на предыдущую запись и зададимся вопросом, что уже знает компилятор о замыкании? Он знает количество параметров (2) и их типы (Double и Double). Знает тип возвращаемого значения (Double). Так как в коде замыкания выполняется всего одна строка, он знает, что ему нужно возвращать в качестве результата его выполнения. Можно ли упростить эту запись как-то еще?
Оказывается, можно. Если замыкание работает только с двумя входными аргументами, в качестве замыкания разрешается передать операторную функцию, которая будет выполняться над этими аргументами (операндами). Теперь наш вызов будет выглядеть следующим образом:
performOperation(1.0, 2.0, +)

Красота!
Теперь можно производить элементарные операции над нашими операндами в зависимости от некоторых условий, написав при этом минимум кода.

Кстати, Swift также позволяет использовать операции сравнения в качестве операторной фуниции. Выглядеть это будет примерно так:
func performComparisonOperation(op1: Double, op2: Double, operation: (Double, Double) -> Bool) -> Bool {
    return operation(op1, op2)
}

println(performComparisonOperation(1.0, 1.0, >=)) // Выведет "true"
println(performComparisonOperation(1.0, 1.0, <)) // Выведет "false"

Или битовые операции:
func performBitwiseOperation(op1: Bool, op2: Bool, operation: (Bool, Bool) -> Bool) -> Bool {
    return operation(op1, op2)
}

println(performBitwiseOperation(true, true, ^)) // Выведет "false"
println(performBitwiseOperation(true, false, |)) // Выведет "true"


Swift — в некотором роде забавный язык программирования. Надеюсь, статья будет полезной для тех, кто начинает знакомиться с этим языком, а также для тех, кому просто интересно, что там происходит у разработчиков под iOS и Mac OS X.
___________________________________________________________________
UPD.: Реальное применение

Некоторые пользователи высказали недовольство из-за отсутствия примеров из реальной жизни. Буквально вчера натолкнулся на задачу, которая может быть элегантно решена с использованием коротких замыканий.

Если вам нужно создать очередь с приоритетом, можно использовать двоичную кучу (binary heap). Как известно, это может быть как MinHeap, так и MaxHeap, т.е. кучи, где в корне дерева находится минимальный или максимальный элемент соотвественно. Базовая реализация MinHeap от MaxHeap будет отличаться по сути только проверочными сравнениями при восстановлении инварианта двоичной кучи после добавления/удаления элемента.

Таким образом, мы могли создать базовый класс BinaryHeap, который будет содержать свойство comparison типа (T, T) -> Bool. А конструктор этого класса будет принимать способ сравнения и затем использовать его в методах heapify. Прототип базового класса выглядел бы так:
class BinaryHeap<T: Comparable>: DebugPrintable {
    private var array: Array<T?>
    private var comparison: (T, T) -> Bool
    private var used: Int = 0

   // Бла-бла-бла

    // Internal Methods
    internal func removeTop() -> T? { //... }
    internal func getTop() -> T? { //... }
    
    // Public Methods:
    func addValue(value: T) {
        if used == self.array.count {
            self.grow()
        }
        
        self.array[used] = value
        heapifyToTop(used, comparison) // Одно из мест, где используется функция сравнения
        self.used++
    }

    init(size newSize: Int, comparison newComparison: (T, T) -> Bool) {
        array = [T?](count: newSize, repeatedValue: nil)
        comparison = newComparison
    }
}

Теперь для того, чтобы создать классы MinHeap и MaxHeap нам достаточно унаследоваться от BinaryHeap, а в их конструкторах просто явно указать, какое сравнение применять. Вот так будет выглядеть наши классы:
class MaxHeap<T: Comparable>: BinaryHeap<T> {
    func getMax() -> T? {
        return self.getTop()
    }
    
    func removeMax() -> T? {
        return self.removeTop()
    }
    
    init(size newSize: Int) {
        super.init(size: newSize, {$0 > $1})
    }
}

class MinHeap<T: Comparable>: BinaryHeap<T> {
    func getMin() -> T? {
        return self.getTop()
    }
    
    func removeMin() -> T? {
        return self.removeTop()
    }
    
    init(size newSize: Int) {
        super.init(size: newSize, {$0 <= $1})
    }
}
Tags:swiftзамыканияios developmentпрограммирование
Hubs: Development for iOS Development of mobile applications Swift
+13
28.6k 114
Comments 21