30 сентября 2013 в 17:37

Чего нам ждать от Ruby 2.1? из песочницы

Несколько дней назад Константин Хаасе, один из ключевых людей в сообществе Ruby, опубликовал запись в своём блоге, посвящённую анонсу предварительной версии Ruby 2.1. Изменений между версиями 2.0 и 2.1 накопилось достаточно, чтобы вчитаться в его изложение, и лучше — на русском языке.

NB: разумеется, Ruby 2.1 содержит все замечательные возможности предыдущей версии — 2.0. Изменения предыдущих версий упоминаться не будут.

Механизм уточнений


Известно, что в Ruby 2.0 введён механим уточнений. Реализация данного механизма оказалась достаточно противоречивой, поэтому в версии 2.0 его функциональность была несколько ограничена и помечена как экспериментальная.

Стоит напомнить, что уточнения позволяют применять манки патчи в рамках единственного Ruby-файла:

module Foo
  refine String do
    def foo
      self + "foo"
    end
  end
end

using Foo
puts "bar".foo

За пределами данного файла экземпляры класса String не будут отвечать на метод foo.

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

module Foo
  refine String do
    def foo
      self + "foo"
    end
  end
end

module Bar
  using Foo
  puts "bar".foo
end

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

Десятичные литералы


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

irb(main):001:0> 0.1 * 3
=> 0.30000000000000004

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

Новая версия Ruby представляет суффикс r для описания десятичных и рациональных дробей:

irb(main):001:0> 0.1r
=> (1/10)
irb(main):002:0> 0.1r * 3
=> (3/10)

Иммутабельные строки


Если в коде содержится объявление строки, то каждый раз при исполнении содержащей его строки кода Ruby создаёт новый объект класса String. Это обусловлено мутабельностью строк. В таких случаях символы ведут себя гораздо эффективнее, так как инициализируются всего один раз. Тем не менее, для сравнения символа со строкой нужно провести преобразование строки в символ или символа в строку. Выполнение таких преобразований — рискованная операция, открывающую потенциальную возможность для DoS-атаки, так как символы не освобождаются при сборке мусора, а любое преобразование символа в строку создаёт новую строку.

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

class Foo
  BAR = "bar"

  def bar?(input)
    input == BAR
  end
end

Часто, чтобы избавиться от мутабельности, выполняют заморозку строки. Заморозка объекта предотвращает его изменение со стороны кода на Ruby, однако не даёт никаких прибавок к производительности:

class Foo
  BAR = "bar".freeze

  def bar?(input)
    input == BAR
  end
end

Это выглядит достаточно нелепо и громоздко. К счастью, Ruby 2.1 предлагает новый синтаксис для решения данной задачи:

class Foo
  def bar?(input)
    input == "bar"f
  end
end

В приведённом коде будет создан иммутабельный объект класса String, и где бы он ни использовался — он будет инициализирован всего один раз.

Не исключено, что такой синтаксис может показаться странным. Тот же самый фрагмент кода можно переписать эквивалентно:

class Foo
  def bar?(input)
    input == %q{bar}f
  end
end

Вообще, вопрос применения суффикса f к массивам и хэшам остаётся открытым.

Обязательные ключевые аргументы


Почему-то в анонсе Ruby 2.0 не были упомянуты обязательные ключевые аргументы. Итак, Ruby 2.0 представляет обязательные ключевые аргументы:

def foo(a: 10)
  puts a
end

foo(a: 20) # 20
foo        # 10

При таком подходе к объявлению методов приходится указывать значения аргументов по-умолчанию. Это не всегда возможно, поэтому Ruby 2.1 позволяет задать обязательные ключевые аргументы:

def foo(a:)
  puts a
end

foo(a: 20) # 20
foo        # ArgumentError: missing keyword: a

Объявление метода возвращает имя метода


В предыдущих версиях Ruby объявление метода при помощи def возвращало nil.

def foo() end # => nil

Теперь это поведение изменилось и имя метода возвращается как символ:

def foo() end # => :foo

Это полезно при метапрограммировании и выполнении подобных трюков. Например, все ли знают, что метод private может принимать аргументы?

# приватным будет только метод foo
class Foo
  def foo
  end

  private :foo

  # метод bar останется незатронутым
  def bar
  end
end

Теперь, когда def возвращает имя объявленного метода, можно легко делать методы приватными:

# приватными будут только методы foo и bar
class Foo
  private def foo
  end

  private \
  def bar
  end

  def baz
  end
end

Удаление лишних байт из строк


Теперь Ruby имеет удобный метод для удаление лишних байт из строк:

some_string.scrub("")

Раньше было сложно добиться одинакового поведения такого метода для всех существующих реализаций Ruby, поэтому также доступна библиотека для этого.

StringScanner поддерживает именованные захваты


Многим нравится класс StringScanner из стандартной библиотеки языка. В частности, он используется в Rails для разбора шаблонов маршрутов. То же самое будет делать Sinatra 2.0.

В версии 1.9 была добавлена поддержка именованных захватов, однако StringScanner их не поддерживал:

require 'strscan'
s = StringScanner.new("foo")
s.scan(/(?<bar>.*)/)
puts s[:bar]

В Ruby 2.0 такой код выбросит исключение:

TypeError: no implicit conversion of Symbol into Integer

Зато при запуске на Ruby 2.1 всё будет хорошо:

foo

Работа с сетевыми интерфейсами


Теперь можно получить доступ к сетевыми интерфейсам при помощи метода Socket.getifaddrs:

require 'socket'

Socket.getifaddrs.each do |i|
  puts "#{i.name}: #{i.addr.ip_address}" if i.addr.ip?
end

Пример вывода такой программы:

lo0: fe80::1%lo0
lo0: 127.0.0.1
lo0: ::1
en0: fe80::1240:f3ff:fe7e:594e%en0
en0: 192.168.178.30
en2: fe80::3e07:54ff:fe6f:147a%en2

Быстрая работа с числами для вычислительных задач


Ruby 2.1 ведёт себя быстрее при работе с большими числами благодаря использованию 128-битных целых чисел в качестве внутреннего представления объектов класса Bignum. Более того, применение GNU Multiple Precision Arithmetic Library даёт дополнительный прирост к производительности.

Изменения в виртуальной машине


Теперь виртуальная машина Ruby наряду с использованием глобального кэша методов выполняет кэширование по месту вызова функции. Про это есть отдельные слайды.

RGenGC


Новая версия Ruby использует новый сборщик мусора на основе поколений. Благодаря этому сборка мусора будет происходить быстрее. До этого использовался консервативный сборщик мусора, работающий по схеме «stop the world — mark — sweep».

На самом деле, старый сборщик никуда не исчез. Такие вещи сложно менять из-за особенностей внутреннего и внешнего программного интерфейса Ruby для языка Си.

Тем не менее, виртуальная машина Ruby 2.1 выполняет классификацию объектов на светлые и тёмные. В зависимости от присвоенного класса определяется поведение сборщика мусора. Имеются операции, которые делают светлый объект тёмным. Например, работа с ним из расширения на языке Си. Такие объекты, как открытые файлы, являются тёмными изначально.

Новый сборщик мусора работает только со светлыми объектами.

Обновление RubyGems


RubyGems получили обновление до версии 2.2.0, которая приносит несколько незначительных усовершенствований.

Ничто не вечно


Не стоит забывать, что недавний релиз является лишь предварительной версией, и всё вышеописанное может измениться.
Димочка @dustalov
карма
55,5
рейтинг 0,0
Уверенный пользователь ПК
Похожие публикации
Самое читаемое Разработка

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

  • 0
    Это уже упоминалось здесь в одном из подкастов, но я перевёл эту статью раньше, чем подкаст был анонсирован.
  • +3
    Для меня лично особенно ценен новый метод String#scrub. В задачах NLP на входе может быть любой мусор и зачастую непросто добиться одинакового поведения на разных реализациях Ruby. Раньше для этого были собственные костыли, теперь есть родное решение.
    • 0
      Вы хотели сказать теперь есть родной костыль.
      ИМХО конечно, но в Ruby-way это как-то не вписывается и воспринимается именно что костылем.
      • +2
        Сам по себе метод нужный и не кажется мне костылём. Это всего лишь фильтрация данных.

        Костыльность заключается в его вынужденной реализации с учётом особенностей JRuby, MRI, и других Ruby. Например, в какой-то версии MRI есть iconv в стандартной библиотеке, в какой-то новой версии его объявили устаревшим; в текущем JRuby 1.7.4 есть баг JRUBY-7007; в Rubinius также регулярно возникают проблемы с Unicode, и так далее.

        Стандартизация метода и требуемого поведения — правильное решение.
        • 0
          Возможно, с этой точки зрения Вы и правы.
  • +1
    refinements очень напомнили implicit conversions из scala
    • +2
      Только они в скале резолвятся в компайл-тайме и вообще нормально задизайнены, в том плане, что всегда известно в каком окружении будет исполнен конкретный кусок кода. В руби это не так. Т.е. и производительность страдает (в руби!!) и дизайн языка.

      Вообще с веткой 2+ в руби лично я решил, что эта школьная поделка ни к чему хорошему не придет.
      • 0
        Различия вы очень правильно описали. А я просто отметил схожесть идей.
      • 0
        В целом, согласен. За последнее время возникает ощущение, что создатели языка Ruby схватились и начали запихивать в себя вещи, которые им приглянулись в дизайне других языков. Чем-то напоминает эволюцию C# от Microsoft, который также набрал в себя слишком многое.
        • +1
          Вообще, как только язык перестает набирать в себя что-то новое, то он начинает умирать. Потому что концепции программирования меняются постоянно и люди ищут, что больше удовлетворит их здоровую лень. Однако в языке должна быть какая-то стержневая идеология, под которую адаптируются все новшества, а иначе может получиться язык в котором проще написать приложения с кучей скрытых ошибок, чем без них.
          Это я сейчас не о руби, а так, пофилософствовать )
      • +2
        Т.е. и производительность страдает (в руби!!)

        Вы так говорите, как будто от руби кто-то ожидает производительности. Его же любят и используют вовсе не из-за этого.
        • +1
          Вы так говорите, как будто это хорошо.

          В том то и дело, что включать в далеко не быстрый интерпретатор фичу, которая при активном использовании (а почему бы и нет! фича ведь очень общая) замедлит его еще в несколько раз, это довольно сомнительный успех.
          • –1
            Нет, я не говорил, что это хорошо. Я говорил, что если нужна высокая производительность, то руби — не самый подходящий выбор.
            Что касается «замедлит в несколько раз» — без бенчмарков это беспредметный разговор.
  • +4
    Блин, да самое важное — это новый GC. А вы все про работу со строками и манки-патчингом.
    • 0
      Верное замечание. Андрей, вот тебе слайды Toward efficient Ruby 2.1 про RGenGC.
    • +2
      гц — это не язык. его можно хоть в патч-версиях имплементации менять. Так что если говорить про развитие языка, то гц вне темы.
  • 0
    Прикольно, узнал несколько фишек из языков а-ля C#
  • 0
    Может кто-то рассказать, какие конкретные проблемы решает #scrub? Из описания в исходниках не очень понятно, что такое invalid bytesequence.
    • 0
      Я могу. Неправильная последовательность байт — это наличие в строке символов, не соответствующих её кодировке. Зачастую данные, приходящие в работающую программу извне могут быть некорректны — как заведомо, так и неосознанно. Лучше отфильтровать некорректные байты сразу, чем потом хвататься за голову при ошибках в боевых условиях.

      Например, существует кодировка UTF-8. Строка "vit\xC3\xA6" является корректной записью слова vitæ и не содержит недопустимых символов. В свою очередь, строка "hello\x00\x20\uDC80there" некорректна. Благодаря методу String#scrub она превратится в безобидное hello there.
      • 0
        Ну об этом я догадывался, мне интересен пример, когда так происходит. Откуда могут взяться неверные байты, которые можно просто вырезать, что бы получилось хорошо? Ведь если они неправильные, то где-то в источнике проблема и нужно выяснять, почему они такие и как должно быть на самом деле, разве не так?
        • 0
          Конечно нет, не так. Мы живём в неидеальном мире. Ни в коем случае нельзя забывать о человеческой невнимательности, которая может заключаться и в технической стороне дела. В реальном мире полно примеров, которые я привёл в предыдущем комментарии.

          Если совсем интересно, то могу поделиться опытом. У меня есть сервис извлечения ключевых слов. У сервиса есть API, им пользуются люди. Некоторое время назад в Squash начали появляться ошибки, связанные с некорректными байтами в строках. Я начал смотреть дампы параметров. Оказалось, что в некоторых входных текстах наряду с нормальными буквами присутствуют неюникодные символы, которые появились в результате: 1) парсинга Веб-сайтов; 2) какой-то кривой конвертации из офисных форматов. Удаление неправильных байт из текстов решило все подобные проблемы.

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