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

[RubyConf 2018] Ruby Next: Make old Rubies quack like a new one

[RubyConf 2018] Ruby Next: Make old Rubies quack like a new one

GitHub: https://github.com/ruby-next/ruby-next
Video: https://www.youtube.com/watch?v=T6epHXlUmG0

Ruby 2.7 is just around the corner. It will bring a lot of new features, including new syntax additions: pattern matching, numbered parameters.

That's good news. The bad news is that not many of us will be able to use these goodies right away: the upgrade cost blocks application developers; gem authors have to support older versions.

What if we were able to use Ruby Next features while running Ruby Current? Maybe, we can cast a metaprogramming spell for that? Yes, we can. And I'll show you how.

Vladimir Dementyev

November 18, 2019
Tweet

More Decks by Vladimir Dementyev

Other Decks in Programming

Transcript

  1. 142 palkan_tula palkan RubyConf 2019 $ ruby -v
 2.5.3p105
 $

    ruby -e "
 case {hello: 'RubyConf'}
 in hello: hello if hello =~ /Rails/
 puts 'My name is David'
 in hello: 'RubyConf'
 puts 'My name is Vova'
 end"
 -e:3: syntax error, unexpected in, expecting when in hello: if hello =~ /Rails/ 7 tl;dr
  2. 142 palkan_tula palkan RubyConf 2019 $ ruby -v
 2.5.3p105
 $

    ruby -ruby-next -e "
 case {hello: 'RubyConf'}
 in hello: hello if hello =~ /Rails/
 puts 'My name is David'
 in hello: 'RubyConf'
 puts 'My name is Vova'
 end"
 My name is Vova 8 tl;dr
  3. 142 palkan_tula palkan RubyConf 2019 I have to write Ruby

    code compatible with older versions 17
  4. 142 palkan_tula palkan RubyConf 2019 module YieldSelfThen refine Kernel do

    unless nil.respond_to?(:yield_self) def yield_self yield self end end alias then yield_self end end 31 Metaprogramming for good
  5. 142 palkan_tula palkan RubyConf 2019 Introduced in 2.0 Has been

    experimental until 2.1 Getting better with every release Won't go away in 3.0 History 33
  6. 142 palkan_tula palkan RubyConf 2019 def send(mid, *args) # iterate

    through the ancestors chain # (included/prepended modules and classes) k = self.class iter = self.class.ancestors.each loop do if k.instance_methods(false).include?(mid) return k.instance_method(mid) .bind_call(self, *args) end k = iter.next end end 36 Regular lookup
  7. 142 palkan_tula palkan RubyConf 2019 def send(mid, *args) k =

    self.class iter = self.class.ancestors.each loop do meth = + # lookup refinements in the execution context + if context.refinements_for(k)&.include?(mid) + context.refinements_for(k)[mid] - if k.instance_methods(false).include?(mid) + elsif k.instance_methods(false).include?(mid) k.instance_method(mid) end return meth.bind_call(self, *args) if meth k = iter.next end end 37 Refined lookup
  8. 142 palkan_tula palkan RubyConf 2019 module YieldSelfThen refine Kernel do

    unless nil.respond_to?(:yield_self) def yield_self yield self end end alias then yield_self end end 41 Define
  9. 142 palkan_tula palkan RubyConf 2019 # succ.rb
 using YieldSelfThen
 


    p ARGV[0].then { |i| i.to_i + 1 } 
 # prec.rb
 p ARGV[0].then { |i| i.to_i - 1 } 
 $ ruby succ.rb 2
 3 
 $ ruby prec.rb 3
 undefined method `then' for "3":String 43 Lexical
  10. 142 palkan_tula palkan RubyConf 2019 48 Kernel#then Proc# >>/# <<

    Enumerable#filter Array#union/#difference 2.6
  11. 142 palkan_tula palkan RubyConf 2019 # stats.rb using RubyNext Dir["**/*"].filter_map

    do |path| next if path =~ %r{[$/]m?spec} File.extname(path) end.tally.sort_by(&:last).reverse_each do |(ext, count)| puts " #{count}\t #{ext}" end 52
  12. 142 palkan_tula palkan RubyConf 2019 # stats.rb using RubyNext Dir["**/*"].filter_map

    do |path| next if path =~ %r{[$/]m?spec} File.extname(path) end.tally.sort_by(&:last).reverse_each do |(ext, count)| puts " #{count}\t #{ext}" end 53
  13. 142 palkan_tula palkan RubyConf 2019 $ ruby -ruby-next stats.rb 67

    .rb 50 5 .md 2 .lock 1 .txt 1 .mspec 1 .gemspec 1 .gemfile 1 .bat 1 .dict 54
  14. 142 palkan_tula palkan RubyConf 2019 55 Only contains undefined or

    modified methods => no-op in Ruby 2.7 using RubyNext
  15. 142 palkan_tula palkan RubyConf 2019 Pattern matching: case i; in

    ... end Numbered params: -> { _1 + _2 } Ruby 2.7 58
  16. 142 palkan_tula palkan RubyConf 2019 def beach(*temperature) case temperature in

    :celcius | :c, (20 ..45) :favorable in :kelvin | :k, (293 ..318) :scientifically_favorable in :fahrenheit | :f, (68 ..113) :favorable_in_us else :avoid_beach end end 68 Example
  17. 142 palkan_tula palkan RubyConf 2019 $ ruby -r ripper -e

    "pp Ripper.sexp(File.read('beach.rb'))" [:program, [[:def, [:@ident, "beach", [1, 4]], [:paren, [:params, [:rest_param, [:@ident, "temperature", [1, 11]]], ], [:bodystmt, [[:case, [:var_ref, [:@ident, "temperature", [2, 7]]], [:in, [:aryptn, nil, [[:binary, [:symbol_literal, [:symbol, [:@ident, "celcius", [3, 6]]]], 70
  18. 142 palkan_tula palkan RubyConf 2019 $ ruby -v
 2.5.3p105 $

    ruby -r ripper -e "pp Ripper.sexp(File.read('beach.rb'))" nil 72
  19. 142 palkan_tula palkan RubyConf 2019 $ ruby -e "pp RubyVM

    ::AbstractSyntaxTree.parse_file('beach.rb')" (SCOPE@1:0-14:3 body: (DEFN@1:0-14:3 mid: :beach body: (SCOPE@1:0-14:3 tbl: [:temperature] args: ... body: (CASE3@2:2-13:5 (LVAR@2:7-2:18 :temperature) (IN@3:2-12:16 (ARYPTN@3:5-3:28 const: nil pre: (LIST@3:5-3:28 75
  20. 142 palkan_tula palkan RubyConf 2019 $ gem install parser $

    ruby-parse ./beach.rb (def :beach (args (restarg :temperature)) (case-match (lvar :temperature) (in-pattern (array-pattern (match-alt (sym :celcius) (sym :c)) (begin (irange (int 20) (int 45)))) nil 78
  21. 142 palkan_tula palkan RubyConf 2019 81 Unparser $ gem install

    unparser 
 $ ruby -r parser/current -r unparser -e "
 p Unparser.unparse(
 Parser ::CurrentRuby.parse('%i[a a b c].tally'))
 " "[:a, :a, :b, :c].tally"
  22. 142 palkan_tula palkan RubyConf 2019 82 Unparser $ gem install

    unparser 
 $ ruby -r parser/current -r unparser -e "
 p Unparser.unparse(
 Parser ::CurrentRuby.parse('%i[a a b c].tally'))
 " "[:a, :a, :b, :c].tally"
  23. 142 palkan_tula palkan RubyConf 2019 83 <<"A #{b}C" #{ <<"A

    #{b}C" A #{b}C } str A #{b}C 
 # => "\nstr\n" Parser is not ideal (dstr (begin (dstr (str "A") (begin (send nil :b)))) (str "\n") (str "str\n") (str "A") (begin (send nil :b)))) " #{"A #{b}"}\nstr\nA #{b}" # => undefined local variable or method `b'
  24. 142 palkan_tula palkan RubyConf 2019 Parse Ruby code using edge

    parser Replace unsupported AST nodes with equivalents for the target version Generate new source code with unparser Ruby Next 85
  25. 142 palkan_tula palkan RubyConf 2019 def transform(source) Parser.parse(source).then do |ast|

    rewriters.inject(ast) do |tree, rewriter| rewriter.new.process(tree) end.then do |new_ast| Unparser.unparse(new_ast) end end end end 86
  26. 142 palkan_tula palkan RubyConf 2019 module Rewriters class MethodReference <

    Base def on_meth_ref(node) receiver, mid = *node.children node.updated( # (meth-ref :send, # (const nil :C) :m) [ # receiver, # -> :method, # s(:sym, mid) # (send ] # (const nil :C) :method ) # (sym :m) end end end 87
  27. 142 palkan_tula palkan RubyConf 2019 module Rewriters class PatternMatching <

    Base def on_case_match(node) # ~500 LOC end end end 88
  28. 142 palkan_tula palkan RubyConf 2019 if ... else if ...

    else ... end #deconstruct & #deconstruct_keys Type and structure checks “Pattern matching” in <2.7 89
  29. 142 palkan_tula palkan RubyConf 2019 90 def fizzbuzz(num) rems =

    [num % 3, num % 5] if rems == [0, 0]
 'FizzBuzz' elsif rems[0] == 0
 'Fizz'
 elsif rems[1] == 1
 'Buzz' else
 raise "NoMatchingPattern"
 end
 end def fizzbuzz(num) case [num % 3, num % 5] in [0, 0] then 'FizzBuzz' in [0, _] then 'Fizz' in [_, 0] then 'Buzz' end end 2.7 2.5 ad-hoc rewrite
  30. 142 palkan_tula palkan RubyConf 2019 91 def fizzbuzz(num) vals =

    [num % 3, num % 5]
 .deconstruct raise TypeError unless Array === vals if (0 === vals[0]) &&
 (0 === vals[1]) 
 'FizzBuzz' elsif (0 === vals[0])
 && (_ == vals[1] || true)
 'Fizz'
 elsif (_ == vals[0] || true)
 && (1 === vals[1])
 'Buzz' else
 raise "NoMatchingPattern"
 end
 end def fizzbuzz(num) case [num % 3, num % 5] in [0, 0] then 'FizzBuzz' in [0, _] then 'Fizz' in [_, 0] then 'Buzz' end end 2.7 2.5 transpile
  31. 142 palkan_tula palkan RubyConf 2019 $ gem install ruby-next $

    ruby-next nextify ./beach.rb -o stdout def beach(*temperature)
 __matchee __ = temperature
 if (((( __matchee_arr __ = __matchee __.deconstruct) || true) && ((Array === __matchee_arr __) || Kernel.raise(TypeError, __matchee __.inspect))) && ((2 == __matchee_arr __.size) && (((:celcius === __matchee_arr __[0]) || (:c === __matchee_arr __[0])) && ((20 ..45) === __matchee_arr __[1]))))
 :favorable
 else
 if (((:kelvin === __matchee_arr __[0]) || (:k === __matchee_arr __[0])) && ((293 ..318) === __matchee_arr __[1]))
 :scientifically_favorable
 else
 if (((:fahrenheit === __matchee_arr __[0]) || (:f === __matchee_arr __[0])) && ((68 ..113) === __matchee_arr __[1]))
 :favorable_in_us
 else
 :avoid_beach
 end
 end
 end
 end 92
  32. 142 palkan_tula palkan RubyConf 2019 93 def beach(*temperature)
 __matchee __

    = temperature
 if (((( __matchee_arr __ = __matchee __.deconstruct) || true) && ((Array === __matchee_arr __) || Kernel.raise(TypeError, __matchee __.inspect))) && ((2 == __matchee_arr __.size) && (((:celcius === __matchee_arr __[0]) || (:c === __matchee_arr __[0])) && ((20 ..45) === __matchee_arr __[1]))))
 :favorable
 else
 if (((:kelvin === __matchee_arr __[0]) || (:k === __matchee_arr __[0])) && ((293 ..318) === __matchee_arr __[1]))
 :scientifically_favorable
 else
 if (((:fahrenheit === __matchee_arr __[0]) || (:f === __matchee_arr __[0])) && ((68 ..113) === __matchee_arr __[1]))
 :favorable_in_us
 else
 :avoid_beach
 end
 end
 end
 end def beach(*temperature) case temperature in :celcius | :c, (20 ..45) :favorable in :kelvin | :k, (293 ..318) :scientifically_favorable in :fahrenheit | :f, (68 ..113) :favorable_in_us else :avoid_beach end end For humans For machines
  33. 142 palkan_tula palkan RubyConf 2019 def beach(*temperature) case temperature in

    :celcius | :c, (20 ..45) :favorable in :kelvin | :k, (293 ..318) :scientifically_favorable in :fahrenheit | :f, (68 ..113) :favorable_in_us else :avoid_beach end end $ ruby benchmark/pattern_matching_array.rb Comparison:
 transpiled: 2165271.2 i/s 
 baseline: 1004383.8 i/s - 2.16x slower 95
  34. 142 palkan_tula palkan RubyConf 2019 case JSON.parse(ARGV[0], symbolize_names: true) in

    {name: "Alice", children: [{name: "Bob", age: age}]} p "Bob age is #{age}" in _ "No Alice" end $ ruby benchmark/pattern_matching_mixed.rb Comparison:
 transpiled: 314105.5 i/s
 baseline: 273998.7 i/s - 1.15x slower 96
  35. 142 palkan_tula palkan RubyConf 2019 module Kernel module_function alias_method :require_without_ruby_next,

    :require def require(path) RubyNext.require(path) rescue => e warn "Failed to require ' #{path}': #{e.message}" require_without_ruby_next(path) end end 100 Kernel#require *Don't try this at home
  36. 142 palkan_tula palkan RubyConf 2019 $LOAD_PATH: where to search for

    “features” $LOADED_FEATURES: required files Two pillars of require 101
  37. 142 palkan_tula palkan RubyConf 2019 module RubyNext module_function def require(path)

    realpath = resolve_feature_path(path) return false if $LOADED_FEATURES.include?(realpath) File.read(realpath) .then(&Language.:transform).then do |source| TOPLEVEL_BINDING.eval source, path $LOADED_FEATURES << path true end end end 102 RubyNext.require
  38. 142 palkan_tula palkan RubyConf 2019 module RubyNext module_function def require(path)

    realpath = resolve_feature_path(path) return false if $LOADED_FEATURES.include?(realpath) File.read(realpath) .then(&Language.:transform).then do |source| TOPLEVEL_BINDING.eval source, path $LOADED_FEATURES << path true end end end 103 RubyNext.require
  39. 142 palkan_tula palkan RubyConf 2019 module RubyNext module_function def resolve_feature_path(path)

    path = " #{path}.rb" if File.extname(path).empty? return path if Pathname.new(path).absolute? $LOAD_PATH.find do |lp| lpath = File.join(lp, path) return lpath if File.file?(lpath) end end end 104
  40. 142 palkan_tula palkan RubyConf 2019 module RubyNext module_function def resolve_feature_path(path)

    path = " #{path}.rb" if File.extname(path).empty? return path if Pathname.new(path).absolute? $LOAD_PATH.find do |lp| lpath = File.join(lp, path) return lpath if File.file?(lpath) end end end 105
  41. 142 palkan_tula palkan RubyConf 2019 module RubyNext module_function def require(path)

    realpath = resolve_feature_path(path) return false if $LOADED_FEATURES.include?(realpath) File.read(realpath) .then(&Language.:transform).then do |source| TOPLEVEL_BINDING.eval source, path $LOADED_FEATURES << path true end end end 106 RubyNext.require
  42. 142 palkan_tula palkan RubyConf 2019 module RubyNext module_function def require(path)

    realpath = resolve_feature_path(path) return false if $LOADED_FEATURES.include?(realpath) File.read(realpath) .then(&Language.:transform).then do |source| TOPLEVEL_BINDING.eval source, path $LOADED_FEATURES << path true end end end 107 RubyNext.require
  43. 142 palkan_tula palkan RubyConf 2019 $ ruby -ruby-next -e "


    case {eta: ARGV[0].to_i}
 in eta: (0 ...10)
 puts 'The end is near'
 in eta: x if x > 10
 puts 'Boooring'
 end" 8
 The end is near 108 -ruby-next
  44. 142 palkan_tula palkan RubyConf 2019 Add transpiled files to releases

    No additional runtime deps* Nextify Gems 114 * polyfills might still be required
  45. 142 palkan_tula palkan RubyConf 2019 $ ruby-next nextify ./lib -V


    Generated: ./lib/.rbnext/rubanok/rule.rb
 Generated: ./lib/.rbnext/rubanok/dsl/matching.rb 115
  46. 142 palkan_tula palkan RubyConf 2019 117 def setup_gem_load_path lib_dir =

    File.dirname(caller_locations(1, 1).first.path) current_index = $LOAD_PATH.index(lib_dir) next_dir = File.join(lib_dir, ".rbnext") if File.exist?(next_dir) $LOAD_PATH.insert current_index, next_dir current_index += 1 end end setup_gem_load_path
  47. 142 palkan_tula palkan RubyConf 2019 The best way to taste

    new features is to start using them every day 139
  48. palkan_tula palkan RubyConf 2019 Thank you! Vladimir Dementyev @ruby-next Evil

    Martians @palkan @palkan_tula evilmartians.com @evilmartians