Swift для дата-сайентиста: быстрое погружение за 2 часа +19



Google объявил, что TensorFlow переезжает на Swift. Так что отложите все свои дела, выбросьте Python и срочно учите Swift. А язык, надо сказать, местами довольно странный.



Для затравки посмотрите небольшую презентацию с объяснением, почему Swift и как с этим связан TensorFlow:



Разработчики TensorFlow, конечно, пока не забывают Python, но основой фреймворка станет именно Swift. Хотя в нем можно будет писать python-подобный код. Но исполняться он все равно будет интерпретатором Python, а это снова значит медленно, непараллельно, неэффективно по памяти, без контроля типов и все прочее.


Поэтому учим Swift с нуля. Ну, не совсем с нуля: предполагается, что вы уже хорошо программируете на Python'е, и поэтому многие конструкции Swift далее будут описаны в сравнении с аналогичными Python'овскими конструкциями.
Статья ни в коем случае не претендует на подробное описание языка. Это лишь первое весьма поверхностное знакомство с основными возможностями языка для тех, кто знает Python.


Общие слова


Swift — довольно новый язык. Это хорошо, потому что он основан на обширной базе более ранних языков. Но в то же время и очень плохо, потому что он пока еще не лишен совсем “детских болезней”. Поэтому язык очень быстро эволюционирует.
Несмотря на то, что в интернете полно статей и туториалов по Swift — все они уже устарели. Многочисленные рецепты со StackOverflow вам скорее всего тоже не подойдут, потому что они относятся к предыдущим версиям языка.


Хронология последних событий: в марте 2016 года вышел Swift 2.2, а в сентябре — уже “сильно другой” Swift 3, через год — “снова другой” Swift 4. Текущая версия 4.1, хотя Swift for Tensorflow — это уже 4.2-dev. До конца года выйдет Swift 5, в котором будет еще больше нововведений даже в самом языке, не говоря уже о библиотеках.


В общем, TLDR: язык пока не готов к серьезной разработке для data science. Поэтому я позволил себе потратить лишь два часа на ознакомление с языком в его текущем виде, чтобы через полгода легче было погружаться в Swift 5 с уже новой версией TensorFlow.


Переменные и константы


Все переменные строго типизированы, поэтому при объявлении придется указывать тип данных. К счастью, компилятор умеет определять тип по начальному значению.


let intConst = 5
let strConst = "strings should be in double quotes"
var nonInitVar: Int   // для переменной без начального значения тип указывается явно
var intVar = 10
var floatVar = 10.0
var doubleVar: Double = 10.5
var strVar = "double quotes only"

Как вы уже догадались, константы объявляются с помощью let, а переменные через var. В качестве хорошего тона и для оптимизации вычислений для всех значений, которые не будут в процессе исполнения программы меняться, рекомендуется именно let. В остальном, все просто и понятно.


Диапазон


В Python'е есть slice, а в Swift'е целая пачка типов Range: открытый, закрытый, неполный снизу и т.д.
Литералами они задаются довольно кратко (но можно было бы и еще короче):


1...5     // от 1 до 5 включительно
1..<5    // не включая 5
...5       // от начала до 5
2…      // от 2 до конца

Для задания диапазонов с шагом, отличающимся от единицы, или с нецелыми числами можно использовать функцию stride:


for i in stride(from: 0.1, to: 0.5, by: 0.1) {
    print(i)
}

или


for i in stride(from: 0.1, through: 0.5, by: 0.1) {
    print(i)
}

Догадаться невозможно, нужно просто знать, что в первом случае (с to) диапазон открыт справа (т.е. 0.5 не включается).


Строки


В каждом языке есть своя чудовищная глупость. Разработчики Swift'а решили, что у них это будут строки. Во-первых, есть два строковых типа String и Substring. Они очень похожи, но отличаются лишь тем, что у Substring нет своей области памяти, и она всегда указывает на кусок памяти какой-то String. Идея понятная и правильная, но все эти нюансы можно было легко скрыть в реализации String.


Дальше хуже. Как получить Substring из String? Вы думаете, тут есть что-нибудь как в Python'е — str[1:10]. Ничего подобного! Нельзя индексировать строки целыми числами. А как надо?


str[str.index(str.startIndex, offsetBy: 1) ..< str.index(str.startIndex, offsetBy: 10)]

Я не шучу. Это официальный способ работы со строками. У каждой дурацкой идеи есть длинное и бессмысленное объяснение. Этот случай не стал исключением.


Обратите еще раз внимание на строку str[str.index(str.startIndex, offsetBy: 1) ..< str.index(str.startIndex, offsetBy: 10)]. В ней инвариантно все, кроме двух целых чисел. Иными словами, из 87 символов 84 лишние!


Чтобы было по-людски, пишем extension для стандартного типа String:


extension String {
    public subscript(i: Int) -> Character {
        return self[index(startIndex, offsetBy: i)]
    }

    public subscript(r: Range<Int>) -> Substring {
        var a = Array(r)
        let start = index(startIndex, offsetBy: a[0])
        let end = index(startIndex, offsetBy: a[-1])
        return s[start...end]
    }
}

Запускаем… Не работает! Компилятор ругается:


error: 'subscript' is unavailable: cannot subscript String with an integer range, see the documentation comment for discussion

Дело в том, что в Swift есть явный хардкод, запрещающий создавать метод subscript, который принимает диапазон из целых чисел.


Ладно, пойдем другим путем, в 10 раз длиннее, видимо таков уж Swift-way:


extension String {
  public subscript(i: Int) -> Character {
    return self[index(startIndex, offsetBy: i)]
  }

  public subscript(bounds: Range<Int>) -> Substring {
    let start = index(startIndex, offsetBy: bounds.lowerBound)
    let end = index(startIndex, offsetBy: bounds.upperBound)
    return self[start ..< end]
  }

  public subscript(bounds: ClosedRange<Int>) -> Substring {
    let start = index(startIndex, offsetBy: bounds.lowerBound)
    let end = index(startIndex, offsetBy: bounds.upperBound)
    return self[start ... end]
  }

  public subscript(bounds: PartialRangeFrom<Int>) -> Substring {
    let start = index(startIndex, offsetBy: bounds.lowerBound)
    let end = index(endIndex, offsetBy: -1)
    return self[start ... end]
  }

  public subscript(bounds: PartialRangeThrough<Int>) -> Substring {
    let end = index(startIndex, offsetBy: bounds.upperBound)
    return self[startIndex ... end]
  }

  public subscript(bounds: PartialRangeUpTo<Int>) -> Substring {
    let end = index(startIndex, offsetBy: bounds.upperBound)
    return self[startIndex ..< end]
  }
}

И в качестве упражнения скопируйте весь этот текст еще раз для типа Substring. Язык не для краткости, это уже понятно.


Зато теперь можно нормально работать со строками:


var str = "Some long string"
let char = str[4]
var substr = str[3 …< 8]
let endSubstr = str[4…]
var startSubstr = str[...5]
let subSubStr = str[...8][2..][1..<4]

Tuple


Неизменяемая последовательность значений, или tuple, в Swift устроена немного иначе, чем в Python'е. Здесь это скорее похоже на смесь tuple и namedtuple.


let tuple = (100, "value", true)

print(tuple.0) // 100
print(tuple.1) // "value"
print(tuple.2) // true

var person = (name: "John", age: 24)
print(person.name, person.age)

// а еще можно так
let tuple2 = (10, name: "john", age: 32, 115)
print(tuple2.1)        // john
print(tuple2.name) // john

А вот распаковывать tuple в аргументы функции нельзя. Раньше было можно. Потом запретили. Возможно, в будущем обратно введут.


Коллекции: массивы, множества и словари


Массив (Array) похож на python'овский list тем, что его размер можно менять, однако все элементы массива должны иметь один тип данных. С Setами та же история: как и в python'овском множестве можно менять состав элементов, но не тип. В словаре придется определить два типа: для ключей и для элементов.


let immutableArray = [5, 10, 15]

var intArr = [10, 20, 30]
var nonInitIntArr: [Int]
var emptyArr: [Int] = []
var otherEmptyArr = [Int]()

var names: [String] = ["John", "Anna"]

var noninitSet: Set<String>
var emptySet: Set<Int> = []
var otherEmptySet = Set<Int>()

var emptyDict: Dictionary<Int, String> = []
var strToArrDict: Dictionary<String, [Int]>
var fullDict: Dictionary<String, Int> = ["john": 24, "anna": 22]

let allKeys = fullDict.keys
let allVals = fullDict.values

Кстати, если попробуете проитерироваться по словарю самым ожидаемым способом:


for k in fullDict.keys {
    print(k, fullDict[k])
}

то неожиданно получите кучку предупреждений от компилятора, потому что тип значений в словаре fullDict на самом деле не Int, а Optional<Int> (то есть может быть nil или int). Про Optional поговорим отдельно, а итерироваться удобней tuple'ами:


for (key, val) in fullDict {
    print(key, val)
}

Циклы


Стандартный обход коллекции:


for item in collection {
    // ...
}

Удобно работать и с диапазонами:


for i in 0...10 {
    // ...
}

Если индексы нужны выборочно, что конструкция резко удлиняется:


for i in stride(from: 0, to: 10, by: 2) {
    // ...
}

Еще есть


while someBool {
    // ...
}

repeat {
    // ...
} while otherBool

Функции


Все, как и ожидалось:


func myFunc(arg1: Int, arg2: String) -> Int {
    // do this
    // do that
    return someInt
}

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


func fn1(a: Int, b: Int){
    // обычные аргументы без меток
}

// при вызове функции имена аргументов необходимо указывать 
fn1(a: 1, b: 10)

func fn2(from a: Int, to b: Int){
    // ...
}

// теперь в качестве имени аргумента указывается метка
fn2(from: 1, to: 10)
fn2(a: 1, b: 10)  // а так уже нельзя 

func fn3(_ a: Int, to b: Int){
    // _ - не использовать имя при вызове
}

// первый аргумент указывается без имени
fn3(1, to: 10)

Есть лямбды, здесь они называются closure


{ (arg1: Int, arg2: String) -> Bool in
    // ...
    return someBool
})

Естественно, closure можно передавать в функции. И вот тут открывается новая внезапность:


someFunc() {
    // do this
    // do that
    return someInt
}

Выглядит как определение функции, только без слова func. Но на самом деле это вызов функции someFunc, которой в качестве последнего аргумента передается closure, заданная в фигурных скобках. Кстати, если closure выступает единственным аргументом функции, то круглые скобки можно опустить.


let descArray = array.sorted { $0 > $1 }
let firstValue = array.sorted { $0 > $1 }.first

Классы и структуры


Для создания сложных типов данных предусмотрены классы и структуры:


struct MyStructure {
    public var attr1: Int
    private var count = 0

    init(arg1: Int) {
        attr1 = arg1
    }

    public func method1(arg1: Int, arg2: String) -> Float {
        // ...
        return 0.0    // этот метод обязательно должен вернуть значение типа Float
    }
}

class MyClass {
    public var attr1: Int
    private var count = 0

    init(arg1: Int) {
        attr1 = arg1
    }

    public func method1(arg1: Int, arg2: String) -> Float {
        // …
        return 0.0    // этот метод обязательно должен вернуть значение типа Float
    }
}

С виду разницы никакой, но она все же есть:


  • структуры по умолчанию являются неизменяемыми (immutable), поэтому методы, изменяющие значения атрибутов, следует предварять ключевым словом mutating;
  • структуры всегда передаются по значению, а классы по ссылке;
  • классы можно наследовать.

Как вы уже знаете по строкам, у классов и структур есть удобный метод subscript (аналог python'овских __getitem__ и __setitem__), позволяющий индексировать данные, чтобы вместо:


let item = someClass.getItem(itemIndex)
let item = someClass.getSubsetOfItems(fromIndex: 0, toIndex: 10)

писать более компактное:


let item = someClass[itemIndex]
let aFewItems = someClass[0...10]

Реализуется он примерно так:


class MyClass {
    private var myData = [Int: Double]()

    public subscript(i: Int) -> Double {
        get {
            return myData[i]!
        }

        set {
            myData[i] = newValue
        }
    }
}

Вы, наверное, спросите: что за newValue такое? А это еще одна неявная конвенция — если для set'а не заданы аргументы, то значение передается через переменную newValue.
Так, а что означает восклицательный знак после myData[i]?


Optional


В строго типизированном языке нужен особый способ работы с отсутствующими значениями. В Swift для этого есть Optional, который принимает значение определенного при декларации переменной типа или значение nil.


var opt: Optional<Int>
var short: Int?
var anOpt: Optional<Int> = Int(32)
var oneMore: Int? = nil

Как с этим работать?


if opt == nil {
    print("Значения нет")
} else {
    print("Значение =", opt!)   // обратите внимание на восклицательный знак
}

Оператор ! предназначен для принудительной распаковки (англ. “force unwrapping”) значения. Если при этом opt был nil, вы получите runtime crash.


Другая, более рекомендуемая конструкция, — блок if-let:


if let val = short {
    print("val - ‘настоящее’ значение short, чистый Int: ", val)
} else {
    print("short равен nil")
}

Для разворачивания опционала с присвоением ему значения по умолчанию существует оператор ??, который незамысловато называется nil-coalescing operator.


print(oneMore ?? 0.0)    // если значения нет, то будет выведен 0.0

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


if let cityCode = person?.contacts?.phone?.cityCode {
    // если человек определен, 
    // и у него есть контактные данные, 
    // в которых указан телефон, 
    // в котором обозначен код города 
} else {
    // если хоть чего-нибудь нет
    print("Код города неизвестен")
}

Python


В Swift for Tensorflow заявлена работа с Python'ом, чтобы можно было писать Python-подобный код:


import Python

let np = Python.import("numpy")
let a = np.arange(15).reshape(3, 5)
let b = np.array([6, 7, 8])

Но сейчас так не работает и можно писать только вот так:


import Python

let np = Python.import("numpy")
let a = np.arange.call(with: 15).reshape.call(with: 3, 5)
let b = np.array.call(with: [6, 7, 8])

Когда будет сделано по-нормальному, пока не известно, потому что нужны изменения в языке, а предложение на изменение еще даже не подано, хотя активно обсуждается (уже в третий раз и пока безрезультатно).


Кроме того, сейчас Swift умеет работать только с Python 2.7, установленным в /usr/local/lib/python27 (опять хардкод). Ни с какими виртуальными средами тоже не совместим. В виду имеющейся разницы между Python 2 и 3 с точки зрения С-структур данных и C-вызовов в ближайшее время эта проблема также не разрешится.


Tensorflow


Наконец-то добрались до главного, ради чего все и затевалось.


Начнем с перемножения матриц:


import TensorFlow

var tensor = Tensor([[1.0, 2.0], [2.0, 1.0]])
for _ in 0...100000 {
    tensor = tensor * tensor - tensor
}

Выглядит точно красивее, короче и понятнее, чем на Python'е c tf.while_loop вкупе с созданием сессии и инициализацией переменных. Вот только пока медленнее, причем в разы. И GPU, конечно же, не поддерживается.


Кстати, для матричного умножения надо использовать не * и не @, а замечательный знак ?. Попробуйте ввести его с клавиатуры.


Давайте уже сделаем нейросеточку!.. Хотя не сделаем: документации нет, готовых слоев нет, оптимизаторов нет, — в общем, ничего еще нет. Само собой, можно вручную перемножать тензоры, рассчитывать градиенты и изменение весов (см. вышеприведенное видео и единственный пример). Но этого мы делать, конечно, не будем.


Вывод


Язык интересный, вот только для реального применения в data science пока не готов. Подождем.

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (7):

  1. Roaming
    /#11346248

    Спасибо за статью. Больше Swift богу Swift!
    Хочу подчеркнуть несколько важных (на мой взгляд) моментов.
    1) Swift (оригинальный) и Swift for TensorFlow это разные вещи. То есть Swift for TensorFlow это отдельный Toolchain (компилятор, библиотеки, синтаксис, функционал и тд). То есть, надо идти в репозиторий, качать и устанавливать отдельный Toolchain и тогда у вас появляется эти функции. Думаю, что так будет очень долго (читай всегда).

    2) Примеров уже доступна целых два :).
    3) API будет сильно меняться еще несколько месяцев. В комьюнити обсуждается много изменений.
    4) Уже есть первая документация, которая будет рости по мере появления новых сборок.

    • Roman_Kh
      /#11346450

      Спасибо за дополнение и полезные ссылки.


      Действительно, выкатили второй пример. Только он пока с комментарием "Note: This model is a work in progress and training doesn't quite work." :-)

  2. FireNero
    /#11346722 / +2

    Google объявил, что TensorFlow переезжает на Swift. Так что отложите все свои дела, выбросьте Python и срочно учите Swift.

    Разработчики TensorFlow, конечно, пока не забывают Python, но основой фреймворка станет именно Swift.

    Можно поинтересоваться откуда эта информация? Судя по оригинальной ссылке и видео это похоже на удобную обвертку над Tensorflow (как и Tensorflow.js), но я не вижу, чтобы где-то писали что Swift станет основным языком, то есть будет первым получать новые плюшки и т.д.

  3. RomanSt
    /#11346724

    Фразы

    TensorFlow переезжает на Swift
    и
    выбросьте Python и срочно учите Swift
    звучат так как будто Python для TF больше не будет. Это искажение информации и начинающего датасайентиста точно может сбить с толку.

  4. ivan2kh
    /#11350950

    Авторы Swift for TensorFlow предоставили увлекательную документацию на свой проект, которая объясняет причины выбора языка, архитектурные решения.
    Вот две ключевые статьи из документации: Automatic Differentiation, Graph Program Extraction.
    И описание дизайна Design Overview.
    На самом деле, Swift for TensorFlow охватывает достаточно узкий диапазон возможностей языка Swift. Так что изучение оригинального Swift, если вы собрались использовать Tensorflow, не рационально, по моему.

  5. Rayan_S
    /#11350966

    Если очень не хочется писать TensorFlow код на питоне, можно пользоваться обёрткой для Julia, которая тоже позволяет не писать tf.while_loop, заменяя эту магию на макрос @tf.


    Код: https://github.com/malmaud/TensorFlow.jl
    И поддержка CUDA есть.

  6. andrew8712
    /#11351726

    Опаньки, Крис Латтнер все-таки нашел себе работу и продвигает Swift в массы