четверг, 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
    }
}

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