10 posts tagged

Swift

Открытый мультипарадигменный объектно-ориентированный язык программирования общего назначения. Создан компанией Apple в первую очередь для разработчиков iOS и OS X.

Линзы или геттеры и сеттеры в функциональном свифте

В идеальном функциональном мире состояние объекта не может изменяться после создания, а каждая функция работает только с теми данными, которые она получает, не имея никакой информации о существовании всего, что есть в програме, кроме других функций. И поскольку одна из самых мощных функций Свифта является возможность писать код в функциональном стиле, то почему бы не разобраться ради интереса как можно работать с не изменяемыми обьектами? Главная идея в том, что поскольку данные не изменяемые, то надо создавать копию этих данных с нужными нам изменениями при помощи пары геттеров и сеттеров, которые называются линзами.

Для начала напишем две структуры:

struct User {
    let name: String
    let age: Int
}
struct Corp{
    let name: String
    let user: User
}

let john = User(name: "John", age: 18)
let corp = Corp(name: "Google", user: john)

Эти структуры полностью immutable, то есть их состояние после создания никак изменить нельзя. Теперь напишем сеттер и геттер для поля name в структуре User, геттер просто возвращает имя, а сеттер создает нового юзера с новым именем:

extension User{
    func getName() -> String {
        return self.name
    }
    func setName(name: String) -> User {
        return User(name: name, age: age)
    }
}

То же самое сделаем для структуры Corp, проверяем:

john.getName() // => "John"
john.setName("John M").getName() // => "John M"
john.getName() // => "John"
corp.setUser(corp.getUser().setName("John M")).getUser().getName() // => "John M"

Теперь введем новую абстракцию – линзу, которая позволит вынести повторяемый код сеттеров и геттеров:

struct Lens <A, B> {
    let get: A -> B
    let set: (B, A) -> A
}

Создадим линзу для имени юзера:

let userNameLens = Lens<User, String>(
    get: { user in user.name },
    set: { (name, user) in User(name:name, age: user.age) }
)
userNameLens(john) // => "John"
userNameLens("Johg 2", corp.getUser()).name // => "John 2"

Линза для структуры Corp будет возвращать пользователя и создавать новую структуру с заданым пользователем.

let corpUserLens = Lens<Corp, User>(
    get: { corp in corp.user },
    set: { user, corp in Corp(name: corp.name, user: user) }
)

corpUserLens.set(
    userNameLens.set(
        "Test User",
        corpUserLens.get(corp)
    ),
    corp
)

Теперь напишем функцию для композиции линз, что-бы можно было легко получить доступ напрямую к имени пользователя структуры Corp. Функция get – это результат get’ов двух линз, set чуть сложнее, на примере композицию двух линз corpUserLens и userNameLens происходит следующее: сначала corpUserLens получит пользователя из Corp, затем userNameLens создаст нового пользователя с новым именем и еще раз линза corpUserLens создаст новую корпорацию с новым пользователем.

func compose<A, B, C>(lhs: Lens<A, B>, _ rhs: Lens<B, C>) -> Lens<A, C> {
    return Lens<A, C> (
        get: { a in rhs.get(lhs.get(a)) },
        set: { (c, a) in lhs.set(rhs.set(c, lhs.get(a)), a) }
    )
}
let composedLens = compose(corpUserLens, userNameLens)

composedLens.get(corp)
composedLens.set("test", corp).getUser().getName()

Для удобства и большей наглядности напишем функцию-оператор для композиции линз, так как из выражения composedLens.set(“test”, corp) не понятно, что в конечном счете будет использована линза изменения имени у пользователя.

func * <A, B, C> (lhs: Lens<A, B>, rhs: Lens<B, C>) -> Lens<A, C> {
    return compose(lhs, rhs)
}

Теперь все намного лучше:

(corpUserLens * userNameLens).get(corp)
(corpUserLens * userNameLens).set("User", corp)

Добавим еще немного синтаксического сахара, заменим set() на оператор:

infix operator ~> { associativity left precedence 100 }
func ~> <A, B> (lhs: Lens<A, B>, rhs: B) -> A -> A {
    return { a in lhs.set(rhs, a) }
}

(userNameLens ~> "Test name")(john)

Для комбинирования линз добавим еще один оператор:

infix operator |> { associativity left precedence 80 }
func |> <A, B> (x: A, f: A -> B) -> B {
    return f(x)
}
func |> <A, B, C> (f: A -> B, g: B -> C) -> A -> C {
    return { g(f($0)) }
}

john |> userNameLens ~> "New Name"

В итоге получится удобная конструкция:

corpUserLens * userNameLens ~> "test name"
|> corpUserLens * ageUserLens ~> 26
|> someOtherLens * someOtherLens2 ~> String()
2016   Lens   Swift

Перечисления

В свифте enum’ы почти как класы, они могут иметь свои методы и переменные. Enum очень удобно использовать для построения Table View в случае когда все ячейки известны зарание и меняться будут редко, например это может быть список настроек.

Допустим у нас в настройках будет три пункта:

enum Settings: Int {
    case FirstName
    case LastName
    case Age
}

Теперь для метода делегата numberOfRowsInSection необходимо указать количество ячеек и поскольку стандартным способом узнать количество кейсов в перечислении не получиться, а константы – лишняя зависимость, то прийдется сделать небольшой хак, добавим метод в enum:

static let count: Int = {
        var max: Int = 0
        while let _ = Settings(rawValue: max) { max += 1 }
        return max
    }()

Дальше добавим отображаемое имя в ячейке для каждого кейса:

var name: String {
        switch self {
        case .FirstName:
            return "First Name"
        case .Last Name:
            return  "Last Name"
        case .Age:
            return "Age"
        }
    }

И последнее, добавим метод для создания ячейки:

func cellForRowAtIndexPath(indexPath: NSIndexPath, tableView: UITableView) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(name, forIndexPath: indexPath)
        cell.titleLabel.text = name
        return cell
    }

Теперь надо вызвать метод перечисления cellForRowAtIndexPath в методе делегата cellForRowAtIndexPath, для каждого кейса будет создана своя уникальная ячейка.
Что-бы добавить новую ячейку в таблицу нужно просто добавить новый кейс в перечисление и все. Еще этот способ удобен тем, что не нужно хардкодить константы индексов для конкретных ячеек, так как можно сделать Settings(rawValue: indexPath.row) и получить нужные значения.

2016   enum   Swift

Сабскрипт

Сабскрипт – это сокращенный способ получить доступ к елементам коллекции, например array[0], где [0] – это сабскрипт. В свифте есть возможность писать свои сабскрипты и это круто, если бы мы писали гру шахматы, то было бы удобно оперировать доской как двумерной матрицей и задавать ходы в таком виде:

board[move: (4,5)] = (2,3)

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

struct Board {
    var positions = [[Position]]()
    
    subscript(x row: Int, y column: Int) -> Position? {
        get {
            if row < positions.count {
                if column < positions[row].count {
                    return positions[row][column]
                }
            }
            print("Range error")
            return nil
        }
    }
}

И обращаться к нему вот так:

board[x: 0, y:3]

Вместо:

board.positions[0][0]

Также сабскрипты можно использовать в расширениях, этот пример добавляет сабскрипт целого числа во встроеный тип String:

extension String{
    subscript(index: Int) -> Character {
        let _index = startIndex.advancedBy(index)
        return characters[_index]
    }
}

Теперь можно получить символ из строки указав индекс:

"436575"[1]
=> 3
2016   subscript   Swift

Апликативные функторы

В отличие от обычных функторов, у апликативных функторов функция так же упакована в контекст как и значения к которым она будет применяться. По дефолту в свифте нет апликативных функторов, но можно легко добавить самому, например для типа Optional добавим функцию apply, которая знает как применить функцию, обернутую в контекст к значениям, которые обернуты в тот же контекст.

extension Optional {
  func apply<U>(f: (Wrapped -> U)?) -> U? {
    switch f {
      case .Some(let someF): return self.map(someF)
      case .None: return .None
    }
  }
}

Теперь можно взять функцию сложения plusTwoNumbers из предыдущего поста об функторах и что-бы применить ее к значению, нужно обернуть в тот же контекст Optional и передать функции apply:

let plus = plusTwoNumbers(5)
Optional.Some(10).apply(Optional.Some(plus))
// => .Some(15)

Или для удобства написать свой оператор:

infix operator <*> { associativity left }

func <*><T, U>(f: (T -> U)?, a: T?) -> U? {
  return a.apply(f)
}

Optional.Some(plus) <*> Optional.Some(10)
// => .Some(15)
2016   applicatives   functor   Swift

Функторы

Функтор – это такой клас типов, которые реализуют функцию map. Это некий контейнер, к элементам которого можно применить функцию и в результате получить новый точно такой же контейнер (структура остается неизменной), но с новыми елементами. Иными словами, функтор применяет функцию к значениям, которые он содержит, а не к себе.

Функтором является тип Optional а также все последовательние типи в свифте: Dictionary, Array и Set.

К примеру, обычное сложение с использованием функтора будет выглядеть так:

func plusTwoNumbers(firstNumber: Int) -> (Int)-> Int {
    return { (secondNumber: Int) -> (Int) in
        return firstNumber + secondNumber
    }
}
let plus = plusTwoNumbers(5)
Optional.Some(10).map(plus)
// => .Some(15)

Функтор определяет, как будет будет реализована функция map и поскольку Optional это функтор, то у него есть своя реализация:

func map<U>(f: T -> U) -> U? {
  switch self {
  case .Some(let x): return f(x)
  case .None: return .None
}

Получается, что если мы в начале имеем None, то и в результате также будет иметь None. В случае с языками, которые не имеют опциональных типов, приходится делать проверку:

let user = Users.findByID(1)
if user != nil {
  return user.name
} else {
  return nil
}

Когда в свифте с исплозованием функтора Optional тоже самое можно сделать проще:

findByID(1).map(getUserName)
2016   functor   Swift

Композиция функций

В функциональном программировании часто используется построение функций, например построение функции f3 из двух функций f1 и f2 применив к ним функцию F, это и называется композицией функций. Расмотрим на простом примере. Создадим несколько функций, которые будут передаваться как параметры последовательно, первая функция создает и возвращает массив с работниками:

func createTeam(count: Int) -> [Employee] {
    var team = [Employee]()
    for i in 1...count {
        let f = i % 2 == 0
        team.append(Employee(id: i, pay: f ? i * 200 : i * 100, type: f ? .Dev : .QA))
    }
    return team
}

Вторая функция выбирает из масива только разработчиков и возвращает новый массив:

func getDevs(employees: [Employee]) -> [Employee] {
    return employees.filter({$0.type == .Dev})
}

И третья функция цепочки возвращает сумарную зарплату работников:

func getAverageSalary(employees: [Employee]) -> Int {
    return employees.reduce(0){$0 + $1.pay}
}

Осталось написать последнюю функцию, которая и будет композицией:

let salary = {count in getAverageSalary(getDevs(createTeam(count)))}

Теперь можно вызвать salary передав ей параметр count:

salary(5)

Также можно создать свой оператор для композиции, например >>> с левой асоциативностью (об кастомных операторах):

infix operator >>> { associativity left }
func >>> <A, B, C>(f: B -> C, g: A -> B) -> A -> C {
    return { x in f(g(x)) }
}

В итоге конструкция получится более изящной:

let salary2 = getAverageSalary >>> getDevs >>> createTeam
salary2(5)
2016   Composition   functional   Swift

Расширения и протоколы

Представим себе ситуацию, вам нужно добавить метод смены цвета для стандартных классов вроде UIView, UIImageView. Поскольку UIImageView наследуется от UIView, то написав расширение для UIView, все обьекты типа UIImageView также получат новый функционал, но проблема в том, что цвет у обычной вью и картинки меняется разными способами, у UIView это backgroundColor, когда у UIImageView это tintColor. Решить проблему можно используя протокол:

protocol ColorCustomizable {
    func setColor()
}

extension UIView: ColorCustomizable {
    func setColor() {
        backgroundColor = UIColor.blackColor()
    }
}

Указывать протокол для UIImageView не нужно, он уже реализован в родительском классе UIView, осталось только переопределить метод:

extension UIImageView {
    override func setColor() {
        tintColor = UIColor.blackColor()
    }
}
2016   extension   protocol   Swift

Карринг на примере сортировки

Карринг — прием в функциональном программировании, который позволяет преобразовать функцию с несколькими агрументами в последовательность новых функций с меньшим количеством аргументов. Карринг полезно применять в тех случаях, когда некоторые параметры функции известны зарание и передавать каждый раз одинаковые значение не имеет смысла. Например у нас есть функция для сортировки списка, у нее есть два параметра: тип сортировки и сам список, в случае, когда функция будет применяться для какого нибудь конкретного типа, например списка чатов, то передавать каждый раз сам список не нужно, так как у нас будет меняьтся только способ сортировки, или наоборот, сортировка будет всегда одинаковая, а исходные данные будут меняться.

Для примера возьмем массив элементов типа ChatEntity, у этого типа есть поле members, будем сортировать массив в зависимости от количества элементов в members. Напишем протокол и реализуем сортировку:

protocol SortChatProtocol {
    func sortList(members: [ChatEntity]) -> [ChatEntity]
}

class Members: SortChatProtocol {
    func sortList(members: [ChatEntity]) -> [ChatEntity] {
        return members.sort({
            return $0.members!.count > $1.members!.count
        })
    }
}

Теперь напишем каррированную функцию, которая сортирует список в зависимости от выбраной реализации, функция принимает обьект типа SortChatProtocol и возвращает функцию, которая в свою очередь принимает список с данными и возвращает отсортированый список:

func chatListSorterBy(order: SortChatProtocol)(chatList: [ChatEntity]) -> [ChatEntity] {
    return order.sortList(chatList)
}

К сожалению, на мой взгляд, в Swift 2.2 испортили синтаксис обьявления каррированных функций, теперь та же функция будет выглядеть так:

func chatListSorterBy(order: SortChatProtocol) -> ([ChatEntity]) -> [ChatEntity] {
    return { (chatList: [ChatEntity]) -> ([ChatEntity]) in
        return order.sortList(chatList)
    }
}

Осталось написать функцию, которая будет вызывать chatListSorterBy с типом сортировки и возвращать функцию с параметром [ChatEntity]:

func createChatListSorterBy(order: SortChatProtocol) -> [ChatEntity] -> [ChatEntity] {
    return chatListSorterBy(order)
}

В результате у нас получится функция sorter, которая принимает только список, так как мы уже зарание указали какую сортровку использовать.

let sorter = createChatListSorterBy(Members())
let sortedList = sorter(notSortedList)
let otherSortedList = sorter(otherNotSortedList)
2016   Currying   functional   programming   Swift

Кортежи

Я окончательно и безповоротно перешел на Swift и не перестаю радоваться этому событию. Swift все-таки намного удобнее и современние, вот например в этом языке есть такая крутая штука как Кортежи (Tuples), это в каком то роде необьявленная структура для временного хранения данных, зачет это нужно? Ну например для возвращениея более одного значения из функции, например у нас есть функция, которая проверяет выиграл игрок или проиграл в зависимости от нажатой кнопки, на obj-c нам пришлось бы сделать две функции, одну для возвращения булевого значения (true – выиграл, false – проиграл), и еще одну для возвращения правильного ответа. С использованием кортежей это будет выглядеть акуратнее:

func checkWin(direction: DirectionType) -> (isWin: Bool, rightDirection: DirectionType) {
    let win = direction == self.winCaseDirection ? true : false
    return (win, self.winCaseDirection)
}

Ну и использовать дальше эту функцию будем так:

let selectedDirection = DirectionType.Right
let isWin = checkWin(selectedDirection).isWin
let rightDirection =  checkWin(selectedDirection).rightDirection
2016   Swift   tuples

Пишем игру на Swift + SpriteKit

Это первая часть из серии “Пишем игру на Swift + SpriteKit”, в которой мы создадим базовые сцены и главного персонажа, добавим физику и научим его прыгать.

Для начала создадим новый проект в Xcode и добавим главную сцену, класс с главной сценой назовем MainScene и добавим метод для общего добавления спрайтов (это можно делать и на сцене геймплея, но данный способ избавит нас от дублирования кода):

File -> New -> Project -> Aplication -> Game (Language: Swift, Game Technology: SpriteKit)

class GameScene: MainScene {
    func addSpriteNodeBy(name: String, position: CGPoint) -> SKSpriteNode{
        let sprite = SKSpriteNode(imageNamed: name)
        sprite.position = position
        addChild(sprite)
        
        return sprite
    }
}

Метод addSpriteNode принимает два параметна (название файла изображения и позицию), инициализирует спрайт, добавляет на сцену и возвращает ссылку на созданый обьект.
Теперь можно создавать сцену геймплея, которая наследует главную сцену и добавить фон при помощи написаного ранее метода addSpriteNodeBy:

class GameScene: MainScene {
    override func didMoveToView(view: SKView) {
        let background = addSpriteNodeBy("background.png", position: CGPointMake(self.frame.width/2, self.frame.height/2))
    }
}

Сцена готова, теперь можно приступить к созданию персонажа, для этого создадим новый класс под названием Character:

class Character: SKSpriteNode {
    override init() {
        let texture = SKTexture(imageNamed: "hero.png")
        super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
    }
    
    init(texture: SKTexture!) {
        super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
    }
    
    override init(texture: SKTexture!, color: UIColor!, size: CGSize) {
        super.init(texture: texture, color: color, size: size)
    }
 }

Так, как наш класс наследует SKSpriteNode, то мы должны обязательно переопределить инициализаторы, в строке let texture = SKTexture(imageNamed: “hero.png”) задаем текстуру нашего персонажа и передаем ее инициализатору супер класса.

Об наследовании, переопределении и инициализации можно почитать тут

Научим нашего персонажа прыгать, добавив ему физическое тело и метод jump в класс Character:

override init() {
        let texture = SKTexture(imageNamed: "FlappyBird.png")
        super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
        
        self.physicsBody?.dynamic = true
        self.physicsBody = SKPhysicsBody(circleOfRadius: 20)
        self.physicsBody?.mass = 0.3
    }

    func jump() {
        let impulseX:CGFloat = 0.0;
        let impulseY:CGFloat = 10.0;
        [self.physicsBody?.applyImpulse(CGVectorMake(impulseX, impulseY), atPoint: self.position)];
    }

Теперь можно добавлять персонажа на главную сцену, в методе didMoveToView класса GameScene пишем:

var character = Character()
addChild(character)

И что-бы он не улетел в космос, добавим на сцену гравитацию, прописав следующую строку:

self.physicsWorld.gravity = CGVectorMake(0.0, -10.0);

Можете поэкспериментировать с силой прыжка, изменяя переменную impulseY и свойство self.physicsBody?.mass. Что-бы персонаж прыгал при тапе по экрану, нужно добавить обработчик событий в класс GameScene:

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        character.jump()
 }

Ну и на конец, нужно добавить границы сцене, что-бы персонаж не проваливался под землю:

let borderBody = SKPhysicsBody(edgeLoopFromRect: CGRectMake(0, 0, self.frame.width, self.frame.height))
        borderBody.friction = 0
        self.physicsBody = borderBody

Итак, у нас есть мир с гравитацией и персонаж который умеет прыгать, на этом пока все.

2015   game   gamedev   ios   Swift