Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Ruby 2 in Ruby on Rails

Ruby 2 in Ruby on Rails

Slides for my keynote "Ruby 2 in Ruby on Rails" at RedDot RubyConf 2017 in Singapore https://www.reddotrubyconf.com/#amatsuda #rdrc2017

Akira Matsuda

June 23, 2017
Tweet

More Decks by Akira Matsuda

Other Decks in Programming

Transcript

  1. A Ruby Committer Who lives in a "Ruby Village" in

    Tokyo "Many of the core contributors like Koichi Sasada, Shyouhei Urabe, Yui Naruse, Zachary Scott and Akira Matsuda live within 10-15 minutes of each other" https:/ /appfolio-engineering.squarespace.com/appfolio- engineering/2017/5/24/how-is-ruby-different-in-japan
  2. pp self name: Akira Matsuda GitHub: amatsuda Twitter: @a_matsuda From:

    Japan " Ruby: committer Rails: committer Gems: many RubyKaigi: the organizer
  3. Today I'm Going to
 Show You The new features that

    the Ruby team recently introduced to the language How the Rails framework uses these new features Some of my works as a "committer of both"
  4. Ruby & Rails Timeline Rails Ruby 2.0.0 - Feb. 24,

    2013 4.0.0 - Jun. 25, 2013 2.1.0 - Christmas, 2013 4.1.0 - Apr. 08, 2014 4.2.0 - Dec. 20, 2014 2.2.0 - Christmas, 2014 2.3.0 - Christmas, 2015 5.0.0 - Jun. 30, 2016 2.4.0 - Christmas, 2016 5.1.0 - Apr. 27, 2017
  5. Ruby & Rails Timeline Rails Ruby 2.0.0 - Feb. 24,

    2013 4.0.0 - Jun. 25, 2013 2.1.0 - Christmas, 2013 4.1.0 - Apr. 08, 2014 4.2.0 - Dec. 20, 2014 2.2.0 - Christmas, 2014 2.3.0 - Christmas, 2015 5.0.0 - Jun. 30, 2016 2.4.0 - Christmas, 2016 5.1.0 - Apr. 27, 2017
  6. Ruby & Rails Timeline Ruby 2 and Rails 4 came

    together Rails 5 supports only Ruby 2.2+
  7. This Means... Rails 5.x (master) codebase can use Ruby <=

    2.2 features Rails 5.x (master) codebase still cannot use Ruby 2.3, 2.4, 2.5 features
  8. New Things in Ruby 2 Core features Module#prepend, public include,

    Refinements, keyword arguments, frozen Strings, Symbol GC Syntax changes private def, %i, &., <<~, String#-@
  9. Initial Implementation of `alias_method_chain` https:/ /github.com/ rails/rails/commit/ 794d93f7a5e class Module

    def alias_method_chain(target, feature) alias_method "#{target}_without_#{feature}", target alias_method target, "#{target}_with_#{feature}" end end
  10. Usage of AMC gem 'activesupport', '4.2.8' require 'active_support/core_ext/module/aliasing' class C

    def foo() p:foo; end end class C def foo_with_bar() foo_without_bar; p:bar; end alias_method_chain :foo, :bar end C.new.foo #=> :foo, :bar
  11. Downside of AMC This feature is basically just a workaround

    Creates some publicly callable intermediate methods `foo_with_bar` and `foo_without_bar` in the example above Behavior of these methods are unpredictable
  12. public Methods gem 'activesupport', '4.2.8' require 'active_support/core_ext/module/aliasing' class C def

    foo() p:foo; end def foo_with_bar() foo_without_bar; p:bar; end alias_method_chain :foo, :bar end p C.instance_methods(false) #=> [:foo, :foo_with_bar, :foo_without_bar]
  13. What's Wrong with These public Methods? gem 'activesupport', '4.2.8' require

    'active_support/core_ext/module/aliasing' class C def foo() p:foo; end def foo_with_bar() foo_without_bar; p:bar; end alias_method_chain :foo, :bar # defining bar first, then baz def foo_with_baz() foo_without_baz; p:baz; end alias_method_chain :foo, :baz end C.new.foo_without_bar #=> :foo
  14. What's Wrong with These public Methods? (2) gem 'activesupport', '4.2.8'

    require 'active_support/core_ext/module/aliasing' class C def foo() p:foo; end def foo_with_baz() foo_without_baz; p:baz; end alias_method_chain :foo, :baz # swapping the definition order of bar and baz def foo_with_bar() foo_without_bar; p:bar; end alias_method_chain :foo, :bar end C.new.foo_without_bar #=> :foo, :baz
  15. Think About This Case You bundled two plugins that extend

    `foo()` method with AMC The "bar" plugin defines a public method `foo_without_bar` But the behavior is unpredictable The behavior depends on the order of plugin loading order So, if you change the sort order in Gemfile, the behavior would change
  16. We Needed a More Robust Monkey-patching Mechanism And so @wycats

    from the Rails team proposed `Module#prepend` as a language core feature
  17. Module#prepend as a Ruby Core Feature (2.0) class C def

    foo() p:foo; end end module M def foo() super; p:bar; end end C.send :prepend, M C.new.foo #=> :foo, :bar
  18. Very Clean Way of Monkey-patching No ugly `foo_with_bar`, `foo_without_bar` methods

    But you still can do that via `super_method` if you really want to The module name appears in `ancestors` Easier to debug
  19. What If We Mix AMC and `prepend`? gem 'activesupport', '4.2.8'

    require 'active_support/core_ext/module/aliasing' class C def foo() p:foo; end end # AMC first class C def foo_with_bar() foo_without_bar; p:bar; end alias_method_chain :foo, :bar end # Then prepend module M def foo() super; p:baz; end end C.send :prepend, M C.new.foo #=> :foo, :bar, :baz
  20. …Is It Working? gem 'activesupport', '4.2.8' require 'active_support/core_ext/module/aliasing' class C

    def foo() p:foo; end end # prepend first module M def foo() super; p:baz; end end C.send :prepend, M # Then AMC class C def foo_with_bar() foo_without_bar; p:bar; end alias_method_chain :foo, :bar end C.new.foo #=> stack level too deep
  21. AMC and `prepend` Cannot Co-exist It's dangerous to use both

    AMC and `prepend` against a single method The result is not only unpredictable Sometimes critical ☠
  22. AMC is Dead Rails terminated the use of AMC in

    Rails 5 Plugin authors must abandon AMC and switch to `prepend` App developers should better refrain from bundling any plugin that still uses AMC
  23. As a Plugin Author We're forced to support `prepend` It's

    hard to maintain a codebase that supports both AMD and `prepend`
  24. Ruby 1's End Many gems stopped supporting Ruby 1.x due

    to this This brought the extinction of Ruby 1.x to the community This is another reason that I like `Module#prepend`
  25. Rails Plugin Authors Use This Idiom Too Often I wanted

    to do this simpler So I proposed to make them public
  26. public `include` Is Available in All Supported Versions of Ruby

    And it's everywhere in Rails' codebase and plugins already You may forget the `send :include` idiom and just use the public `include`
  27. database_rewinder's case (using) # lib/database_rewinder/cleaner.rb using DatabaseRewinder::MultipleStatementsExecutor ... private def

    delete_all(ar_conn, tables, multiple: true) ... if multiple ar_conn.execute_multiple sql ...
  28. A Gem Internal Public Method The `execute_multiple` method in the

    example is - A public method - Visible only inside the gem - Invisible from the application code
  29. Super private Monkey- patching Very useful feature for plugin authors

    / monkey-patchers Since it's "file scoped", we can define any method that is only available inside the plugin
  30. Refinements in Rails I introduced refinements in Active Support 5.1

    codebase To extend `Array#sum` https:/ /github.com/rails/rails/pull/27363
  31. The `Array#sum` Patch - class Array - alias :orig_sum :sum

    - end + # Using Refinements here in order not to expose our internal method + using Module.new { + refine Array do + alias :orig_sum :sum + end + }
  32. In This Case The previous code was defining a public

    `Array#orig_sum` method Although it was an internal method for calculating `Array#sum` The method used to be visible to all Active Support users Refinements made it perfectly "file scoped"
  33. File Scoped!
 File Scoped? While implementing `Array#sum`, I realized that

    it's not actually file scoped! And so I reported what I found https:/ /bugs.ruby-lang.org/issues/13109
  34. This Works # Put a refinement first using Module.new {

    refine Object do def foo() p 'hello'; end end } # Then calling the method defined in the refinement class Object def bar() foo; end end Object.new.bar #=> "hello"
  35. But This Doesn't Work! # Put a method call first

    class Object def bar() foo; end end # Then define the actual method definition in a refinement using Module.new { refine Object do def foo() p 'hello'; end end } Object.new.bar #=> doesnot_work.rb:2:in `bar': undefined local variable or method `foo' for #<Object:0x007f8f2a0251c8> (NameError)
  36. So The Scope Is Not Actually "File Scope" It's "file

    scope + physical position of using" scope I thought it's a bug, but Matz said "it's an intended behavior"
  37. This Behavior Is Sometimes Annoying I said refinements is useful

    for creating a library-internal "super private" method But when you define a private method, you usually put it below the public methods, right? However, refinements definition has to be put at the top of the file
  38. And… This Complex Behavior Is Still Undocumented for Now Maybe

    this behavior is very hard to explain Or maybe because nobody was aware of this beavior Anyway this is the "intended behavior"
  39. Example (Taken from Shugo's RubyKaigi Slides) class BinaryCodedDecimal def +(other)

    figure1 = self.figure figure2 = other.figure ... end protected attr_accessor :figure end
  40. What Matz Thinks About protected "(If I would re-design Ruby)

    now? I'll reject protected" https:/ /twitter.com/yukihiro_matz/status/ 180090910451834881
  41. Usage of protected in Rails Almost everyone misuses `protected` In

    most cases, you can just rewrite your `protected` to `private`, and it should still work
  42. I Made This But I Didn't Push This to The

    Upstream Although all tests were passing Because it caused some unintentional update on the API document
  43. Finally I Pushed Them Last Year Made a separate commit

    per each component Making sure that tests didn't break per each component Adding `:doc:` to all previously `protected` methods Last year, for Rails 5.1
  44. BTW

  45. A Text Editor Written in Ruby Based on curses Ruby

    binding That he created and he still maintains He's going to do a presentation about this at an international Ruby conference
  46. Defining a private Method (1) class Klass ... # Changes

    the visibility of all methods defined below private def private_method; end end
  47. Defining a private Method (2) class Klass ... def private_method;

    end # Changes the visibility of this single method private :private_method end
  48. The private def Syntax class Klass ... # Now we

    can do this in one line.
 # Don't have to repeat the method name! private def private_method; end end
  49. usa's Patch `def` statement used to return nil, but he

    changed it to return the defined method name in Symbol https:/ /github.com/ruby/ruby/commit/0f0b60ea86 `private` (`protected`, `public`) takes a Symbol and changes the method's visibility The patch was written in August 2013
  50. Still Only 17 Occurrencies in Rails! (As of Today) %

    git grep -c "private def" actioncable/test/channel/stream_test.rb:1 actionmailer/test/abstract_unit.rb:2 actionpack/test/abstract_unit.rb:2 actionpack/test/controller/new_base/render_context_test.rb: actionview/test/abstract_unit.rb:2 actionview/test/template/form_helper/form_with_test.rb:1 activemodel/test/cases/helper.rb:2 activerecord/lib/active_record/connection_adapters/ abstract_mysql_adapter.rb:1 activerecord/test/cases/query_cache_test.rb:1 activesupport/test/abstract_unit.rb:2 railties/test/abstract_unit.rb:2
  51. Because There Was a Documentation Problem class Klass def public_method_1;

    end private def private_method; end def public_method_2; end end
  52. The RDoc Problem RDoc couldn't properly generate the document The

    `private` (`protected`, `public`) prefix was changing the scope of all following methods It breaks the Rails API document https:/ /github.com/rdoc/rdoc/issues/355
  53. The RDoc Problem class Klass def public_method_1; end private def

    private_method; end # This method is public and has to appear in the doc, but RDoc was treating this as a private method def public_method_2; end end
  54. Why Did RDoc Have
 Such Bug? Because RDoc has its

    own Ruby parser Nobody made a change to that parser along with the change in Ruby 2.1
  55. lib/rdoc/ruby_lex.rb # coding: US-ASCII # frozen_string_literal: false #-- # irb/ruby-lex.rb

    - ruby lexcal analyzer # $Release Version: 0.9.5$ # $Revision: 17979 $ # $Date: 2008-07-09 10:17:05 -0700 (Wed, 09 Jul 2008) $ # by Keiju ISHITSUKA([email protected]) # #++ require "e2mmap" require "irb/slex" require "stringio" ## # Ruby lexer adapted from irb. # # The internals are not documented because they are scary. class RDoc::RubyLex ...
  56. lib/rdoc/ruby_lex.rb # coding: US-ASCII # frozen_string_literal: false #-- # irb/ruby-lex.rb

    - ruby lexcal analyzer # $Release Version: 0.9.5$ # $Revision: 17979 $ # $Date: 2008-07-09 10:17:05 -0700 (Wed, 09 Jul 2008) $ # by Keiju ISHITSUKA([email protected]) # #++ require "e2mmap" require "irb/slex" require "stringio" ## # Ruby lexer adapted from irb. # # The internals are not documented because they are scary. class RDoc::RubyLex ... WTF
  57. But Why Didn't RDoc Use Ripper? Because Ripper was not

    yet created when Dave Thomas created RDoc
  58. I Tried, But... Shouldn't we rewrite the RDoc Ruby parser

    from the IRB parser to Ripper? I tried to do this before, but gave up Because the RDoc parser is a super chaotic legacy code
  59. Future Work But I brought this to Asakusa.rb, and someone

    is actually working on this The result is supposed to be presented soon at
  60. I Gave Up Replacing The Whole RDoc Parser But I

    could make a tiny patch for RDoc to add `private def` support last year
  61. It Took 3 And Half Years Since `private def` syntax

    has been introduced to Ruby But it's finally unleashed in Rails RDoc >= 5.1 can properly parse `private def` So it won't break the document any longer
  62. Pseudo Keyward Arguments in Rails (As a Hash) def deliver_later(options

    = {}) def form_for(record, options = {}, &block) def link_to_if(condition, name, options = {}, html_options = {}, &block)
  63. Pseudo Keyward Arguments in Rails Uses a Ruby Hash to

    pass in arguments with names Method calls apparently look better than something like
 `form_for(@user, nil, false, :post, false, nil, false)`
  64. Pseudo Keyward Arguments in Rails But OTOH the method definition

    looks miserable The argument list becomes like "options as any Hash", or even "*args" It tells nothing unless we write a thorough documentation comment So hard for this non-statically typed language
  65. Giving a Default Option Value # actionpack/lib/action_dispatch/routing/redirection.rb def redirect(*args, &block)

    options = args.extract_options! status = options.delete(:status) || 301 ...
  66. Giving a Default Option Value The defaults have to be

    manipulated inside the method And they have to be properly documented outside of the method
  67. Verifying Option Keys # activerecord/lib/active_record/nested_attributes.rb def accepts_nested_attributes_for(*attr_names) options = {

    allow_destroy: false, update_only: false } options.update(attr_names.extract_options!) options.assert_valid_keys(:allow_destroy, :reject_if, :l imit, :update_only)
  68. assert_valid_keys Raises when an unexpected key was given Again, have

    to be declared inside the method, and be documented outside We very often forget to do this Then unhandled keys would just be ignored
  69. Ruby 2 Has The Built-in Keyword Arguments! Since 2.0 Proposed

    and implemented by mame, the insane genius
  70. Rewriting with Ruby 2 Kwargs (before) # activesupport/lib/active_support/message_verifier.rb def initialize(secret,

    options = {}) raise ArgumentError, "Secret should not be nil." unless secret @secret = secret @digest = options[:digest] || "SHA1" @serializer = options[:serializer] || Marshal end
  71. Rewriting with Ruby 2 Kwargs (patch) -def initialize(secret, options =

    {}) +def initialize(secret, digest: 'SHA1', serializer: Marshal) raise ArgumentError, "Secret should not be nil." unless secret @secret = secret - @digest = options[:digest] || 'SHA1' - @serializer = options[:serializer] || Marshal + @digest = digest + @serializer = serializer end
  72. Merits of Ruby 2 Kwargs The logic becomes simple (obviously)

    The behavior becomes more strict (invalid keys, missing keys, etc.) Runs faster (in most cases, I guess) Creates less garbage objects (maybe) The method definition becomes a great documentation (most important IMO)
  73. A Branch that I Created for Experimenting Kwargs in Rails

    https:/ /github.com/amatsuda/rails/tree/ kwargs Back in Jan. 2013 Before Ruby 2.0.0 stable release In order to experiment the new feature in a real codebase
  74. Then I Found A Problem While Experimenting This Some methods

    in Rails take Ruby keyword as an option name
 (e.g. if, unless, end)
  75. But We Can Never Access The Given Parameter def x(if:

    nil) p if end x if: true #=> syntax error, unexpected keyword_end
  76. Kwargs Introduced A New Way to Define An Unaccessible Lvar

    `if = 1` is not a valid Ruby code, so this was never possible Kwargs opened a new possibility!
  77. I Broght This Problem to ruby-core (Couldn't find a ticket.

    Maybe at the developers' meeting, or maybe I discussed with ko1 or mame in person?) Then we introduced a new feature in 2.1
  78. You Can Also Set def x(nil: 1) binding.local_variable_set :nil, 2

    p binding.local_variables p nil p binding.local_variable_get :nil end x #=> [:nil], nil, 2
  79. Kwags as a 1st Citizen API in Rails Should run

    faster Makes the code more readable And generates a better API document Rails master now supports only >= 2.1 (actually 2.2.2) that does have `local_variable_get`
  80. require_relative and Rails This feature was originally introduced to Ruby

    because Ruby excluded '.' from `$LOAD_PATH` Because '.' in `$LOAD_PATH` may be a potential security risk A "Rails effect" to the Ruby language
  81. require_relative May Run Faster Than require Because it's a very

    simple implementation It directly requires a file from `rb_current_realfilepath` Instead of scanning the file through all `$LOAD_PATH` entries
  82. require_relative Patch for Rails Another huge patch from me that

    was never pushed to the upsteram https:/ /github.com/amatsuda/ rails/compare/ master...require_relative
  83. Why Didn't I Push This Because I couldn't see any

    significant performance improvement
  84. Why No Perf Improvement? I haven't investigated deeper, but ko1

    said maybe because Bundler is doing a good work(?) Or maybe I had better use a slow HDD when benchmarking?
  85. Yomikomu by ko1 (2015) An experimental gem https:/ /github.com/ko1/yomikomu No

    significant perf improvement on real-world apps (ko1@RailsConf 2016)
  86. bootsnap (2017) Shopify's refined version of yomikomu With actual perf

    improvement! Proposed to be bundled in Rails' default Gemfile https:/ /github.com/rails/rails/pull/29313 Still misterious
  87. Encoding in Ruby Every String object has its encoding Ruby

    can handle various encodings `Encoding.list.size` #=> 101 Even matz had been using non-utf8 Encoding until several years ago
  88. Encoding in Rails Rails has to face with various encodings

    Such as server OS encoding, DB encoding, program file encoding, HTTP request encoding, URL encoding, Cookie encoding Rails is a web framework, and the recent web defacto encoding is UTF-8 Rails deals with YAML and JSON files, both of which supports only UTF (usualy UTF-8)
  89. Basic Strategy Keep every String in the Rails world be

    UTF-8 Convert all non-UTF-8 Strings to UTF-8 at the boundary between the outside and Rails
  90. Rails Needs to Be Defensive Against Inputs from Outside For

    example we're validating URLs not to include any broken (possibly maliscious) UTF-8 chars
  91. Rails Seems to Be Supporting Multiple Encodings But nobody actually

    uses any other encoding than UTF-8, even in Japan I'm willing to drop multiencodings support from Action View I told this last year, but still I haven't published a patch I'm sorry, I'm so lazy...
  92. ActiveSupport::Multibyt e Active Support includes an original multibyte chars handling

    utilities Since Ruby 1.8 era Includes a huge set of Unicode data in Active Support for this feature AS/values/unicode_tables.dat
  93. Improvements in Ruby 1.9: Code Set Independent Encoding system 2.0:

    UTF-8 script encoding by default 2.2: Unicode normalization 2.4: Non-ASCII case conversion 2.4: Unicode 9.0 support in Regexp \X
  94. We're Ready to Remove Active Support Original Multibyte Library We

    can do everything using the language core features We can remove unicode_tables.dat from Active Support Smaller gem size Faster boot time Less memory consumption Multibyte chars handling will become faster Because it's implemented in C in Ruby No longer need to maintain that misterious code in Active Support
  95. Actually, We Have to Drop It As Soon As Possible

    Because @norman the long time maintainer has "completely retired from open source" And so I became the main maintainer of Haml after him
  96. Removing ActiveSupport::Multibyte Rails 5 still supports Ruby < 2.4 It

    can't be completely removed in Rails 5 But we can use Ruby native implementation if RUBY_VERSION >= '2.4' We made a PR for this already https:/ /github.com/rails/rails/pull/28067
  97. String#freeze PRs Got so many "I froze some String literals"

    PRs in the past few years Because freezing Strings resulted in good micro- benchmark results
  98. No Thank You, This Looks Too Ugly... # inflector/methods.rb def

    underscore(camel_cased_word) return camel_cased_word unless /[A-Z-]|::/.match? (camel_cased_word) word = camel_cased_word.to_s.gsub("::".freeze, "/".freeze) word.gsub!(/(?:(?<=([A-Za-z\d]))|\b) (#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'.freeze }#{$2.downcase}" } word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze) word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze) word.tr!("-".freeze, "_".freeze) word.downcase! word end
  99. So, I Proposed to Introduce a New "Magic Comment" in

    2015 To be more accurate, I proposed to reconsider akr's proposal once rejected in 2013 https:/ /bugs.ruby-lang.org/issues/ 8976#note-30 The proposal was accepted, and included in Ruby 2.3
  100. The "fstring" Literal Maybe you've never heard of this before

    Actually a little bit faster than calling `String#freeze` method Very rarely used, but introduced in ERB recently by @k0kubun
  101. I Would Like to Put The Magic Comment to All

    Files in Rails But not now, because Rails 5.x still supports Ruby 2.2 This can be done when Rails drops Ruby 2.2 support in the next major
  102. Rails 5 Was Decided to Support Ruby 2.2+ Because of

    This Feature GC collects dynamically created Symbols Disables Symbol fixation attack
  103. Another Thing We Might Be Able to Do Reduce redundunt

    Stringifications May cause some compatibility issues but worth trying I haven't done anything yet though
  104. Fixnum & Bignum => Integer Use of Fixnum and Bignum

    has been deprecated We saw massive number of warnings in the gems Almost fixed in major libraries
  105. If you're Still Seeing the Warning Update bundled gems Patch

    it and PR if master is still not fixed Unbundle it and find a maintained alternative if your PR is left unmerged
  106. Array#append, Array#prepend (2.5) "Active Support can serve as an experimental

    lab for future core features in Ruby ❤" https:/ /twitter.com/dhh/status/ 871034291786002433
  107. Array#sum, Enumerable#sum Ruby version is slightly different from the original

    Active Support version The Ruby version ignores non- numeric value It's OK because Ruby is a general purpose language, while Active Support is "web specific" language extension
  108. But The Situation Is So Complicated!! Rails 5 supports both

    Rubies that have and don't have `sum` `sum` is defined on both Enumerable and Array, where `Enumerable < Array` Native `sum` is faster, so it's preferrable because it's written in C Even if there's native `sum`, it has to fall back to `super` on non-numeric value It takes an argument It takes a block
  109. Somehow Done Before Releasing 5.1.0 Stable Not perfect, but works

    Can be made simpler when we dropped Ruby < 2.3 support
  110. Ruby 2.3, 2.4, and 2.5 Features These Features Cannot Yet

    Be Used In The Rails Codebase Because Rails master still supports 2.2 But you can use them in your apps! Update your production Ruby version to the newest stable, and start using these features now! There's no reason to stay on old version of Ruby
  111. Wrap Up Ruby is evolving day by day Rails is

    also getting better Ruby and Rails are in a very good relationship I'm having fun working on both projects There're always so many things to do because both Ruby and Rails are always changing I hope you to join us if you're interested!
  112. end