Ruby 2.1 в деталях (Часть 2)

в 10:09, , рубрики: ruby

Ruby 2.1 в деталях (Часть 2)

Refinements

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

Теперь к методу #using для активации уточнений на уровне файла добавился метод Module#using для активации в пределах модуля. Однако использование уточнений по-прежнему ограничено лексической областью видимости, т.е. они не будут активны при повторном открытии модуля.

module NumberQuery
  refine String do
    def number?
      match(/A(0|-?[1-9][0-9]*)z/) ? true : false
    end
  end
end

module Example
  using NumberQuery
  "42".number?   #=> true
end

module Example
  "42".number?   #=> #<NoMethodError: undefined method `number?' for "42":String>
end

Объявления уточнений теперь наследуются при использовании Module#include, т.е. вы можете группировать уточнения, определенные в разных модулях, в одном и активировать их все, вызывая #using только для этого модуля.

module BlankQuery
  refine Object do
    def blank?
      respond_to?(:empty?) ? empty? : false
    end
  end

  refine String do
    def blank?
      strip.length == 0
    end
  end

  refine NilClass do
    def blank?
      true
    end
  end
end

module NumberQuery
  refine Object do
    def number?
      false
    end
  end

  refine String do
    def number?
      match(/A(0|-?[1-9][0-9]*)z/) ? true : false
    end
  end

  refine Numeric do
    def number?
      true
    end
  end
end

module Support
  include BlankQuery
  include NumberQuery
end

class User
  using Support
  # ...
  
  def points=(obj)
    raise "points can't be blank" if obj.blank?
    raise "points must be a number" unless obj.number?
    @points = obj
  end
end
String#scrub

Метод String#scrub был добавлен в Ruby 2.1 для помощи в работе со строками, содержащими некорректные байты.

# create a string that can't be sensibly printed

# 'latin 1' encoded string with accented character
string = "öops".encode("ISO-8859-1")
# misrepresented as UTF-8
string.force_encoding("UTF-8")
# and mixed with a UTF-8 character
string = "¡#{string}!"

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

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

# replace with 'replacement character'
string.scrub        #=> "¡�ops!"
# delete
string.scrub("")    #=> "¡ops!"
# replace with chosen character
string.scrub("?")   #=> "¡?ops!"
# yield to a block for custom replacement
# (in this case the invalid bytes as hex)
string.scrub {|bytes| "<#{bytes.unpack("H*").join}>"}   #=> "¡<f6>ops!"

Тот же результат может быть достигнут вызовом метода #encoding с текущей кодировкой и invalid: :replace в качестве аргументов:

string.encode("UTF-8", invalid: :replace)                 #=> "¡�ops!"
string.encode("UTF-8", invalid: :replace, replace: "?")   #=> "¡?ops!"
Улучшения производительности в классах Bignum/Rational

Классы Bignum и Rational теперь используют GNU Multiple Precision Arithmetic Library (GMP) для улучшения производительности.

Удален 4 уровень $SAFE

Задание $SAFE = 4 должно было переводить Ruby в режим «песочницы» и позволять выполнение недоверенного кода. Однако это не было особенно эффективным, т.к. требовало немалого количества кода, разбросанного по всему интерпретатору, да и практически никогда не использовалось, почему и было в итоге удалено.

$SAFE = 4   #=> #<ArgumentError: $SAFE=4 is obsolete>
clock_gettime

Ruby получил доступ к системной функции clock_gettime() с помощью метода Process.clock_gettime, который предоставляет доступ к различным значениям даты. В качестве первого аргмента методу должен передаваться id времени:

Process.clock_gettime(Process::CLOCK_REALTIME)   #=> 1391705719.906066

Передав Process::CLOCK_REALTIME, вы получите отметку времени Unix в качестве возвращаемого значения. Оно будет также соответствовать Time.now.to_f, но без создания объекта Time, поэтому выполнится несколько быстрее.

Process.clock_gettime можно также использовать для доступа к «монотонным» часам, которые не зависят от перевода системных часов. Это может применяться для критичных временных замеров или бенчмаркинга.

Однако значение монотонных часов имеет смысл только при сравнении с другой такой же отметкой:

start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
sleep 1
Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time   #=> 1.0051147330086678

Еще одним значением, которое может быть использовано для бенчмаркинга, является CLOCK_PROCESS_CPUTIME_ID. Оно работает так же, как и монотонные часы, т.е. постоянно увеличивается, но отличие в том, что оно увеличивается только когда CPU выполняет какую-либо работу. Эта отметка также имеет смысл только в сравнении с другой подобной отметкой.

start_time = Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
sleep 1
Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID) - start_time   #=> 0.005225999999999981

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

Чтобы проверить доступность доступность конкретного вида часов, достаточно проверить то, что определена соответствующая константа.

Process.const_defined?(:CLOCK_PROCESS_CPUTIME_ID)   #=> true
Process.const_defined?(:CLOCK_THREAD_CPUTIME_ID)    #=> false

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

Обновление RubyGems

Встроенная версия RubyGems была обновлена до версии 2.2. К поддержке Gemfile была добавлена поддержка Gemfile.lock, как часть работы по поддержке всех фич Bundler'а в RubyGems.

Опция --file(или -g) для gem install теперь не требует обязательного задания файла зависимостей, она автоматчески определяет наличие Gemfile. gem install также будет генерировать Gemfile.lock, если он еще не создан, и учитывать версии, указанные в нем, если он уже создан.

$ ls
Gemfile
$ gem install -g
Fetching: going_postal-0.1.4.gem (100%)
Installing going_postal (0.1.4)
$ ls
Gemfile
Gemfile.lock

Полный список изменений можно найти в фале History.

Удалены устаревшие фичи Rake

Встроенный Rake обновлен до версии 10.1.0, в котором удалено множество deprecated-фич. В более старых версиях Rake уже давно выводилось предупреждение по поводу этих фич, поэтому проблем с совместимостью быть не должно.

Если вам нужно больше подробностей, смотрите полный список изменений в Rake версий 10.0.3 и 10.1.0.

Обновление шаблона RDoc

Теперь в Ruby включена версия RDoc версии 4.1, в которой содержится обновление шаблона по умолчанию с некоторыми улучшениями в плане организации доступа. Полный список изменений можно посмотреть в файле History.

Имя процесса

Был добавлен новый метод Process.setproctitle, позволяющий задавать имя процесса без обращения к переменной $0. Также был добавлен метод Process.argv0 чтобы получить исходное значение $0.

Например, у вас есть следующий фоновый скрипт:

data.each_with_index do |datum, i|
  Process.setproctitle("#{Process.argv0} - job #{i} of #{data.length}")
  process(datum)
end

тогда при запуске ps вы увидите примерно следующее:

$ ps
  PID TTY           TIME CMD
  339 ttys000    0:00.23 -bash
 7321 ttys000    0:00.06 background.rb - job 10 of 30
Замороженный Symbol

Объекты Symbol теперь составляют компанию целым и вещественным числам в качестве «замороженных» (frozen) объектов.

:foo.frozen?                               #=> true
:foo.instance_variable_set(:@bar, "baz")   #=> #<RuntimeError: can't modify frozen Symbol>

Это изменение было сделано для улучшения сборки мусора для таких объектов в будущих версиях Ruby.

Исправлена утечка области видимости

При использовании ключевых слов private, protected, public или module_function без аргументов в строке, выполняемой с помощью eval, instance_eval или module_eval, область видимости метода «протекает» в родительскую область видимости, т.е. в примере ниже метод foo будет закрытым:

class Foo
  eval "private"
  
  def foo
    "foo"
  end
end

В версии 2.1 это исправлено и foo будет открытым.

#untrusted? теперь псевдоним для #tainted?

Ранее в Ruby было два набора методов, чтобы помечать объекты как недоверенные, первый состоит из методов #tainted?, #taint и #untaint, второй — #untrusted?, #untrust и #trust. Они работают одинаково, но при этом выставляют разные флаги у объектов.

Теперь эти методы унифицированы и выставляют один и тот же флаг, причем более предпочтительным является #tainted? и компания, а при вызове #untrusted? и др. будут появляться ворнинги.

string = "foo"
string.untrust
string.tainted?   #=> true

выведет предупреждение

example.rb:2: warning: untrust is deprecated and its behavior is same as taint
return в лямбдах

Лямбды отличаются от блоков и Proc-объектов тем, что return возвращает управление из лямбды, а не из вызывающего метода. Однако есть исключение, если лямбда передается с & и вызывается с помощью yield. Теперь это исправлено.

def call_with_yield
  yield
end

def test
  call_with_yield(&lambda {return "hello from lambda"})
  "hello from method"
end

test   #=> "hello from method"

Пример выше выведет «hello from lambda» в Ruby <= 2.0.0.

Адреса интерфейсов

Появилась возможность получить детали сетевых интерфейсов в системе с помощью метода Socket.getifaddrs. Он возвращает массив объектов Socket::Ifaddr.

require "socket"

info = Socket.getifaddrs.find do |ifaddr|
  (ifaddr.flags & Socket::IFF_BROADCAST).nonzero? &&
    ifaddr.addr.afamily == Socket::AF_INET
end

info.addr.ip_address   #=> "10.0.1.2"
Поддержка именованных групп в StringScanner

StringScanner#[] теперь принимает в качестве аргментов объекты Symbol и возвращает значения соответствующих именованных групп.

require "strscan"

def parse_ini(string)
  scanner = StringScanner.new(string)
  current_section = data = {}

  until scanner.eos?
    scanner.skip(/s+/)
    if scanner.scan(/;/)
      scanner.skip_until(/[rn]+/)
    elsif scanner.scan(/[(?<name>[^]]+)]/)
      current_section = current_section[scanner[:name]] = {}
    elsif scanner.scan(/(?<key>[^=]+)=(?<value>.*)/)
      current_section[scanner[:key]] = scanner[:value]
    end
  end

  data
end
YAML.safe_load

В YAML (или точнее в Psych, лежащий в основе реализации) был добавлен метод safe_load. По умолчанию могут быть десериализованы следующие классы: TrueClass, FalseClass, NilClass, Numeric, String, Array и Hash. Для десериализации других классов, если вы уверены в их безопасности, нужно передать их в качестве аргумента.

Если будет передан объект неразрешенного класса, будет возбуждено исключение Psych::DisallowedClass, которое также может быть получено как YAML::DisallowedClass.

require "yaml"
YAML.safe_load(":foo: 1")             #=> #<Psych::DisallowedClass: Tried to load unspecified class: Symbol>
YAML.safe_load(":foo: 1", [Symbol])   #=> {:foo=>1}
Поддержка MDNS и LOC записей в Resolv

Библиотека Resolv DNS получила базовую поддержку многоадресного DNS поиска. Он не поддерживает непрерывные запросы, и не может выполнять обнаружение сервиса, но все равно это весьма удобное нововведение (для полной поддержки DNS Service Discovery попробуйте гем dnssd).

require "resolv"

resolver = Resolv::MDNS.new
resolver.getaddress("example.local")   #=> #<Resolv::IPv4 10.0.1.2>

Связка с библиотекой resolv-replace дает возможность использовать имена mDNS с большинством сетевых библиотек в Ruby.

require "resolv-replace"
require "net/http"

Resolv::DefaultResolver.replace_resolvers([Resolv::Hosts.new, Resolv::MDNS.new])
Net::HTTP.get_response(URI.parse("http://example.local"))   #=> #<Net::HTTPOK 200 OK readbody=true>

Resolv также получил возможность запрашивать DNS LOC записи.

require "resolv"

dns = Resolv::DNS.new

# find.me.uk has LOC records for all UK postcodes
resource = dns.getresource("W1A1AA.find.me.uk", Resolv::DNS::Resource::IN::LOC)

resource.latitude    #=> #<Resolv::LOC::Coord 51 31 6.827 N>
resource.longitude   #=> #<Resolv::LOC::Coord 0 8 37.585 W>

И наконец, последнее изменение в Resolv, теперь можно получать полные сообщения DNS с помощью метода Resolv::DNS#fetch_resource.

require "resolv"

dns = Resolv::DNS.new
dns.fetch_resource("example.com", Resolv::DNS::Resource::IN::A) do |reply, reply_name|
  reply        #=> #<Resolv::DNS::Message:0x007f88192e2cc0 @id=55405, @qr=1, @opcode=0, @aa=0, @tc=0, @rd=1, @ra=1, @rcode=0, @question=[[#<Resolv::DNS::Name: example.com.>, Resolv::DNS::Resource::IN::A]], @answer=[[#<Resolv::DNS::Name: example.com.>, 79148, #<Resolv::DNS::Resource::IN::A:0x007f88192e1c80 @address=#<Resolv::IPv4 93.184.216.119>, @ttl=79148>]], @authority=[], @additional=[]>
  reply_name   #=> #<Resolv::DNS::Name: example.com.>
end
Сообщения об ошибках в классе Socket

В сообщения об ошибках были добавлены адреса сокетов.

require "socket"

TCPSocket.new("localhost", 8080)   #=> #<Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 8080>
Ускоренный Hash#shift

Производительность Hash#shift была значительно увеличена, что в сочетании с упорядоченной вставкой, появившейся в Ruby 1.9, дает возможность реализовать LRU-кэш.

class LRUCache
  def initialize(size)
    @size, @hash = size, {}
  end

  def [](key)
    @hash[key] = @hash.delete(key)
  end

  def []=(key, value)
    @hash.delete(key)
    @hash[key] = value
    @hash.shift if @hash.size > @size
  end
end
Улучшение производительности классов Queue, SizedQueue и ConditionVariable

Классы Queue, SizedQueue и ConditionVariable были ускорены засчет реализации их на C (ранее были реализованы на Ruby).

Перехват внутреннего исключения в Timeout

Теперь стало невозможным перехватывать исключения, используемые внутри класса Timeout для прерывания выполнения блока. Это деталь внутренней реализации, в то время как внешнее исключение Timeout::Error осталось неизменным и может быть перехвачено.

require "timeout"

begin
  Timeout.timeout(1) do
    begin
      sleep 2
    rescue Exception
      # no longer swallows the timeout exception
    end
  end
rescue StandardError => e
  e   #=> #<Timeout::Error: execution expired>
end
Множества

В класс Set были добавлены методы #intersect? и #disjoint?. Метод #intersect? возвращает true, если объект и аргумент имеют хотя бы один общий элемент и false в противном случае, #disjoint? работает наоборот.

require "set"

a = Set[1,2,3]
b = Set[3,4,5]
c = Set[4,5,6]

a.intersect?(b)   #=> true
b.intersect?(c)   #=> true
a.intersect?(c)   #=> false

a.disjoint?(b)   #=> false
b.disjoint?(c)   #=> false
a.disjoint?(c)   #=> true

Другим важным изменением в Set является то, что метод #to_set будет возвращать сам объект, а не созданную копию.

require "set"

set = Set["foo", "bar", "baz"]
set.object_id          #=> 70286489985620
set.to_set.object_id   #=> 70286489985620
Упрощение обработки ответов WEBrick

Теперь тело HTTP ответа от WEBrick может быть присвоено любому объекту с методами #read и #readpartial. Ранее это могли быть только объекты IO или String. Пример ниже реализует класс, выводящий полученный ответ каждую секунду в течение 10 секунд.

require "webrick"

class EnumeratorIOAdapter
  def initialize(enum)
    @enum, @buffer, @more = enum, "", true
  end

  def read(length=nil, out_buffer="")
    return nil unless @more
    until (length && @buffer.length >= length) || !fill_buffer; end
    if length
      part = @buffer.slice!(0, length)
    else
      part, @buffer = @buffer, ""
    end
    out_buffer.replace(part)
  end

  def readpartial(length, out_buffer="")
    raise EOFError if @buffer.empty? && !fill_buffer
    out_buffer.replace(@buffer.slice!(0, length))
  end

  private
  def fill_buffer
    @buffer << @enum.next
  rescue StopIteration
    @more = false
  end
end

server = WEBrick::HTTPServer.new(Port: 8080)

server.mount_proc "/" do |request, response|
  enum = Enumerator.new do |yielder|
    10.times do
      sleep 1
      yielder << "#{Time.now}rn"
    end
  end

  response.chunked = true
  response.body = EnumeratorIOAdapter.new(enum)
end

trap(:INT) {server.shutdown}
server.start
Numeric#step

Метод #step класса Numeric теперь вместо позиционных аргументов может принимать именованные аргументы by: и to:. Аргумент to: является необязательным, если он не задан, последовательность будет бесконечной. При использовании позиционных аргментов этого можно достичь, указав nil в качестве первого аргумента.

0.step(by: 5, to: 20) do |i|
  puts i
end

выведет

0
5
10
15
20

0.step(by: 3) do |i|
  puts i
end

0.step(nil, 3) do |i|
  puts i
end

в обоих случаях выведут

0
3
6
9
12
... and so on
IO

Метод IO#seek теперь наряду с константами IO::SEEK_CUR, IO::SEEK_END и IO::SEEK_SET принимает объекты Symbol :CUR, :END и :SET

В качестве второго аргемнта теперь можно передавать IO::SEEK_DATA и IO::SEEK_HOLE (или :DATA и :HOLE). Когда они заданы, первый аргумент используется как минимальный размер данных/пустого места для перехода.

f = File.new("example.txt")

# sets the offset to the start of the next data chunk at least 8 bytes long
f.seek(8, IO::SEEK_DATA)

# sets the offset to the start of the next empty space at least 32 bytes long
f.seek(32, IO::SEEK_HOLE)

Эта может поддерживаться не на всех платформах, что можно проверить с помощью IO.const_defined?(:SEEK_DATA) и IO.const_defined?(:SEEK_HOLE).

Использование IO _nonblock без возбуждения исключений

Методы IO#read_nonblock и IO#write_nonblock могут принимать именованный аргумент exception:. Если он задан в false (по умолчанию true), методы будут возвращать при ошибке соответствующий объект Symbol вместо возбуждения исключений.

require "socket"

io = TCPSocket.new("www.example.com", 80)

message = "GET / HTTP/1.1rnHost: www.example.comrnConnection: closernrn"
loop do
  IO.select(nil, [io])
  result = io.write_nonblock(message, exception: false)
  break unless result == :wait_writeable
end

response = ""
loop do
  IO.select([io])
  result = io.read_nonblock(32, exception: false)
  break unless result
  next if result == :wait_readable
  response << result
end

puts response.lines.first
IO игнорирует внутреннюю кодировку, если внешняя ASCII-8BIT

Если вы задаете внутреннюю и внешнюю кодировки по умолчанию, Ruby будет преобразовывать из внешней кодировки во внутреннюю. Исключением является случай, когда внешняя кодировка ASCII-8BIT, в этом случае преобразования не происходит.

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

File.read("example.txt", encoding: "ascii-8bit:utf-8").encoding   #=> #<Encoding:ASCII-8BIT>
#include и #prepend теперь открыты

Методы #include и #prepend теперь открыты, это касается классов Module и Class.

module NumberQuery
  def number?
    match(/A(0|-?[1-9][0-9]*)z/) ? true : false
  end
end

String.include(NumberQuery)

"123".number?   #=> true

require "bigdecimal"

module FloatingPointFormat
  def to_s(format="F")
    super
  end
end

BigDecimal.prepend(FloatingPointFormat)

decimal = BigDecimal("1.23")
decimal.to_s   #=> "1.23" # rather than "0.123E1"

В третьей части будут новые методы в классах Module и Object, изменения в сетевых классах и другие обновления в стандартной библиотеке.
Первая часть

Автор: rsludge

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js