Ruby 2.4.0 Preview

December 01, 2016

올해 크리스마스에 릴리스될 예정인 Ruby 2.4.0에서 어떤 것들이 추가/변경되는지에 대해서 알아봅시다.

들어가기 전에

이 글은 サンプルコードでわかる!Ruby 2.4の新機能と変更点를 보고 작성된 글입니다. 다른 점은 이렇습니다.

다음과 같은 구성으로 되어 있습니다.

그러면 이제 하나씩 살펴보도록 하죠.

Breaking change

FixnumBignumInteger로 통합됨

2.4에서는 FixnumBignumInteger로 통합되었습니다.

# Ruby 2.3
1.class # => Fixnum
10_000_000_000_000_000_000.class # => Bignum

# Ruby 2.4
1.class # => Integer
10_000_000_000_000_000_000.class # => Integer

Fixnum # => Integer
# warning: constant ::Fixnum is deprecated
Bignum # => Integer
# warning: constant ::Bignum is deprecated

이전부터 여러가지로 말이 많았던 모양입니다만 이번 버전을 기점으로 드디어 하나로 통합되었네요. 그리고 C 레벨에서는 여전히 분리되어 있지만, rb_cFixnum, rb_cBignum 상수가 제거 되어서 사용할 수 없어집니다. Fixnum이나 Bignum에 의존하는 코드를 작성하고 계셨다면 호환성에 문제가 생길 수 있으니 주의하세요.

Reference

#round의 기본 반올림 방식 변경

Caption: 이 변경사항은 2.4.0-rc1에서 제거되었습니다. 자세한 설명은 해당 이슈를 확인하세요.

앞으로는 even-half, 가장 가까운 짝수로 반올림됩니다. 기존의 사사오입을 사용하고 싶다면 half 옵션을 사용하세요.

# Ruby 2.3
2.5.round # => 3
3.5.round #=> 4

# Ruby 2.4
2.5.round # => 2
3.5.round # => 4
3.55.round(1) # => 3.6

2.5.round(half: :up) # => 3
3.5.round(half: :up) # => 4

2.5.round(half: :even) # => 2
3.5.round(half: :even) # => 4

2.3부터 sprintf('%1.0f', 2.5)가 even-half를 사용하도록 변경되었는데, 이와 맞추어 일관적으로 동작하게끔 만들었다고 합니다.

Reference

Unicode 버전이 9.0.0으로 갱신

Reference

ASCII 이외의 문자들에 대한 대소문자 지원

위의 변경사항과 함께 String#upcase, String#downcase, String#capitalize, String#swapcase 등 대소문자와 관련된 메소드들이 UTF-8, UTF-16BE/LE, UTF-32BE/LE, ISO-8859-1~16의 대소문자를 지원합니다.

'Türkiye'.upcase
#=> "TüRKIYE" in 2.3
#=> "TÜRKIYE" in 2.4

Reference

TRUE, FALSE, NIL이 제거 예정이 됨

이 3개의 상수는 제거 예정 목록에 추가되었습니다. 앞으로는 true, false, nil을 사용해주세요.

Reference

DateTime#to_time, Time#to_time이 이제 시간대 정보를 유지합니다.

require "date"

# Ruby 2.3
DateTime.strptime("2015-11-12 CET", "%Y-%m-%d %Z").to_time.to_s
#=> "2015-11-12 08:00:00 +0900"
Time.new(2005, 2, 21, 10, 11, 12, "+01:00").to_time.to_s
#=> "2005-02-21 18:11:12 +0900"

# Ruby 2.4
DateTime.strptime("2015-11-12 CET", "%Y-%m-%d %Z").to_time.to_s
# => "2015-11-12 00:00:00 +0100"
Time.new(2005, 2, 21, 10, 11, 12, "+01:00").to_time.to_s
# => "2005-02-21 10:11:12 +0100"

이전에는 실행 환경의 시간대를 사용했습니다.

Reference

RDoc 버전 갱신

5.0.0으로 갱신되었습니다.

Shellwords.shellsplit의 버그 수정

큰 따옴표로 감싼 문자열 내의 백슬러시가 올바르게 처리되지 않는 문제가 수정되었습니다.

require "shellwords"

# Ruby 2.3
Shellwords.shellsplit(%q|printf "%s\\n"|) #=> ["printf", "%sn"]
Shellwords.shellsplit(%q|printf '%s\\n'|) #=> ["printf", "%s\\n"]

# Ruby 2.4
Shellwords.shellsplit(%q|printf "%s\\n"|) #=> ["printf", "%s\\n"]
Shellwords.shellsplit(%q|printf '%s\\n'|) #=> ["printf", "%s\\n"]

이는 POSIX sh의 동작과 일관성을 가져가는 형태입니다.

Reference

OpenSSL을 외부 젬으로 분리

이제 여기에서 관리합니다만, 여전히 기본으로 설치됩니다. 추가 변경사항(i.e. OpenSSL 1.1.0 지원 추가)은 History.md를 참고하세요.

Reference

Tk를 stdlib에서 제거

이제 여기에서 관리됩니다.

Reference

XMLRPC를 stdlib에서 제거

이제 여기에서 관리됩니다.

Reference

Useful change

Multiple assignment

2.4부터는 조건 평가에서도 다중 할당이 가능해집니다. 이전까지는 조건식에서 다중 할당을 허용하지 않았으며, 파싱 에러가 발생했습니다.

# 버전에 관계없이 동작하는 코드
tmp = some_method_returning_array_or_nil
a, b = tmp
if tmp
  # method returned an array (possibly empty)
else
  # method returned nil.
end

# Ruby 2.3 => error: multiple assignment in conditional
# Ruby 2.4 => works!
if (a, b = some_method_returning_array_or_nil)
  # do somethings with a, b
else
  # ...
end

평소에는 되는데 조건식에서만 안되는건 이상하다며, 이쪽에서도 사용할 수 있게 해달라는 이슈를 해결한 패치.

Numeric#finite?, Numeric#infinite?

1.finite? # => true
1.infinite? # => nil
1.0.finite? # => true
1.0.infinite? # => nil
1r.finite? # => true
1r.infinite? # => nil
Complex(1).finite? # => true
Complex(1).infinite? # => nil

FloatBigDecimal에는 해당 메소드가 있는데 Integer에는 없어서 다형성을 이용한 코드를 작성할때 곤란하다는 이슈에 대응하는 변경사항입니다.

Reference

#ceil, #floor, #truncate가 인자로 자릿수를 지정할 수 있게 변경

지금까지는 반드시 정수로 끊었던 것을, #round와 마찬가지로 인자를 받아 특정 소수점 자리수에서 끊을 수 있게 되었습니다.

3.45.floor    # => 3
3.45.ceil     # => 4
3.45.truncate # => 3

3.45.floor(1)    # => 3.4
3.45.ceil(1)     # => 3.5
3.45.truncate(1) # => 3.4

Reference

Array.concat, String#concat, String#prepend가 여러 인자를 받을 수 있게끔 변경

바로 코드를 보시죠.

s = ""
s.concat("a", "b", "c")
s.prepend("A", "B", "C")
s # => "ABCabc"

a = []
a.concat(["a"], ["b", "c"])
a # => ["a", "b", "c"]

a = [1]
a.concat(a, a)
a # => [1, 1, 1]

주의해야할 부분은 마지막 예제입니다. 내부적으로는 <<를 반복할 뿐이라서, [1, 1, 1, 1]를 반환할 수 있는데, 직관적으로는 지금 구현이 자연스럽다는 판단 하에 이렇게 구현된 것으로 보이네요.

Reference

Enumerable#uniq, Enumerator::Lazy#uniq

Enumerable에도 uniq 메소드가 추가되었습니다. 지금 있는 uniq 메소드는 Array에서만 사용할 수 있었습니다.

olimpics = {
  1896 => "Athens",
  1900 => "Paris",
  1904 => "Chikago",
  1906 => "Athens",
  1908 => "Rome"
}

each_city_first_time = olimpics.uniq { |k,v| v }

# [[1896, "Athens"], [1900, "Paris"], [1904, "Chikago"], [1908, "Rome"]]

(1..Float::INFINITY).lazy.uniq { |x| (x**2) % 10 }.first(6)
# => [1, 2, 3, 4, 5, 10]

본래 제안은 uniq를 위해서 매번 배열로 변환해서 작업하기 귀찮으니까 some_enums.uniq_each 같은 게 있었으면 좋겠다, 였는데 그냥 uniq 구현하고 체이닝하면 되잖아요, 로 마무리 된 제안.

Reference

Enumerable#chunk를 블럭 없이 호출하면 열거자를 반환하게끔 변경

2.4부터는 블럭 없이 Enumerable#chunk를 호출할 수 있게 됩니다. 결과로는 빈 열거자가 돌아오게 됩니다.

(1..3).chunk # => <Enumerator: 1..3:chunk>
(1..3).chunk.to_a # => []

장기적으로는 chunk_while, slice_after, slice_before, slice_when도 전부 동일한 동작을 하게 변경될지도.

Reference

Comparable#clamp

값을 특정 범위 내로 수속하도록 만드는 함수가 추가되었습니다.

# 버전에 관계없이 동작하는 동일한 코드
[[0, value].max, 100].min


# Ruby 2.4
101.clamp(0, 100) # => 100
12.clamp(0, 100)  # => 12  
-1.clamp(0, 100)  # => 0

-1.clamp(nil, 100) # => ArgumentError

넘기는 인자는 Comparable을 구현해야 합니다.

Reference

Hash#transform_values, Hash#transform_values!

2.4에는 Hash#transform_values 메소드가 추가됩니다. 이는 내부 요소들을 열거하면서 블록에 넘긴 규칙대로 값을 변경한 새 해시를 반환하는 메소드입니다. 또한 자기 자신을 변경하는 뱅 메소드도 함께 추가됩니다.

{ a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 }
# => { a: 2, b: 4, c: 6}

이는 Rails의 ActiveSupport에 있는 transform_values를 루비 본체에 도입한 것입니다.

Reference

Hash#compact, Hash#compact!

2.4에서는 해시에서 값이 nil인 키를 전부 제거하는 Hash#compact, 파괴적인 버전인 Hash#compact! 메소드가 추가됩니다.

# some_hash.delete_if { |k,v| v.nil? } 와 동치

h1 = {a: 1, b: nil, c: 2}
h1.compact # => {a: 1, c: 2}
h1 # => {a: 1, b: nil, c: 2}

h2 = {a: 1, b: nil, c: 2}
h2.compact! # => {a: 1, c: 2}
h2 # => {a: 1, c: 2}

h3 = {a: 1, c: 2}
h3.compact! # => nil
h3 # => {a: 1, c: 2}

참고로 Rails에도 동일한 메소드가 존재합니다.

Reference

Set#compare_by_identity, Set#compare_by_identity?가 추가

Set에서 값을 비교하는 것이 아닌, 같은 객체인지를 비교하는 메서드가 추가되었습니다.

a1, a2 = "a", "a"
b1, b2 = "b", "b"
c = "c"
array = [a1, b1, c, a2, b2]

iset = Set.new.compare_by_identity
iset.merge(array) # => #<Set: {"a", "b", "c", "a", "b"}>
array.map(&:object_id).sort == iset.map(&:object_id).sort # => true

set = Set.new
set.merge(array) # => #<Set: {"a", "b", "c"}>
array.map(&:object_id).sort == iset.map(&:object_id).sort # => false

Reference

String.new(capacity: size)

성능을 위해서 기존의 동적인 리사이즈가 아닌 생성 시점에 버퍼 사이즈를 넘겨주는 것으로 긴 문자열 처리 성능을 향상시킵니다. 벤치마킹 코드를 여기에서 참조했습니다.

require "benchmark/ips"

Benchmark.ips do |bench|
  bench.report("Without capacity") do
    append_me = " " * 1_000
    template  = String.new

    100.times { template << append_me }
  end

  bench.report("With capacity") do
    append_me = " " * 1_000
    template  = String.new(capacity: 100_000)

    100.times { template << append_me }
  end

  bench.compare!
end

# Warming up --------------------------------------
#     Without capacity     1.250k i/100ms
#        With capacity     2.267k i/100ms
# Calculating -------------------------------------
#     Without capacity     13.323k (± 5.3%) i/s -     67.500k in   5.081524s
#        With capacity     23.060k (± 7.7%) i/s -    115.617k in   5.043542s
# 
# Comparison:
#        With capacity:    23059.8 i/s
#     Without capacity:    13322.5 i/s - 1.73x  slower

지정한 버퍼 사이즈를 넘는 경우, 버퍼를 지정하지 않은 경우와 동일하게 리사이징을 수행합니다.

Reference

Regexp#match?

전역 변수를 변경하지 않는 패턴 매칭 메소드입니다.

regexp = /\d+-\d+-\d+/
regexp.match?('2016-09-01') # => true
$~ # => nil

regexp.match('2016-09-02')  # => true
$~[0] # => '2016-09-02'

regexp =~ '2016-09-03'      # => true
$~[0] # => '2016-09-03'

regexp === '2016-09-04'     # => true
$~[0] #=> '2016-09-04'

regexp.match?('2016-09-01') # => true
# 변경되지 않음
$~[0] #=> '2016-09-04'

MatchData 미사용 / 전역변수 미사용 등의 이유로 기존의 match보다 빠르다고 합니다.

Reference

Symbol#match의 반환값이 MatchData로 변경됨

String#match, Regexp#match와 다르게 매칭이 발생한 인덱스를 반환하던 동작을 다른 것과 동일하게 수정됩니다.

"".match(//) # => #<MatchData "">
//.match("") # => #<MatchData "">
//.match(:"") # => #<MatchData "">

:"".match(//) # => 0 until 2.3
:"".match(//) # => #<MatchData ""> from 2.4

Reference

MatchData#named_captures

루비의 정규표현식에서는 이름 있는 캡쳐를 지원하고 있습니다만, 이 메소드는 캡쳐된 것들을 좀 더 쉽게 반환받을 수 있게 해줍니다. 일반 캡쳐의 경우에는 반환해주지 않습니다.

'12'.match(/(?<a>.)(?<b>.)(?<c>.)?/).named_captures
# => {"a"=>"1", "b"=>"2", "c"=>nil}

'12'.match(/(.)(.)(.)?/).named_captures
# => {}

Reference

MatchData#values_at이 이름 있는 캡쳐를 지원

지금까지는 인덱스 값을 넘겨서 값을 가져올 수 있었습니다만, 이제는 캡쳐된 이름을 넘겨도 값을 돌려받을 수 있습니다.

# Ruby 2.3
'12'.match(/(.)(.)(.)?/).values_at(0,1)
# => ["12", "1"]

# Ruby 2.4
'12'.match(/(?<a>.)(?<b>.)(?<c>.)?/).values_at(:a, :b)
'12'.match(/(?<a>.)(?<b>.)(?<c>.)?/).values_at("a", "b")
# => ["1", "2"]

Reference

IRB binding 추가

이제 binding.irb를 사용해서 Pry의 binding.pry와 동일하게 세션을 열 수 있게 됩니다.

require "irb"

s = "hello"
puts s

binding.irb

Reference

Logger.new에 키워드 인수 추가

level, progname, datetime_format, formatter 인수가 추가되었습니다.

require "logger"

# until 2.3
logger = Logger.new($stdout)
logger.level = :info

# from 2.4
logger = Logger.new($stdout, level: :info)

이전에는 생성 후에 넘겨줘야 했던 값들이 키워드 인수로 들어가서 코드를 좀 더 보기 좋게 작성할 수 있게 되었습니다.

Reference

CSV의 새 옵션

표준에 맞지 않는 CSV의 파싱을 허용하는 liberal_parsing 옵션이 추가되었습니다.

input = '"Johnson, Dwayne",Dwayne "The Rock" Johnson'
CSV.parse_line(input)
# => CSV::MalformedCSVError

CSV.parse_line(input, liberal_parsing: true)
# => ["Johnson, Dwayne", 'Dwayne "The Rock" Johnson']

Dir.empty?, File.empty?, Pathname#empty?

디렉터리와 파일이 비어있는지 확인할 수 있는 메소드들이 추가되었습니다.

Dir.empty?("some_dir") # => true or false
File.empty?("file") # => true or false
Pathname("foo").empty? # => true or false

참고로 File.empty?File.zero?의 별칭입니다.

IO#gets, IO#readline, IO#each_line, IO#readlines, IO#foreach이 chomp 플래그를 키워드 인수로 받을 수 있게 변경

chomp 옵션을 통해서 개행 문자를 제거할 수 있게 되었습니다.

File.read("temp.txt")
# => "1234\n5678\n9010\n"

File.open("temp.txt") do |f|
  f.readlines
  # => ["1234\n", "5678\n", "9010\n"]
end

File.open("temp.txt") do |f|
  f.readlines(chomp: true)
  # => ["1234", "5678", "9010"]
end

\n을 매 행마다 뒤에 붙이는 것은 POSIX의 “Line” 정의대로라면 올바른 동작이지만 처리하다보면 결국 매번 chomp해야하므로, 처음부터 처리된 상태로 받을 수 있도록 해줍니다.

Reference

Kernel#clonefreeze 플래그를 키워드 인수로 받음

clone은 객체를 얕은 복사한 뒤 반환합니다. 원본이 얼려진 상태라면 열려진 상태로 반환하는데, 2.4에서는 이 플래그를 사용해서 얼리지 않은 객체 사본을 얻을 수 있게 됩니다. 지금까지는 한번 얼린 객체의 안 얼린 사본을 얻을 방법이 (코어 레벨에서는) 존재하지 않았습니다.

a = Object.new
def a.b; 2 end

a.freeze
c = a.clone
c.frozen? # => true
c.b       # => 2

d = a.clone(freeze: false)
def d.e; 3; end

d.frozen? #=> false
d.b       # => 2
d.e       # => 3

Reference

Warning 모듈 추가

경고 메시지를 처리하는 Warning 모듈이 추가됩니다. 이를 통해 외부 라이브러리가 경고 메시지를 처리할 수 있게 됩니다.

module Warning
  # only one class method of this.
  def self.warn(msg)
    warnings << msg
    super
  end

  # save occured warnings
  def self.warnings
    @warnings ||= []
  end
end

Warning.warnings # => []

Fixnum == Integer
# (irb):31: warning: constant ::Fixnum is deprecated
Warning.warnings
# => ["(irb):31: warning: constant ::Fixnum is deprecated\n"]

Reference

Etc

단항 연산자 &에서도 Refinement가 적용되도록 변경

이전 버전까지는 Refinement로 정의한 메소드를 & 연산자를 사용해서 사용할 수 없었습니다. 2.4부터는 이러한 접근을 하는 경우에도 정상적으로 동작하도록 문법이 변경되었습니다. 예제를 보시죠.

module Example
  refine String do
    def pugs
      puts "Pugs!"
    end
  end
end

using Example

puts "with directly call"
('1'..'3').map { |s| s.pugs }

# Work with 2.4, NoMethodError < 2.4
puts "with & operator"
('1'..'3').map(&:pugs)

Reference

모듈에 Refinement를 사용할 수 있게 됨

Refinement는 클래스에 대해서만 사용할 수 있었습니다. 이를 모듈에도 사용할 수 있도록 확장됩니다.

module Extension
  refine Kernel do
    def foo
      puts "bar"
    end
  end
end

using Extension
# => `refine': wrong argument type Module (expected Class) (TypeError)
# => Success in 2.4

Reference

send로 호출하는 경우에도 Refinement가 적용되도록 변경

이전 버전까지는 Refinement로 정의한 메소드를 send 메소드를 통해서 사용할 수 없었습니다. 2.4부터는 이 방식이 동작하도록 문법이 변경되었습니다. 예제를 보시죠.

module Example
  refine String do
    def pugs
      puts "Pugs!"
    end
  end
end

using Example

puts "with directly call"
"dog".pugs

# Work with 2.4.0, NoMethodError < 2.4
puts "with send"
"dog".send(:pugs)

Refinement 명세적으로는 기존의 동작이 올바르나, 많은 사람들이 send가 일반 메소드 호출과 동등한 동작을 할 것이라고 기대하기 때문에 명세를 변경하기로.

Reference

Module.used_modules

현재 스코프에서 사용중인 Refinement 모듈의 목록을 가져오는 메소드가 추가됩니다.

module Re1
  refine String do
  end
end

module Re2
  refine Integer do
  end

  refine String do
  end
end

using Re1
p Module.used_modules # => [Re1]
using Re2
p Module.used_modules # => [Re2, Re1]

Reference

쿠키 구분자

RFC2965에서는 ;,를 허용했지만 RFC6265에서는 이제 ;만을 허용합니다. 이에 따라 루비의 CGI와 WEBrick에서도 ;만을 허용하도록 변경됩니다.

Reference

IPAddr#==, IPAddr#<=>가 에러를 발생하지 않게 됨.

앞으로는 false를 반환하게 됩니다.

IPAddr.new("1.1.1.1") == "sometext"
# until 2.3
# => IPAddr::InvalidAddressError: invalid address
# from 2.4
# false

Reference

Net::HTTP.post가 추가됨

require "net/http"
require "uri"

Net::HTTP.post URI("http://www.example.com/api/search"),
               { "q" => "ruby", "max" => "50" }.to_json,
                 "Content-Type" => "application/json"

기존의 http_form 메소드가 있지만 이 메소드는 파라미터를 application/x-www-form-urlencoded로 인코딩한다는 전제로 작성되어 있습니다. 반면 이쪽은 요청의 본문을 사용하기 때문에 JSON 등을 넘길 수 있으며, 헤더값도 지정할 수 있습니다.

OptionParser#parseinto 옵션이 추가

이를 통해서 파싱한 옵션 목록을 넘겨준 해시를 통해 얻어올 수 있게 되었습니다.

require "optparse"
require "optparse/date"
require "optparse/uri"

config = {}

cli =
  OptionParser.new do |options|
    options.define("--from=DATE", Date) do |from|
      config[:from] = from
    end

    options.define("--url=ENDPOINT", URI) do |url|
      config[:url] = url
    end

    options.define("--names=LIST", Array) do |names|
      config[:names] = names
    end
  end

args = %w[
    --from  2016-02-03
    --url   https://blog.blockscore.com/
    --names John,Daniel,Delmer
  ]

cli.parse(args, into: config)

config
# {
#   from: Date.parse("2016-02-03"),
#   url: URI("https://blog.blockscore.com/"),
#   names: %w(John Daniel Delmer)
# }

Readline.quoting_detection_proc, Readline.quoting_detection_proc= 추가

내부에서 이스케이프 판정을 위해서 사용하던 rl_char_is_quoted_pquoting_detection_proc로 노출시켜 원하는 판정 로직을 사용할 수 있게 되었습니다.

require "readline"

completion_text = nil
Readline.completion_proc = -> (text) do
  completion_text = text
  ["completed"]
end
Readline.completer_word_break_characters = " "
Readline.completer_quote_characters = "\"'"
Readline.quoting_detection_proc = -> (text, index) do
  index > 0 && text[index-1] == "\\"
end

while line = Readline.readline("> ") do
  p line
  p completion_text
  completion_text = nil
end

예제에서는 검출 대상이 이스케이프 되어 있는지를 확인하는 람다를 주입합니다. 그리고 \ 를 포함하는 단어를 자동완성할 수 있게끔 합니다. 동작은 다음과 같습니다.

# normal excution
> self\ fil<tab>
# => fil 검출 성공

# with above code
> self\ fil<tab>
# => self\ fil 검출 성공

Reference

Thread#report_on_exception, Thread.report_on_exception 플래그 추가

다른 스레드에서 발생한 예외를 보고할지 말지를 지정하는 플래그가 추가되었습니다.

def some_thread_work
  puts "Starting some parallel work"
  thread =
      Thread.new do
        sleep 0.1
        fail "something very bad happened!"
      end
  sleep 0.2
  puts "Done!"
end

some_thread_work
# => Starting some parallel work
# => Done!

Thread.report_on_exception = true
some_thread_work
# => Starting some parallel work
# => #<Thread:0x007f8de204f070@(irb):6 run> terminated with exception:
# => (irb):8:in `block in some_thread_work': something very bad happened! (RuntimeError)
# => Done!

Reference

CLOCK_MONOTONIC_RAW_APPROX, CLOCK_UPTIME_RAW, CLOCK_UPTIME_RAW_APPROX 지원

macOS 10.12(Sierra)에서 clock_gettime()가 대응되어서 Process.clock_gettimeclock_id 용 상수를 지원하는 플랫폼에 macOS 10.12가 추가되었습니다. 또한 이에 따라 CLOCK_MONOTONIC_RAW_APPROX, CLOCK_UPTIME_RAW, CLOCK_UPTIME_RAW_APPROX라는 상수 플래그를 추가했습니다.

RubyVM::Env이 제거됨

사용자가 직접 사용할 일이 없는 상수였던 RubyVM::Env가 제거되고 내부 객체로 편입되었습니다.

RubyVM::Env
# => RubyVM::Env in 2.3
# => NameError: uninitialized constant RubyVM::Env in 2.4

Reference

TracePoint#callee_id

실제 메소드 이름과 이를 호출한 메소드 이름을 명확히 구분하기 위해 callee_id가 추가됩니다.

def some_method
end
alias aliased_method some_method

TracePoint.new(:call) do |tp|
  p [tp.method_id, tp.callee_id] # => [:some_method, :aliased_method]
end.enable do
  aliased_method
end

기존에도 호출한 메소드를 가져올 수 있었지만, 느리고 CFUNC의 호출 메소드 이름을 가져올 수 없다는 문제가 있었다고 합니다.

Reference

지원 플랫폼 변경

FreeBSD 4 미만은 더이상 지원되지 않습니다.

Improvements

Array#maxArray#min의 성능 개선

배열을 위한 전용 #max, #min이 추가되었습니다. 이는 특정 조건에서 임시 배열을 생성하지 않습니다.

벤치마크 코드를 보죠.

# from https://blog.blockscore.com/new-features-in-ruby-2-4/
# but there is some change.

require 'benchmark/ips'

Benchmark.ips do |bench|
  NUMS = 100.times.map { rand(100) }

  # By binding the Enumerable method to our array
  # we can bench the previous speed in Ruby 2.3
  ENUM_MIN  = Enumerable.instance_method(:min).bind(NUMS)

  # Bind the `#min` method to our test array also
  # so our benchmark code is as similar as possible
  ARRAY_MIN = Array.instance_method(:min).bind(NUMS)

  bench.report('Array#min') do
    ARRAY_MIN.call
  end

  bench.report('Enumerable#min') do
    ENUM_MIN.call
  end

  bench.compare!
end

링크에서 가져온 벤치를 약간 수정했습니다. 릴리스 노트대로라면 해당 알고리즘을 사용하는 조건 중에 길이가 0x100 이하(256)인 경우라는 조건이 언급되어 있기 때문입니다.

직접 2.3.3과 2.4.0에서 실행해본 결과는 다음과 같습니다.

Warming up --------------------------------------
           Array#min   118.546k i/100ms
      Enumerable#min    33.764k i/100ms
Calculating -------------------------------------
           Array#min      1.905M (± 3.4%) i/s -      9.602M in   5.046874s
      Enumerable#min    382.884k (± 3.1%) i/s -      1.925M in   5.031430s

Comparison:
           Array#min:  1904793.1 i/s
      Enumerable#min:   382883.6 i/s - 4.97x  slower

결과는 이렇습니다. 링크에서 언급하는 결과와는 다르게 좀 더 많이 빨라진 것을 볼 수 있습니다.

열거용 메소드를 Array에 넣는 것이 좋은 아이디어는 아니지만, Math#max와 같은 메소드가 없어서 [a, b].max와 같은 호출을 자주 하게 되며, 대량의 데이터에서 최소/최대값을 구하는 작업은 꽤 빈번하므로 추가합시다, 라는 제안이 수락된 결과.

Reference

Enum.sum, Array#sum

다른 점은 배열에 최적화가 되어있나 아니냐의 차이입니다.

[1, 2, 3].sum    # => 6
[1, 2, 3].sum(1) # => 7

["1", "2", "3"].sum("") # => "123"

초기값은 0이며 첫번째 인자로 변경할 수 있습니다.

서드파티 라이브러리에서도 동일한 이름의 메소드를 정의했을 가능성이 있습니다. 대부분 동일한 동작을 할 것이므로 큰 문제는 없습니다만, 문제 없음을 보장해주지는 않습니다. i.e. ActiveSupport

Reference

스레드 교착상태 탐지 개선

루비는 대기 중인 스레드의 교착상태를 탐지할 수 있었지만, 디버깅을 위한 충분한 정보가 리포트에 포함되지 않았습니다. 루비 2.4의 교착상태 탐지는 스레드의 백트레이스와 의존하고 있는 스레드에 대한 정보를 보여주게 됩니다.

Thread.current.name = "MainThread"
z = Thread.new { Thread.stop }
a, b = Thread.new { 1 until b; b.join }, Thread.new { 1 until a; a.join }
a.name = "aaaaa"
b.name = "bbbbb"
z.name = "zzzz"
a.join

교착상태를 발생시키는 코드를 이전 버전에서 실행할 경우,

fatal: No live threads left. Deadlock?
	from (irb):9:in `join'
	from (irb):9
	from /Users/user/.rbenv/versions/2.3.1/bin/irb:11:in `<main>'

2.4에서 실행할 경우,

fatal: No live threads left. Deadlock?
4 threads, 4 sleeps current:0x007fef09406bc0 main thread:0x007fef09406bc0
* #<Thread:0x007fef0987caf8@MainThread sleep_forever>
   rb_thread_t:0x007fef09406bc0 native:0x007fff79c40000 int:0
   (irb):40:in `join'
   (irb):40:in `irb_binding'
   /Users/user/.rbenv/versions/2.4.0-preview3/lib/ruby/2.4.0/irb/workspace.rb:87:in `eval'
   . . .
   /Users/user/.rbenv/versions/2.4.0-preview3/bin/irb:11:in `<main>'
* #<Thread:0x007fef09844428@zzzz@(irb):35 sleep_forever>
   rb_thread_t:0x007fef09769220 native:0x00700000108000 int:0
   (irb):35:in `stop'
   (irb):35:in `block in irb_binding'
* #<Thread:0x007fef0a04e650@aaaaa@(irb):36 sleep_forever>
   rb_thread_t:0x007fef0975f970 native:0x0070000020b000 int:0
    depended by: tb_thread_id:0x007fef09406bc0
    depended by: tb_thread_id:0x007fef09761ac0
   (irb):36:in `join'
   (irb):36:in `block in irb_binding'
* #<Thread:0x007fef0a04e560@bbbbb@(irb):36 sleep_forever>
   rb_thread_t:0x007fef09761ac0 native:0x0070000030e000 int:0
    depended by: tb_thread_id:0x007fef0975f970
   (irb):36:in `join'
   (irb):36:in `block in irb_binding'

	from (irb):40:in `join'
	from (irb):40
	from /Users/user/.rbenv/versions/2.4.0-preview3/bin/irb:11:in `<main>'

이와 같이 좀 더 상세한 정보를 얻을 수 있게 됩니다.

Reference

해시 테이블 성능 개선

해시 테이블(st_table)의 내부 구조에는 양방향 연결 리스트를 사용하고 있었습니다만, 이 대신 삽입순 배열과 개방 주소법이 도입되었습니다.

간단하게 요약하자면 최근 CPU에서는 복층 캐시 덕분에 메모리 상의 가까운 곳에 있을 수록 데이터를 빠르게 읽어올 수 있으므로, 기존의 방식보다는 이번에 도입한 방식의 성능이 좋다고 합니다. CPython, PHP(!), GCC는 이미 이 방식으로 갈아탔다고 하네요.

관심과 시간이 있으시다면 이슈에서 벌어진 토론을 직접 읽어보시면 좋을거 같습니다.

Reference

Conclusion

여기까지 루비 2.4에서 예정된 변경점을 살펴봤습니다. 어떠셨나요? 개인적으로는 Integer 통합과 round의 동작 변경이 가장 크게 느껴졌네요. 어딘가 분명 영향이 있지 않을까 하는 걱정도 들고..

마이너 패치답게 전체적으로 실용성 높은 메소드의 추가, 성능 개선이 이루어진 편입니다. 특히 해시의 성능 개선은 꽤 인상적입니다. 미리미리 2.4 업그레이드에 문제가 될 것은 없는지 확인해서 크리스마스에 배포될 공짜 점심을 기다리는 건 어떨까요? XD

Reference