понедельник, 14 сентября 2015 г.

Поездка на Пихтовый Гребень

12 сентября в субботу наша группа любителей велопокатушек устроила очередную вылазку на природу. Так как кататься по окрестностям Академгородка нам надоело, то на этот раз мы решили выбраться куда-нибудь подальше и предприняли попытку заехать на одну из высочайших вершин Новосибирской области – Пихтовый Гребень (аж целых 494 метра).

Для читателей разного уровня ленивости я выкладываю два варианта отчёта о поездке: сокращённый и полный.


Сокращённый

Мы не доехали.


Полный

Выехали на машинах мы поздно – в 10 утра, и это было, пожалуй, главной причиной, почему мы в итоге не добрались до Пихтового Гребня.

Где-то в 12:45 мы приехали на территорию горнолыжного комплекса "Юрманка", где полюбовались красотою осеннего вида горы Глухариная, собрали велосипеды и поехали.

Расстояние до Гребня было 32 км. Если ехать со стандартной средней скоростью велосипеда в 15 км/ч, то это приблизительно 2 часа до Гребня и 2 часа обратно. Однако оказалось, что местность и рельеф в окрестностях Пихтового Гребня довольно сильно отличаются от того, к чему мы привыкли, поэтому средняя скорость оказалась не 15 км/ч, а где-то 7 км/ч.

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

Сразу же после брода в нашей группе произошло неприятное происшествие – у Люды спустило колесо. Мы встали на отдых, заменили колесо, и поехали дальше.

Ещё через несколько километров мы опять наткнулись на Ик. Я сразу подумал: зачем какой-то дурак проложил дорогу так, что она два раза пересекает одну и ту же реку.

Не проехав и пятидесяти метров после брода, мы наткнулись на какой-то мелкий приток Ика, не отмеченный на карте:

Мелкий приток оказался коварной речкой – его небольшая ширина создала ложное впечатление, что его можно преодолеть прямо на велосипеде, что и попытался сделать Лёха (предварительно разогнавшись до большой скорости). Итог: брызги и замоченные шорты. Далее Люда, чтобы не нести кроссовки на себе, попыталась их перебросить на другой берег, но неудачно замахнулась и кроссовок улетел в Ик. В этот момент у меня ёкнуло сердце, так как я думал, что кроссовок сейчас уплывёт, и кому-то придётся догонять его вплавь. К счастью, кроссовок попал на кочку, и Лёха героически пошёл в кусты его доставать:

Далее было ещё два брода, много сложных подъёмов и грязи (несмотря на то, что всю неделю до этого стояла тёплая сухая погода).

В 17:00 мы вновь наткнулись на Ик:

На Ике мы встретили толпу квадрациклистов, возвращавшихся с Гребня, которые сказали нам, что видели свежие следы медведя. Тут я окончательно понял, что Гребень нам не светит, и мы развернулись. Пока мы ехали обратно, мы придумывали тактику отбивания от медведя.

В 20:00 мы были в Юрманке.

P.S. Серёга всю дорогу ехал без футболки:

пятница, 21 августа 2015 г.

Про tail call elimination или почему с диалектами Haskell нужно быть осторожным

В последнее время развелось много различных диалектов Haskell: PureScript, Frege, GHCJS, Elm, Haste и т.д.
В этой статье мы рассмотрим, насколько ли хорошо эти диалекты пригодны для функционального программирования. Анализ мы будем проводить с точки зрения наличия в компиляторах этих языков одной важной для функционального программирования оптимизации – устранения хвостовых вызовов (tail call elimination, далее TCE).


Мотивация

TCE – это оптимизация компилятора, которая позволяет избежать "раздувания" стека, когда функции вызывают друг друга и эти вызовы являются хвостовыми. Типичный пример таких хвостовых вызов – это взаимная рекурсия:
isOdd x = if (x == 0) then False else isEven (x - 1)
isEven x = if (x == 0) then True else isOdd (x - 1)
Как видно из кода программы, функция isOdd вызывает функцию isEven в хвостовой позиции, а значит стек-фрейм функции isOdd уже больше не понадобится и он может быть переиспользован для функции isEven. GHC такой код успешно оптимизирует, и функция isOdd может быть вызвана со сколько угодно большим числом.
Приведённый выше пример сильно надуман, но это было сделано исключительно для простоты демонстрации проблемы. На самом деле, хвостовые вызовы в функциональном программировании встречаются чрезвычайно часто (значительно чаще, чем в императивных программах). Например, некоторые конечные автоматы очень удобно реализовывать через взаимно рекурсивные хвостовые функции. Скажем, программу, определяющую является ли список последовательностью чередующихся двух других списков, можно красиво реализовать через конечный автомат:
match :: Eq a => [a] -> [a] -> [a] -> Bool
match list list1 list2 = match1 list where

    match1 [] = True
    match1 list | list1 `isPrefixOf` list = match2 $ drop (length list1) list
    match1 _ = False

    match2 [] = True
    match2 list | list2 `isPrefixOf` list = match1 $ drop (length list2) list
    match2 _ = False
Например, если вызвать match с аргументами [1,1,2,1,1,2] [1,1] [2], то он вернёт True.
Ещё один пример – свёртка списка функций в одну функцию:
composeAll :: [Int -> Int] -> (Int -> Int)
composeAll [] = id
composeAll (x:xs) = composeAll xs . x
Этот пример сворачивает список функций [f1, f2, ... , fn] в одну функцию fn ∘ ... ∘ f2 ∘ f1, т.е. каждая функция вызывает следующую функцию в хвостовой позиции. Для того чтобы конечная функция не упала с переполнением стека на большом списке, компилятор должен поддерживать TCE.
Далее мы рассмотрим, насколько хорошо поддерживают TCE различные диалекты Хаскелля.

PureScript

PureScript – это строгий диалект Хаскелля, транслирующий в JavaScript. Установку PureScript я выполнил по этой инструкции и реализовал вышеописанные примеры.
Пример с isOdd и isEven выглядит следующим образом:
import Prelude
import Data.List
import Control.Monad.Eff.Console

isOdd n = if n == 0 then false else isEven $ n - 1
isEven n = if n == 0 then true else isOdd $ n - 1

main = isOdd 1000000 # show # log
Функции isOdd и isEven транслировались вот в такой JS-код:
var isOdd = function (n) {
    var _0 = n === 0;
    if (_0) {
        return false;
    };
    if (!_0) {
        return isEven(n - 1);
    };
    throw new Error("Failed pattern match: " + [ _0.constructor.name ]);
};
var isEven = function (n) {
    var _1 = n === 0;
    if (_1) {
        return true;
    };
    if (!_1) {
        return isOdd(n - 1);
    };
    throw new Error("Failed pattern match: " + [ _1.constructor.name ]);
};
Как видно, никакой TCE здесь не пахнет. Запуск этой программы через Node.js (который не поддерживает TCE) падает с ошибкой переполнения стека:
var isEven = function (n) {
                      ^
RangeError: Maximum call stack size exceeded
    at isEven (D:\temp\purescript\output\Main\index.js:16:23)
    at isOdd (D:\temp\purescript\output\Main\index.js:12:16)
    at isEven (D:\temp\purescript\output\Main\index.js:22:16)
    at isOdd (D:\temp\purescript\output\Main\index.js:12:16)
    at isEven (D:\temp\purescript\output\Main\index.js:22:16)
    at isOdd (D:\temp\purescript\output\Main\index.js:12:16)
    at isEven (D:\temp\purescript\output\Main\index.js:22:16)
    at isOdd (D:\temp\purescript\output\Main\index.js:12:16)
    at isEven (D:\temp\purescript\output\Main\index.js:22:16)
    at isOdd (D:\temp\purescript\output\Main\index.js:12:16)
* ERROR: Subcommand terminated with error code 1
Второй пример я реализовывать не стал, но зато реализовал третий:
composeAll :: List (Int -> Int) -> (Int -> Int)
composeAll Nil = id
composeAll (Cons x xs) = composeAll xs <<< x

fs :: List (Int -> Int)
fs = replicate 100000 (+1)

main = 100 # composeAll fs # show # log
По идее, этот код должен прибавить единицу к числу 100000 раз, но он валится с ошибкой переполнения стека.

Frege

С Хаскеллем под JVM всё обстоит почти так же плохо, как и с PureScript. JVM, как и Node.js, не поддерживают TCE, а значит TCE ложится на плечи транслятора в Java. Но Frege выполнил эту задачу лишь частично. Из трех примеров без StackOverflowError выполнился только первый. Второй и третий упали приблизительно с такой ошибкой:
java.lang.StackOverflowError
 at frege.runtime.Fun1$1.eval(Fun1.java:63)
 at frege.runtime.Delayed.call(Delayed.java:198)
 at frege.runtime.Delayed.forced(Delayed.java:257)
 at frege.prelude.PreludeMonad$?$_bullet?eb83462e.eval(PreludeMonad.java:4921)
 at frege.prelude.PreludeMonad$?$_bullet?eb83462e.eval(PreludeMonad.java:4915)
 at frege.runtime.Fun2$1.eval(Fun2.java:58)
 at frege.runtime.Fun1$1.eval(Fun1.java:63)
 at frege.runtime.Delayed.call(Delayed.java:198)
 at frege.runtime.Delayed.forced(Delayed.java:257)
 at frege.prelude.PreludeMonad$?$_bullet?eb83462e.eval(PreludeMonad.java:4921)
 at frege.prelude.PreludeMonad$?$_bullet?eb83462e.eval(PreludeMonad.java:4915)
 at frege.runtime.Fun2$1.eval(Fun2.java:58)
 at frege.runtime.Fun1$1.eval(Fun1.java:63)
 at frege.runtime.Delayed.call(Delayed.java:198)
 ...

Elm и Haste

Elm и Haste – ещё два диалекта Haskell, транслирующиеся в JS (первый строгий, второй ленивый). Elm упал с переполнением стека на примере с isOdd и isEven. Haste упал на втором примере со списком из 500 тыс. значений.

GHCJS

Последняя надежда осталась на GHCJS – транслятор Haskell в JS. Особенность это транслятора заключается в том, что он полностью совместим с Хаскеллем, в отличие от всех вышеперечисленных диалектов. Также он поддерживает практически все стандартные модули Haskell. Из минусов этого компилятора – большой размер сгенерированных js-файлов, в которых невозможно разобраться, и медленный старт, а также сложность установки GHCJS (я так и не смог установить GHCJS под Windows, пришлось тестировать на Linux).
Все три примера успешно скомпилировались через GHCJS "как есть". Сгенерированный код я запускал через Node.js, и во всех трёх случаях код отрабатывал без ошибок, какие бы большие числа/списка я не подсовывал.

Итог

Как видно, писать функциональные программы на диалектах Хаскелля нужно с осторожностью, так как в любой момент есть риск напороться на переполнение стека. С первого взгляда, это может показаться незначительной проблемой, но на самом деле это не так. Когда ваша программа станет достаточно большой, таких мест в ней может оказаться достаточно много. И не удивляйтесь потом, почему ваша программа работает на GHC, но не работает на вашем диалекте Хаскелля, падая на больших входных данных.

воскресенье, 16 августа 2015 г.

Должен ли программист знать математику?

Должен ли программист знать математику – это вопрос, на который я пару лет назад ответил бы с колебанием, но сегодня я могу ответить, что да – однозначно должен.

Почему вдруг именно сейчас сформировалось моё мнение? Дело в том, что мне недавно исполнилось 27 лет, а в среде программистов 27 лет (плюс-минус два года) – это такой возраст, когда многие достигают некоторого "потолка" в своей карьере. То есть среднестатистический программист к этому возрасту уже прошёл ступени от начинающего разработчика до опытного разработчика (тимлид/сеньор/архитектор). И тут у них возникает вопрос – что делать дальше? Существует два пути: либо продолжить карьерный рост, став менеджером/начальником/директором, или остаться программистом и пытаться расти дальше профессионально.

И вот тут-то начинает решать знание математики. Программисты, которые не знают математику, расти дальше профессионально не могут. Ну просто они достигли той грани, до которой ещё можно было расти исключительно засчёт экстенсивного расширения своих навыков, увеличивая количество баззвордов в своём резюме, но подняться выше которой нельзя без определённых фундаментальных математических знаний. Например, после 5 лет написания веб-сайтов ты вряд ли сможешь написать свою распределённую отказоустойчивую базу данных, если у тебя нету знаний хотя бы основ матанализа, сколько бы баззвордов ты не знал. Таким образом, тебе рано или поздно надоедает клепать однообразные CRUD'ы, а решать более сложные задачи ты не способен, поэтому ты выбираешь путь менеджера.

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

Что касается меня, то я определённо не собираюсь становиться менеджером. Некоторая математическая база у меня есть, которая сильно пригодилась в моей текущей работе. А дальше посмотрим. Математическая база – это, конечно хорошо, но хочется копать вглубь и развиваться в какой-то конкретной области. Возможно, это будет связано с функциональным программированием.

понедельник, 20 июля 2015 г.

Ковариантность/контравариантность в Java без wildcard'ов

Все вы знаете, что полиморфный тип может быть либо ковариантен, либо контравариантен, либо инвариантен.

Например:

  • Тип Optional<A> ковариантен. То есть, например, всегда можно привести Optional<Integer> к Optional<Number> и это будет безопасно.
  • Тип Consumer<A> контравариантен. Это значит, что всегда можно безопасно привести Consumer<Number> к Consumer<Integer>.
  • Наконец, тип List<A> инвариантен, так как он не является ни ковариантным, ни контравариантным . То есть нельзя безопасно привести не от List<Integer> к List<Number> и не от List<Number> к List<Integer>
Так получилось, что в Java ввели полиморфные типы не сразу, а лишь в пятой версии. Поэтому, когда в то время типы снабжали параметрами, оказалось, что 99% типов оказались инвариантными. Но что делать в тех случаях, когда программист имеет переменную типа List<Integer>, а функция принимает аргумент типа List<Number>? Как привести List<Integer> к List<Number>, если List не является ковариантным? Для этого в Java ввели костыль под названием usage-site variance: каждый раз объявляя переменную, вы решаете, является ли тип этой переменной ковариантным, контравариантным или инвариантным. Например:
  • Если вам нужно только читать из списка, то вы объявляете тип переменной как ковариантный: List<? extends Number>.
  • Если вам нужно только писать в список, то вы объявляете тип переменной как контравариантный: List<? super Number>.
  • Наконец, если вам нужно и читать из списка, и писать в него, то вы пишете просто List<Number>, то есть объявляете тип переменной как инвариантный.
Печально то, что люди используют usage-site variance даже в тех случаях, когда тип является ковариантным или контравариантным, усложняя сигнатуры функций и таская по всему проекту типы с этими дурацкими символами вопроса (wildcard'ами).

В этом посте я предлагаю альтернативу wildcard'ам. Конечно, лучше бы если бы в Java, наконец, ввели declaration site variance на уровне языка, т.е. возможность указать ковариантность/контравариантность типа во время его объявления (Optional<out A> / Consumer<in A>), и я не понимаю, почему это до сих пор не сделали.

Итак, правило следующее. Если вы объявляете ковариантный тип, то вы добавляете метод widen() со следующей сигнатурой:

public class Optional<A> {
  ...

  public static <B, A extends B> Optional<B> widen(Optional<A> opt) {
    return (Optional) opt;
  }
}

Теперь всегда, когда вам нужно привести Optional<Integer> Optional<Number> вы пишите:

Optional<Integer> opt = ...;
Optional<Number> opt2 = Optional.widen(opt);

И всё, никаких wildcard'ов.

Аналогично для контравариантных типов вы добавляете метод narrow():

public interface Consumer<A> {
  ...

  public static <A, B extends A> Consumer<B> narrow(Consumer<A> con) {
    return (Consumer) con;
  }
}

Теперь если нужно привести Consumer<Number> к Consumer<Integer>, вы пишите:

Consumer<Number> con = ...;
Consumer<Integer> con2 = Consumer.narrow(con);
По-моему, такой подход делает код намного чище. Я понимаю, что мы заменили одно уродство на другое, но я думаю, что моё уродство является менее страшным.

четверг, 18 июня 2015 г.

Про дублирование кода

Я не понимаю людей, которые всерьёз оправдывают дублирование кода. Ну не понимаю и всё. Причём, говорят они вовсе не о мелких часто повторяемых конструкциях, а о вполне больших и сложных кусках логики на несколько сотен строк кода!
В свою защиту они приводят следующие аргументы:
  • "Вот когда мне понадобится третий раз скопировать код, тогда и вынесу общую часть в отдельный модуль".
  • "Эти куски кода лежат в разных проектах, поэтому чтобы выделить общую часть нужно создавать общую зависимость".
По-моему, всё это полнейший бред. Единственное логичное объяснение, почему люди не хотят бороться с дублированием кода, – это элементарная лень и непонимание последствий. А последствия дублирования кода часто бывают катастрофическими.
Обычно проявляется это следующим образом: в программе обнаруживается критический баг, причиной которого является допущенная ошибка в этом самом куске кода. Это баг назначают не на автора кода, а на другого члена команды. Этот член команды понятия не имеет, что где-то в другом месте проекта есть точно такой же кусок кода, и соответственно исправляет ошибку только в одном месте, но не исправляет в другом. Итог: баг закрыт, но только для определённых сценариев использования программы, когда вызывается именно этот участок кода. Но другие сценарии, когда вызывается другой, скопированный участок кода, по-прежнему будут работать ошибочно.
Другой вариант развития событий – член команды (который не в теме копипасты кода) оказался более проницательным и всё-таки смог обнаружить, что у этого кода есть дубликат в другой части проекта. Соответственно, он выбирает одно из двух: либо исправляет ошибку в обоих местах (по-сути, оставляет дублирование), либо героически берётся выносить дублирующийся код в общий модуль. Но выносить дублирующийся код – это часто задача не из простых, потому что ты должен отлично понимать логику, которую писал автор, чтобы ничего не поломать. А если логика сложная, то ты потратишь уйму драгоценного времени для её понимания. Хорошо если автор этого кода ещё работает в проекте, и у него можно спросить, что же он там имел в виду. А если он уже давно уволился? Вот если бы автор кода сразу вынес код в общий модуль, то он бы сделал это гораздо быстрее, так как тогда был в теме и держал все детали в голове.
Дублирование кода – это способ выиграть полчаса времени при написании программы засчёт траты в будущем от одного часа до целого рабочего дня. Это может быть приемлемо для какого-нибудь прототипа на Питоне, который вы точно знаете, что он будет гарантированно выброшен, но совершенно неприемлемо для больших и сложных программ, которые будут жить, развиваться и поддерживаться следующие несколько лет.

среда, 17 июня 2015 г.

Методы, принимающие на вход Object

Возможно я буду уже 1000-ным (или 10000-ным) человеком, который это говорит, но я всё же повторю ещё раз.

Методы, у которых есть аргументы типа Object – это полнейший пиздец.

Примеры таких методов:
  • Object.equals(Object)
  • Objects.equals(Object, Object)
  • Collection.contains(Object)
  • Collection.remove(Object)
  • Map.containsKey(Object)
  • Map.containsValue(Object)
Почему это лютейший пиздец, я поясню на примере. Допустим, у вас в программе есть переменная типа Set<Integer> и где-то в коде есть проверка:
Integer x = ...;
if (set.contains(x)) {
  // Do something
}
Теперь в один прекрасный день вам нужно поменять тип множества, скажем, на Set<String>. Вы меняете, и... код компилируется! Но проверка теперь работает некорректно, т.к. вы проверяете, содержит ли ваше множество число, что всегда будет возвращать false. Вы сделали фатальную ошибку в программе, но компилятор вас об этом не предупредил!
Так жить нельзя. Java – это пример статического языка, который не является типобезопасным. Статически типизированный и нетипобезопасный язык – что это если не пиздец?
Есть ли решение у этой проблемы? Ну не знаю, equals(Object) навсегда вшит во все объекты. Так будет в Java 9, 10, 11 и в 20. Поменять сигнатуру метода невозможно, т.к. это нарушит обратную совместимость. Можно использовать typeclass-подход, который используется в functionaljava, но разве кто-нибудь так будет делать?
Остаётся надеяться на IDE и статические анализаторы, хотя это частичное решение, не полное.

вторник, 19 мая 2015 г.

Числа с плавающей точкой не являются ассоциативными

Ассоциативность  это свойство, при котором для любых трёх элементов множества x, y, z выполняется равенство (x + y) + z = x + (y + z). Многие множества удовлетворяют этому свойству:
  • Целые числа (в том числе машинные целые числа) по операции сложения / умножения.
  • Строки по операции конкатенации.
  • Предикаты по операции конъюнкции / дизъюнкции.  
  • ... и многие многие другие.
Однако некоторые множества не являются ассоциативными, например, числа с плавающей точкой (по сложению / умножению).
Это легко проверить. Для этого мы воспользуемся библиотекой functionaljava, в которой есть возможность тестирования свойств (аля Haskell QuickCheck). Тест будет выглядеть следующим образом:
Property prop = Property.property(Arbitrary.arbDouble, Arbitrary.arbDouble, Arbitrary.arbDouble,
        (x, y, z) -> Property.prop((x + y) + z == x + (y + z)));

CheckResult check = prop.check();

System.out.println("Passed: " + check.isPassed());
Если выполнить этот код, то он выведет false. У класса CheckResult также можно спросить, какие именно числа нарушают ассоциативность:
for (List<Arg<?>> args : check.args()) {
    double x = (Double) args.index(0).value();
    double y = (Double) args.index(1).value();
    double z = (Double) args.index(2).value();
    System.out.printf("(%s + %s) + %s = %s%n", x, y, z, (x + y) + z);
    System.out.printf("%s + (%s + %s) = %s%n", x, y, z, x + (y + z));
}
Этот код выведет примерно следующее:
(5.094895128230203 + -7.839963840335238) + -1.8589779099062191 = -4.604046622011254
5.094895128230203 + (-7.839963840335238 + -1.8589779099062191) = -4.6040466220112535
Как видите, при разной расстановке скобок результаты будут разными.

вторник, 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. В чистые функции их не стоит пихать.

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

четверг, 19 марта 2015 г.

Первые впечатления от Haskell

Где-то с полгода я начал подумывать о том, чтобы изучить новый язык программирования. У меня было три кандидата: Haskell, Rust и Clojure. В тот момент я как раз дочитывал книгу FP in Scala, откуда я узнал про монады, функторы и прочие функциональные абстракции, базирующиеся на концепции чистоты. Однако понимание этих абстракций мне казалось неполным без практики их использования в функционально чистом языке. Таким образом, я сделал логичный, по моему мнению, вывод и продолжил изучать ФП на Haskell, лишённого всего этого мусора альтернативных парадигм, который присутствует в Rust и Clojure.

Итак, общее впечатление следующее – Haskell мне определённо нравится. Т.е. если оценивать мой уровень удовлетворённости языком по десятибалльной шкале, то Haskell – это где-то 8.5/10 (Scala – 6/10, Java 8 – 4/10).

Что мне больше всего понравилось Haskell? В первую очередь, конечно же чистота языка. Чистота в обоих смыслах: чистота функций и полное отсутствие ООП-парадигмы. Это очень здорово, что ты можешь посмотреть на сигнатуру функции и понять, может ли она производить побочные эффекты или нет. Это позволяет соединять мелкие функции друг с другом, образуя более крупные, которые будут работать предсказуемым образом. Отсутствие ООП означает, что нет возможности сделать страшные вещи вроде непроверенных приведений из базового класса в подкласс. Да, есть алгебраические типы данных, скажете вы, но количество "наследников" ADT всегда ограничено (data Color = R | G | B) и фиксировано в текущем модуле, так что вы всегда можете проверить, что перебрали все варианты, сопоставляя выражения с образцами в вашей функции.

В целом, Haskell выглядит чрезвычайно продуманным языком. Если написан код, то его сложно интерпретировать двумя способами. Например, нельзя перепутать применение функции и ссылку на функцию, как в Scala, т.к. в Haskell функции являются объектами первого класса. Или, например, нельзя перепутать функцию с типом или классом, т.к. все функции должны начинаться с маленькой буквы, а типы/классы – с большой. Вообще, Haskell задумывался таким образом, чтобы код на нём не получался двусмысленным. В этом он, конечно, уступает LISP, в котором программист пишет сразу разобранное синтаксическое дерево, но среди многих других нелисповых языков Haskell в этом плане сильно впереди. Например, в некоторых языках есть фичи вроде перегрузки функций, функций с аргументами по умолчанию и переменным количеством аргументов. В Haskell ничего этого нету, из-за чего порой код получается немножко длиннее, но зато мы получаем другое ценное свойство – возможность легко читать и размышлять над кодом программы.

Ещё одна важная особенность языка – единообразие всего и вся. Например, в Java у нас есть примитивы, интерфейсы, классы, перечисления, массивы, аннотации, но в Haskell есть только один способ объявить тип – ADT. Вообще ADT – это то, чего мне остро не хватает в ООП-языках, а в Scala выглядит довольно громоздко и порой работает криво (вроде случая, когда у Nil тип Nil, а не List). Или, например, в Scala можно сделать функции каррированными, а можно и не сделать. В Haskell же все функции каррированы, а значит все функции могут быть частично применены.

Синтаксис Haskell очень приятен. Благодаря системе вывода типов Хиндли-Милнера типы выражений можно вообще не указывать, в итоге некоторые выражения выглядят настолько короткими и лаконичными, что могут посостязаться в этом плане даже с динамическими языками. Фигурные скобки, точки с запятой не нужны. В Haskell нету такого обилия круглых скобок, которые есть во многих других языках. Это достигается благодаря тому, что применение функции пишется не посредством указания аргументов в скобках, а посредством перечисления аргументов через пробел. В итоге, частичное применение аргументов работает просто перечислением лишь части аргументов. В Haskell есть строгие правила индентации, которые запрещают писать код с какими-попало отступами. В итоге код становится читабельнее.

Свойство языка, без которого разговор о Haskell был бы бессмысленным – это полиморфизм. Вряд ли будет преувеличением, если я скажу, что Haskell является языком с максимальным количеством переиспользованного кода. Любая хоть сколько-нибудь повторяющаяся функциональность выносится в абстракцию. Если две функции делают похожие вещи, то у них выделяется абстрактная часть и выносится в полиморфную функцию. В типичном коде Haskell большой процент функций является функциями высшего порядка, часто перегруженными. Общие свойства типов выносятся в классы: Eq, Show, Num, Read, Bounded, ... Некоторые типы требуют классов с более сложным видом: (* -> *) -> Constraint вместо * -> Constraint. Среди таких классов монады, функторы, аппликативные функторы. Но и на этом не останавливаются. Монады генерализуются ещё дальше – в стрелки и т.д.

Наконец, стоит упомянуть ещё об одной особенности Haskell, которая отличает его от других языков: ленивости. В Haskell выражения не вычисляются до тех пор, пока не потребуются. В итоге можно делать совершенно обалденные вещи вроде бесконечных списков, рекурсивных типов и бесплатного потокового ввода-вывода. Потоковый I/O – это когда ты не читаешь/пишешь всё сразу, а частями, по мере необходимости. В итоге, программа которая читает содержимое файла и пишет его в другой файл выглядит так, как будто она читает всё сразу, но на самом деле не выделяет при этом в памяти блоки размером с весь файл.

Среди прочих приятных "мелочей" – статическая компиляция, кроссплатформенность, легковесные потоки (bye-bye код с асинхронными callback'ами), потокобезопасность, огромное количество готовых библиотек.

Пожалуй хватит о преимуществах. Теперь поговорим о недостатках. Их не так много, но они есть.

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

Во-вторых – это сложность написания производительного кода. Т.к. в Haskell весь код ленив, то можно запросто достичь такой ситуации, когда в памяти накоплены гигабайты отложенных вычислений, но они не вычисляются, потому что ещё не затребованы. Чтобы такого не происходило, нужно понимать тонкости работы ленивых вычислений.

Наконец, в-третьих – это большое количество расширений компилятора. Для меня как Java программиста вообще такое слово незнакомо. Т.е. Java-код либо компилируется, либо нет, и не существует никаких расширений, которые можно подключить. В Haskell же есть огромное количество расширений, которые нужно помнить. Практически ни одна библиотека не написана исключительно на чистом Haskell – везде используется как минимум одно-два расширения. Некоторые библиотеки используют десятки расширений, и код таких библиотек понять для меня пока нереально.




воскресенье, 8 марта 2015 г.

Почему люди не хотят учиться функциональному программированию?

Для меня любой код на императивном языке уже давно стал синонимом слова "говнокод". Открывая исходники любой библиотеки, скажем, на Java, я вижу говнокод. Да, бывает разной степени качества код, но даже самый-самый лучший код на императивном языке в любом случае плох. Т.е. если ты, читатель, пишешь на императивном языке, то ты говнокодер. Запомни это.
Есть небольшое исключение из этого правила - критичные для производительности места в коде, которые ну никак нельзя написать функционально, чтобы при этом получилось быстро. Но в типичном проекте такого кода, как правило, немного. В остальных случаях нет и не может быть никакого оправдания писать на императивном языке.
Хорошо. Теперь разберёмся, почему люди не хотят учиться функциональному программированию.
Ответ прост. Причина точно такая же, как и со стариками, которые не хотят учиться пользоваться компьютером. Для них это совершенно новый неизвестный прибор с непонятными кнопками, иконками, папками, ярлыками, интернетами. Научиться этим пользоваться - это немало усилий надо приложить. К тому же зачем нужен компьютер, если всё и так можно узнать из телевизора и газет?
Императивные программисты думают точно так же. Учить ФП язык - это ж сколько времени и сил нужно потратить? И, главное, зачем учить эти ваши монады, когда можно всё то же самое написать на Java?
Таким образом, императивные программисты они подобны старикам, не желающим учиться пользоваться компьютером.
Т.е. что мы имеем в итоге: толпа хомячков продолжает писать тонны говнокода и тормозит развитие IT-индустрии из-за своей лени и слабоумия. Тогда у меня возникает вопрос - может, это, вон из профессии, а? Ну или менее радикальный вариант - идите в менеджеры, что ли, раз не хотите учиться.

четверг, 5 марта 2015 г.

Кривая Java

Всё-таки Java - кривой язык. Вот взять пример:

package y;

public class A {
    public final B b = new B();
}

class B {
    public int f() {
        return 0;
    }

}

Т.е. я объявил класс A, в котором есть публичное поле класса B, но B виден только в пакете y.

Теперь если попробовать взять поле b из экземпляра класса A, то начнутся сюрпризы. Такой код успешно компилируется и работает:

package x;

import y.A;

public class Main {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(a.b); // печатает y.B@15db9742
    }

}

Дальше - веселье. Если позвать у b любой метод, то будет ошибка компиляции:

package x;

import y.A;

public class Main {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(a.b.f()); // Ошибка: The type B is not visible
    }

}

Причём даже если позвать метод toString(), который точно есть у всех объектов, то всё равно будет ошибка компиляции!

package x;

import y.A;

public class Main {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(a.b.toString()); // Ошибка: The type B is not visible
    }

}

Но несмотря на это, такой код уже работает:
package x;

import y.A;

public class Main {
    public static void main(String[] args) {
        A a = new A();
        Object b = a.b;
        System.out.println(b.toString()); // печатает y.B@15db9742
    }
}

Короче, пипец, товарищи.

вторник, 3 февраля 2015 г.

Использование gif вместо статических картинок для примеров кода

Пришла в голову идея использовать гифки для демонстрирования примеров кода в блоге:

Человек лучше воспринимает код пошагово, чем весь кусок кода сразу. Что думаете?

понедельник, 2 февраля 2015 г.

Диаграмма языков

Есть четыре парадигмы языков, которые часто путают и смешивают в кучу:
  • Императивная парадигма
  • Декларативная парадигма
  • Фунциональная парадигма
  • Объектно-ориентированная парадигма

Я решил нарисовать простую диаграмму, которая расставляет всё на свои места. Заранее предупреждаю, что это моё личное субъективное видение, которое не претендует на стопроцентную истину:


Таким образом, есть две прямо противоположные непересекающиеся парадигмы: императивная и декларативная. В императивных языках мы описываем решение задачи через последовательность инструкций, которые указывают машине что делать. Декларативная парадигма же не содержит никаких инструкций, а вместо них даёт некоторое описание, которое машина должна интерпретировать.
Пример чисто императивного языка  C, пример чисто декларативного языка  HTML.
Теперь самое интересное: объектно-ориентированная и функциональная парадигмы. Обе они не являются строгим подмножеством императивной и декларативной парадигм соответственно. Да, есть языки, которые объектно-ориентированны и при этом чисто императивны (Java, C++), и есть языки, которые функциональны и чисто декларативны (Agda).
Но в целом, ничто не запрещает функциональному языку быть императивным (Haskell), а объектно-ориентированному языку  декларативным (C# со своим LINQ).
А есть вообще языки, которые сразу поддерживают и императивную, и декларативную, и объектно-ориентированную, и функциональную парадигмы (Scala, Nemerle, OCaml).

воскресенье, 1 февраля 2015 г.

Простенькая задачка для собеседования на Java-программиста

Я вообще ненавижу concurrency. В Java это ад и боль, усложнение понимания кода программы, дедлоки и деградация производительности. Поэтому всеми способами стараюсь её избегать. И тем не менее, это основы, которые нужно понимать. Поэтому предлагаю простую задачку для собеседования на Java-программиста.

Задача: реализовать класс Ref<A>, который представляет собой изменяемую ссылку. У класса должно быть два метода: set и get. set - устанавливает значение ссылки, get - получает значение ссылки или блокируется до тех пор, пока там не появится ненулевое значение.

Решений можно придумать много. На ум приходят synchronized/notify, AtomicReference, блокирующие очереди, примитивы вроде CountDownLatch и т.д. Приведу решение с synchronized/notify как самое натуральное, по моему мнению:

public class Ref<A> {
    private A a;

    public Ref() {
    }

    public synchronized A get() throws InterruptedException {
        while (a == null) {
            wait();
        }

        return a;
    }

    public synchronized void set(A a) {
        this.a = a;
        if (a != null) {
            notifyAll();
        }
    }

}
Ничего сверхсложного, но есть пара нюансов, которые нужно понимать, в частности:
  • Не забыть делать проверку a == null в цикле (правило).
  • Нужно вызывать notifyAll, а не notify (правило).
Если кандидат успешно решает задачу, то можете быть на 90% уверенным, что программист хорош. Спросите у него ещё пару простых вопросов на алгоритмы и смело нанимайте.