2017-11-28

О Котлине

Тихо и незаметно вот уже второй настоящий коммерческий проект пишем с нуля и полностью на Котлине. И это ещё не считая всякой персональной мелочи под Андроид. Тихо и незаметно Котлин стал моим основным языком. Весь новый код по возможности стараюсь писать на Котлине.
А ещё три года назад всё выглядело не так однозначно. Была старушка Scala. Был странный Ceylon. И был подающий надежды, но с непонятным будущим, Kotlin.
В мире Scala не всё так однозначно. Typesafe стала Lightbend, и для всех их Scala продуктов заявлена поддержка отличного Java API. Scala перестал считать себя супер-языком, и стремится выжить в мире JVM.
Про Ceylon вообще ничего не слышно. Википедия говорит, новые версии выпускались стабильно, а в августе 2017 его передали в Eclipse Foundation.
А вот Kotlin расцвёл. Его настолько сильно продвигали в сторону Android разработки, что в результате его назначили first-class supported language для Android. И это хорошо. А ещё появился Kotlin/Native, который позиционируют для разработки под iOS. А ещё появляется всё больше чисто котлиновых библиотек, которые вовсю используют плюшки языка. А ещё в Котлине 1.1 добавили (пока экспериментальную) весьма интересную поддержку корутин. Минимальными изменениями в самом языке, без введения кучи новых ключевых слов, стало можно делать библиотеки, реализующие всякую асинхронщину вроде async/await, генераторов и всего такого.
В общем, пора писать на Котлине, если вы ещё не начали писать на Котлине. Котлин — прекрасен. Хотя не все с этим ещё согласны.
Kotlin logo (latest)
Объясняешь студентам: State — это такой класс, от которого нужно, чтобы его экземпляры были различными, в той степени, в которой нужно. Поэтому достаточно в нём иметь лишь одно строковое поле name. Но также нужно переопределить метод equals(), чтобы корректно сравнивать, и метод hashCode(), чтобы использовать в качестве ключа HashMap, и метод toString(), для удобства.
То есть в Яве нужно вот так:
public final class State {

    private final String name;

    public State(String name) {
        this.name = name;
    }

    public boolean equals(Object object) {
        if (this == object) return true;
        if (!(object instanceof State)) return false;
        if (!super.equals(object)) return false;

        State state = (State) object;

        if (name != null ? !name.equals(state.name) : state.name != null) return false;

        return true;
    }

    public int hashCode() {
        int result = super.hashCode();
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }

    public String toString() {
        return "State{" +
                "name='" + name + '\'' +
                '}';
    }

}
А в Котлине для всего этого достаточно лишь:
data class State(
    private val name: String
)
Это дата классы. Они финальные. Из них нельзя построить красивую иерархию наследования. Но это и не нужно. Они идеально подходят для передачи и представления данных. Вот структуры и всё.
data class SampleData(
    val visibleReadOnly: String,
    private val invisibleReadOnly: String,
    var readWrite: String
) {
    val internalReadOnly = invisibleReadOnly.toUpperCase()
}

val instance = SampleData(
        visibleReadOnly = "A",
        invisibleReadOnly = "B",
        readWrite = "C"
)

instance.visibleReadOnly    // "A"
//instance.visibleReadOnly = "a"  // compilation failure
//instance.invisibleReadOnly  // compilation failure
instance.readWrite  // "C"
instance.readWrite = "c"
instance.internalReadOnly   // "B"
//instance.internalReadOnly = "b" // compilation failure
Большинство свойств нашего класса данных нужно задавать в конструкторе. Наличие именованных параметров позволяет делать конструкторы принимающими сколь угодно много аргументов. И это остаётся удобным. Просто всегда вызывайте конструкторы с именованными аргументами.
А ещё есть и дефолтные значения параметров. Это чертовски запутывает, если пользоваться старыми добрыми позиционными параметрами. Но если пользоваться именованными — опять всё ок.
Кроме параметров конструктора, можно задать и обычные проперти класса. Но тогда имеет смысл делать эти проперти чем-то производным от параметров конструктора.
Ещё можно и методы добавлять, не проблема. Но, как правило, это не нужно.
Если у вас на всех пропертях стоит val, вы получите честную иммутабельность. Задаёте все свойства в конструкторе, а потом можете их читать. Вполне солидно.
Но ещё есть метод copy(), который позволяет «мутировать» ваши иммутабельные дата классы. Он принимает тот же набор параметров, что и конструктор, только все с дефолтными значениями. И создаёт копию дата-объекта, с изменёнными свойствами.
val copy = instance.copy(visibleReadOnly = "AAA")
Именованные аргументы, да ещё и с дефолтными значениями — вообще хорошо. Иногда создаётся иллюзия, будто кодишь на Питоне.
Kotlin logo (previous)
Заметьте, как правило дата классы в Котлине не соответствуют соглашению Java Beans. В бинах подразумевается конструктор без аргументов. А в дата классах наоборот, все свойства принято передавать в конструкторе. В бинах подразумеваются сеттеры и геттеры. В дата классах, если хотите иммутабельность и ставите val, у вас будут только геттеры.
Не все сериализаторы умеют корректно работать с иммутабельными дата классами Котлина. Gson (или его котлиновая обёртка Kotson) — умеет. А вот в Spark соответствующий Encoder ещё не завезли.
Эмулировать Java Beans приходится как-то так:
data class JavaBean(
    var property1: String? = null,
    var property2: String? = null
)

val bean = JavaBean()
bean.property1 = bean.property2
Совсем пустой конструктор в дата классе нельзя. Приходится делать конструктор с дефолтными (нуловыми) значениями для всех свойств. Некрасиво страшно.
Kotlin logo (oldest)
Если посмотреть на котлиновые библиотеки, например, на Mockito-Kotlin, то окажется, что они интенсивно эксплуатируют две возможности Котлина: экстеншен функции и функциональные литералы.
Экстеншен функции — это штука, покраденная, вероятно, из C#. Ну или дальнейшее развитие идеи friend function из C++, если хотите. В общем, это совершенно левые функции, которые, тем не менее, ведут себя как методы объекта (любого нужного типа). Они имеют доступ к target объекту через this. Но они не имеют доступ к приватным свойствам и методам объекта. Технически это просто синтаксический сахар. Но удобный. И приятный.
target.doSomething()
//vs
doSomething(target)
Можно сделать так:
fun Long.asSeconds(): Instant = Instant.ofEpochSecond(this)
fun Long.asMillis(): Instant = Instant.ofEpochMilli(this)

val now = 1511759774L.asSeconds()
val nowMillis = 1511759774000L.asMillis()
«Отсутствующие» явно конструкторы в Котлине поначалу вызывают недоумение. Но потом как-то укладывается. Ведь, в конце концов, чаще всего конструктор используется, чтобы передать и сохранить параметры. А для этого вполне достаточно val/var объявлений в круглых скобках. Если нужно что-то посложнее, есть блок init { }, или даже возможность задать вторичные конструкторы.
Зато из-за такого упрощения конструкторов класс-исключение в Котлине записывается в одну строку.
class MyException(message: String? = null, cause: Throwable? = null) : Exception(message, cause)
Кстати, любители сhecked exceptions, ваше время прошло. Как был Ява единственным языком с этой критикуемой концепцией, так им и остался. В Kotlin нет checked exceptions. И нет ключевого слова throws. Впрочем, как вы заметили, checked exceptions нет и в свежих API, добавленных в Яву, например, в java.time.
В Котлине можно писать inline функции. Это позволяет проделывать удивительные, для Явы, фокусы. Например, узнавать тип переменной типа в генериках. Например, вот какая магия запрятана в mockito-kotlin:
inline fun <reified T : Any> mock(
      // тут много опциальных параметров
): T = Mockito.mock(T::class.java, withSettings(
      // сюда эти параметры передаются
))!!
Не пугайтесь !!. Это просто требование получить не null, и выкинуть NullPointerException, если получился всё же null.
Интересно, что тип T известен. Можно получить его класс, и передать в Mockito.
Используется это так:
val mock: MyInterface = mock()
Магия в том, что это inline функция. И reified можно использовать только с inline. Тело функции подставляется в место вызова. И вывод типов Котлина вполне может определить (а в данном примере тип задан весьма явно), что это за T здесь.
Android with Kotlin
Другая очень милая фича Котлина — функциональные литералы. Они сделали их гениально просто и красиво — просто фигурные скобки. Плюс возможность, если последний аргумент функции — функция, указать эти фигурные скобки после круглых скобок (а круглые вообще упустить, если нет других аргументов).
Поэтому можно написать вот такое:
class RunNotTooFrequently(
    val interval: Long
) {

    private var lastRun = 0L

    fun run(block: () -> Unit) {
        val now = System.currentTimeMillis()
        if ((now - lastRun) > interval) {
            lastRun = now
            block()
        }
    }

}

val runner = RunNotTooFrequently(2000)
runner.run {
    // do something
}
В стандартной библиотеке полным-полно таких функций, принимающих функции.
val stream = Files.newInputStream(Paths.get(logFile))

stream.bufferedReader(StandardCharsets.UTF_8).useLines { lines ->
    for (line in lines) {
        // process line
    }
}
В Котлине, в отличие от Явы, где тоже теперь есть всякие лямбды, функции — более полноправные члены языка. Можно просто фигачить функции в .kt файле, снова и питонячьем стиле, и это будет работать. Только будет не очень удобно, потому что все функции тогда будут объявлены в пакете, сам .kt файл никакого неймспейса не создаёт. Соответственно, и при импорте придётся их использовать по их имени. И беда-беда, если имена пересекутся.
А ещё в Котлине есть синтаксис для описания типа функции. Ну там, количество и типы аргументов, и тип возвращаемого значения. Это гораздо удобнее костылей в виде функциональных интерфейсов, что остался в Яве. Но из-за этого бывает, что при вызове какого-нибудь Ява-метода, который ожидает именно что функциональный интерфейс, иногда нужно функциональный литерал Котлина приводить к этому интерфейсу.
Интероперабилити с Явой просто отличный. Не помню серьёзных проблем, чтобы что-нибудь явовое вызвать из Котлина. Наоборот делать не приходилось.
Ну разве что один раз поймал багоособенность с varargs. В Яве что массив указать, что кучу аргументов в vararg метод передать — результат одинаков. В Котлине, если передаёшь массив, нужно не забыть звёздочку перед ним. Иначе передастся именно что один аргумент-массив, что просто невозможно в Яве. Впрочем, там был vararg на Object (each(Object... values)), что лишний раз подтверждает, что типы всё же нужно делать строже.
Kotlin island
Мы юзаем Котлин вместе со Spring и Spring Boot. Оно работает.
Местами получается забавно:
@SpringBootApplication
open class Application

fun main(args: Array<String>) {
    SpringApplication.run(Application::class.java, *args)
}
Главное, не забывать делать классы и методы, которые помечены спринговыми аннотациями, open. Оказывается, современный Спринг очень любит заниматься кодогенерацией, наследовать ваши бедные классы и переопределять методы. По умолчанию в Котлине все классы и методы закрыты от переопределения. Спрингу нужно явно открывать. Есть, конечно, плагин к Gradle, который сделает это неявно. Но лишней магии лучше избегать.
Kotlin vs Java
Люблю Котлин за его лаконичность. Вот сколько строк на Яве займёт вот это?
val intVal = (doc.getValue("number") as? Number)?.toInt() ?: 0
Тут из некоего документа извлекается некое значение (Object, будь он неладен), которое может быть числом, целым или длинным. Нужно получить именно Int. А если не удалось, пусть будет нуль.

2017-11-12

Об ИИ

Что-то в последнее время меня «покусали» роботы. Немножко пополнил своё представление о нашем с вами ближайшем будущем.
За последние месяцы:
Neurons
Джеф Хокинс (не путать со Стивеном Хокингом) — это чувак, который сделал Palm Pilot, придумал рукописный ввод Graffiti, основал Palm Computing и Handspring. Он делал планшеты ещё тогда, когда они назывались наладонниками. Но, если верить книге, он с детства мечтал разобраться в том, как работает человеческий мозг. Поэтому, заработав достаточно денег в IT сфере, в начале двухтысячных он публикует эту книгу и основывает институт по изучению мозга.
Хокинс критикует существующие направления исследований за то, что они не пытаются создать единую теорию функционирования мозга (точнее его интересует в первую очередь неокортекс). Нейрофизиологи могут сказать, какая часть мозга возбуждается, когда мы видим определённые образы, ощущаем определённые эмоции или думаем о чём-то определённом, но не могут сказать, что это означает. Психологи и психиатры могут сказать, как определённые переживания в прошлом могут сказаться на нашем поведении в будущем, и как избавиться от ненужных переживаний, но не имеют понятия о том, какие физические процессы при этом происходят в мозгу.
Хокинс предлагает объединить усилия и излагает свою теорию функционирования неокортекса. Делает это он из айтишных побуждений, чтобы создать действительно мыслящие машины. Он и направления искусственного интеллекта в связи с этим критикует.
Дело в том, что даже нейронные сети, которые вроде как повторяют структуру мозга, работают не так, как мозг. Нейронные сети сначала обучаются, а потом работают. А мозг постоянно обрабатывает сигналы, и постоянно обучается. Это непрерывный процесс.
По Хокинсу неокортекс — это иерархическая машина узнавания паттернов во входных сигналах и прогнозирования (этих сигналов). Входные сигналы — это сигналы от органов чувств. Ответная реакция мозга — это в первую очередь движения, причём движения глаз, включая саккады, и движения языка и гортани при речи — это всё тоже ответы мозга.
Интересна иерархия. На нижних уровнях распознаются простые вещи: для зрения — линии разных направлений, для слуха — тона звуков. Несколькими уровнями выше распознаются уже целые образы: лица, мелодии. Ещё выше распознаются абстрактные понятия вроде людей, кошек, собак вообще. Где-то на этом уровне появляется то, что мы воспринимаем как наше создание, то есть умение оперировать абстракциями.
Все эти уровни работают одинаково, узнают и предсказывают. Только чем выше по иерархии, тем более абстрактны понятия, которые они узнают и предсказывают. И чем выше по иерархии, тем больше мозготоплива (буквально) требуется для принятия решения и реакции на окружающие взаимодействия, потому что буквально задействовано больше нейронов. Слоёв этих, похоже, что около сотни (исходя из типичного времени реации и типичной скорости распространения нервных импульсов).
Обучение как раз и заключается в том, чтобы перенести возможность принятия решения на более низкие уровни. Довести до автоматизма, выработать привычку. На низком уровне решение принимается быстрее и дешевле. Но работает оно только на заученных паттернах. Если встречается что-то незаученное, приходится задействовать более высокие уровни иерархии, «включать сознание».
Самые нижние слои имеются у всех млекопитающих. Где-то там мы подобны мышкам и ёжикам. У обезьян слоёв побольше. Но человек может оперировать более абстракными понятиями и делать более долгосрочные прогнозы, чем обезьяна. Наверху есть вершины осознания, доступные только человеку.
Что касается достижений концепции Хокинса в сфере искуственного интеллекта, то их наработки вполне доступны. Смотрите numenta.com.
Бодрствование
Иван Пигарёв — физиолог. Занимается исследованием сна. Развивает висцеральную теорию сна.
Ставили опыты. Не давали мышкам спать. И мышки умирали через несколько дней. Умирали от множественных повреждений внутренних органов. А мозги были целёхоньки. То есть сон нужен не мозгу, а внутренним органам.
Терия в том, что во время сна мозг (кора мозга) отключается от сигналов внешней среды, а подключается к сигналам от внутренных органов. Этих сигналов весьма и весьма много. И наши суперумные мозги занимаются их обработкой. Хотя сознание тут не задействовано. И без этой обработки и регулировки, на автономном обеспечении, внутренние органы просуществуют недолго.
Выглядит очень и очень странно. Каким образом те нейроны, которые в бодрствующем состоянии натренированы на распознование зрительных образов, во сне обрабатывают сигналы от кишечника? Впрочем, тут подсказывают, что свёрточные нейронные сети, натренированные на распознавании изображений, можно успешно использовать и в других областях, если им отрезать и заменить входные и выходные слои. Возможно, что-то подобное происходит и в мозге. Меняем внешние слои обработки сигналов и выходных воздействий со слоёв приёма и выдачи внешних сигналов на слои приёма и выдачи внутренних сигналов, и вовсю задействуем крутую натренированную нейросеть глубинных слоёв.
Кстати, сон — это не привилегия высших животных. Мухи тоже спят. Похоже, это забавный эволюционный механизм. Если уж у нас появились такие сложные внутренности, почему бы не воспользоваться нашей крутой центральной нервной системой, чтобы порулить внутренностями? Во сне.
Но куда опять потерялось сознание? В базальных ганглиях, которые неактивны во сне? Впрочем, боюсь, что в структуре мозга может быть много нелогичного. И сознание может не быть где-то конкретно. Не инженер же проектировал. Но, по Хокинсу, сознание должно быть где-то в самой глубине свёрточной сети, оперируя максимальными абстракциями. Как можно отключить внутренности свёрточной сети? Или сознание — это действительно что-то особенное и сбокут.е.?
Персональный вывод: надо спать. Сон важнее, чем первая пара. Хочется спать — надо спать. А то всякие язвы желудка и всё такое...
А роботам спать не надо. У них же нет внутренних органов. Ну или для самодиагностики можно выделить дополнительную нейросеть, попроще. А вот где сознание? Неужели, если мы разберёмся в работе неокортекса и скопируем его, то получим умные, но совершенно бессознательные машины? Может, это и хорошо?
Взрывной рост ИИ
Тим Урбан — один из основателей сайта Wait But Why. Оттуда, и с разрешения Тима, Макс Дорофеев взял обезьяну и рационального типа, сосуществующих у нас в голове, для своих джедайских техник.
Про ИИ Тим Урбан пишет подробно. Рассматривает оптимистичные прогнозы Рэймонда Курцвейла. И опасения Илона Маска и компании, в лице Института будущего человечества, по поводу сверхинтеллекта.
Никто не сомневается, что рано или поздно, так или иначе, интеллект, подобный человеческому, и даже значительно его превосходящий, будет создан. Вопрос: чем (или кем) будет этот сверхинтеллект? И какое отношение у него будет к человеку?
Курцвейл чрезвычайно оптимистичен. Он считает, что человечество и созданный им сверхинтеллект — это будет одно и то же (привет, киборги!). А значит, всем будет хорошо.
И понятно, чем обеспокоены Маск и Гейтс. Искусственный сверхинтеллект не будет человеческим. И не будет животным. Ему будут совершенно неведомы эволюционные мотивы выживания и сосуществования. Он не будет проходить путь многолетнего воспитания. И он будет весьма могущественным. Настолько могущественным, что человек сдержать его не сможет. И как он отнесётся к человеку? Трансгуманизм по всей красе.
Азимову было хорошо. Три закона роботехники являлись неотъемлемой частью позитронного мозга. Мозга человеческого раба. Были его моралью. Но как привить мораль искусственному интеллекту, который мы сейчас можем построить, никто не знает. Да что там, мы не знаем даже, что такое человеческая мораль. Да и не факт, что сверхинтеллект не сможет обойти собственные моральные ограничения.
Her
В этом смысле фильм «Она» («Her», «Её»?) очень оптимистичен. ИИ родился, научился у человека любить, развился и умотал куда-то, оставив человечество в полнейшем недоумении. Ну прям как «Волны гасят ветер» у Стругацких. Но будут ли у настоящего сверхинтеллекта столь тёплые чувства по отношению к человечеству? Отсутствие ответа на этот вопрос как раз и пугает.
Впрочем, ныняшняя «умная операционка», в лице Алисы от Яндекса, хоть и весьма неплохо умеет гонять речь в текст и текст в речь, весьма тупа. И всякие домашние роботы, в большинстве своём игрушки, от Aibo до Cozmo, и даже вполне серьёзные роботы-ассистенты, тоже тупые. Ну не производят они впечатление умного (или хотя бы живого) существа. Даже до котёнка нормального не дотягивают.
Так что, надеюсь, ещё лет десять-пятнадцать у нас ещё есть. У человеков. Ещё лет десять-пятнадцать не бояться того, что появится кто-то умнее нас. А за это время, глядишь, и свыкнемся с этой мыслью. Или сами себя уничтожим, вместе с ростками потенциального сверхинтеллекта. Уничтожим по глупости, конечно. Ибо отказываться от прогресса вообще, и сверхинтеллекта в частности, никто даже не собирается. К Батлерианскому джихаду мы пока явно не готовы.