вторник, 28 апреля 2015 г.

Про RoboVM

На прошедшем хакатоне я решил попробовать себя в совершенно новой для себя области  разработка под iOS. Эта задача была для меня настоящим вызовом, так как до этого у меня не было ни опыта мобильной разработки, ни опыта разработки в Xcode. И вообще, я даже не являюсь пользователем Mac'а. С другой стороны, задачу сильно облегчило то, что писал я не на Obective-C под Xcode, а на Java+Scala под Eclipse, которые мне хорошо знакомы.

Для программирования под iOS на Java я использовал RoboVM  AOT-компилятор байт-кода в нативный код ARM для создания нативных приложений под iOS. До этого для программирования на Java я использовал исключительно Oracle HotSpot, так что использование AOT-компилятора также являлось для меня новым опытом.

Впечатления RoboVM оставил хорошие. Из того, что я понял  это полноценная Java 7, поддерживающая все её основные возможности кроме динамической загрузки классов, так как динамическая компиляции под iOS запрещена. Hibernate, скорее всего, не получится использовать :).

На сайте RoboVM лежит отличный тьюториал, в котором подробно расписаны шаги для создания своего первого проекта под iOS. Сначала я установил плагин RoboVM для Eclipse и создал RoboVM iOS Project:



После создания проекта я нажал кнопку Run As iOS Simulator (iPad). Первый запуск длился где-то минуты три, так как RoboVM сначала должен был спомпилировать всю стандартную библиотеку Java (robovm-rt.jar) и классы для разработки под iOS (robovm-objc.jar и robovm-cocoatouch.jar): в сумме это около трёх тысяч class-файлов. Стоит оговориться, что сидел я на довольно старом и тормозном Mac mini, поэтому на современном железе процесс занял бы намного меньше.

Немного больше пришлось повозиться с запуском приложения под настоящим iPad'ом. Загвоздка состояла в том, что для запуска под iPad / iPhone нужно быть членом программы iOS Developer, а для этого нужно заплатить корпорации Яблоко 100 долларов. К счастью, в нашей конторе уже был купленный аккаунт, и я просто вбил в Xcode его логин и пароль, после чего запуск из под Eclipse заработал.

Наконец, преодолев все вышеописанные технические шаги, я начал писать код. Процесс разработки оказался простым и приятным. Каждый следующий запуск приложения был уже не таким долгим как первый и длился где-то 20 секунд. В консоли Eclipse можно было смотреть логи System.out, а также stacktrace'ы исключений, но в них отсутствовали номера строк, так как я использовал бесплатную версию RoboVM. Также утверждается, что в платной версии можно использовать отладчик.

Немного смутило полное отсутствие javadoc'ов в классах для работы с iOS, но это компенсировалось нормальными именами классов и методов. Насколько я помню, я даже ни разу не лез в гугл, а просто искал нужные классы, смотрел список их методов и выбирал нужные. Хотя с другой стороны, для своего калькулятора я не использовал ничего кроме простейших контролов, поэтому не возьмусь утверждать, что так же легко будет разрабатывать более сложные приложения с нетривиальной логикой и навороченным дизайном.

Код калькулятора я выложил на github. Финальное приложение выглядит следующим образом (не судите строго):



Для полноты эксперимента я также решил использовать в проекте Scala. В частности, парсер и интерпретатор калькулятора написаны на Scala. Со Scala были мелкие проблемы, которые решились вынесением всего Scala-кода в отдельный проект и jar-файл. RoboVM пришлось компилировать также классы из стандартной библиотеки Scala (а это ещё 3000 классов), в итоге весь финальный бинарный файл распух до 35 MB (ipa-файл  8MB).

Вообще говоря, RoboVM вроде бы не компилирует все-все классы, которые лежат в Build Path. Вместо этого он берёт только те классы, на которые ссылается ваш код, а также все их транзитивные зависимости. Поэтому если вы в проекте где-то используете Class.forName(), то он может кинуть ClassNotFoundException. Этого можно избежать путём явного прописывания имён таких классов в файле robovm.xml. В частности, в моём случае при инициализации Scala-классов где-то происходила попытка загрузки классов, связанных с шифрованием, но их не было в рантайме, и вылетала такая ошибка:
java.lang.SecurityException: META-INF/TYPESAFE.SF has invalid digest for scala/Function2$mcIJI$sp$class.class in /Users/user/Library/Developer/CoreSimulator/Devices/0532E093-3660-4EB8-B8CB-AA5E09677E6F/data/Containers/Bundle/Application/533E98CB-A2E0-40F1-B7AE-DDB9C5838D6A/Calculator.app/lib/org.scala-lang.scala-library_2.11.6.v20150224-172222-092690e7bf.jar
Проблема решилась добавлением следующих классов в robovm.xml:
<forceLinkClasses>
  <pattern>com.android.org.conscrypt.OpenSSLProvider</pattern>
  <pattern>com.android.org.conscrypt.OpenSSLSignature</pattern>
  <pattern>com.android.org.conscrypt.OpenSSLMessageDigestJDK</pattern>
  <pattern>com.android.org.conscrypt.JSSEProvider</pattern>
  <pattern>com.android.org.conscrypt.OpenSSLRSAKeyFactory</pattern>
</forceLinkClasses>

В итоге, своим экспериментом с RoboVM, длившемся два дня, я остался доволен. В процессе работы компилятор вёл себя чрезвычайно стабильно, и я не встретил ни одного бага. Плагин для Eclipse также работал стабильно.

Это вовсе не значит, что я ручаюсь за RoboVM на 100% и рекомендую разрабатывать на нём под iOS. Решайте сами, использовать его или нет. Но как минимум, знайте, что появилась ещё одна альтернатива для написания iOS приложений, которая выглядит на мой взгляд довольно перспективно и привлекательно.

суббота, 25 апреля 2015 г.

Продолжаю осваивать Haskell

Решил ненадолго прервать копание Хаскелля в глубину (это занятие, как вы знаете, может длиться бесконечно) и немножко попрактиковаться в кодировании: написал простой текстовый калькулятор с функциями и переменными. Parsec я использовать не стал, т.к. хотелось написать свой парсер для лучшего понимания. Получилось 200 строк кода – я считаю, что это неплохо для такого объёма функциональности. Вряд ли на каком-нибудь статическом языке получилось бы короче. В частности, таблица всех функций уложилась всего в четыре строчки:
functions :: (Floating a) => [(String, a -> a)]
functions = [("exp", exp), ("sqrt", sqrt), ("log", log), ("sin", sin), ("tan", tan), ("cos", cos),
  ("asin", asin), ("atan", atan), ("acos", acos), ("sinh", sinh), ("tanh", tanh), ("cosh", cosh),
  ("asinh", asinh), ("atanh", atanh), ("acosh", acosh), ("abs", abs)]
Очень красиво, я считаю. Никакого лишнего мусора.

Таблица операторов уложилась в 10 строчек:
divide expr loc l r | (r == 0.0) = throwError CalculatorException "Evaluation error. Division by zero" expr loc
divide _ _ l r = l / r

const2 = const . const

binaryOperators :: (Eq a, Floating a) => [BinaryOperatorInfo a]
binaryOperators = [
  BinaryOperatorInfo Assign '=' 0 undefined,
  BinaryOperatorInfo Plus '+' 1 (const2 (+)),
  BinaryOperatorInfo Minus '-' 1 (const2 (-)),
  BinaryOperatorInfo Multiply '*' 2 (const2 (*)),
  BinaryOperatorInfo Divide '/' 2 divide,
  BinaryOperatorInfo Power '^' 3 (const2 (**))]
Заметьте, в таблице указаны приоритеты операторов, которые учитывает парсер. При добавлении нового оператора код парсера менять не нужно – он универсален.

Ну и таблица констант. 2 строчки:
constants :: (Floating a) => [(String, a)]
constants = [("pi", pi), ("e", exp 1)]

Как видите, я в коде решил попробовать исключения. Впечатление двоякое: с одной стороны, он делает код простым и избавляет от необходимости протаскивать значение наверх по стеку функций, с другой – исключения в чистом языке являются несколько чужеродными. Кроме того, возникли проблемы с ленивостью, точнее непонимания мною ленивых вычислений, в результате чего мой первоначальный код не ловил исключения, несмотря на то что был обёрнут в catch-блок. Проблема решилась заменой функции return на функцию evaluate. В общем, вывод таков – исключения лучше использовать только при работе с IO. В чистые функции их не стоит пихать.

Вопрос читателям: нормально ли оформлен код? Я что-то всё никак не могу понять, как лучше всего вставлять код в посты с красивой подсветкой и рамкой.