2017-12-09

О Spring

Spring — это весна. Spring — это пружина. Spring — это родник. Springfield — это городок, где живут Симпсоны. Плюс ещё стопицот одноимённых городков в Соединённом Королевстве, Австралии и Соединённых Штатах.
А ещё есть Spring Framework. Фреймворк, который знают все явисты. Возникший когда-то как легковесная альтернатива Ынтырпрайзным ЯваБобам (EJB).
Spring Logo
Помню, как лет десять назад мы перелезали на Спринг с таких, ныне экзотичных вещей, как Apache Struts, или даже просто месива из сервлетов и JSP. Тогда надо было писать большущие простыни XMLя с описанием всех бинов. Уже тогда надо было подглядывать в исходники Спринга, чтобы понять, как туда воткнуть что-то нестандартное с точки зрения разработчиков фреймворка.
Сейчас в Спринге есть аннотации. И конфигурацию можно описывать Ява кодом. Или вообще не описывать, а просто помечать нужные классы как @Component.
Сейчас есть Spring Boot. Вообще крайне странная штуковина.
С одной стороны, он сильно упрощает начальное создание Спринг приложения. Это — POM артефакты, содержащие целые группы спринговых и внешних зависимостей, с тщательно подобранными (хочется в это верить) версиями, гарантированно работающими друг с другом. Это — автоматические настройщики, которые создают бины, исходя из набора свойств в application.yml или application.properties, и сканируют пакеты в поисках классов и методов помеченных хитрыми аннотациями, чтобы и их настроить. Это — плугины к Maven и Gradle, которые умеют собирать красивые суперджары, с внедрённым Jetty или Netty для веб приложений.
С другой стороны, Spring Boot весьма усложняет попытки уйти в сторону и сделать что-то непредусмотренное. Притащить другую версию библиотеки почти наверняка не выйдет, потому что у артефакта Spring Boot уже есть своя версия. Причём какая это версия, простого способа узнать нет, ибо там очень много транзитивных зависимостей. Чтобы воткнуть нестандартную конфигурацию, банально несколько подключений к разным MongoDB, придётся отключать автоконфигураторы. Какие именно отключать, придётся находить методом научного тыка, ибо нет простого списка этих конфигураторов с описанием того, за что они отвечают и что делают.
Spring Boot Logo
В тех случаях, когда наш любимый, акторный, но пока не очень человеколюбивый фреймворк использовать нельзя, мы берём Spring, Spring Boot и Kotlin. И нормально.
Точка входа в приложение, Application.kt, выглядит забавно.
@SpringBootApplication
@EnableAutoConfiguration(exclude=arrayOf(MongoAutoConfiguration::class, MongoDataAutoConfiguration::class))
@EnableScheduling
@EnableCaching
open class Application

fun main(args: Array<String>) {
    SpringApplication.run(Application::class.java, *args)
}
main() тут получается в классе ApplicationKt. Но Спрингу и его Буту нужен ещё один класс, открытый, ибо станет бином. И вот этот класс и становится @SpringBootApplication.
Современный Спринг делает страшные вещи с вашими классами. То, что будет бином, т.е. любые классы, помеченные @SpringBootApplication, @Configuration, @Component, @Repository, @Service, @Controller, а также любые методы, помеченные @Bean, в Котлине должны быть open. Потому что вы (почти) никогда не получите на выходе (в IOC) именно ваш класс. Вы получите его наследника. Куда будут аккуратно засунуты все объявленные @Autowired и @Value.
Любой класс, куда вы навешаете эти аннотации, станет частью Спринга. Вам понадобятся спринговые зависимости, чтобы эти аннотации объявить. И если этот класс случайно окажется в пакете, где его найдёт автоконфигурация, он сразу окажется в IOC. А если не получится найти все нужные @Autowired и @Value, то приложение рухнет на старте.
Поэтому мы так не делаем. Мы стараемся делать сервисы и компоненты, которые вообще не зависят от Спринга. Все другие сервисы и компоненты, а также конфигурационные значения, которые нужны для работы этого сервиса, передаются в конструкторе. Как это и положено с нормальными объектами. Куча аргументов конструктора в Котлине — не проблема. Мы просто используем именованные аргументы.
Сами внешние сервисы и компоненты в аргументах конструктора представлены интерфейсами. Интерфейс легко замокать. И можно и нужно протестировать этот компонент как положено, юнит тестами. А ещё типы бинов удобнее представлять интерфейсами, тогда можно тихо и незаметно подменить реализацию. Кстати, Спринг вполне корректно различает генерик интерфейсы, с разными типами переменных типа.
class EntityInsertService(
    private val name: String = "",
    private val mongoOps: MongoOperations,
    private val collectionName: String,
    private val executor: Executor
) : IInsertService<Entity> {

    override fun insert(data: Entity) {
        //...
    }

    override fun flush() {
        //...
    }

}
Такие компоненты без аннотаций легко выносятся в (почти) независимые от Спринга библиотеки и переиспользуются между разными приложениями.
А в самом приложении они уже подключаются через объявление конфигурации.
@Configuration
open class InsertServiceConfiguration {

    @Value("\${insert.concurrency:2}")
    private var concurrency: Int = 2

    @Value("\${insert.collection:data}")
    private lateinit var collection: String

    @Autowired
    private lateinit var mongo: MongoOperations

    @Bean
    open fun insertService(): IInsertService<Entity> {
        val executor = Executors.newWorkStealingPool(concurrency)
        return EntityInsertService(
            name = "entityInsert",
            mongoOps = mongo,
            collectionName = collection,
            executor = executor
        )
    }

}
Kotlin Logo
Вот эти вот insert.concurrency — это проперти приложения. Как известно, их можно объявлять в application.properties. Но мы, конечно же, предпочитаем более сложновложенный application.yml. Из этих пропертей тоже можно конструировать бины, списки и даже мапы.
Вот вам нестандартная конфигурация с кучей Монг:
mongodb:
  connections:
    source:
      host: 172.31.22.180
      port: 27017
      database: project
      serverSelectionTimeout: 10000     # https://scalegrid.io/blog/understanding-mongodb-client-timeout-options/
      connectTimeout: 5000
      socketTimeout: 1000
    target:
      host: localhost
      port: 27017
      database: test
      serverSelectionTimeout: 10000
      connectTimeout: 5000
      socketTimeout: 1000
      writeConcern: acknowledged        # http://mongodb.github.io/mongo-java-driver/3
Можно, конечно, наваять @Configuration класс, куда внедрить все эти значения через @Value. Но мне понадобилось иметь произвольное количество таких подключений. Их все можно прочитать в мапу с помощью @ConfigurationProperties.
data class MongoConnectionProperties(
    var host: String = "localhost",
    var port: Int = 27017,
    var database: String = "test",
    var serverSelectionTimeout: Int = 0,
    var connectTimeout: Int = 5000,
    var socketTimeout: Int = 1000,
    var writeConcern: String = "acknowledged",
    var connectionsPerHost: Int = 10
) {
    val writeConcernValue: WriteConcern
        get() = WriteConcern.valueOf(writeConcern.toUpperCase())
}

data class MongoConnections(
    val connections: MutableMap<String, MongoConnectionProperties> = mutableMapOf()
) {
    operator fun get(name: String): MongoConnectionProperties
        = connections[name] ?: throw NoSuchElementException("No MongoDB connection mongodb.connections.$name")
}

@Configuration
open class MongoConnectionsConfiguration {

    @Bean
    @ConfigurationProperties(prefix="mongodb")
    open fun allMongoProperties(): MongoConnections {
        return MongoConnections()
    }

}
Эта мапа должна быть мутабельной, а объекты должны быть настоящими ява бинами, с полным набором сеттеров, чтобы Спринг смог эту мапу заполнить. Поэтому тут в котлиновых датаклассах сплошные var и дефолтные значения. Для итогового набора свойств тоже нужно придумывать свой класс, нельзя просто вернуть коллекцию, потому что списки и мапы в качестве типа бина обрабатываются Спрингом по-особому: он пытается туда впихнуть все бины из контекста, подходящие по типу, а нам это совсем не нужно.
Из бина описания можно сделать настоящий объект MongoTemplate, который уже можно использовать.
@Configuration
open class MongoConnectionsConfiguration {

    @Bean
    @Scope("prototype")
    open fun mongoOperations(name: String): MongoOperations {
        val properties = allMongoProperties()[name]
        val options = MongoClientOptions.builder()
            .writeConcern(properties.writeConcernValue)
            .serverSelectionTimeout(properties.serverSelectionTimeout)
            .connectTimeout(properties.connectTimeout)
            .socketTimeout(properties.socketTimeout)
            .connectionsPerHost(properties.connectionsPerHost)
            .build()
        val dbFactory = SimpleMongoDbFactory(
            MongoClient(ServerAddress(properties.host, properties.port), options),
            properties.database)
        return MongoTemplate(dbFactory)
    }

}
Правда здесь у нас получается бин, который прототип, да ещё принимающий строку параметром. Получить такие бины из ApplicationContext труда не составляет. А вот @Autowired или @Inject для них уже не работают.
Поэтому приходится, если нужно, все эти прототипы создавать явно, и засовывать в синглетон коллекцию. Опять своего отдельного типа. Славься Котлин дата классами.
data class MongoOperationsMap(
    val mongos: Map<String, MongoOperations>
)

@Configuration
open class MongoConnectionsConfiguration {

    @Bean
    open fun allMongoOperations() : MongoOperationsMap {
        val map: MutableMap<String, MongoOperations> = mutableMapOf()
        for ((name, _) in allMongoProperties().connections) {
            map.put(name, mongoOperations(name))
        }
        return MongoOperationsMap(map)
    }

}
Ну а дальше этот наш дата класс можно автовайрить куда угодно.
Springfield
Иногда приходится бороться со Спрингом вообще, и со Спринг Бутом в частности. И хочется выкинуть этот Спринг нафиг. Со всеми его странными и несовместимыми обёртками вокруг обычного Монго драйвера.
Но как представишь, сколько вопросов возникнет без Спринга: Где создавать компоненты? Как их связывать друг с другом? Как автоматически и многопоточно подключаться к какой-нибудь очереди? Какой шедулер взять? Какой кэш взять и как его прикрутить?
И решаешь: пусть Спринг остаётся. Всё же он берёт на себя громадную кучу инфраструктурных проблем. А иногда с ним пободаться даже полезно.

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, и даже вполне серьёзные роботы-ассистенты, тоже тупые. Ну не производят они впечатление умного (или хотя бы живого) существа. Даже до котёнка нормального не дотягивают.
Так что, надеюсь, ещё лет десять-пятнадцать у нас ещё есть. У человеков. Ещё лет десять-пятнадцать не бояться того, что появится кто-то умнее нас. А за это время, глядишь, и свыкнемся с этой мыслью. Или сами себя уничтожим, вместе с ростками потенциального сверхинтеллекта. Уничтожим по глупости, конечно. Ибо отказываться от прогресса вообще, и сверхинтеллекта в частности, никто даже не собирается. К Батлерианскому джихаду мы пока явно не готовы.

2017-10-30

Об Apache Spark

А вот вам заметки полного нуба об Apache Spark.
Apache Spark Logo
Именно Apache Spark™. А то есть ещё какой-то веб-фреймворк Spark. Не говоря уже о Twilight Sparkle. Плохое название. Гуглите осторожнее.
Apache Spark — это такая штука для распределённых вычислений. Эту всякую бигдату обрабатывать. Причём это не платформа для распределённых вычислений, типа Hadoop, а скорее фреймворк для распределённых вычислений. На нём можно писать эти распределённые вычисления. На Scala, Java, Python или R. Примерно однотипно на всех языках. А выполнять эти распределённые вычисления уже на кластере в Hadoop (точнее YARN) или в Apache Mesos. (Господи, всюду Апач.)
Чертовски привлекательно, что можно обойтись и без кластера, а запустить Спарк в локальной JVM. При этом, если у вас достаточно мощная машинка, и HDFS вы всё равно не используете, а значит, локальность данных вас особо не волнует, даже в таком локальном запуске можно сожрать изрядно оперативы и ядер ЦПУ, и перемолоть изрядную кучку данных.
Как-то так:
$ $SPARK_HOME/bin/spark-submit \
    --class "my.runners.App" \
    --master local[4] \
    target/scala-2.11/my-assembly-0.1.jar \
    --data-mongo-uri mongodb://... \
    --target-mongo-uri mongodb://...
Вот так локально мы и перемалываем несколько десятков гигабайт недельных данных за пару десятков минут, чтобы найти некоторые аномалии...
R я не знаю. Python в Spark не пробовал. А Spark изначально запилен на Scala. Пришлось немного освоить Scala.
Согласно последней политике продвижения Scala, все Scala продукты должны иметь хороший Java API. И у Спарка он есть. Так и написано в жалком подобии явадоков: вот этот метод для Скалы, а вот этот для Явы.
На Яве получается заметно многословнее, чем на Скале. Нет имплиситов, поэтому тот же Encoder везде надо явно втыкать. Encoder — это такая штука, которая описывает "схему" обрабатываемых данных. Данные же нужно гонять между узлами кластера, и для этого надо понимать, какой они "формы". Примитивные типы, case классы Scala, некоторые коллекции поддерживаются из коробки. Для Java ещё java beans тоже энкодятся. Как правило, этого достаточно.
Но я ведь попробовал пописать на Kotlin. Осторожно, spark-kotlin — это для веб-фреймворка, а не для Apache Spark. И на Котлине мне не понравилось. Уж лучше слегка научиться Скале.
Имплиситов в Котлине нет. Энкодеры надо прописывать явно. Data классы Котлина поддерживаются при этом не в Kotlin-way.
Вот такой data class работать не будет:
data class KotlinDataClass(
    val a: String
)
Потому что это не java bean. Тут конструктор не умолчательный. И сеттеров нет.
Нужно сделать java bean:
data class JavaBean(
    var a: String? = null
)
Вот теперь будет работать в Спарке. Хотя выглядит чудовищно с точки зрения Котлина.
Пользоваться приходится API для Java, JavaSparkContext и JavaRDD. Потому что скаловых коллекций Котлин не умеет (слава богу).
Бывает, что Котлин видит пару методов с разными сигнатурами. Один принимает скаловую функцию. Другой принимает какой-нибудь org.apache.spark.api.java.function.MapFunction. Второй метод явно добавили для Java API. Но Котлин эти сигнатуры не различает. Приходится явно функциональный литерал Котлина приводить к типу Function. Уродливо.
import org.apache.spark.api.java.function.MapFunction

//...

return df.map(
    MapFunction { o: MyObject ->
        //...
    },
    MySchema.myEncoder
)

//...
Через пару дней мучений, я всё же решил: Скала, так Скала.
Spark Bricks
Всё в Спарке крутится вокруг трёх штук, которые являются развитием одной и той же идеи. Это RDD (Resilient Distributed Datasets), DataFrame и Dataset. Их можно прочитать, из файлов (в том числе и HDFS) или из БД. Их можно записать, в файлы (в том числе и HDFS) или в БД. Над ними можно делать операции. Примерно те же операции, что и над Stream, что в Java 8: map(), flatMap(), filter(), reduce(), groupBy(), вот это всё. Только вот весь бигдатный набор данных будет побит на партиции, и каждая партиция будет обработана на воркере в кластере, а результат будет собран в кучку на том узле, который инициировал вычисления и называется Driver.
Разница между RDD, DataFrame и Dataset в типизации. RDD исторически был первым API Спарка. И он умеет работать только с кортежами (которые tuple). Т.е. все вот эти мапы и прочее вы будете делать с кортежами.
Dataset (и DataFrame) — это более новый API Спарка. В DataFrame вы имеете дело со схемой данных. Известны имена и типы столбцов. Соответственно, по именам можно стобцы извлекать, удалять из набора данных, и всё такое. Можно об этом думать как об очень длинной SQL таблице. Даже некоторые операции можно выражать на подмножестве SQL.
В Dataset вы имеете дело с объектами. Типа ORM. Очень удобно для таких объектов использовать case классы Скалы. На самом деле, технически DataFrame не существует, это Dataset[Row]. Но в работе гораздо удобнее более типизированный Dataset[MyCaseClass]. Вы задаёте класс при загрузке данных, и сразу получаете датасет нужных объектов. По ходу манипуляций у вас получатся другие объекты, и их снова можно сохранить в какую-нибудь коллекцию какой-нибудь БД.
Схема (т.е. набор и типы колонок) вполне успешно самостоятельно выводится из набора полей класса. Но есть возможность указать её самостоятельно. Это полезно, если у вас в какой-нибудь Mongo коллекции завалялись документы разных форм. Тогда указание схемы позволит выбрать только документы нужной формы, пригодные для обработки.
Читать/загружать наши датасеты можно из файлов, из любой реляционной БД через JDBC, из MongoDB. Если читаем из HDFS, вовсю работает локальность данных. Т.е. воркерам достанется на обработку та партиция, которая расположена на том же узле. Если читаем из реляционной БД, на локальность данных всем насрать, как я понимаю. Если читаем из MongoDB, может учитываться расположение шардов, если они у вас есть. Разбиение на партиции — это обязанность коннектора к БД. Для Монги есть свой коннектор, который понимает шарды. Для JDBC есть один общий коннектор, который может работать с любыми JDBC драйверами.
Это бигдата. Читать в датасет вам придётся всю таблицу или коллекцию. Целиком. Впрочем, в MongoDB есть возможность задать шаг aggregate, который будет вставлен в самом начале. Очень имеет смысл добавить туда $match, который выберет только то, что нужно.
Коннектор к MongoDB читает очень странно. На мой взгляд. Сначала делается большой aggregate(), в котором выполняется $sample, выбираются только _id порядка 10% всех документов. А потом уже выбираются все документы, с разбиением по диапазонам (от и до) _id. Вообще-то _id не образуют континуум, совпадающий с запрошенными данными, хоть они и упорядочены. В результате, как минимум, некоторые документы бывают пропущены, ибо их _id не попали в первоначальный $sample. Может в Big Data так принято, но мне такой алгоритм кажется странным.
В бигдате много странного и непривычного. Спарк не умеет обновлять данные. Он легко может создать новую таблицу или коллекцию и выгрузить туда весь датасет, который получился в результате вычислений. Но дальше уже ваша забота, куда эту таблицу засунуть. Как правило, её нужно как-то смержить с имеющимися данными. Например, как-то так:
INSERT IGNORE INTO existing_data (...)
SELECT ... FROM spark_result;
В случае с MongoDB можно попробовать сделать апдейт и в Spark. Опять таки, всё зависит от коннектора. Коннектор MongoDB умеет обращать внимание на _id документа. И если в датасете есть _id, будет сделан upsert, а не insert. Таким образом, можно загрузить датасет оригинальной коллекции и сджойнить с результатами обработки, обновить объекты. А потом обновлённый результат выгрузить в оригинальную коллекцию, обновить документы. Это всё работает, но только если вы вызовете правильный метод MongoSpark.save[D](dataset: Dataset[D], writeConfig: WriteConfig). Другие методы сохранения в MongoDB плевать хотели на _id.
Ну и лучше так не делать. Во-первых, вам придётся грузить в Спарк на одну коллекцию больше. Во-вторых, joinWith() — не самая лёгкая операция для Спарка. В-третьих, вы полностью перепишете оригинальную коллекцию, и все изменения, внесённые в неё третьей стороной с момента загрузки в Спарк, до момента выгрузки из Спарка, будут потеряны. Проще выгрузить из Спарка результат вычислений в отдельную коллекцию, а потом смержить уже средствами Монги. Это небыстро, но надёжно.
db.spark_result.find().forEach(
    function(doc) { db.existing_data.update(
        { ...update key... },
        { $set: { ...update operation... } },
        { upsert: true, multi: true })
    })
Apache Spark — типичный инструмент Big Data. Всё что вы можете: загрузить громадный объем данных, обработать его по кусочками, в параллель на узлах кластера, и выгрузить результат снова одним большим куском данных. Никакой модификации имеющихся данных, только создание новых. И при этом ещё могут быть погрешности в чтении :)
А иногда надо разветвиться. Подсчитать на исходных данных какую-нибудь тяжёлую статистику. А потом, по этой статистике, родить несколько разных результатов. Теоретически, для каждого результата нужен свой конвейер. Но тогда придётся для каждого конвейера заново вычитывать исходные данные и считать статистику. Скучно и неоптимально.
Поэтому в Спарке есть кэш. Метод persist(). Любой шаг конвейера можно сохранить в кэш. И начать новый конвейер с этого кэша. Будет быстро. Главное, чтобы памяти хватило. А не хватит памяти, можно использовать диск. На воркерах. Будет медленее, но это всё равно быстрее, чем вычитывать всё заново из внешней БД. Тем более, что Спарк хранит в кэше объекты, сериализованные с помощью Kryo. Главное, всё правильно подтюнить.
Похоже, в тех случаях, когда всё равно нужно перелопатить добрые сотни миллионов записей, чтобы получить какие-то результаты, Apache Spark является отличной альтернативой попытке перелопатить это средствами самой БД. Выйдет быстрее (потому что обработка будет делаться на других узлах, а не в БД) и значительно гибче (потому что это всё же код на Scala, где можно делать почти всё, что угодно). Вот только хочется нормального API и для Kotlin :)
Twilight Sparkle

2017-10-15

Об играх

В игры я почти не играю. Раньше играл. А сейчас почти не играю.
Нет, я, конечно, снова прошёл весь Carmageddon, когда он вышел под Android. И с удовольствием резался в Plague Inc.. И даже где-то у меня есть аккаунт в Steam, где пылится честно купленная на распродаже за нуль рублей Portal.
Carmageddon
Но я сейчас о другом. Я, оказывается, ни разу не задумывался, как делаются современные игры. А это прям отдельная Вселенная.
В студенческие годы, я, конечно, писал игрушки. Под DOS, на C и C++. Развлекался с трёхмерной графикой, освещением и текстурами в режиме 320х200 c 256 цветами. Трёхмерные крестики-нолики делал. А ещё в текстовом режиме были «Быки и коровы» и «Жизнь».
Чуть позже я немного развлекался с Borland C++ Builder (именно ещё от Borland). И наклепал для Windows тетрис (любой уважающий себя программист должен написать тетрис), решалку японских кроссвордов и даже симулятор MK-61 (ну как бы уже не совсем игра).
С тех пор игр не писал. А зачем? Какой от них толк? Интереснее сделать что-нибудь полезное. Для себя.
Ludum Dare
Но случайно таки оказался недавно на омском Ludum Dare. И там я узнал, что игры пишутся совсем не так, как пишутся обычные сетевые приложения, или как обычные гуёвые приложения.
Как работает обычный сервер? Он ждёт запросов. Пока запросов нет, он ничего не делает. Когда приходит запрос, он его обрабатывает. Запускает ли он для этого другой процесс, или другой поток, или всё делает в общем (для кучи соединений) потоке-воркере (то, что называется асинхронным) — не важно. Важно, что всегда есть запрос-ответ. Нет запроса — ничего не происходит (как правило).
Запросы создаёт клиент. Именно он является инициатором всего этого безобразия. Нет клиентов — нет запросов — ничего не происходит.
Аналогично в GUI. У нас есть события: движения мышки, нажатия кнопок и клавиш. Когда событие происходит, оно должно быть обработано. Графическим элементом, над которым оно произошло, окном и т.д. И тут, кстати, не придумали особо изящного способа обработки всех этих событий, кроме как одного, в одном потоке, бесконечного (но блокирующегося) цикла. Да, это тоже асинхронщина.
Пока вы двигаете мышкой из одного угла экрана в другой, происходят тысячи событий, в десятках разных приложений. Но если вы взялись ковырять в носу, и убрали руки с мыши и клавиатуры, то событий не будет. И снова ничего не будет происходить. Ну разве что курсор будет мигать. На старых терминалах мигание вообще делалось аппаратно.
Общее тут то, что пока нет события или запроса, ничего не делается. А когда события и запросы пошли, их все нужно успеть обработать. В современных GUI это не всегда тривиально. Но оно так и работает.
Java GUI Event Loop
В играх всё по-другому.
Даже роли клиента и сервера отличаются. Да, клиент по-прежнему подключается к серверу. Но дальше идут не запрос-ответ, а отправка сообщений. В обе стороны. Клиент сообщает о действиях игрока. Сервер сообщает о состоянии игры (изменённое действиями и этого, и других игроков). Существенное правило безопасности: сервер знает всё, а вот клиент должен знать только то, что ему положено знать. Нарушение этого правила порождает интересные возможности для тех, кого называют читерами (к примеру, в World of Tanks это вообще весело).
В играх игрок тоже жмёт кнопки и елозит мышкой. И это тоже может порождать события. Но эти события никого особо не интересуют. События не порождают немедленных действий по их обработке. Да, координата мышки, или номер нажатой кнопки могут быть где-то запомнены, чтобы знать, что кнопка нажималась. Но и всё.
Потому что в играх — постоянный polling. В бесконечных циклах. С задежкой. Это, конечно, не обязательно должны быть while (true) в отдельных потоках со sleep() внутри. Можно всё сделать хитрее, на таймерах и всё такое. Но сути дела это не меняет.
Есть цикл для отрисовки. Который даёт те самые fps. Тут всё просто. Берётся состояние игры, с точки зрения игрока. И отрисовывается. Столько раз в секунду, сколько надо. Само состояние может меняться реже или чаще.
Есть цикл опроса действий игрока. Тут как раз мы выясняем, какие кнопочки нажаты, где находится мышка. И, либо прямо модифицируем состояние игры, в случае сингл плеера. Либо передаём намерения игрока на сервер, в случае сетевого мультиплеера.
На сервере намерения игроков накапливаются. А потом происходит тик игры. Это самый главный цикл, где изменяется глобальное состояние игры. Раз и навсегда. Тут просчитываются всякие перемещения, уроны, жизни и т.п. И тут очень важным параметром является время, прошедшее с предыдущего тика. Как правило это время не может быть в точности постоянным. И это надо учитывать, дабы персонажи, как минимум, перемещались равномерно. И изменённое состояние игры рассылается всем игрокам.
Из-за ограничений в скорости сети, эти рассылки идут значительно реже, чем fps. Что порождает некоторые особенности. Все эти опросы, просчёты, отрисовки происходят каждый в своём цикле. И они в общем случае не синхронизированы. Нужны какие-то правила синхронизации.
Например, что делать, если игрок упорно движется в определённом направлении, а с сервера обновлённого состояния, где произошло перемещение в данном направлении, ещё не пришло? Клиент может взять на себя смелость и изменить местоположение игрока, чтобы при прорисовке перемещение происходило плавно. Это называется интерполяцией. Если прогнозы клиента, и обновлённое состояние с сервера совпали — всё хорошо. А вот если выяснится, что игрока по пути кто-то убил, придётся как-то выкручиваться и дорисовывать, что сначала мы дошли туда, а потом, метром ранее, умерли.
Пересылка сообщений. Циклы опроса. Асинхронная пересылка сообщений. Синхронизация состояний. Безопасность и доверие. Вот это вот всё — игры.
Может, я рассматриваю лишь частный случай игродельной индустрии. Но мы на Людуме клепали так.
Game Loop
Почему же классические клиент-серверные архитектуры, а так же GUI, построены по-другому? Дело в экономии ресурсов? Выгодно ничего не делать, пока нет событий? В играх ведь все циклы: отрисовки, опроса, просчёта следующего состояния игры — колбасят всегда, независимо от действия или бездействия игрока. CPU и сеть никто особо не жалеет.
С другой стороны, где-то циклы с постоянными опросами всё же встречаются? Уж не в ядрах ли ОС? Тот самый поллинг сетевой карты вместо прерываний. Нет? А в RTOS, случайно, не осуществляется такой же тотальный контроль над временем выполнения задач? Ну не успел выстрел игрока попасть в вычисления данного состояния игры, значит выстрела на данном шаге не будет.
Какая-то другая Вселенная — эти игры. Где бы это применить, не начиная писать игры? :)

2017-10-01

О сэре Максе из Ехо

Наконец-то дочитал «Лабиринты Ехо» и «Хроники Ехо». Думаю, теперь можно написать про Макса Фрая. Точнее про сэра Макса из Ехо. В надежде, что кто-нибудь тоже захочет прочитать эти прекрасные книги.
Ехо
Рукописи в издательство носили, конечно же, обычные люди: Светлана Мартынчик (ага, женщина) и Игорь Стёпин. Но автором значится некто Макс Фрай. Или Max Frei, если латиницей. Максимально свободный. Или без Макса. И вовсе никогда не Максим.
Вот Макс Фрай и пишет автобиографические очерки. О том, как он был сэром Максом в Ехо. И как туда попал. И как туда вернулся. Где его всегда называли только по имени. Что там ещё было с Максом Фраем кроме Ехо, этого я ещё не читал.
Порядок чтения очень важен. Это же Кастанеда для маленьких. Не прочтёте предыдущие книжки, не поймёте, на что ссылаются и объявляют ложным последующие книжки. Ну ладно, Макс Фрай ничто не объявляет ложным, чем запутывает и себя, и читателя ещё больше.
Сначала «Лабиринты Ехо», потом «Хроники Ехо», потом, видимо, «Сновидения Ехо». За точным порядком можете консультироваться у Википедии. Кстати, последние книжки «Сновидений Ехо» вышли в этом (2017) году, так что история продолжается.
Жанр? Определённо фэнтэзи. Или как там называется это всё, с волшебством, эльфами (хотя тут эльфы весьма странные) и всем прочим? Определённо детектив. Потому что почти всегда имеется преступление и расследование. А ещё мистика, ужасы, комедия, мелодрама. Да всё, что угодно. Поэтому я нарекаю жанр историй сэра Макса из Ехо «сериалом».
Это действительно сериал. Длинный и увлекательный. С громадным количеством длинных нескучных диалогов. С несильно большим количеством действия. Впрочем, после каждого действия офигевать приходится и читателям, и сэру Максу. Сэра Макса мог бы сыграть, к примеру, Егор Бероев. А вот сэра Джуффина Халли конечно же должен бы играть Рутгер Хауэр, как он выглядел, к примеру, в «Десятом королевстве».
Карта Мира
Слово «Ехо» надо читать именно как «Е́хо», а не «Ёхо» или «Эхо». Ехо — это столица Соединённого Королевства, крупнейшего государства, расположенного на материке Хонхона в Мире Стержня. Ехо построен вокруг Сердца Мира. Сердце Мира — это то место, где выходит один из концов Стержня, на котором и держится этот Мир.
У Сердца Мира отлично работает Очевидная (или бытовая) магия, и чёрная, и белая. Именно потому Ехо построили здесь. Чтобы магистрам многочисленных магических орденов было веселее сражаться друг с другом. Впрочем, оказалось, что злоупотребления Очевидной магией разрушают основу Мира. В результате, ради спасения Мира, все магические ордены были упразднены или побеждены, а у власти Соединённого Королевства остались, собственно, король, Гури́г VII, а также Орден Семилистника, Благостный и Единственный. Впрочем, это всё произошло более чем за сто лет до описываемых событий.
Зато, помимо Очевидной есть ещё и Истинная магия. В которой большинство героев вполне сведущи и более уважают. А ещё есть... А фиг его знает, что там ещё есть.
Макс попал в Ехо, то есть в Мир Стержня, из Мира Паука. По указанию Джу́ффина Ха́лли он сел на трамвай на улице, где трамваи отродясь не ходили. Это самая первая версия появления сэра Макса в Ехо. Есть ещё и другие. Более мистичные, прагматичные и циничные. Но все они вполне истинны. Ибо иначе и быть не может.
Трамвай между мирами
Мир Паука — это наш с вами мир, дорогие читатели. Паука, потому что мы, как пауки, любим оплетать окружающую действительность «паутиной», привязывая к себе вещи и людей. И привязываясь к ним сами.
А ещё в нашем мире дофига Вершителей. Вершитель — это существо, чьи желания исполняются всегда, рано или поздно, так или иначе.
Вы никогда за собой не ощущали, что вы — Вершитель? А не замечали Вершителей вокруг? Не забывайте про «рано или поздно, так или иначе», что повышает пикантность ситуации. Так что Вершители — вовсе не счастливые или удачливые люди. А теперь представьте, что Вершителей набралось миллиарды. Как в нашем мире. Бардак вполне ожидаем, не так ли?
А вот в Мире Стержня последним Вершителем был легендарный король Мёнин. И очень понадобился ещё один. И им стал сэр Макс. От короля Мёнина сэр Макс натерпелся немало шуточек (или серьёзностей) сомнительной доброты. Даже как-то таскал в собственной груди его меч, дада, воткнутый насквозь.
Кроме Мира Стержня и Мира Паука существует бесчисленное множество других миров. И одно из призваний практикующего Истинную магию (а также почти всех призраков) — путешествовать между мирами. Или по Тёмной стороне Мира. Или по Изнанке Тёмной стороны. Или по Сновидениям. Настоящий колдун всегда найдёт, где поразвлечься.
А некоторые, особо сильные колдуны, вполне могут создавать новые миры, или разрушать целые миры. Сэр Макс, кстати, между делом создал несколько миров в самом начале своих приключений. За что потом понадобилось нести ответственность.
Итак.
Игрушечный Макс
Сэр Макс. Ночное Лицо Почтеннейшего Начальника Малого Тайного Сыскного Войска города Ехо. Сидит по ночам в кабинете сэра Джуффина, на всякий случай, замещает. Смерть на королевской службе, носит Мантию Смерти. Ибо может плеваться ядом. Владыка Фангахра, властитель народа Хенха из Пустых Земель. Оказался в центре политических игр, в результате которых Пустые Земли присоединились к Соединённому Королевству. Вершитель. Его Смертный Шар не убивает, а подчиняет. Человек Тёмной Стороны. Сновидец. Может творить множество самых удивительных чудес, но сам не понимает, как это у него получается. Балагур, душа компании.
Джуффин
Сэр Джу́ффин Ха́лли. Почтеннейший Начальник Малого Тайного Сыскного Войска города Ехо. В прошлом Кеттари́йский Охотник, Кеттари́ец, Чи́ффа, известный и опытный наёмный убийца. Старый опытный колдун, специализирующийся на Истинной магии, и радеющий о судьбе Мира. Человек Тёмной Стороны. Сновидец. Основал Тайный Сыск, дабы творить всякие магические непотребства, когда всем остальным это запрещено Кодексом Хре́мбера. Начальник и учитель сэра Макса.
Мелифаро
Сэр Мелифа́ро. Дневное Лицо Почтеннейшего Начальника Малого Тайного Сыскного Войска города Ехо, то есть заместитель сэра Джуффина в дневное время. Страж (границы Тёмной стороны). Лучший сыщик, применяет дедуктивные и прочие методы, как и положено детективу. Сын автора «Энциклопедии Мира» сэра Ма́нги Мелифа́ро. Не имеет имени, ибо, когда он родился, отец как раз путешествовал, собирал материал для экциклопедии, и не нашёл времени выбрать сыну хоть какое-нибудь имя. Модник. Балагур. Постоянный оппонент сэра Макса в колкостях и остротах.
Шурф Лонли-Локли
Сэр Шу́рф Ло́нли-Ло́кли. Мастер Пресекающий Ненужные Жизни, штатный убийца Тайного Сыска. Владеет Перчатками Смерти, ими, собственно, и убивает, когда нужно. Человек Тёмной Стороны. В прошлом Безумный Рыбник, известный исторический персонаж конца Эпохи Орденов. Состоял в Ордене Дырявой Чаши, в результате имеет привилегию пить из дырявой посуды, и это у него прекрасно получается. В силу необходимости вынужден быть Истиной, точно соблюдать правила и предписания и требовать подобное от других. Невозмутимый педант. Любитель и знаток поэзии и литературы. Лучший друг сэра Макса, вместе они попадали в совершенно странные истории и неоднократно спасали жизнь друг другу.
Меламори
Леди Меламо́ри Бли́мм. Мастер Преследования Затаившихся и Бегущих. Встаёт на их след, и никуда они не денутся, леди притащит их в Тайный Сыск, как бы они ни сопротивлялись. Первая (и долгое время единственная) леди Тайного Сыска. Первая и последняя девушка сэра Макса в Ехо. Уезжала на далёкий и таинственный материк Арваро́х. Вернулась весьма необычным для уроженки Угула́нда способом: превратилась в буривуха и прилетела. Что свидетельствует о недюженном таланте к фиг знает какому виду магии.
Кофа Йох
Сэр Ко́фа Йо́х. Мастер Слышащий. Или Кушающий-Слушающий, по едкому замечанию сэра Мелифаро. Меняет внешность (кардинально, магическим образом) и шляется по трактирам Ехо, узнавая все свежие слухи. Старый и опытный колдун, специалист в Очевидной магии. На Тёмную сторону попасть не может. Снов не видит. В прошлом — Генерал Полиции Правого Берега. Гонялся в те времена за Кеттарийским Охотником, пару раз даже почти поймал. Сын Хумхи Йоха, одного из семи легендарных «Отцов Основателей» Ордена Семилистника.
Сэр Нуммино́рих Ку́та. Штатный Нюхач Тайного Сыска. Очень чутко различает запахи. По запаху может сказать, где бывал и чем питался человек последние несколько дней. Талантливый сновидец. Ученик сэра Макса.
Теххи
Леди Теххи Шекк. Девушка сэра Макса, между леди Меламори и леди Меламори. Дочка Лойсо Пондохвы, в результате — не вполне человек, а скорее сильное наваждение. Зеркало, подстраивается под собеседника, чем вызывает неодолимую симпатию.
Лойсо Пондохва
Ло́йсо Пондо́хва. Основатель и Великий Магистр Ордена Водяной Вороны. Один из величайших колдунов своего времени. Тяготеет к разрушениям, в результате стал героем многочисленных поговорок вроде: «Сам Лойсо Пондохва ногу сломает». Его именем пугают маленьких детей. Стёр с лица земли родной город сэра Джуффина Халли Кетта́ри. Был пойман Джуффином Халли (ещё Кеттарийским Охотником), при содействии Шурфа Лонли-Локли (ещё немного Безумным Рыбником). Был освобождён сэром Максом. Был спасён сэром Максом от жажды разрушений. Друг и тайный собеседник сэра Макса.
Ну вы поняли. Куча новых слов, географических названий, странных имён. С фиг поймёшь какими ударениями. Много магии. Разной магии. Странной магии. Много разных чудес, разной степени чудесатости. А главное, почти на каждой странице найдётся настолько милая фразочка, что уже устаёшь дёргать их на цитаты.
Пока писал всё это, глянул, о чём там «Сновидения Ехо». Да, это продолжение. Сэр Макс снова возвращается в Ехо.
Как будто прочитать как будто волшебным несуществующим увидительным пережитые написанные изданные как будто о настоящем волшебном многозначном удивительном как будто радостные и полезные узнать и полюбить.
Мелифаро и Шурф