- оператор безопасной навигации вглубь объектов (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, и подготавливаются другие. В связи с этим опрос:
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (40)
rinat_crone
20.11.2015 10:42… тогда в image окажется false, а не nil. А если проверять на nil?, то код будет выглядеть еще запутаннее.
Отдельно на nil проверять не нужно, кстати, он отработает аналогично false:
irb(main):001:0> !!nil => false
cvss
20.11.2015 13:09Проверять на nil может понадобиться, если кто-то может легитимно вернуть false. Например, если бы хранилось булево значение user.profile.confirmed, то при user && user.profile && user.profile.confirmed кто-то, получив false при user nil, может вызвать user.profile.resend_confirmation.
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 есть, но в языках общего назначения ни разу не встречал.cvss
20.11.2015 13:22Интересное поведение. В Groovy есть чуть-чуть похожий оператор '*.', он вместе с безопасной навигацией введен. Но и в нем это сильно отличается от описанного вами.
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.
shock_one
21.11.2015 01:40Проблема безопасной навигации (или композиции функций) в том, что есть некое супер-значение, которое населяет все типы и ведет себя отлично от других граждан этого типа отказываясь отвечать на ожидаемые сообщения (если вы не в Objective-C или SmallTalk, где nil принимает любое сообщение и возвращает себя же). Поговаривают, что индустрия разработки программного обеспечения из-за этого потеряла как минимум миллиард долларов.
Наряду с тем, что добавили в Ruby, иной популярный способ избавиться от злополучного гражданина — явно указывать, что значение может отсутствовать. Обычно такой тип называет Option, или Maybe. На мой взгляд, это решение лучше потому, что программист точно знает, что в этом месте может ничего не быть и остаток цепочки не выполнится. К сожалению, добавить Option в язык ретроспективно не очень просто, а в язык с динамической типизацией вообще не представляется возможным.
Недостаток второго метода в том, что теперь возвращаемое значение предыдущей функции не совпадает с принимаемым значением (или self в OO языках) последующей и их нельзя просто так взять и связать точкой. Но эта проблема решена с помощью композиции в некой категории Клейсли, при чем, она предлагает более общее решение, которое помогает не только при вероятном отсутствии значения, но и в других ситуациях. Например, неопределенности, которая часто встречается при parsing'е того же программного кода. Интересная концепция, кстати, рекомендую почитать на досуге.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 это не нужно, выражения выглядят гораздо короче, понятней и естественней.mayorovp
21.11.2015 11:42Тип Bag, который в OCL — это же монада List, а не Maybe :)
Ares_ekb
21.11.2015 12:12Не совсем List :) Если совсем строго, то в OCL 4 вида коллекций: Bag, Set, OrderedSet, Sequence. Которые отличаются упорядоченностью и уникальностью элементов. Bag — это не упорядоченная коллекция не уникальных элементов. List ближе к Sequence.
Я и говорю, что Bag — это «некий аналог Maybe, но со своими отличиями», который вместе с операторами "." и "->" позволяет добиваться примерно того же, для чего предназначен Maybe, но проще.
prijutme4ty
20.11.2015 17:18В Ruby 2.3 появилась очень неоднозначная фича: сравнение хэшей. bugs.ruby-lang.org/issues/10984
cvss
20.11.2015 18:56Я это как-то пропустил, так как на практике сравнения хэшей, кроме matcher в rspec, нигде больше не встречал.
А вопрос, действительно, получается интересный. Сначала вводился только метод проверки на включение субхэша, а потом оказалось, что это хорошо укладывается в частично упорядоченные множества.prijutme4ty
20.11.2015 19:02По-моему, как раз очень плохо укладывается. Я там об этом в багтрекере довольно подробно написал. Плюс, как мне в комментариях к обсуждению в ЖЖ объяснили, главная проблема — несогласованность операций `<=` и `==`.
cvss
20.11.2015 19:21Там не укладывается из-за того, что вместо для несравниваемых элементов отдают false вместо nil. Если отдавать nil, то все будет в порядке.
С '==' в ЖЖ не видно, откуда проблема. Есть утверждение, что '==' оказалась более узкая, чем антисимметрия, но чем обосновано это утверждение, не написано.prijutme4ty
20.11.2015 19:39Это будет лучше, но даже это не может исправить ситуацию. ИМХО, введение операторов (встроенных, не внешних компараторов как в c++) для несравнимых объектов — большая ошибка проектирования. И ее надо выкорчевывать, в том числе, избавляясь от этого оператора в методе Class.
Меня, как программиста, не желающего заморачиваться — иначе я сидел бы не на руби — строгим, математически корректным прописыванием всех возможных веток кода для сравнения (коих оказывается больше трех), сильно смущает, что !(a<b || a>b) не эквивалентно a==b. В ЖЖшном треде ezz666 расписал это сильно более обстоятельно, опираясь на честное определение отношения порядка.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 и не должен тут работать.
cvss
20.11.2015 20:03partially ordered set является нормальной строгой математикой и не должно никого смущать. Классы являются отличным образцом и выкорчевав оператор <=> вы без всякой пользы убъете возможность строить граф наследования классов.
У вас есть обоснование того, что это большая ошибка проектирования?prijutme4ty
20.11.2015 20:32Нет, подождите. Я не хочу qsort, это лишь частный пример.
Я хочу чтобы встроенные операторы был годны для всего спектра применений (и qsort — лишь один из случаев), абсолютно консистенты и согласовывались сo здравым смыслом. Для этого оператор должен быть определен лишь на полностью упорядоченных множествах. Изыски вроде poset-ов — не типовая задача, не нужно из-за них захламлять ненужными сложностями типовое использование операторов сравнения.
Ради сортировки не-линейно упорядоченных множеств можно сделать специальный метод, в описании которого прописать четкие требования к оператору сравнения и описать правила работы с несравнимыми элементами.
Обоснование, почему это ошибка проектирования? Легко!
Если вы хотите сделать граф наследования, вполне можно определить методы `#subclass?`, `#superclass?`, `#unrelated?`, а не усложнять функциональность оператора `<=>`. Ни малейших проблем.
Но разработчики руби решили обобщить оператор <=> на этот случай, и теперь вам приходится всякий раз рассматривать отдельную ветку a<=>b == nil. Даже если вы предполагаете, что все ваши элементы сравнимы. Но вы работаете в динамически-типизированном языке и семантика оператора <=> по факту допускает эту (лишнюю для большей части применений) ветку. А значит, вы должны ее обработать и либо заморочиться работой с poset-ами, либо, по-меньшей мере, бросить исключение.
О том, как такие обобщения влияют на код, посмотрите статью про сравнение Float-ов. С ними проблема следующая: у них есть значения +-Inf и NAN. Из-за этого написать корректный компаратор становится весьма сложной задачей. Но работа с бесконечностями и неопределенностями, может, и заслуживает такого обобщения, усложняющего код (и то не факт, что подход был выбран правильно). А вот сравнение классов/хэшей оператором вместо метода — точно не заслуживает.cvss
20.11.2015 21:14Я думаю, что вы видите проблему на ровном месте, там, где ее нет. strict weak order является подклассом partial order. Вы работаете с более узким классом strict weak order и из-за этого требуете, чтобы оператор не работал для более широкого класса. А, например, у меня последние пол-года половина задач связана с зависимостями и топологической сортировкой, и для меня это рутина, а не изыски.
Вам с qsort наличие <=> у хэша ничем не помешает. Он выдает на nil такое же исключение, как и на сравнение числа со строкой. Если вы сейчас не проверяете на nil, то у вас будут вылезать ошибки и при смешивании типов в одном списке:
> 'a' <=> 2 => nil
Если же вы полагаетесь на то, что у вас в массиве ничего кроме чисел не будет, то каким образом вам помешает <=> у хэша, ведь у вас в массиве ничего кроме чисел нет?
Возможно, для того, чтобы сделать более удобным и строгим для вас язык, стоило бы наоборот, ввести что-то типа <===>, который бы выдавал исключение вместо nil.prijutme4ty
20.11.2015 21:21Да, аргумент про то, что в массиве могут быть объекты разных типов справедлив. Убедили, что хуже не станет. Спасибо!
Eklykti
20.11.2015 20:55Приложения работают под Nginx + Apache + mod_passenger
А зачем тут апач?cvss
20.11.2015 21:24Для удобства и упрощения конфигурации.
Eklykti
20.11.2015 21:45Чем настройка апача проще настройки нгинкса? По-моему, только лишнее звено, вносящее дополнительные задержки, учитывая, что пассажир и с nginx неплохо интегрируется.
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 в нашем случае нельзя, так как мы изолируем клиентов друг о друга и по правам доступа (безопасность) и по ресурсам (нагрузка соседей).Eklykti
20.11.2015 23:47Насчёт прав доступа — пассажир в nginx по дефолту запускает воркеры под тем юзером, которому принадлежит config.ru. Вот насчёт ресурсов хз, не интересовался.
cvss
21.11.2015 00:19Просто под юзером — это малая часть задачи. Чтобы все было хорошо, нужно еще для каждого пользователя правильно выставлять контекст выполнения — доступные файловые системы, переменные окружения, лимиты памяти/стека/процессора, приоритеты выполнения.
cvss
21.11.2015 00:27Еще немаловажная причина не складывать разных пользователей в nginx с интегрированным passenger — разным пользователям в эксплуатации могут требоваться разные версии passenger. Одному нужна более новая версия с исправленным багом, с которым он сталкивается, другому наоборот, никакие лишние изменения не нужны.
Поэтому лучше как в нашем случае — passenger от nginx отдельно.
tgz
20.11.2015 22:36> image = user && user.profile && user.profile.thumbnails && user.profile.thumbnails.large
Это они так изобретают Maybe?
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?prijutme4ty
21.11.2015 06:14Уоу. А можете кинуть ссылку на обход Enumerable? Интересно, для чего это исходно задумывалось.
cvss
/me глядя на голосование: интересно, какие причины побуждают использовать 2.3.* в продакшне
matiouchkine
Скучная офисная жизнь обрыдла, а на костюм для base-jumping’a денег не хватает?
Nakilon
Чем больше проблем, тем больше работы.
Чем больше работы, тем больше зарплаты.