понедельник, 7 января 2013 г.

Вкусные лямбды и коллекции в Scala

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

Долгое время эталоном вкусного языка для меня являлся исключительно Ruby. Его динамичность вплоть до добавления новых методов в рантайме позволяет создавать потрясающие DSL и писать всякие человеко-читаемые вещи вроде Time.now + 3.hours или c.format(100).should == '$ 100', не говоря уже о встроенных в язык приятных сладостях вроде однострочных концевых условий (parts.reverse! unless self.postfix?) и возможности опускать ненужные скобки.

Остальные языки, которые я знал, оставались по вкусности далекоо-далеко позади. Конечно, это не было трагедией. Я не однобокий человек и, любя Ruby, спокойно писал диплом и работал в UGENE на C++. Но на C++ я никогда не стал бы писать для души: оскомину набьёт быстро.

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

Но все же в Scala есть такое место, где она может быть лаконичнее и красивее даже чем Ruby! Это цепочки коротких действий (map, filter, foreach) над коллекциями или, в общем случае, итераторами.

Понятно, что сделать это красиво можно только в языке, поддерживающим лямбда-функции, и в стандартной библиотеке которого эти map, filter, foreach являются методами абстрактной итерируемой сущности. В этом Scala и Ruby равны (а Java и C++ тихо курят в стронке). Но если действие не одно, а целая цепочка, то уже становится важно и то, как именно может быть записано каждое из них.

Всё познаётся в сравнении. Далее приведены те самые два вкусных примера на Scala из реального проекта, которые и побудили меня написать это. Ниже — как то же самое выглядело бы на Ruby.

Резюме:
  1. Значительно украшают запись таких конструкций в Scala три вещи:
    • возможность записывать методы в операторной нотации: a + b есть всего лишь сокращённая запись вызова метода +, а именно: a.+(b), но это работает также и с методами map, filter и любым другим методом с одним параметром;
    • параметр лямбды можно не объявлять, используя имя по умолчанию: _;
    • функция — тоже объект: если объявлена функция load, то её можно просто передать в функцию foreach как аргумент, для этого не надо городить лямбду, как в руби.
  2. Кроме синтаксических лакомств, итераторы в Scala по своей функциональности ближе к функциональной парадигме (прошу прощения за каламбур), и на них можно лепить ленивые вычисления любой степени сложности
    • в Ruby это тоже возможно, но только после «пропатчивания» стандартных классов. Стандартные методы хороши только при работе с простыми конечными коллекциями.

P. S. А если кого-то интересуют серьёзные доводы, из-за которых я взялся за изучение Scala, надеясь, что он сможет заменить мне Ruby (понятно, что не в поисках вкусностей), то здесь особенно важно, что он:
  • довольно функциональный (⇒ кое-какие вещи выражаются лаконичнее),
  • но при этом строго-типизированный язык (⇒ меньше ошибок в рантайме),
  • c мощнейшим механизмом выведения типов (⇒ их крайне редко нужно указывать явно),
  • компилируемый в JVM (⇒ проще с переносимостью),
  • с мощным Pattern Matching'ом (⇒ небольшим «свитчем» можно выразить сложную логику)
  • и широчайшими возможностями для того, что обозначается глубоким и непереводимым на русский язык словом Concurrency.

Примеры этого всего можно найти в том самом реальном проекте, упомянутом выше.

А почерпнул я эти факты и получил хороший, годный kick-start из отличных кратких лекций Владимира Парфиненко:



15 комментариев:

  1. Интересно, если Scala такой крутой язык, то зачем Jetbrains сделали Kotlin? Не лучше ли было улучшить интеграцию Scala в IDEA?

    ОтветитьУдалить
    Ответы
    1. Честно говоря, мне самому не понятно до конца. Слышал, что автор Groovy честно признался, что если бы он вовремя узнал про Scala, то даже не начал бы разрабатывать Groovy, и позднее вышел из его коммьюнити. От JetBrains такого вроде не слышно.

      Тут они сами приводят сравнение. Честно говоря, ничего фундаментального не нашёл, хотя интересные моменты есть, вроде декларируемой очень быстрой компиляции, nullable-типов, якобы исключающих NPE, и что-то там с делегацией. Не знаю, не проникся, в общем.

      Может, это всё ещё как-то мотивированно тем, что JetBrains пытается продвигать свой Meta Programming System, а как можно обкатать среду для создания языков лучше, чем запилив новый язык? :)

      Удалить
    2. Насчет скорости компиляции добавлю свои пять копеек: компилятор scalac еще тот тормоз на больших проектах, очень часто возникает желание убить кого-нибудь, пока ждешь.

      Удалить
  2. Кстати, оператор switch - зло, ИМХО, т.к. в большинстве случаев нарушает OCP

    ОтветитьУдалить
    Ответы
    1. Ну, это же мешает избегать этого большинства случаев и пользоваться им в остальных...

      На самом деле, к данному случаю это мало относится. Switch — это простейший частный случай Pattern Matching'а, а так это гораздо больше, чем switch. Просто у меня не получилось это иначе кратко объяснить.

      Лучше всего просто глянуть первую лекцию, начиная с 22 слайда (≈ ¾ полоски времени). В сочетании с Scala-вской фичей "case classes" на одном паттер матчинге можно чуть ли не рекурсивный спуск написать, а сама технология матчинга хорошо расширяема (можно гибко добавлять свои матчеры). В лекции это хорошо описано. И всё это так, ну прям так по-функциональному будет, что душа любого математика должна просто растаять :)

      Единственный минус, когда начинаешь жесть месить на паттерн матчинге, возможна просадка производительности и объёма байт-кода: видел ишью в их трекере, вроде в 2.10 уже пофиксили. И то, это надо постараться извратиться :)

      Удалить
  3. >> и широчайшими возможностями для того, что обозначается глубоким и непереводимым на русский язык словом Concurrency

    Интересно, чего же там такого широкого, чего я не могу сделать в Java?

    ОтветитьУдалить
    Ответы
    1. Всё же послушай на досуге вторую упоминаемую лекцию, с 17-го слайда (что посередине под кнопкой Play). Я не смогу это объяснить лучше Парфа :) Если кратко — там используется принципиально другая концепция: вместо тредов большую часть времени можно оперировать future'ами, promise'ами и actor'ами (в последних пока не шарю), причём в своём представлении всё это очень выигрывает от функциональных возможностей языка.

      Плюс бесплатные параллельные коллекции.

      Также можно посмотреть в моём проекте на этот файл, асинхронная работа с HTTP с помощью чудесной библиотечки Dispatch, в UpdatesController же можно посмотреть, как в итоге обрабатываются эти промисы.

      Удалить
    2. Хм, но это только в версии 2.10 появится, если я правильно понял. Просто я сначала перешел по твоей ссылке про Concurrency: там все в стиле Java описано, поэтому и решил возмутиться :)

      Удалить
    3. В примере про Actor'ов синтаксис в точности такой же как Erlang :) Рад, что не изобрели велосипед, а использовали привычный синтаксис. Вообще, надо будет поизучать все эти языки

      Удалить
    4. А вообще, ждите от меня поста насчет возможности реализации в Java тех же возможностей Concurrency, которые есть в Scala :)

      Удалить
    5. Насчёт 2.10: в самом языке futures, promises и ators они действительно появятся в 2.10, точнее, появились 4 дня назад (а в RC так намного раньше).

      Но мой проект на 2.9.2. Просто концепция этих futures, promises и actors была озвучена давно, и они уже появились в сторонних либах, как-то реализованные их создателями. Так постепенно происходит процесс стандартизации этих вещей, путём вбирания в себя удачных реализаций.

      Насчёт неизобретания велосипеда: заметно, что Scala не просто молодой язык, но вообще вбирающий в себя много всего из тех, что уже были на момент его создания, и это действительно радует. Не только Erlang, но и Ruby с его DSL'ями и RSpec'ом (-> scala-test), и другие оказали на него влияние.

      Удалить
  4. Здарофф, посоны :) Ваня попросил меня комменты оставить. Я почитал ваше обсуждение... Ну, у меня коммент только один, такого, набросного содержания %) Все эти ваши скалы, котлины, груви, руби выглядят такой около-явной тусовочкой (С++ тут упоминать ваще не надо), пытающейся сделать из Java что-то новое. И вот минус всей этой тусовочки - абсолютное несмотрение в сторону .NET, где 2013-й год уже давно на дворе. Там, например, аналоги "map, filter, foreach" не просто методы абстрактной сущности, а просто свободные функции (в терминах С#, ес-но). Или тот же параллелизм. AsyncEnumerator - ХУЙ у вас такое будет где-то в ваших котлинах. Короче, очень рекомендую забурить следующие темы:

    1) LINQ (LINQ to objects, LINQ to SQL, expression tree, PLINQ).
    2) RX - ваще отрыв головы. Все эти ваши map/reduce/foreach - баловство в песочнице по сравнению с этой библиотекой.
    3) AsyncEnumerator (он же wait/async в C# 5).

    ОтветитьУдалить
    Ответы
    1. Посвящается самоотверженным шарпофагам:
      https://www.youtube.com/watch?v=11bVWXH9MCs&feature=player_embedded

      Удалить
    2. Денис, твоя позиция понятна.

      Но мне кажется, что вся эта позиция всё-таки строится на двух основных заблуждениях:

      1) 640K^W^W win32 ought to be enough for anybody. В некоторых ситуациях хочется мультиплатформенности большей, чем традиционная майкрософтская "мультиплатформенность". Только Unity со своей кросс-компиляцией выглядит светлым облачком на этом фоне.

      2) Что 2013-й год наступил только в .NET-тусовочке, а языки-компилируемые-в-JVM === Java == древний-неразвивающийся-отстой. (как в ней оказались Ruby и Python, которые вообще не того поля ягоды и по многим вещам уже очень далеко ушли, непонятно)

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

      Действительно, даже не забуриваясь в то, во что ты предлагаешь забуриться, можно установить (примерное!) соответствие между этим и аналогами в Scala.

      1a) LINQ: SQL-like-syntax имхо вещь не сильно полезная в хозяйстве, а все etension-методы, что лежат в его основе, являются методами trait'а IterableLike, и следовательно, есть везде, где надо (ведь trait - это mixin, по ним возможно множественная имплементация).
      1b) PLINQ: Parallel collections (list.par -> параллельный list)
      2) RX: На первый взгляд, очень многословный и запутанный способ реализовать в точности то же самое, что дают Promise и Future в комплекте с Either считанными символами (как в том же Dispatch)
      3) AsyncEnumerator, await: см. выше.

      Итого "очень очень не хватать" мне будет только:
      1) LINQ to SQL, штука которая мне не когда не нравилась своей концепцией. Мне больше по душе ORM в духе ActiveRecord(ruby)/EBean(java,scala)
      2) Expression Tree - это действительно, заменить нечем, наверное. Впроштука, как я понял, страшная и нужная только в каких-то редких случаях.

      ...и всё. Уже совсем другая картинка, не правда ли? Мне лично уже не кажется, что отказываясь от .Net я теряю что-то невосполнимое.

      P.s.
      @cypok: пятёрочка за стиль :)
      @Аноним: нуу-ну, баян же, да и не представляться вежливым людям не пристало :)

      Удалить