Ruby 2.3.0 preview 1

By Mario Alberto Chavez November 12, 2015

ruby web development

Ruby 2.3.0 preview 1 was announced and, as usual it includes  plenty of improvements and new features.

Three new features are highlighted in the announcement email:

Did you mean

did_you_mean.gem is an aid to debugging Ruby programs for cases when the error is related to NameError and NoMethodError.

When a Ruby program breaks due to one of these exceptions, you will receive help in the form of a suggestion for a class name, method name or variable name.

class Account
  def initialize
    @full_name = 'John Doe'
  end

  def name
    full_name
  end
end

> account = Acount.new
NameError: uninitialized constant Acount
Did you mean?  Account

> account = Account.new
> account.nams
NoMethodError: undefined method `nams' for #<Account:0x007fb2291db068>
Did you mean?  name

> account.name
NameError: undefined local variable or method `full_name' for #<Account:0x007fb2292b00d8 @full_name="John Doe">
Did you mean?  @full_name

Simple but handy tool for debugging.

Safe navigation operator

If you are a Rails developer you might have used Object#try  in you programs.

Object#try is used to call a method on an object when this object might be nil. Instead of raising an exception, Object#try responds with nil.

@user = nil
@user.profile # this will raise NoMethodError: undefined method `profile' for nil:NilClass

@user.try(:profile) # this will return nil

If you are not using Rails nor including ActiveSupport in your program, then you are left to test if the caller is nil before trying to call a method on it.

@user = nil
@user.profile if !@user.nil? # as a shorthand this could be if @user

Ruby 2.3.0 includes a new operator: Safe navigation operator.

Operator &. will work in the same way as Object#try works in ActiveSupport.

class Profile
  attr_accessor :full_name

  def initialize
    self.full_name = 'John Doe'
  end
end

class User
  attr_accessor :profile

  def build_profile
    self.profile = Profile.new
  end
end

> user = User.new
> puts "Full name: #{user&.profile&.full_name}"
Full name:
> user.build_profile
> puts "Full name: #{user&.profile&.full_name}"
Full name: John Doe

&. will even work with chained calls.

Frozen String Literal Pragma

Ruby 3.0 might have strings being immutable by default, this is a big change that will cause incompatibility and might make harder to upgrade to Ruby 3.0.

In Ruby, having immutable strings is an opt-in functionality but it requires you to explicitly mark any string with Object#freeze.

Freezing strings that will not change in our programs has the advantage of reducing object allocation and memory usage.

# memory.rb
require 'get_process_mem'

mem = GetProcessMem.new
GC.start
GC.disable

# Not freeze strings
before_mem = mem.mb
before_stats = GC.stat
1_000.times { 'hello' }
after_stats = GC.stat
after_mem = mem.mb

delta_allocated_objects = after_stats[:total_allocated_objects] - before_stats[:total_allocated_objects]
delta_memory = after_mem - before_mem

puts "Without frozen strings, allocated objects: #{delta_allocated_objects} - used memory: #{delta_memory}"


# Freeze strings
before_mem = mem.mb
before_stats = GC.stat
1_000.times { 'hello'.freeze }
after_stats = GC.stat
after_mem = mem.mb

delta_allocated_objects = after_stats[:total_allocated_objects] - before_stats[:total_allocated_objects]
delta_memory = after_mem - before_mem

puts "With frozen strings, allocated objects: #{delta_allocated_objects} - used memory: #{delta_memory}"

GC.enable
# memory.rb

> ruby memory.rb
Without frozen strings, allocated objects: 1001 - used memory: 0.0390625
With frozen strings, allocated objects: 1 - used memory: 0.0

Ruby 2.3.0 helps you prepare for the string change in Ruby 3.0.

First, it introduces a pragma that we can use per ruby file to tell Ruby that we want to freeze strings by default: # frozen_string_literal: true

# string_change.rb
class StringChange
  def change(value)
    value << ' changed'
  end
end

# string_frezze.rb
# frozen_string_literal: true
load './string_change.rb'

class StringFreeze
  def hit_change
    value = 'freeze'
    new_value = StringChange.new.change(value)

    puts new_value
  end
end

StringFreeze.new.hit_change

Running this program with ruby string_frezze.rb gives you the following error:

$ ruby string_freeze.rb                                                                                                                        
/Users/mariochavez/Development/temp/string_change.rb:4:in `change': can't modify frozen String (RuntimeError)
    from string_freeze.rb:8:in `hit_change'
    from string_freeze.rb:14:in `<main>'

string_frezze.rb which include the pragma, creates all strings literals frozen but string_change.rb tried to change the string.

For a very small program it is easy to see where the string was created and where there was an attempt to modify it. For larger programs, this might not be that intuitive. This is way the --enable-frozen-string-literal-debug flag was introduced.

If you run the same program with the --enable-frozen-string-literal-debug flag, then the error is more helpful.

$ ruby --enable-frozen-string-literal-debug string_freeze.rb                                                                                   
/Users/mariochavez/Development/temp/string_change.rb:4:in `change': can't modify frozen String, created at string_freeze.rb:7 (RuntimeError)
    from string_freeze.rb:8:in `hit_change'
    from string_freeze.rb:14:in `<main>'

It not only tells you where the attempt to change the string was done, but also where the string was originally created.

Finally for those who want to run their programs as if they were running on Ruby 3.0 - without the need of pragma to freeze strings - you can run your programs with --enable-frozen-string-literal.

ruby --enable-frozen-string-literal --enable-frozen-string-literal-debug string_freeze.rb

Conclusions

Its always nice to receive new Ruby versions more than improvements or performance benefits, but also with new features and functionality. As Matz mentioned during his RubyConf 2014 keynote:

We have to feed the sharks, if not they will go away for new shining things.

But in this case, is especially important to have the chance to try and start preparing for the future, easing as much as possible, the transition to Ruby 3.0.

If you want to play with Ruby 2.3.0 preview1, in the announce email you will find the link to download it. If you are using rbenv and/or ruby-build you can use this custom definition to install it.

# 2.3.0-preview1
install_package "openssl-1.0.1p" "https://www.openssl.org/source/openssl-1.0.1p.tar.gz#bd5ee6803165c0fb60bbecbacacf244f1f90d2aa0d71353af610c29121e9b2f1" mac_openssl --if has_broken_mac_openssl
install_package "ruby-2.3.0-preview1" "http://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.0-preview1.tar.gz#dc8f9d48392a2bb226df5f4b4fd2074d81af155cdf3f3799139a6e31e012aefe" ldflags_dirs autoconf standard verify_openssl

Just save it to a file, like 2.3.0-preview1, and install it with the following command:

$ rbenv install ./2.3.0-preview1

Enjoy Ruby!


Mario Alberto Chavez

Software Engineer