Предыстория

В соревнованиях по городскому ориентированию типа «Бегущего города» на каждом этапе нужно сначала разгадать как можно больше загадок, а затем быстро добраться до отгаданного и без ошибок занести все ответы в маршрутный лист. В промежутке между напряжённой работой мысли и выходом на трассу также нужно выполнить скучные механические действия, построив примерный маршрут перемещения от точки к точке. Традиционный способ в духе старой школы — распечатать или купить огромную бумажную карту, выбрать порядок посещения точек, не вызывающий эстетического отторжения, и, закусив язык, старательно нарисовать точечки и стрелочки. Рисование я не люблю с детства, поэтому быстро задумался, как заменить старую школу с полуоптимальными эвристиками и ручным трудом на новую, со строгими алгоритмами.

(На самом деле, от того, как быстро и качественно будет построен маршрут, результат соревнований зависит примерно никак, но программиста, увидевшего возможность автоматизировать рутинную работу, остановить практически невозможно.)

Перед очередным БГ я упёрся и набросал прототип-бутерброд из консольной утилиты-«бекенда» на Rust и кривого-косого «фронтенда» на JS. Фронтендом можно было накидать точек на карту. В бекенд можно было загрузить OpenStreetMap-карту и построить по точкам из фронтенда оптимальный маршрут. Обмен данными между фронтендом и бекендом происходил через прошедшую испытание временем технологию копирования в текстовый файлик, потому что лень и сжатые сроки.

Прототип в полевых условиях проявил себя отлично, и я решил, что настало время превратить его в GUI-приложение, которое надо непременно написать под Макось на Swift. GUI — потому что хотелось максимальной простоты использования и заодно быстроты реакции интерфейса (контраст между скоростью карт в браузере и, например, приложения Apple Maps меня до сих пор не перестаёт удивлять). Макось — потому что я пользуюсь преимущественно ей. А Swift — потому что я что, дурак, что ли, на Objective C писать.

Получилось как-то так:

Вдохновлённый успехом прототипа, в начале работ над GUI-версией я надеялся получить что-то полезное за несколько дней. Ну максимум за пару недель.

«Чего там копаться, кинул контрол с картой на форму, дальше — дело техники», — думал я.

«Хрен тебе, а не дело техники», — думали в корпорации Apple.

Календарного времени в итоге ушло два месяца с небольшим. Естественно, далеко не всё это время я что-то программировал, но всё равно, некоторое несоответствие между ожидаемым и реальным сроком налицо.

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

Хорошее: сам Swift

Восторженные отзывы про Swift я слышал и раньше, но много значения им не придавал. Оказалось, что язык для своей области применения действительно неплох.

  1. Автоматический вывод типов есть. Нулевых указателей/ссылок нету, есть система опциональных типов с синтаксическим сахаром сверху (if let value = maybeValue и прочее). Переменные по умолчанию рекомендуется делать неизменяемыми; компилятор возмущается, если видит не по делу навешенный квалификатор изменяемости. В общем, минимальные для текущего десятилетия приличия соблюдены.

  2. Есть традиционное ООП с наследованием (чтобы не пугать людей), но есть и протоколы — примерный аналог typeclasses в Haskell или traits в Scala. Есть алгебраические типы данных с паттерн-матчингом, почему-то замаскированные под enum’ы (видимо, опять чтобы люди не пугались).

  3. Интересный подход к обработке ошибок: в целях предотвращения испуга людей есть знакомые ключевые слова try, throw и catch, но исключений при этом нет. На гитхабе выложена внушительная статья, поясняющая, почему так. Даже если Swift вам неинтересен, статью всё равно рекомендую как хороший обзор способов работы с ошибками в современном мире.

  4. Прекрасная поддержка Юникода, ни в одном другом языке такого не видел.

  5. Не тормозной. Производительность меня волновала в последнюю очередь, но приятно, что построение маршрута безо всяких оптимизаций (я даже ни разу профайлер не запускал) работает с вполне приемлемой скоростью.

Из замеченных граблей:

  1. Компилятор сыроват ищщо. Порой сегфолтится: Да-да

  2. Странные дырки в достаточно строгой системе типов. Попробуйте, например, угадать, чем будет отличаться результат выполнения варианта А от варианта Б в таком простом кусочке кода. Да-да, именно так.

Но в целом баланс определённо положительный. Писать на языке приятно.

Так себе: экосистема

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

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

Скажем, для сериализации/десериализации объектов на диск отлично подошёл Realm. Нашёл я его как раз в переломный момент, когда уже почти готов был разбить ноутбук об стену, читая восхитительную документацию по официально рекомендованному средству от Apple (Core Data).

Для HTTP-запросов есть хорошая библиотека Alamofire, устраняющая боль от официального NSURLSession. Для работы с JSON – SwiftyJSON, у которой прямо в начале документации написано, какую боль она устраняет.

Для многопоточности нет ничего, поэтому приходится пользоваться встроенным GCD (это не то, что вы подумали, а Grand Central Dispatch). Выглядит это как портянка из вызовов dispatch_async, выстроенных в форме «мы засунули коллбек в твой коллбек, чтобы ты мог передать коллбек из коллбека». Жить как-то можно, но, чёрт возьми, мы в 2016 году или где.

Все эти поиски лучшей жизни, помноженные на путаницу с пакетными менеджерами (неофициальных есть два, официальный есть один, но пока не работает), конечно, немного выматывают, но суммарно на них ушло не так много времени. Львиную долю нервов и усилий я потратил совсем в другом месте.

Ужасное: GUI

Я твёрдо пообещал себе, что матерных слов в этой заметке не будет, так что полную гамму эмоций, возникавших у меня в процессе разработки графического интерфейса под операционную систему Mac OS X с использованием фреймворка Cocoa, к сожалению, передать не получится.

Процесс работает так.

Сначала надо с помощью XCode мышкой натаскать нужные контролы на форму и упорядочить их неважно как, всё равно вид контролов в XCode будет иметь мало общего с тем, что увидит пользователь. Затем случайным блужданием по графу возможных конфигураций контролов на форме необходимо за несколько десятков итераций найти вариант, не сильно расходящийся со своим видением идеального интерфейса.

(В новых версиях Cocoa появился инновационный механизм constraints, который обещает упростить этот этап. Нужно описать свой GUI системой линейных уравнений, после чего специальный решатель на этапе исполнения автоматически расположит контролы нужным образом. На практике механизм мне запомнился в основном грустным сообщением «не знаю, как это решать, попробуйте что-нибудь ненужное удалить, пожалуйста?»)

Затем надо мышкой же перетащить контролы в редактор, чтобы в коде создались нужные обработчики событий и привязки к контролам. Иногда без объяснения причин обработчики создаваться отказываются, что вносит ещё больше веселья в цирк.

Когда мучительный процесс подгонки завершается, контролы расположены как надо, и грезится, что победа не за горами, вот-вот можно будет писать содержательный код, обязательно всплывает Непреодолимое Препятствие. Например, нельзя поставить на карте точку по двойному клику. Совсем-совсем нельзя. Отнаследоваться от класса карты, переопределив поведение, тоже не получится. Нет, и контекстное меню по правой кнопке тоже нельзя.

В зависимости от упёртости, на преодоление Непреодолимого Препятствия может уйти от дня до бесконечности. Когда ты, наконец, доходишь до мысли установить в своём приложении глобальный перехватчик кликов мышки и через хитрые преобразования координат всё-таки выставляешь точку по двойному клику… нет, обещал же без мата.

На эти приключения дополнительно наслаивается полная закрытость исходников. В сложных случаях отладиться можно только методом научного тыка, и никак иначе. Когда ты привык, что в любой момент можешь посмотреть, как реализованы все компоненты стека вплоть до ядра ОС, постоянные удары головой об очередной чёрный ящик вызывают особую фрустрацию. К концу разработки я поймал себя на том, что совершенно серьёзно записываю себе в блокнотик что-то в духе «осталось сделать поиск пути с помощью A* и решатель TSP (заложить 2…3 часа), а потом добавить на карту кнопку „проложить маршрут“ (заложить пару дней)».

Кнопку я в итоге так и не добавил.

Резюме

Конечный результат меня устраивает, но к программированию GUI на Cocoa хочется не прикасаться больше никогда. В следущий раз, если зачем-то захочется GUI, надо брать что-нибудь вроде QT.