На прошой неделе вышел превью Ruby 2.3.0. Что есть интересного из изменений:

  • оператор безопасной навигации вглубь объектов (safe navigation operator), #11537
  • в Hash и Array добавлен метод dig для доступа вглубь вложенных хэшей или массивов #11688
  • движение в сторону неизменяемых (frozen) по умолчанию строковых литералов (обоснование, обсуждение #11473

Оператор безопасной навигации


В Ruby появился оператор безопасной навигации, аналогичный оператору ‘?.’ в Groovy и некоторых других языках. Оператор применяется для сокращения выражений, где выполняется проверка существования объекта и затем обращение к методу объекта только в случае положительной проверки:

obj.nil? && obj.some_method

Если используется цепочка объектов и методов, то проверка может выглядеть громоздко и несколько раз впустую выполнять промежуточные методы. Например, типичный для Ruby on Rails случай безопасного получения большой картинки профиля:

image = user && user.profile && user.profile.thumbnails && user.profile.thumbnails.large

Здесь метод profile будет выполнен три раза и два раза будет выполнен метод thumbnails. Оптимизированный вариант будет выглядеть еще сложнее:

image = user && (profile = user.profile) && (thumbnails = profile.thumbnails) && thumbnails.large

При этом, может оказаться не совсем корректный результат, если один из объектов в движении вглубь будет отсутствовать — тогда в image окажется false, а не nil. А если проверять на nil?, то код будет выглядеть еще запутаннее.

Есть также способ двигаться вглубь цепочки без проверок, перехватывая исключение, например, обычным способом:

image = begin
		user.profile.thumbnails.large
	rescue NoMethodError
		nil
	end

или вызывающим всеобщее неодобрение постфиксным rescue:

image = user.profile.thumbnails.large rescue nil

ActiveSupport для решения такой проблемы поставляет методы try и try!:

image = user.try(:profile).try(:thumbnails).try(:large)

Эти методы добавляются в класс Object и при вызове проверяют сначала существование вызываемого метода и если он отсутствует, то возвращают nil. В случае, если user имеет метод #profile, он будет выполнен и на его результате дальше по цепочке будет вызван try(:thumbnails). Если user будет nil, то #try вернет nil и так по цепочке #try будет вызван у nil еще два раза. Медленно? Зато коротко.

Что делает новоявленный safe navigation operator в Ruby


По задаче #11537 для безопасной навигации в Ruby 2.3.0 добавлен сначала оператор ‘.?’, измененный позже на ‘&.’. В новом синтаксисе выражение из примеров можно записать так:

image = user&.profile&.thumbnails&.large

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

Если в метод передаются аргументы, то, в отличие от try, они вычисляются только в том случае, если объект существует и метод реально вызывается. Например, для ActiveSupport в выражении obj.try(:foo, bar()) всегда будет выполняться bar(), даже если obj не существует. Но в выражении obj&.foo(bar()), аргумент bar() будет вычислен только тогда, когда ojb не равен nil.

Безопасная навигация также может использоваться при присваивании значения атрибуту:

obj&.attr += 1

С момента появления feature request до его подтверждения и коммита с первым вариантом реализации прошло лишь чуть больше одного месяца.

Навигация вглубь вложенных хэшей и массивов с помощью #dig.


Feature #11688 добавляет в Hash и Array метод dig, который используется для безопасного получения значения из вложенных хэшей и массивов. Этот метод заменяет выражение:

value = hash[:a].nil? ? nil : hash[:a][:b].nil? ? nil : hash[:a][:b][:c]

или:

value = hash[:a][:b][:c] rescue nil

на:

value = hash.dig(:a, :b, :c)

Доступ во вложенные хэши и массивы часто применяется при обработке параметров HTTP-запросов, получаемых Rails-приложениями, или при работе с YAML или JSON структурами. Добавление dig для обращения вглубь не только предоставляет удобный метод доступа, но и ускоряет его в несколько десятков раз.

Метод dig также на днях добавлен в Struct, но в первое превью 2.3.0 попасть не успел.

Неизменяемость строк


В Ruby 3 все строковые литералы будут неизменными. По поводу неизменности споры идут давно и сейчас движение в эту сторону стало более конкретным. Выражается оно в появлении “магического” комментария frozen-string-literal, присутствие которого включает по умолчанию неизменность для всех строковых литералов, и в добавлении --enable/--disable=frozen-string-literal для управления таким поведением.

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

А у нас небольшой опрос


Мы подготовили хостинг Ruby-приложений в контейнерах Jet9. Как на облачном хостинге, так и на отказоустойчивом кластере.

Одна из причин, почему первым на Jet9 появляется Ruby — мы унифицируем обслуживание наших собственных внутренних сервисов и внутренних сервисов наших клиентов (сайты, биллинг, документооборот, багтрекер, тикеты и прочее). Чтобы упростить себе жизнь и сократить расходы на обновление и сопровождение зоопарка из разных дистрибутивов, HA-кластеров с разными конфигурациями и отдельных физических серверов, мы переносим все на пару типовых HA-кластеров на платформе Jet9. Большая часть написана на Ruby — используются собственные приложения и сторонние (Redmine, Gitlab). Таким образом, поддержку Ruby на Jet9 мы сделали в том числе и для себя, на себе же ее и проверяем.

Приложения работают под Nginx + Apache + mod_passenger (5.0.21), это самый удобный способ. Но можно использовать standalone Passenger или другие серверы приложений (Unicorn, Puma). Сейчас доступны версии 2.2.2 и 2.2.3, и подготавливаются другие. В связи с этим опрос:
Какой Ruby вы используете в продакшене

Проголосовало 249 человек. Воздержалось 157 человек.

Какой Ruby вы хотите использовать в продакшене

Проголосовало 210 человек. Воздержалось 186 человек.

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

Комментарии (40)


  1. cvss
    20.11.2015 04:04

    /me глядя на голосование: интересно, какие причины побуждают использовать 2.3.* в продакшне


    1. matiouchkine
      20.11.2015 15:58

      Скучная офисная жизнь обрыдла, а на костюм для base-jumping’a денег не хватает?


    1. Nakilon
      23.11.2015 14:18

      Чем больше проблем, тем больше работы.
      Чем больше работы, тем больше зарплаты.


  1. rinat_crone
    20.11.2015 10:42

    … тогда в image окажется false, а не nil. А если проверять на nil?, то код будет выглядеть еще запутаннее.

    Отдельно на nil проверять не нужно, кстати, он отработает аналогично false:

    irb(main):001:0> !!nil
    => false
    


    1. cvss
      20.11.2015 13:09

      Проверять на nil может понадобиться, если кто-то может легитимно вернуть false. Например, если бы хранилось булево значение user.profile.confirmed, то при user && user.profile && user.profile.confirmed кто-то, получив false при user nil, может вызвать user.profile.resend_confirmation.


      1. rinat_crone
        20.11.2015 17:29

        В Вашем примере, конечно, код «попахивает», но идею я понял и согласен.


  1. Ares_ekb
    20.11.2015 13:12

    «Проблема» безопасной навигации интересно решена в OCL.

    Опциональные свойства имеют тип Bag (набор значений, коллекция). Если свойству не присвоено значение, то возвращается пустая коллекция. Иначе возвращается коллекция с одним значением (значением свойства).

    При этом оператор "." применяется не ко всей коллекции, а к каждому элементу отдельно.

    На языке OCL выражение «user.profile.thumbnails.large» будет интерпретировано так:

    1) Получаем у user значение свойства profile.
    1а) Если оно указано, то получаем коллекцию с этим значением,
    1б) Иначе получаем пустую коллекцию.

    2) Затем у каждого элемента этой коллекции обращаемся к свойству thumbnails и формируем из полученных значений новую коллекцию.
    2а) Если на первом шаге получили коллекцию с одним профилем, то
    2а1) Если у профиля есть thumbnail, то получаем коллекцию с иконкой этого профиля
    2а2) Если у профиля thumbnail не указан, то получаем пустую коллекцию иконок
    2б) Иначе (если на первом шаге получили пустую коллекцию), то и тут получаем пустую коллекцию

    И т.д. Описал наверно очень туманно :) Но по-моему это идеальное решение, странно что в других языках такого нет. Нет, ещё в XPath есть, но в языках общего назначения ни разу не встречал.


    1. cvss
      20.11.2015 13:22

      Интересное поведение. В Groovy есть чуть-чуть похожий оператор '*.', он вместе с безопасной навигацией введен. Но и в нем это сильно отличается от описанного вами.


      1. Ares_ekb
        20.11.2015 14:05

        Да, очень похож! К тому же в OCL этот оператор, как и в Groovy, разворачивается в вызов операции collect.

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

        И ещё отличие, что в OCL входная и выходная коллекции оператора "." могут содержать разное количество элементов. А в Groovy, как я понимаю, они всегда равны по размеру.

        Если кому-то интересно, то тут спецификация OCL (раздел 7.4.10), а тут статья на русском. В OCL всего два оператора для навигации: точка "." и стрелочка "->".

        Если точка применяется к объекту, то интерпретируется как и в большинстве языков типа Ruby, Java.

        Если точка применяется к коллекции, то она разворачивается в оператор collect (как и в Groovy): aSet->collect(name).

        Стрелочка обычно применяется к коллекции, например, чтобы посчитать сумму значений: aSet->sum()

        Но если применена к одиночному объекту типа «anObject->sum()», то объект неявно кастится во множество: anObject.oclAsSet()->sum()

        Таким образом в OCL выражение «user.profile.thumbnails.large» развернется в «user.profile->collect(thumbnails)->collect(large)»

        Удобно то, что всего два оператора для навигации. И с пустыми множествами или синглетонами работать удобней, чем с null.


    1. shock_one
      21.11.2015 01:40

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

      Наряду с тем, что добавили в Ruby, иной популярный способ избавиться от злополучного гражданина — явно указывать, что значение может отсутствовать. Обычно такой тип называет Option, или Maybe. На мой взгляд, это решение лучше потому, что программист точно знает, что в этом месте может ничего не быть и остаток цепочки не выполнится. К сожалению, добавить Option в язык ретроспективно не очень просто, а в язык с динамической типизацией вообще не представляется возможным.

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


      1. Ares_ekb
        21.11.2015 08:08

        Конечно, я знаю о монадах. В Haskell есть интересное обобщение монад — стрелочки :) Тип Bag, который в OCL используется для опциональных значений — это фактически и есть некий аналог Maybe, но со своими отличиями.

        В OCL логика очень простая. Обычно в разных ЯП если свойство может принимать несколько значений, то для него используется какой-нибудь тип-коллекция (множество, список, массив, ...). А в OCL вообще для всех свойств, множественность которых отличается от 1, используются коллекции. В т.ч. для свойств со множественность 0..1.

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

        Если программист видит, что у свойства тип Bag, то он понимает, что значение опциональное.

        Добавляем к этому два оператора для навигации "." и "->". Первый применяется к одиночным значениям, второй — к коллекциям.

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

        Всё, получаем удобный язык без всяких Maybe, null и т.п. На самом деле, null в OCL есть, но у него немного другая роль, чем в обычных ЯП.

        Чем это лучше… Например, при использовании Maybe приходится использовать явно bind, return. А в OCL это не нужно, выражения выглядят гораздо короче, понятней и естественней.


        1. mayorovp
          21.11.2015 11:42

          Тип Bag, который в OCL — это же монада List, а не Maybe :)


          1. Ares_ekb
            21.11.2015 12:12

            Не совсем List :) Если совсем строго, то в OCL 4 вида коллекций: Bag, Set, OrderedSet, Sequence. Которые отличаются упорядоченностью и уникальностью элементов. Bag — это не упорядоченная коллекция не уникальных элементов. List ближе к Sequence.

            Я и говорю, что Bag — это «некий аналог Maybe, но со своими отличиями», который вместе с операторами "." и "->" позволяет добиваться примерно того же, для чего предназначен Maybe, но проще.


            1. mayorovp
              21.11.2015 13:02

              Все равно как монада, Bag ближе к List чем к Maybe.


  1. prijutme4ty
    20.11.2015 17:18

    В Ruby 2.3 появилась очень неоднозначная фича: сравнение хэшей. bugs.ruby-lang.org/issues/10984


    1. cvss
      20.11.2015 18:56

      Я это как-то пропустил, так как на практике сравнения хэшей, кроме matcher в rspec, нигде больше не встречал.

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


      1. cvss
        20.11.2015 18:58

        Кстати, тогда логично еще добавить <=> в Set.


      1. prijutme4ty
        20.11.2015 19:02

        По-моему, как раз очень плохо укладывается. Я там об этом в багтрекере довольно подробно написал. Плюс, как мне в комментариях к обсуждению в ЖЖ объяснили, главная проблема — несогласованность операций `<=` и `==`.


        1. cvss
          20.11.2015 19:21

          Там не укладывается из-за того, что вместо для несравниваемых элементов отдают false вместо nil. Если отдавать nil, то все будет в порядке.

          С '==' в ЖЖ не видно, откуда проблема. Есть утверждение, что '==' оказалась более узкая, чем антисимметрия, но чем обосновано это утверждение, не написано.


          1. prijutme4ty
            20.11.2015 19:39

            Это будет лучше, но даже это не может исправить ситуацию. ИМХО, введение операторов (встроенных, не внешних компараторов как в c++) для несравнимых объектов — большая ошибка проектирования. И ее надо выкорчевывать, в том числе, избавляясь от этого оператора в методе Class.
            Меня, как программиста, не желающего заморачиваться — иначе я сидел бы не на руби — строгим, математически корректным прописыванием всех возможных веток кода для сравнения (коих оказывается больше трех), сильно смущает, что !(a<b || a>b) не эквивалентно a==b. В ЖЖшном треде ezz666 расписал это сильно более обстоятельно, опираясь на честное определение отношения порядка.


            1. cvss
              20.11.2015 19:59

              > сильно смущает, что !(a<b || a>b) не эквивалентно a==b
              Это не должно смущать. В ЖЖ, насколько я вижу, у вас произошла путаница из-за того, что вы захотели qsort, и для этого потребовали strict weak ordering. И, вероятно, смешали strict weak ordering, strict partial ordering и weak partial ordering.

              Но вы предъявили избыточное требование к тому, что это требование и не должно выполнять. В partially ordered set равенство определяется антисимметрией, то есть (a <= b && b <= a) -> a == b. И при таком условии и топологическая сортировка работает, и выполняются все операции сравнения, там где есть отношение порядка. qsort и не должен тут работать.


            1. cvss
              20.11.2015 20:03

              partially ordered set является нормальной строгой математикой и не должно никого смущать. Классы являются отличным образцом и выкорчевав оператор <=> вы без всякой пользы убъете возможность строить граф наследования классов.

              У вас есть обоснование того, что это большая ошибка проектирования?


              1. prijutme4ty
                20.11.2015 20:32

                Нет, подождите. Я не хочу qsort, это лишь частный пример.
                Я хочу чтобы встроенные операторы был годны для всего спектра применений (и qsort — лишь один из случаев), абсолютно консистенты и согласовывались сo здравым смыслом. Для этого оператор должен быть определен лишь на полностью упорядоченных множествах. Изыски вроде poset-ов — не типовая задача, не нужно из-за них захламлять ненужными сложностями типовое использование операторов сравнения.
                Ради сортировки не-линейно упорядоченных множеств можно сделать специальный метод, в описании которого прописать четкие требования к оператору сравнения и описать правила работы с несравнимыми элементами.

                Обоснование, почему это ошибка проектирования? Легко!
                Если вы хотите сделать граф наследования, вполне можно определить методы `#subclass?`, `#superclass?`, `#unrelated?`, а не усложнять функциональность оператора `<=>`. Ни малейших проблем.
                Но разработчики руби решили обобщить оператор <=> на этот случай, и теперь вам приходится всякий раз рассматривать отдельную ветку a<=>b == nil. Даже если вы предполагаете, что все ваши элементы сравнимы. Но вы работаете в динамически-типизированном языке и семантика оператора <=> по факту допускает эту (лишнюю для большей части применений) ветку. А значит, вы должны ее обработать и либо заморочиться работой с poset-ами, либо, по-меньшей мере, бросить исключение.

                О том, как такие обобщения влияют на код, посмотрите статью про сравнение Float-ов. С ними проблема следующая: у них есть значения +-Inf и NAN. Из-за этого написать корректный компаратор становится весьма сложной задачей. Но работа с бесконечностями и неопределенностями, может, и заслуживает такого обобщения, усложняющего код (и то не факт, что подход был выбран правильно). А вот сравнение классов/хэшей оператором вместо метода — точно не заслуживает.


                1. cvss
                  20.11.2015 21:14

                  Я думаю, что вы видите проблему на ровном месте, там, где ее нет. strict weak order является подклассом partial order. Вы работаете с более узким классом strict weak order и из-за этого требуете, чтобы оператор не работал для более широкого класса. А, например, у меня последние пол-года половина задач связана с зависимостями и топологической сортировкой, и для меня это рутина, а не изыски.

                  Вам с qsort наличие <=> у хэша ничем не помешает. Он выдает на nil такое же исключение, как и на сравнение числа со строкой. Если вы сейчас не проверяете на nil, то у вас будут вылезать ошибки и при смешивании типов в одном списке:

                   > 'a' <=> 2
                   => nil
                  


                  Если же вы полагаетесь на то, что у вас в массиве ничего кроме чисел не будет, то каким образом вам помешает <=> у хэша, ведь у вас в массиве ничего кроме чисел нет?

                  Возможно, для того, чтобы сделать более удобным и строгим для вас язык, стоило бы наоборот, ввести что-то типа <===>, который бы выдавал исключение вместо nil.


                  1. prijutme4ty
                    20.11.2015 21:21

                    Да, аргумент про то, что в массиве могут быть объекты разных типов справедлив. Убедили, что хуже не станет. Спасибо!


  1. Eklykti
    20.11.2015 20:55

    Приложения работают под Nginx + Apache + mod_passenger
    А зачем тут апач?


    1. cvss
      20.11.2015 21:24

      Для удобства и упрощения конфигурации.


      1. Eklykti
        20.11.2015 21:45

        Чем настройка апача проще настройки нгинкса? По-моему, только лишнее звено, вносящее дополнительные задержки, учитывая, что пассажир и с nginx неплохо интегрируется.


        1. cvss
          20.11.2015 22:31

          В первую очередь это удобнее клиентам. Сайт с привязкой к кластеру, DNS и CDN создается через веб-панель в один клик и не нужно по зарываться в документацию и конфигурировать отдельности все эти компоненты. Apache также нужен для настройки сложной структуры через .htaccess, организации хранения документов через WebDAV и для обслуживания других платформ, например, под mod_php — phpmyadmin и awstat. В связке Redmine + LDAP + SVN без Apache не обойтись. Поэтому для простоты и для минимизации расхода ресурсов по дефолту используется apache.

          Если клиенту ничего этого не нужно, он отключает их конфигурации apache и запускает так, как ему нужно и passenger, и unicorn, и puma. По скорости будет все то же самое, что и с apache, но чуть меньше будет расход памяти.

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


          1. Eklykti
            20.11.2015 23:47

            Насчёт прав доступа — пассажир в nginx по дефолту запускает воркеры под тем юзером, которому принадлежит config.ru. Вот насчёт ресурсов хз, не интересовался.


            1. cvss
              21.11.2015 00:19

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


            1. cvss
              21.11.2015 00:27

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

              Поэтому лучше как в нашем случае — passenger от nginx отдельно.


  1. tgz
    20.11.2015 22:36

    > image = user && user.profile && user.profile.thumbnails && user.profile.thumbnails.large

    Это они так изобретают Maybe?


    1. mayorovp
      21.11.2015 11:50

      Это стандартная замена Maybe для большинства динамически типизированных языков.


      1. tgz
        21.11.2015 22:45

        То есть в рубях эта «стандартная замена» появилась только к версии 2.3?
        До этого была видимо не очень стандартная…


        1. mayorovp
          22.11.2015 08:22

          Нет, в версии 2.3 появился оператор &.

          А этот способ был всегда.


          1. tgz
            22.11.2015 23:34

            Ну видимо через несколько релизов они еще и >>= сделают. Будет очередной невыразительный захардкоженый оператор типа $%.


  1. Dreyk
    21.11.2015 01:40

    Еще появилась непонятная возможность обходить Enumerable при помощи хеша, превращенного в прок

    hash = {a: 1, b: 2, c: 3}
    arr = [:a, :b, :d]
    arr.map(&hash); # => [1, 2, nil]
    

    и еще Numeric#positive? Numeric#negative?


    1. prijutme4ty
      21.11.2015 06:14

      Уоу. А можете кинуть ссылку на обход Enumerable? Интересно, для чего это исходно задумывалось.


      1. mediarium
        21.11.2015 22:07

        Есть подозрение, что это аналог метода extract!(*keys) из Rails.