Почему стоит изучить Clojure?


    Что такое хороший язык программирования? Какими качествами и характеристиками он должен обладать? Ответ дать сложно. Вот одно из возможных определений: хороший ЯП должен хорошо решать возложенные на него задачи. Ведь ЯП — лишь инструмент в руках программиста. А инструмент обязан помогать нам в работе. В конце концов, это же и есть причина его создания. Разные ЯП стараются решать разные проблемы (с переменным успехом). Цель, которая ставилась при проектировании Clojure — сделать написанные нами программы простыми. И, как следствие, ускорить их создание, тестирование. А главное, уменьшить время на их понимание, изменение и сопровождение.


    Clojure rocks?


    Предупрежу сразу — в статье не будет кусочков кода, демонстрирующих крутизну Clojure. Не будет фраз, подобных «в языке X это заняло 5 строчек а в Clojure всего 4». Это же отвратительный критерий для качества языка! В конце концов, мне совершенно все равно, смогу ли я записать qsort в 2 строчки, или мне придется напрячь пальцы на целых 5 — в реальной жизни я буду использовать библиотечную функцию!

    Лямбдами сейчас никого не удивишь, они есть везде (ну почти, хотя обычно к 8й версии они появляются везде). Обработка коллекций (в том числе параллельная), списковые выражения, разнообразные синтаксический сахар — этого сейчас хватает во многих языках. По правде говоря, я просто обожаю такие статьи. Но подобные сравнения совершенно не годятся для сравнения качества языков! Это как измерять скорость ЯП по тому, насколько быстро программа выводит «Hello, world!». Ну, если только мы не измеряем скорость HQ9+. Если подумать, то подобные детали не столь уж и важны для больших систем. По мере роста проекта нас все меньше и меньше волнует, используем ли мы скобочки или отступы, инфиксную или префиксную запись. Лишняя строчка при нахождении суммы массива уже перестает всех заботить — на первое место выходят проблемы иного рода.

    Сложность


    Системы, которые мы создаем, по своей природе изменчивы. Было бы очень хорошо, если бы требования не изменялись. Просто замечательно, если бы в самом начале разработки можно было предусмотреть все ситуации наперед. Увы, в реальной жизни нам постоянно приходится доделывать, переделывать, улучшать, переписывать, заменять, оптимизировать… Самое неприятное — со временем сложность системы только растет. Постоянно, непрерывно. В начале разработки все просто и прозрачно, любое изменение делается быстро, никаких «костылей». Красота. Со временем ситуация перестает быть столь радужной и веселой. Даже малейшая правка кода потенциально может повлечь за собой лавинообразные изменения поведения системы. Приходится тщательно изучать, анализировать код, пытаться предугадать побочные эффекты от каждого изменения. Именно так, со временем мы буквально не можем досконально проанализировать все возможные последствия от наших изменений.

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

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

    Просто и легко


    Но «просто» (simple) вовсе не означает «легко» (easy). Это разные понятия. На эту тему Рич Хики (автор Clojure) даже сделал известный доклад Simple Made Easy. На хабре опубликован перевод слайдов. Простота — понятие объективное. Это отсутствие сложности (complexity), отсутствие переплетения, спутанности, малое количество связей. С другой стороны, «легко» весьма субъективно. Легко ли управлять велосипедом? Выиграть партию в шахматы? Говорить на немецком? Я не знаю немецкого, но ведь это не повод говорить «этот язык не нужен, он слишком сложный». Он сложен для меня, да и то только потому, что я его банально не знаю.

    Мы все привыкли, что вызов функции записывается как f(x, y). Нам привычно программировать в рамках ООП. Это обыденно. Но на самом деле легкое не обязательно просто. Мы лишь привыкаем к сложности некоторых вещей, начинаем ее игнорировать, воспринимать как данность. Пример функции:

    (defn select
      "Returns a set of the elements for which pred is true"
      {:added "1.0"}
      [pred xset]
        (reduce (fn [s k] (if (pred k) s (disj s k)))
                xset xset))
    


    Выглядит очень… странно! Надо потратить некоторое время на изучение языка, освоение его концепций, чтобы он стал легким. Но простота (или сложность) постоянна. Если мы хорошо изучим инструмент, то количество внутренних зависимостей все равно не изменится. Он не станет сложнее или проще, хотя будет для нас легче.

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

    Побочные эффекты


    Каковы источники сложности в наших программах? Один из них — побочные эффекты. Полностью обойтись без них нельзя, но мы можем их локализовать. И язык должен нам в этом помогать.

    Clojure — язык функциональный, он стимулирует нас писать чистые функции. Результат таких функций зависит лишь от входных параметров. Не надо ломать голову «хм, а что будет, если перед вызовом этой функции я запущу вот эту». Никаких если, есть входные данные, есть выходные. Сколько бы раз мы не запустили функцию — ее результат будет один и тот же. Это предельно упрощает тестирование. Не нужно перебирать различные порядки вызова или воссоздавать (симулировать) правильное внешнее состояние.

    Чистые функции проще анализировать, с ними буквально можно «поиграться», посмотреть как они ведут себя на живых данных. Проще отлаживать код. Мы всегда можем воспроизвести проблему с чистой функцией — достаточно передать ей на вход параметры, вызывающие ошибку, ведь результат функции не зависит от того, что выполнялось ранее. Чистые функции предельно просты, даже если выполняют большую работу.

    Разумеется, Clojure поддерживает функции высших порядков, их композицию.

    ((juxt dec inc) 1)            ; => [0 2]
    ((comp str *) 1 2 3)          ; => "6"
    (map (partial * 10) [1 2 3])  ; => [10 20 30]
    (map (comp inc inc) [1 2 3])  ; => [3 4 5]
    


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

    Но они (грязные функции) не обладают состоянием. Они лишь служат средством взаимодействия с внешним миром. Как мы увидим далее, Clojure отделяет состояние нашей программы при помощи опосредованных ссылок.

    Иммутабельность


    Все структуры данных в Clojure иммутабельны. Нет способа изменить элемент вектора. Все что мы можем — создать новый вектор, у которого будет изменен один элемент. Очень важный момент в том, что Clojure сохраняет алгоритмическую сложность (по времени и памяти) для всех стандартных операций над коллекциями. Ну почти, вместо O(1) для векторов мы имеем O(lg32(N)). На практике, для даже коллекций из миллионов элементов lg32(N) не превышает 5.

    Достигается подобная сложность благодаря использованию персистентных коллекций. Идея в том, что при «изменении» структуры старая версия и новая разделяют большую часть внутренних данных. При этом старая версия остается полностью рабочей. Более того, мы имеем доступ ко всем версиям структуры. Это важный момент. Конечно, ненужные версии будут собраны сборщиком мусора.

    (def a [1 2 3 4 5 6 7 8])
    ; a -> [1 2 3 4 5 6 7 8]
    (def b (assoc a 3 999))
    ;b -> [1 2 3 999 5 6 7 8]
    


    Из коробки Clojure поддерживает односвязные списки, вектора, хеш-таблицы, красно-черные деревья. Есть реализация персистентной очереди (для стека можно использовать список или вектор). И все иммутабельно. Для повышения производительности можно создавать собственные типы-записи.

    (defrecord Color [red green blue])
    (def a (Color. 0.5 0.6 0.7)
    ; a => {:red 0.5, :green 0.6, :blue 0.7}
    


    Тут мы объявляем структуру с 3 полями. Компилятор Clojure создаст объект с 5 полями (2 «лишних»). Одно поле для метаданных, в нашем случае это будет null. 3 поля для собственно данных. И еще одно поле — для дополнительных ключей. Даже если для повышения скорости в нашей программе мы объявляем структуру с явным перечислением полей, то Clojure все равно оставляет нам возможность добавлять дополнительные значения.

    (defrecord Color [red green blue])
    (def b (assoc a :alpha 0.1))
    ; b => {:alpha 0.1, :red 0.5 :green 0.6, :blue 0.7} 
    


    И да, для структур данных в Clojure есть специальный синтаксис:

    ; Вектор
    [1 2 3]
    ; Хеш-таблица
    {:x 1, :y 2}
    ; Множество
    #{"a" "b" "c"}
    


    Состояние


    Итак, у нас есть чистые функции, они определяют бизнес-логику нашего приложения. Есть грязные функции, служащие для взаимодействия в внешними системами (сокеты, БД, web-сервер). И есть внутреннее состояние нашей системы, которое в Clojure хранится в виде опосредованных ссылок (references).

    Есть 4 вида стандартных ссылок:
    • var — аналог thread-local переменных, служат для задания контекстных данных: текущее соединение с БД, текущий HTTP-запрос, параметры точности для математических выражений и подобное;
    • atomатомарная ячейка, позволяет обновлять состояние синхронно, но не координированно;
    • agent — легковесный аналог для actor (хотя, в некотором смысле они являются антиподами, об этом ниже), служат для асинхронной работы с состоянием;
    • ref — ячейки транзакционной памяти, предоставляет синхронную и координированную работу с состоянием.


    Все глобальные переменные хранятся в var (включая функции). Поэтому их можно переопределять «локально».

    (def ^:dynamic *a* 1)
    (println a) ; => 1
    (binding [a 42] (println a)) ; => 42
    


    Тут мы указали компилятору, что переменная a должна быть динамической, т.е. хранится внутри ThreadLocal. Использование ThreadLocal несколько уменьшает производительность, поэтому не применяется для всех var-ячеек по умолчанию. Но, если понадобится, то любую var-ячейку можно сделать динамической уже после создания (что часто используется в тестах).

    В тестах можно подменять целые функции.

    ; тут происходит работа с БД, сокетами и т.п.
    (defn some-function-with-side-effect [x] ...)
    
    ; а эту функцию мы хотим протестировать
    (defn another-function [x] ...)  
    
    (deftest just-a-test
      ...
        (binding [some-function-with-side-effect (fn [x] ...)]   ; вешаем mock-функцию
          (another-function 123))
      ...)
    


    Все ссылки в Clojure поддерживают операцию deref (получить значение). Для var-ячеек это выглядит так:

    ; создаем ячейку #'a
    (def a 123)
    (println a)            ; => 123
    (println #'a)          ; => #'user/a
    (println (deref #'a))  ; => 123
    


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

    (let [x (atom 0)]
      (println @x)   ; => 0
      (swap! x inc)  ; CAS-операция
      (println @x))  ; => 1
    


    Функция swap! принимает атом и «мутирующую» функцию. Последняя принимает текущее значение атома, и должна вернуть новое. Тут очень кстати оказываются персистентные структуры данных. Например, мы можем хранить в атоме вектор из миллиона элементов, но «мутирующая» функция будет выполнятся достаточно быстро для CAS (мы помним, что сложность операций над персистентными коллекциями такая же, как и у обычных, мутабельных). Или мы можем обновить пару полей у хеш-таблицы:

    (def user (atom {:login "theuser" :email "theuser@example.com"}))
    (swap! account assoc :phone "12345")
    ; эквивалентно такому коду
    (swap! account (fn [x] (assoc x :phone "12345")))
    


    Важно, чтобы функция была чистой, поскольку она может выполнится несколько раз. Мы не можем (не должны!) писать что-то вроде:

    (swap! x (fn [x] (insert-new-record-to-db x) (inc x)))
    


    Агенты


    Агенты служат для поддержки состояния, которое непосредственно связано с побочными эффектами. Идея проста. У нас есть ячейка, к ней «прикреплена» очередь из функций. Функции поочередно применяются к значению, которое хранится в этой ячейке, результат функции становится новым значением. Все вычисляется асинхронно в отдельном пуле потоков.

    (def a (agent 0))  ; начальное значение
    
    (send a inc)
    (println @a)  ; => 1
    
    (send a (fn [x] (Thread/sleep 100) (inc x)))
    (println @a)  ; => 1
    
    ; спустя 100 мс
    (println @a)  ; => 2
    


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

    (def a (agent 0))
    (def b (agent 0))
    
    (send a (fn [x]
                   (send b inc)  ; посылаем сообщение в b
                   (throw (Exception. "Error"))))
    
    (println @b)
    ; -> 0, сообщение так и не дошло
    


    Напрашивается некая аналогия с моделью акторов. Они схожи, но есть принципиальные отличия. Состояние агентов явно, в любой момент времени можно вызывать deref и получить значение агента. Это противоречит идее акторов, где мы можем узнать состояние только опосредованно, путем посылки и приема сообщений. В случае с акторами мы даже не можем быть уверены, что опросив его состояние мы «случайно» не изменим его. Агент абсолютно надежен в этом смысле — его состояние можно поменять только функциями send и send-off (которые между собой отличаются лишь тред-пулом, в котором будет обрабатываться наше сообщение).

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

    Акторы пытаются разделить состояние нашей программы на небольшие части, которые легче разнести или изолировать. Операции обновления и чтения состояния сводятся к посылке сообщений. Иногда это крайне полезно (например, при выполнении erlang-программы на нескольких узлах). Но чаще этого не требуется. Иногда даже наоборот. Так, в агентах удобно хранить большие объемы информации, которые нужно шарить между потоками: кеши, сессии, промежуточные результаты математических вычислений и т.п.

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

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

    STM


    Программная транзакционная память — одна из ключевых фишек Clojure. Реализована посредством MVCC. И сразу пример:

    (def account1 (ref 100)
    (def account2 (ref 0))
    
    (dosync 
      (alter account1 - 30)
      (alter account2 + 30))
    


    Мы увеличиваем одно значение и синхронно уменьшаем другое. Если что-то пойдет не так (исключение), то вся транзакция будет отменена:

    (println @account1)  ; => 70
    (println @account2)  ; => 30
    
    (dosync
      (alter account1 * 0)
      (alter account2 / 0))  ;  => ArithmeticException
    
    ; значения не изменились
    (println @account1)  ; => 70
    (println @account2)  ; => 30
    


    Очень похоже на привычный ACID, но только без Durability. При входе в транзакцию все ссылки словно замораживаются, их значения фиксируются на время всей транзакции. Если при чтении/записи ссылки обнаруживается, что она уже поменяла свое значение (другая транзакция завершилась и подпортила нам жизнь), то происходит перезапуск текущей транзакции. Поэтому внутри транзакции не должно быть побочных эффектов (ввод-вывод, работа с атомами). И тут как нельзя кстати оказываются агенты.

    (def a (ref 0))
    (def b (ref 0))
    (def out-agent (agent nil))
    
    (dosync
      (println "transaction") 
      (alter a inc)  ; может привести к рестарту транзакции
      (let [a-value @a
            b-value @b]
        (send-off out-agent (fn [_] (println "a" a-value "b" b-value))))
      (alter b dec)) ; также может привести к рестарту
    


    Все сообщения для агентов придерживаются вплоть то того момента, когда транзакция будет завершена. В нашем примере изменение ссылок a и b может повлечь рестарт транзакции, слово «transaction» может быть напечатано несколько раз. Но код внутри агента будет выполнен ровно один раз, и уже после того, как транзакция завершится.

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

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

    При конкурентном доступе транзакции-читатели не блокируют друг друга, примерно как при использовании ReadWriteLock. Более того, транзакции-писатели не блокируют читателей! Даже если в текущий момент выполняется транзакция, которая изменяет ссылку, мы можем получить значение без блокировки.

    Агенты и STM-ссылки дополняют друг друга. Первые не подходят для координированного изменения состояния, вторые не позволяют работать с побочными эффектами. Но их совместное использование делает наши программы прозрачнее и проще (менее запутанными), нежели при использовании «классических» средств (мьютексы, семафоры и подобное).

    Метапрограммирование


    Сейчас у многих языков есть те или иные средства метапрограммирования. Это AspectJ для Java, AST-трансформации для Groovy, декораторы и метаклассы для Python, различная рефлексия.

    Clojure, как представитель семейства Lisp, для этих целей использует макросы. С их помощью мы можем программировать (расширять) язык средствами самого языка. Макрос — «обыкновенная» функция, с тем лишь различием, что выполняется во время компиляции программы. На вход макроса передается еще не скомпилированный код, результат выполнения макроса — новый код, который компилятор уже компилирует.

    (defmacro unless [pred a b]
      `(if (not ~pred)
        ~a
        ~b))
    
    (unless (> 1 10)
      (println "1 > 10. ok")
      (println "1 < 10. wat"))
    


    Мы создали собственную управляющую конструкцию (инверсный вариант if). Все что для этого нужно сделать — написать функцию!

    Макросы используются в Clojure весьма широко. Кстати, многие встроенные в язык операторы на самом деле являются макросами. Например, вот реализация or:

    (defmacro or
      ([] nil)
      ([x] x)
      ([x & next]
          `(let [or# ~x]
             (if or# or# (or ~@next)))))
    


    Даже defn всего лишь макрос, разворачивающийся в def и fn. Кстати, деструктуризация тоже реализована при помощи макросов.

    (let [[a b] [1 2]]
      (+ a b))
    
    ; развернется во что-то вроде...
    
    (let* [vec__123 [1 2]
           a (clojure.core/nth vec__123 0 nil)
           b (clojure.core/nth vec__123 1 nil)] 
      (+ a b))
    


    Недавно в Java появился try-with-resources. При этом 7ю версию Java мы ждали всего-то несколько лет. Для Clojure достаточно написать всего несколько строчек:

    (defmacro with-open [[v r] & body]
      `(let [~r ~v]
        (try
          ~@body
          (finally
            (.close ~v)))))
    


    В других языка ситуация получше, но все равно далека от идеальной. Важно не наличие той или иной конструкции в языке, а возможность добавить свою. Поэтому неудивительно, что, скажем, паттерн-матчинг для Clojure реализован в виде отдельной подключаемой библиотеки. Просто нету необходимости включать подобные вещи в ядро языка, гораздо целесообразнее реализовывать их в виде макроса. Аналогичная ситуация с поддержкой монад, логическим програмированием, продвинутой обработкой ошибок и другими расширениями языка. Есть даже опциональная статическая типизация!

    Нельзя не упомянуть и про удобство создания DSL. Для Clojure их создано очень много. Это и генерация HTML, и роутинг HTTP-запросов, и работа с реляционными базами данных, и работа с бинарными протоколами, и валидация данных… Создавать их просто и эффективно (хотя в этом деле нужно знать меру).

    Clojure (как и все Lisp-подобные языки) обладает очень важной особенностью — он гомоиконен. Другими словами, нету надобности в отдельном представлении для исходного кода программы, не нужно создавать лишние уровни абстракции в виде некоего дополнительного AST-дерева, программа и есть это дерево. Причем это дерево не из каких-то специальных структур, это обычные списки, векторы и символы. И мы можем работать с нашей программой точно также, как и с обычными данными.

    (defn do2 [x]
      (list 'do x x))
    
    (do2 '(println 1)) 
    ; => '(do (println 1) (println 1))
    ;   что эквивалентно
    ; => (list 'do (list 'println 1) (list 'println 1))
    


    При всей своей мощи макросы в Clojure не ухудшают читаемость программы (если, конечно, использовать их в меру). Ведь макрос всего лишь функция, а мы всегда можем однозначно определить, какая функция используется в текущем контексте. Например, если мы видим код (dosomething [a b] c), то легко узнать, что же скрывается за именем dosomething, достаточно лишь взглянуть в начало файла (где происходит импорт других модулей). Если это макрос — то его семантика постоянна и известна. Нам не нужны продвинутые IDE, чтобы разобраться в таком коде. Хотя, конечно, продвинутые среды разработки умеют «развернуть» макрос на месте, позволяя посмотреть, во что превратит нашу программу компилятор.

    Полиморфизм


    Для создания полиморфных функций у Clojure есть 2 механизма. Изначально язык поддерживал только мультиметоды — мощное средство, но чаще всего избыточное. Начиная с версии 1.2 (а на данный момент актуальна версия 1.5.1) в язык добавили новую концепцию — протоколы.

    Протоколы похожи на Java-интерфейсы, но не могут наследовать друг друга. В каждом протоколе описывается набор функций.

    (defprotocol IShowable
      (show [this]))
    ; ...
    (map show [1 2 3])
    


    Этим мы объявляем 2 сущности — собственно протокол, а также функцию show. Это обычная Clojure-функция, которая при своем вызове ищет наиболее подходящую реализацию на основе типа первого аргумента. Отдельно мы объявляем нужные структуры данных, и указываем для них реализацию протокола.

    (defrecord Color [red green blue]
      IShowable
      (show [this] 
        (str "<R" (:red this) " G" (:green this) " B" (:blue this))))
    


    Можно реализовать протокол для стороннего типа (даже встроенного).

    (extend-protocol IShowable
    
      String
      (show [this] (str "string " this))
    
      clojure.lang.IPersistentVector
      (show [this] (str "vector " this))
    
      Object
      (show [this] "WAT"))
    
    (show "123")    ; => "string 123"
    (show [1 2 3])  ; => "vector [1 2 3]"
    (show '(1 2 3)) ; => "WAT"
    


    Можно добавлять реализацию протоколов к уже существующим типам, даже если у нас нету доступа к исходным кодам. Тут не происходит никаких магических манипуляций с байткодом или подобных трюков. Clojure создает глобальную таблицу тип -> функция реализации, при вызове метода протокола происходит поиск в этой таблице по типу первого аргумента с учетом иерархии. Таким образом, декларация новой реализации для протокола сводится к обновления глобальной таблицы.

    Но иногда протоколов бывает недостаточно. Например, для двойной диспетчеризации. В этом (и не только) случаях нам пригодятся мультиметоды. При объявлении мультиметода мы указываем специальную побочную функцию-диспатчер. Диспатчер получает те же аргументы, что и мультиметод. Поиск конечной реализации происходит уже по значению, которое вернул диспатчер. Это может быть тип, ключевое слово или вектор. В случае вектора происходит поиск наиболее подходящей реализации по нескольким значениями.

    (defmulti convert
      (fn [obj target-type] [(class obj) target-type]))
    
    (defmethod convert [String Integer] [x _] (Integer/parseInt x))
    (defmethod convert [String Long] [x _] (Long/parseLong x))
    (defmethod convert [Object String] [x _] (.toString x))
    (defmethod convert [java.util.List String] [x _] (str "V" (vec x)))
    
    (convert "123" Integer)   ; -> 123
    (convert "123" Long)      ; -> 123
    (convert 123 String)      ; -> "123"
    (convert [1 2 3] String)  ; -> "V[1 2 3]"
    


    Тут мы объявили абстрактную функцию, реализация которой выбирается на основе типа первого аргумента и значения второго (это должен быть класс). Конечно, Clojure учитывает иерархию типов при поиске подходящей реализации. Использовать типы удобно, но их иерархия строго фиксирована. Зато мы можем создавать собственные ad-hoc иерархии из ключевых слов.

    ; задаем связи "ребенок-родитель"
    (derive ::rect ::shape)
    (derive ::square ::rect)
    (derive ::circle ::shape)
    (derive ::triangle ::shape)
    
    (defmulti perimeter :type)  
    ; тут мы сократили код за счет того, что  :type ~ (fn [x] (:type x))
    
    (defmethod perimeter ::rect [x] (* 2 (+ (:h x) (:w x))))
    (defmethod perimeter ::triangle [x] (reduce + ((juxt :a :b :c) x)))
    (defmethod perimeter ::circle [x] (* 2 Math/PI (:r x)))
    
    (perimeter {:type ::rect, :h 10, :w 3})           ; -> 26
    (perimeter {:type ::square, :h 10, :w 10})        ; -> 40
    (perimeter {:type ::triangle, :a 3, :b 4, :c 5})  ; -> 12
    (perimeter {:type ::shape})                       ; -> throws IllegalArgumentException
    


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

    (derive java.util.Map ::collection)
    (derive java.util.Collection ::collection)
    (derive ::tag .java.lang.Iterable) ; -> ClassCastException
    


    Мы можем «унаследовать» тип от кейворда (но не наоборот). Это удобно для создания открытых для расширения групп классов.

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

    Здравый смысл


    Язык ничего не значит без инфраструктуры. Без сообщества, набора библиотек, фреймворков, различного рода утилит. Одна из сильных сторон Clojure — использование платформы JVM. Интеграции с Java (в обоих направлениях) крайне проста. Ни для кого не секрет, что существует просто громандное количество библиотек для Java (не будем обсуждать их качество). Их все можно напрямую использовать из Clojure. Хотя и количество нативных библиотек достаточно велико (и постоянно растет).

    Активно развиваются плагины для Eclipse и IDEA. Для сборки проектов уже давно стандартом де-факто стала утилита leiningen, используемая всем сообществом. Имеются разнообразные фреймворки, как для создания WEB приложений, так и асинхронных серверов

    Разработан сервер приложений Immutant (обертка для JBoss AS7). Immutant предоставляет интерфейсы для работы с Ring (HTTP стек для Clojure), асинхронными сообщениями, распределенным кешированием, выполнению задач по расписанию, распределенным транзакциям, кластеризации и прочим вещам. При этом развертывать и настраивать Immutant крайне просто.

    У Clojure есть и альтернативные реализации, например порт под .Net CLR. Но, по правде говоря, больше всего внимания заслуживает ClojureScript, порт для JavaScript. Конечно, там нет средств многопоточности, и, как следствие, транзакционной памяти и агентов. Но все остальные средства языка доступны, включая персистентные структуры, макросы, протоколы и мультиметоды. А интеграция между ClojureScript и JavaScript настолько же хороша и проста, как между Clojure и Java (а местами даже лучше).

    Что дальше?


    А дальше все просто. У нас есть инструмент. Рабочий, надежный. Не серебрянная пуля, но достаточно универсальный. Простой. Да, возможно, придется потратить некоторое время для его освоения. Многое может показаться непривычным и странным. Но это лишь дело привычки, быстро понимаешь — вся красота языка в его органичности, тонкой стыковке отдельных элементов в единое целое.

    Познакомиться с Clojure стоит. Однозначно. Даже если этот инструмент не подойдет Вам по той или иной причине, то идеи, которые в него заложены, окажутся весьма полезными.
    Поделиться публикацией
    Комментарии 55
      +5
      Хорошая статья, спасибо.

      Я бы ещё добавил, что в прямом соответствии с идеологией «Simple made easy» в Clojure просто потрясающе сделан интерфейс взаимодействия с коллекциями данных и вообще со структурами. Все коллекции обрабатываются единообразно за счёт абстракции «последовательности». Для доступа к элементам последовательностей есть много функций, обычных и высшего порядка, которые позволяют получить желаемый результат вне зависимости от того, работают ли они со списком, вектором или множеством (а иногда и словарём). Близкая к этому тема — ленивые коллекции, которые используются повсюду в языке. Они тоже являются «последовательностями», поэтому работа с ними не отличается от работы, например, с вектором, но при этом у них ленивая семантика.

      Вообще стандартная библиотека Clojure великолепна. Особенно если сравнить её с библиотекой, например, Common Lisp. Единообразие, логичность и принцип наименьшего удивления во все поля, при этом мощность не страдает.
        0
        По поводу коллекций — такое чувство, что вы описываете C# LINQ. Это я не разжигаю, а просто хочу заметить, что подобные вещи далеко уже не в новинку.
          +2
          Отчасти соглашусь. Удобная работа с коллекциями есть во многих языках. С другой стороны, последовательности в Clojure иммутабельны, в отличии от итераторов. Вполне законно сделать что-то вроде:

          (use 'clojure.java.io)
          (with-open [rdr (reader "/tmp/test.txt")]
            (let [lines (line-seq rdr)]
              (println "Строк" (count lines))
              (println "Символов" (reduce + (map count lines)))
              (println "Слов" (count (mapcat #(re-seq #"\w+" %) lines)))))
          


          Тут последовательность одновременно иммутабельная и ленивая (считывание происходит когда мы считаем количество слов). Мы можем не боятся передавать такой «итератор» в другие функции, или даже помещать его в глобальные переменные. Ну и последовательности могут быть бесконечными + иммутабельными.

          (defn- make-fib
            ([] (concat [0 1] (make-fib 0M 1M)))
            ([a b] (lazy-seq (cons (+ a b) (make-fib b (+ a b))))))
          
          (def fibs (make-fib))
          
          ; считаем 500е число
          (time (println (nth 500) fibs))
          
          ; числа уже просчитаны, и хранятся в виде односвязного списка
          (time (println (nth 500) fibs))
          


            0
            В C# бесконечные коллекции можно реализовать и все операции LINQ, которые могут быть ленивыми, таковыми являются. Неизменяемость коллекции при foreach-итерации есть, неизменяемость элементов коллекции не гарантируется.

            Замечу также, что на LINQ сильно повлиял Haskell, поэтому красота, простота и удобство функциональных языков там сохранены настолько, насколько это было возможно.
              0
              Согласен, LINQ мощная и полезная штука. И тут Clojure собственно мало отличается.

              С другой стороны, в этом плане Clojure отличается от того же Haskell. Там map принимает список. Но для Haskell это совершенно не проблема ввиду тотальной ленивости. А вот, скажем, для других Lisp-ов уже вызывает некоторые неудобства. Там cons-ячейка строго определенная мутабельная структура. Для Clojure это любой объект, который реализует Java-интерфейс с 4 методами.

              Коллекции, последовательности и функции для работы с ними в Clojure абстрактны. И реализуются они через Java-интерфейсы. И не стоит забывать, что коллекции персистентные, этого в LINQ нету.
                –1
                > Коллекции, последовательности и функции для работы с ними в Clojure абстрактны. И реализуются они через Java-интерфейсы.

                Ну в LINQ то же самое — если класс реализует IEnumerable или IEnumerable, то с ним можно работать с помощью LINQ или вызывая LINQ-методы напрямую. Хотя может я вас не так понимаю.
                  +2
                  Я не противопоставляю LINQ и Clojure, наоборот. В Clojure если объект реализует ISeq или Seqable, то с ним можно работать при помощи стандартных функций. Подход одинаков. Но он отличается, например, от CL, Schema, Erlang и других.

            +8
            Конечно, не в новинку. Lisp, 1958 год.
          +5
          Очень интересная статья, давно уже испытываю интерес к лиспоподобным языкам, хочется что то попробовать в этой области. Но меня всегда тормозит такая штука, что не совсем понятно, куда его применять. Интересно было бы почитать, как Closure полезен в хозяйстве простого программиста.
            +8
            Clojure — язык общего назначения, применять его можно куда угодно. На нём можно писать и полноценные вебсайты (причём очень удобно и просто, гораздо проще чем, например, с использованием Java-фреймворков), и довольно сложные вычисления (я писал на нём несколько не самых простых лаб по вычислительной математике, пришлось, правда, использовать много низкоуровневой магии чтобы была достаточная производительность, но ничего чересчур сложного там нет).

            Интересный факт: в Clojure нет огромного стандартного стека для веб-приложений, такого, как, например, JSF+EJB3+сервлеты в джаве (хотя никто не мешает писать сервлеты на Clojure — я пробовал, и это гораздо лучше, чем на джаве). Хоть в джаве и есть много фреймворков для создания веб-приложений, но все они так или иначе основаны на JavaEE (за очень редкими исключениями), и все они достаточно объёмны и тяжеловесны и тащат огромное множество зависимостей вплоть до ещё более объёмных серверов приложений. Вместо этого в Clojure есть несколько слабо связанных слоёв, представленных одной или несколькими наиболее используемыми библиотеками, которые можно комбинировать в любом порядке. При этом каждая библиотека по отдельности минимальна по размеру и очень проста в использовании. Типичная структура веб-приложения на Clojure — Ring как HTTP-платформа (то есть слой общения с сетью, как правило, с использованием Jetty) + Compojure для роутинга запросов и в качестве основы для написания middleware + sqlkorma для абстракции работы с БД. По вкусу — один из множества шаблонизаторов (Enlive, Hiccup и другие). Простейшее standalone-приложение состоит из 2 файлов — project.clj для сборки Leiningen'ом и один файл с кодом обработчика HTTP-запросов, всё вместе — не более сотни строк. При этом общую структуру даже при развитии проекта практически невозможно испортить — пространства имён в качестве единицы инкапсуляции дают очень большую гибкость в построении и развитии архитектуры.

            Также я вообще не видел, где можно так же удобно писать GUI-программы, как на Clojure, если только вы не испытываете отвращения к Swing — с помощью потрясающей библиотеки Seesaw. Seesaw предоставляет декларативное описание интерфейсов плюс очень здорово спроектированый набор функций и протоколов для работы практически со всеми функциями Swing, а также много всяких дополнительных мелочей, вроде базовой реализации FRP (Functional Reactive Programming) для интерфейсов — декларативное описание потоков данных в интерфейсе и реакций на события. Я думал, что на Swing невозможно писать GUI без боли, а оказывается, что на Swing можно писать GUI с большим удовольствием)
              0
              Спасибо за развернутый ответ, теперь понятно, куда копать
              • НЛО прилетело и опубликовало эту надпись здесь
              +2
              Спасибо за статью.

              > вся красота языка в его органичности, тонкой стыковке отдельных элементов в единое целое.
              Полностью согласен. Это — прямое отражение гибкости и силы ума создателя языка. Поэтому мне так нравятся языки из семейства лиспов.
              Кстати, интересно узнать, какие ещё есть языки, созданные под влиянием идеи единообразия, общей органичности и гармонии в средствах. Говорят, такими же приятными выглядят Ruby и Erlang. Так ли это? Есть ли что-то подобно простое и поэтому красивое, при этом используемое в реальной работе?
                +1
                Например есть Scala: тоже JVM-ориентированный, функциональный но более прагматичный. Используется в реальной работе как минимум в Twitter и LinkedIn.
                  +1
                  Наслышан о Скале как о реинкарнации C++. С++ вряд ли можно назвать гармоничным, красивым и тем более простым языком.
                    0
                    Интересное мнение, впервые о нем слышу :)
                      0
                      Я пытался перейти на Scala раза три (как до изучения Clojure, так и после). И да, меня не покидало чувство «это же C++ для Java». В смысле не набор конкретных возможностей, а подход в целом, большое набор фич, общая сложность и подобное.
                        +1
                        Была одна статья, где его сравнили с С++, но это не так. Во-первых все же С++ язык более низкого уровня, во-вторых Scala — мультипарадигмальный функциональный язык. Ну и в-третьх Scala OOP отличается от Javaвского, а соответственно и С++шного.
                          0
                          > Наслышан о Скале как о реинкарнации C++

                          Звучит как «не читал, но осуждаю....»
                          Ну и больше слушайте сарафанное радио. Там еще и не такое расскажут…
                            0
                            Вы заметили, что Скале оценок я не давал, однако Вы мне приписали негативное мнение об этом языке? Слышал об отношении к ней от профессиональных разработчиков, которые посвятили ФП не год и не два и успешно разрабатывают на функциональных языках проекты мирового уровня. Если мнения таких людей для Вас не ценнее сарафанного радио, то Вас я точно слушать в этом вопросе не буду :)
                              0
                              >которые посвятили ФП не год и не два

                              Вот в этом и проблема. Scala не ФП язык. Вот поэтому многие и ошибаются на его счет.
                              И ваше высказывание выглядит примерно так:
                              «Слышал о новом методе удаления аппендицита от крутейшего стоматолога, и он считает что это не надо».
                              Немного передергиваю конечно, но это чтобы явно показать суть. Т.е. вроде как оба врачи, но занимаются разными вещами и стоматологу реально сложно оценить такие новшества, хотя он и крутой спец в своей области.

                              Scala очень удачно смешивает ООП и ФП.
                              Чистые функциональщики плюются, ибо это не по феншую и претит им.
                              ООП-щики плюются, потому как сложно и непривычно.
                              И только те, кто смог понять что это и как это использовать, нарадоваться не могут новым возможностям.
                                0
                                > Scala очень удачно смешивает ООП и ФП.
                                Вот об этом мне и говорили, поэтому и интересуюсь. С++ в своё время «удачно» смешивал процедурный С с ООП. В любом случае, Скалой не пользовался и осуждать не буду.
                          0
                          Моя любимая фишка Scala — статическая типизация с достаточно умным type inferrence, позволяющем не засорять код лишними указаниями типов.
                          Clojure, на сколько я помню, типизирован динамически?
                            0
                            Да, типизация динамическая. Хотя при работе с Java-классами опционально можно указывать типы. Поддерживается примитивный вывод типов.
                            (let [s (.toString [1 2 3])]
                              (println (.length s)))  ; тут компилятор знает, что s - строка
                            
                            (def x "123")
                            (println (.length ^String x))  ; явно указываем тип
                            

                        –4
                        выглядит не лучше brainfuck
                          +2
                          Придумать что-то лаконичнее и красивее Brainfuck сложно.
                          Хотя лично мне не нравится его синтаксис, я бы заменил "." на "!", а "," на "?". Но это так, придирки.

                          С другой стороны, писать на Brainfuck не очень удобно, правда, попробуйте.
                            0
                            Не туда ответил
                          –8
                          Не знаю, не знаю. Не нашёл пока причин, чтобы изучить Clojure.
                            0
                            Язык интересный. Я бы его изучил лишь бы для того что бы в очередной раз поломать себе голову. Очень многое из того что вы рассказали уже есть в таком прекрасном языке как Scala (и при этом в довольно приятном синтаксисе).

                            Еще вопрос об interopobillity. Это конечно здорово что язык JVM-ный и что можно использовать многие JVM библиотеки, но согласитесь, лучше всегда иметь idiomatic обертку, а не пулять null-и направо и налево. А поскольку язык, по понятным причинам, не на широкого пользователя, то такие обертки придется писать Вам самому, на каждый чих.

                            На последок, хотелось бы добавить, что я не начал бы серьезный проект ни на Clojure, ни даже на Scala (да будь Clojure/scala хоть в 1000 раз «круче» Java), потому что разработчиков найти очень сложно. Не каждый может себе позволить финты ушами как например Twitter.
                              0
                              Про Smalltalk и C++ тоже так говорили. Разработчиков мало, да и процедурный код не идиоматичный, библиотеки новые…
                                +2
                                Еще вопрос об interopobillity. Это конечно здорово что язык JVM-ный и что можно использовать многие JVM библиотеки, но согласитесь, лучше всегда иметь idiomatic обертку, а не пулять null-и направо и налево. А поскольку язык, по понятным причинам, не на широкого пользователя, то такие обертки придется писать Вам самому, на каждый чих.

                                Обёртки на каждый чих придётся писать только если у вас либо много легаси-кода на джаве, с которым необходимо взаимодействовать, либо если у вас в проекте используется очень много java-библиотек, к которым никто ещё не написал обёртки. Не могу сказать, что это случай большинства проектов. Как правило, в проектах используются либо pure Clojure-библиотеки (та же Compojure или, например, Hiccup), либо уже готовые и обычно достаточно качественные обёртки (например, clj-time для Joda Time или Ring для Jetty).
                                При этом, если действительно будет нужно, писать обёртки для библиотек в Clojure не то что не сложно, а очень просто. Во-первых, это всё же не взаимодействие на бинарном уровне, как между компилируемыми языками, а в рамках одной платформы. Во-вторых, интероп в Clojure очень хорошо продуман. Прямо из Clojure можно генерировать обычные классы практически любой сложности, а те ограничения, что есть, абсолютно несущественны для обеспечения интерфейса к библиотекам. Можно сказать, что Clojure предоставляет настолько широкие возможности для взаимодействия с объектно-ориентированной средой Java, насколько это вообще возможно для не-ОО языка. В самых распространённых случаях создание класса, реализующего какой-нибудь интерфейс для сторонней библиотеки выглядит так, как реализация протокола в примере кода в посте.
                                Также если сама библиотека на Java сделана хорошо, то к ней и обёртки не особо могут быть нужны. Вызов методов синтаксически почти не отличается от вызовов функций, поэтому можно использовать непосредственно саму библиотеку, без обёрток.
                                • НЛО прилетело и опубликовало эту надпись здесь
                                  +4
                                  Как то одиноким осенним вечером решил познакомиться с этим языком и сделал что то невозможное ) За неделю написал веб приложение на 1000+ строк кода. Производительность была очень высокой, фичи выходили одна за другой, все казалось просто и ясно. Причем я писал не только на Clojure, но заюзал еще и ClojureScript. Сам язык я кажется понял всего за день, по крайней мере базовые его конструкции, настолько они мне показались органичными и естественными.
                                  Я открыл для себя много вещей, о которых даже как то и не подозревал. Предыдущий мой опыт был связан в основном с Ruby/Php.
                                  Все бы было хорошо, если не одно но, — пока мало проектов на языке и соответственно трудно найти работу.
                                  Очень рекомендую ознакомиться.
                                    0
                                    Тут мы явно указали компилятору, что переменная a должна быть динамической, т.е. хранится внутри ThreadLocal. Это увеличивает производительность, поскольку в реальности нам не нужно переопределять большинство ссылок, а ThreadLocal накладывает дополнительные расходы.

                                    Немного не понял. Так все-таки размещение во ThreadLocal увеличивает производительность или несет («накладывает») доп. расходы?
                                      0
                                      Изначально в Clojure все var-перменные хранились в ThreadLocal. Но в версии 1.3 сделали оптимизацию — теперь нужно явно указывать, хотим ли мы такое поведение, или нет. Добавили даже «защиту от дурака» — если забыть добавить ^:dynamic для переменной с именем *some-var-name* (т.е. со звездочками), то Clojure печатает в терминал предупреждение.

                                        0
                                        Там все-таки лучше поправить на «Это УМЕНЬШАЕТ производительность, поскольку в реальности»
                                        0
                                        Не туда ответил.
                                        0
                                        В разделе что дальше не хватает ссылочек на хорошие книжки/ресурсы для изучение сабжа. А то как в анекдоте получается: хочу! хочу!
                                          0
                                          Для введения советую alexott.net/ru/clojure/clojure-intro/index.html (спасибо alexott за статью) и java.ociweb.com/mark/clojure/article.html

                                          При изучении полезными будут ресурсы:
                                          www.clojure.org/ — официальный сайт, документация есть, но, пожалуй, лучше учить не по ней.
                                          learn-clojure.com/ — много различных ссылок, в том числе на книги.
                                          clojuredocs.org/ — неофициальная документация по стандартной библиотеке, очень полезная вещь при поиске конкретной функции.

                                          Очень советую www.4clojure.com/ — сайт с набором задачек (от тривиальнейших, до вполне объемных). Гораздо интереснее знакомится с языком на практике. Хотя там нету задач на многопоточность и т.п.
                                          • НЛО прилетело и опубликовало эту надпись здесь
                                              0
                                              Уточнение, относительно недавно они обновили документацию на clojure.org (для Clojure 1.5), и теперь она опять актуальна.
                                                0
                                                Я советовал учить не по документации не потому что она устарела. На мой взгляд, лучше начать с туториалов или книг.
                                            0
                                            Еще из сильных сторон Clojure — активное сообщество умных людей, интересующихся инновациями.

                                            Например: LightTable, Twitter Storm.
                                              0
                                              Кто там говорил, что Perl трудночитаем?))
                                                0
                                                А вот это Вы зря. Чужой код на Clojure читается весьма хорошо.
                                                Зависит от автора и его стиля (всякое бывает), но в целом читаемость у языка на очень хорошем уровне.
                                                –6
                                                Мдааа. Я бы назвал этот язык не Clojure, а BrainFuck++ или BrainFuck#. Во времена пышного расцвета паскаля и бейсика этот язык смотрелся бы вполне органично. Сейчас этот язык смотрится, как сказал ТС, СТРАННО. Без реального углубления в него очень сложно понять, есть ли у него изюминка, но при современном выборе лично я даже не стану в него углубляться. Для каких то задач в вебе проще воспользоваться Perl/PHP, куски проекта на C++/Asm, что то на Java/JavaScript/Flash. Для программирования под локальные приложения — C++/Asm/Delpfi Очень сложно сдвинуть в первую очередь себя ради изучения языка, если не понимаешь, почему для проекта нужно выбрать именно его. Озвученные мной языки имеют огромнейшее сообщество, документацию, примеры исходников и соответствующую поддержку. Этот язык напоминает мне эсперанто и подобные синтетические языки. Язык, который держится на небольшой группке непонятно чем мотивированных людей, имеет мало шансов на жизнь и широкое использование крупными компаниями в своих проектах. Соответственно, время потраченное на его изучение до нужного уровня, скорее всего не будет сконвертировано со временем в достойную зарплату. Зачем учить довольно редкий язык? Ради хобби? Может лучше потратить время на бильярд, боулинг, плавание или велосипед? Талия будет стройней, да и профилактика геморроя в молодом возрасте никому не мешала.
                                                • НЛО прилетело и опубликовало эту надпись здесь
                                                    –2
                                                    А почему Вы считаете, что я не знаком с функциональным программированием? На Erlang-е несколько проектов «для себя» писал. Но лично на мой взгляд, этот язык более «читабельный». Я начинал с паскаля, С, бейсика, ассемблера 86х. После этого пришлось использовать для тех или иных задач много других языков, большей частью интерпретируемых. Не зная подробного синтаксиса этих языков, даже на первый взгляд можно разобрать общий смысл куска кода. Поэтому переход с одного на другой занимает не очень много времени. Программы на вышеупомянутом Erlang-е относительно читабельны для человека, который видит их первый раз. Но Clojure меня совсем не порадовал.

                                                    К вопросу о личной эффективности можно относиться по-разному. Сам специалист может быть очень высокого класса и вполне трезво оценивать свой уровень. Но за него он попросит и больше денег. Руководителю проекта проще держать команду «рабочих лошадок» среднего класса. Они не будут требовать большой зарплаты, обычно они менее «творческие» личности, ввиду чего их результаты работы более предсказуемы. Поэтому при старте проекта адекватный тим-лид вместо 3-х экспертов, пишущих на экзотическом языке, скорее наберет 10 взаимозаменяемых кодеров на классическом С++ или Java. После окончания проекта проще найти людей в тех. поддержку. Иногда очень узкая и маловостребованная квалификация может обернуться работой в престижной компании с баснословной зарплатой. Но кушать нужно каждый день, поэтому более распространенные языки дадут больше шансов быстро найти достойную работу. В случае «затыков» при программировании на экзотическом языке, еще сложнее найти достойную поддержку для решения вопроса. Я сам иногда учил новый для себя язык, т.к. знал, что на нем эту задачу решить проще. Но для меня всегда распространенность языка приоритетней перед его функционалом. Это мое личное ИМХО, которое я никому не навязываю. И которое не претендует на истину.
                                                    +2
                                                    Скажите, а Вы владеете слепым набором? Для его освоения нужно потратить время на обучение, и немало времени. На первых порах скорость будет очень низкая, будет жутко неудобно и непривычно. Зато результат себя окупает сполна! Хотя, если Вы за день набираете не так уж много текста, то потраченное время может окупится очень не скоро (или вообще никогда).

                                                    Примерно схожая ситуация с Clojure. Для некоторых людей (не для всех!) использование Clojure может дать выигрыш в скорости, но только в долговременной песпективе. Т.е. глупо надется, что вот за пару часов освоил азы языка, и бац — стал кодить в 10 раз быстрее.

                                                    А вообще, для разных задач разные инструменты. Так, для веб сайтов (fronend) вполне подойдет и PHP, для автоматизации задач администрирования — Perl/shell. Просто не надо пытаться распространить данные решения на все сферы только потому, что они привычны для Вас.
                                                    –2
                                                    Я довольно быстро печатал двумя пальцами, поэтому переход на слепой метод печати принес незначительный выигрыш. Времени тоже заняло не очень много, примерно неделю. Но сравнивать слепой метод и язык программирования не очень корректно. Слепому методу неплохо бы научить всех пользователей ПК и общий выигрыш будет гарантирован. Это будет приносить пользу ежедневно и гарантированно. А экзотические языки лучше сравнивать с узкой специализацией. У меня 5 или 6 знакомых закончили университет по узконаправленной специальности физики. Один из них уехал то ли в США, то ли в Канаду на высокие заработки. Остальные оказались наравне с джамшутами-разнорабочими. Специальность если и есть, то в этом конкретном городе он нафиг никому не нужен. Нет спроса. Зато есть спрос на PHP, Perl, C++, Asm, 1C, Java и т.д. Один поехал на север слесарем в газодобычу. Другой окончил курсы на бирже труда, сейчас оператор башенного крана. Третий ходил по клиентам устанавливал Консультант+. Про остальных не знаю, дороги разошлись. Время, потраченное на то, что еще неизвестно, пригодится или нет — время украденное у жизни. Пока не будет у языка распространения на уровне флэша или 1С, лично я такой язык изучать не буду. Хотя каждый сам выбирает свои приоритеты и риски.
                                                      0
                                                      Так Вы мир не поменяете :)
                                                        +1
                                                        Я человек семейный, мне важна стабильность и надежность дохода. Борьба с ветряными мельницами типа изменения всего населения не по мне. Хотя в соседнем посте предлагал поставить на дорогах камеры, спарить их с пулеметом и уменьшать количество правонарушителей.
                                                      0
                                                      Пока не будет у языка распространения на уровне флэша или 1С
                                                      У меня для вас плохие новости. И да, я знаю Clojure там занимает >50 место, просто не забываем про java interop.
                                                        0
                                                        Андрей, Ваши статьи — это лучшее, что я читал по Clojure на русском языке. Спасибо Вам огромное за них!
                                                          0
                                                          Спасибо, теперь я знаю что учить дальше.

                                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                          Самое читаемое