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

Ruby 2 Methodology

Ruby 2 Methodology

Slides for RubyConf 2015 talk "Ruby 2 Methodology" http://rubyconf.org/program#prop_1641

Akira Matsuda

November 17, 2015
Tweet

More Decks by Akira Matsuda

Other Decks in Programming

Transcript

  1. So Glad that RubyConf Still Has the ”Ruby In Depth”

    Track This Year Makes me feel like I came to a Ruby conference
  2. If You Want to See More Ruby Talks RubyKaigi is

    the conference you should go!
  3. I'm Going to Talk About Modern Usage of Ruby's Method

    Not just introducing the features I’d like to tell my stories With lots of Ruby code in the slides
  4. Sometimes We Want to Define a Method with Weird Name

    Which character can/ cannot be a (part of) Method name?
  5. amatsuda/rfd I do this a lot in my app It

    might be interesting to see a real use case for methods with such weird names
  6. rfd/commands.rb # Change current directory (cd). define_method(:'@') do process_command_line preset_command:

    'cd' end # Execute a shell command in an external shell. define_method(:!) do process_shell_command end # Execute a command in the controller context. define_method(:':') do process_command_line end
  7. Listing up ASCII Chars that Can/ Cannot Be Used for

    Method Name h = {def: [], define_method: []} (' '..'~').each do |c| begin eval "def #{c}() puts '#{c}'; end" h[:def] << c rescue SyntaxError define_method(c) { puts c } h[:define_method] << c end end puts h
  8. Result :def => "!", "%", "&", "*", "+", "-", "/",

    "<", ">", "A"-"Z", "^", "_", "`", "a"-"z", "|", "~" :define_method => " ", "\"", "#", "$", "'", "(", ")", ",", ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "=", "?", "@", "[", "\\", "]", "{", "}"
  9. When to Use Kernel#send When calling a method with abnormal

    name When dynamically changing a Method to call When calling a Method from Outside of scope
  10. When Dynamically Changing a Method to Call # actionpack/lib/abstract_controller/base.rb module

    AbstractController class Base ... private ... def process_action(method_name, *args) send_action(method_name, *args) end alias send_action send ... ennd
  11. When Calling a Method from Outside of Scope Class.new {

    private def priv() p:hello end }.new.send :priv #=> :hello
  12. private class C private def hello() p:hello end end C.new.hello

    #=> private method `hello' called for #<C:0x007ff143cb2d70> (NoMethodError)
  13. private class C private def hello() p:hello end def greeting()

    hello end end C.new.greeting #=> :hello
  14. If There's a lvar with the Same Name, the private

    Method Would Never Be Called class C private def hello() 'hello' end def greeting(hello = 'hola') p hello end end C.new.greeting #=> "hola"
  15. Put Parens()? class C private def hello() 'hello' end def

    greeting(hello = 'hola') p hello() end end C.new.greeting #=> "hello"
  16. Put Parens Sure it works, but we don't want to

    add parens to all method calls... Ruby is not JavaScript!
  17. Prepend “self.” to private Method Calls class C private def

    hello() 'hello' end def greeting(hello = 'hola') p self.hello end end C.new.greeting
  18. Prepend self. to private Method Calls class C private def

    hello() 'hello' end def greeting(hello = 'hola') p self.hello end end C.new.greeting #=> private method `hello' called for #<C:0x007fe7564799e8> (NoMethodError)
  19. WTF

  20. Our Real App Problem at Ubiregi Inc. We changed a

    public method in a Rails controller to be private, then we saw a NoMethodError because of “self.” But we thought this should not be an error
  21. So, We Wrote a Patch Feature #11297 Proposed and patched

    by @soutaro, the CTO at Ubiregi Inc.
  22. Matz Accepted this “It changes the concept of private methods

    a little. It's OK to merge the patch if the document is updated at the same time..”
  23. Another Solution to Call it with “self.” class C -

    private def hello() 'hello' end + protected def hello() 'hello' end def greeting(hello = 'hola') p self.hello end end C.new.greeting #=> "hello"
  24. protected Method Can't Be Called from Outside class C protected

    def hello() 'hello' end end C.new.greeting #=> undefined method `greeting' for #<C:0x007fc5b42b3a28> (NoMethodError)
  25. protected Method Can't Be Called from Outside class C protected

    def hello() 'hello' end end class D def greeting C.new.hello end end D.new.greeting #=> protected method `hello' called for #<C: 0x007fc040b0de90> (NoMethodError)
  26. protected Method Can Be Called from Other Instance of the

    C Class class C protected def hello() 'hello' end def greeting() p C.new.hello end end C.new.greeting #=> "hello"
  27. Difference Between private and protected class C private def hello()

    'hello' end def greeting() p C.new.hello end end C.new.greeting #=> private method `hello' called for #<C:0x007f879e443ae0> (NoMethodError)
  28. So, I Wrote a Patch 150 occurrences of protected could

    actually be replaced to private https://github.com/amatsuda/ rails/commit/6832343
  29. Only a Few Proper Use Cases of protected in Rails

    More than 90% of protected in Rails code actually means private
  30. An Example of a Proper Use Case of protected in

    Rails # AP/lib/action_controller/metal/strong_parameters.rb module ActionController class Parameters < ActiveSupport::HashWithIndifferentAccess def dup super.tap do |duplicate| duplicate.permitted = @permitted end end protected def permitted=(new_permitted) @permitted = new_permitted ennnd
  31. The RDoc Problem class C # This is a public

    method def pub() end protected # This is a protected method def prot() end private # This is a private method def priv() end end
  32. My Rails Patch I want to change the method scope

    accurately, but that spoils so much documentations!
  33. This Is How We Abuse protected, and Why We Can't

    Stop Abusing protected in Rails
  34. ✨(2.1) def Returns the Method Name In combination with `private

    :foo` syntax, now we can do Java like
 `private def foo() end` Proposed and implemented by usa (@unak)
  35. Another RDoc Problem class C # This is a public

    method public def pub() end # This is a protected method protected def prot() end # This is a private method private def priv() end end
  36. And Even Worse… class C # This is a private

    method private def priv() end # This should be documented def pub1() end # This should also be documented def pub2() end end
  37. This Is Why We Still Don't Use “private def” Style

    Definition in Rails We need to patch RDoc first Patches are welcome!
  38. Inspecting Defined Methods Via Class.instance_methods class Person def hello p

    'hello' end end p Person.instance_methods(false) #=> [:hello]
  39. Getting an Instance of Method from an Instance of the

    Person Class class Person def hello p 'hello' end end p Person.new.method(:hello) #=> #<Method: Person#hello>
  40. Calling the Method class Person def hello p 'hello' end

    end Person.new.method(:hello).call #=> "hello"
  41. Passing in a Parameter class Person def say(something) p something

    end end Person.new.method(:say).call('hello') #=> "hello"
  42. Getting an Instance of Method from the Person Class class

    Person def hello p 'hello' end end p Person.instance_method(:hello) #=> #<UnboundMethod: Person#hello>
  43. Calling The UnboundMethod class Person def hello p 'hello' end

    end Person.instance_method(:hello).call #=> undefined method `call' for #<UnboundMethod: Person#hello> (NoMethodError)
  44. So, What's Defined on the UnboundMethod? class Person def hello

    p 'hello' end end p Person.instance_methods(false).sort #=> [:==, :arity, :bind, :clone, :eql?, :hash, :inspect,
 :name, :original_name, :owner, :parameters,
 :source_location, :super_method, :to_s]
  45. Difference Between Method and UnboundMethod p m = Method.instance_methods(false).sort #=>

    [:==, :[], :arity, :call, :clone, :curry, :eql?,
 :hash, :inspect, :name, :original_name, :owner,
 :parameters, :receiver, :source_location, :super_method,
 :to_proc, :to_s, :unbind] p u = UnboundMethod.instance_methods(false).sort #=> [:==, :arity, :bind, :clone, :eql?, :hash, :inspect,
 :name, :original_name, :owner, :parameters,
 :source_location, :super_method, :to_s] p m - u #=> [:[], :call, :curry, :receiver, :to_proc, :unbind] p u - m #=> [:bind]
  46. Binding an UnboundMethod to an Instance of Person class Person

    def hello p 'hello' end end p um = Person.instance_method(:hello) #=> #<UnboundMethod: Person#hello> person = Person.new p meth = um.bind(person) #=> #<Method: Person#hello> meth.call #=> "hello"
  47. Can a Cat Say Hello? class Person def hello() p

    'hello' end end class Cat def hello() p 'Meow' end end um = Person.instance_method(:hello) cat = Cat.new meth = um.bind(cat) meth.call
  48. No, A Cat Is not a Person class Person def

    hello() p 'hello' end end class Cat def hello() p 'Meow' end end um = Person.instance_method(:hello) cat = Cat.new meth = um.bind(cat) meth.call #=> bind argument must be an instance of Person (TypeError)
  49. Transplanting a Method From a Module module Greeter def hello

    p 'hello' end end class Cat define_method :hello, Greeter.instance_method(:hello) end Cat.new.hello #=> "hello"
  50. Method Transplanting Seems like there’s nothing different from including the

    whole Module in this case You can cherry-pick a Method without including the whole Module
  51. ✨ (2.2, 2.3) Transplanting a Method From a Class module

    Greeter def hello() p 'hello' end end class Person include Greeter end class Cat define_method :hello, Person.instance_method(:hello) end Cat.new.hello #=> "hello"
  52. Method Transplanting Methods can now be transplanted from a Class

    to another Class If the Method originally comes from a Module So many Module-based Rails Methods became portable now!
  53. Passing in a Parameter to Method#call class Person def say(something)

    p something end end Person.new.method(:say).call('hello') #=> "hello"
  54. Methods Take Parameters, Then Parameters Can Be Accessed as Local

    Variables (lvar) in the Method def say(something) ... end def say something = 'hello' ... end # same kind of variable
  55. Inspecting a Method's Parameter class Person def say(something) p something

    end end p Person.new.method(:say).parameters #=> “hello” #=> [[:req, :something]]
  56. An Optional Parameter class Person def say(something, options = {})

    p something end end p Person.new.method(:say).parameters #=> [[:req, :something], [:opt, :options]]
  57. rest, block class Person def say(something, options = {}, *args,

    &blk) p something end end p Person.new.method(:say).parameters #=> [[:req, :something], [:opt, :options], [:rest, :args], [:block, :blk]]
  58. action_args A Rails plugin Placed under Asakusa.rb organization respecting Ko1's

    work on Method#parameters The plugin code is 99% written by me Something that makes your Rails app's controller code like Merb's controller Initially named ”Merbish” I ALWAYS use this when working on Rails apps
  59. What action_args Does # Before class UsersController < ApplicationController def

    show @user = User.find params[:id] end end # After class UsersController < ApplicationController def show(id) @user = User.find id end end
  60. It Used not to Be Able to Handle filters' Parameters

    It's supported in recent versions!
  61. mame (@mametter) Ruby 2.0 release manager Known by his crazy

    quine works, such as 
 mame/quine-relay
  62. Method#parameters for kwargs class Person def say(word:, volume: :loud, **args)

    p word end end p Person.new.method(:say).parameters #=> [[:keyreq, :word], [:key, :volume], [:keyrest, :args]]
  63. Looks Good, Isn’t it? - def truncate(truncate_at, options = {})


    + def truncate(truncate_at, omission: '...', separator: nil) - def number_to_currency(number, options = {}) + def number_to_currency(number, locale: nil, format: nil, unit: nil, **options) - def cattr_accessor(*syms, &blk) + def cattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, &blk) - def truncate(text, options = {}, &block)
 + def truncate(text, length: 30, escape: true, **options, &block)
  64. I Found a Specification Problem Reserved words like if, unless,

    end can be a keyword But we can’t touch the variable
  65. Reserved Words in Ruby (Method Parameter) def a(if) end #=>

    syntax error, unexpected keyword_if, expecting ')'
  66. But Wait, How Can We Access the Variable? def say(something,

    if:) p something if if end say 'Hello', if: true #=> syntax error, unexpected keyword_end #=> syntax error, unexpected end-of-input, expecting keyword_end
  67. So I Reported this Problem Maybe via Twitter or at

    the meeting or Asakusa.rb or something (couldn’t find the issue) Then ko1 and nobu made a fix
  68. kwargs + Reserved Word + binding.local_variable_get def say(something, if:) p

    something if binding.local_variable_get(:if) end say 'Hello', if: true #=> "Hello"
  69. Rails 5 Coming soon (?) Supports only Ruby >= 2.2

    Major version bump is a good chance to introduce an API incompatibility Now we have fast kwargs and binding.local_variable_get I think we're ready to revise the *args API and move to kwargs
  70. Usage of AMC class Record def save() p 'Saved.' end

    def save_with_validation p 'Validated.' save_without_validation end alias_method_chain :save, :validation end Record.new.save #=> "Validated." #=> "Saved."
  71. You Still Can Call the Un- monkeypatched Version class Record

    def save() p 'Saved.' end def save_with_validation p 'Validated.' save_without_validation end alias_method_chain :save, :validation end Record.new.save_without_validation #=> "Saved."
  72. What If We Had Another AMC Chain? class Record def

    save() p 'Saving...' end def save_with_validation p 'Validating...' save_without_validation end alias_method_chain :save, :validation def save_with_callback save_without_callback p 'Calling back...' end alias_method_chain :save, :callback end Record.new.save #=> "Calling back..." #=> "Validating..." #=> "Saving..."
  73. Now Let's save Without validation class Record def save() p

    'Saving...' end def save_with_validation p 'Validating...' save_without_validation end alias_method_chain :save, :validation def save_with_callback save_without_callback p 'Calling back...' end alias_method_chain :save, :callback end Record.new.save_without_validation #=> "Saving..."
  74. *_without_* Methods Are Harmful Unpredictable behaviour The method name (probably)

    lies! *_without_* shouldn't be callable *_without_* shouldn’t even be defined
  75. Module#include class Record def save super p 'Saved.' ennd module

    Validation def save p 'Validated.' ennd Record.send :include, Validation Record.new.save #=> "Validated." #=> "Saved."
  76. Common Idiom of Module#prepend in 2.0 class Record def save()

    p 'Saved.' end end module Validation def save p 'Validated.' super ennd Record.send :prepend, Validation Record.new.save #=> "Validated." #=> "Saved."
  77. No Other Method Definition Than render # html5_validators/action_view/form_helpers.rb module Html5Validators

    module ActionViewExtension module PresenceValidator def render ... super end end module LengthValidator def render ... super end end module NumericalityValidator def render ... super ennnnd module ActionView::Helpers::Tags::TextField prepend Html5Validators::ActionViewExtension::NumericalityValidator prepend Html5Validators::ActionViewExtension::LengthValidator prepend Html5Validators::ActionViewExtension::PresenceValidator end
  78. Module#prepend Always Comes with send Because it initially was a

    private method in 2.0 In conformity to Module#include
  79. Isn't It Natural to Make it public? The main purpose

    of this method is monkey-patching Usually done from outside, rather than from inside Also Module#include should be public (Object#extend is already public)
  80. Code Complexity So many method definitions with the same name

    e.g. `def save`, `def render` Hard to read through the code Hard to debug
  81. ActiveRecord.has_many `super` def save(*args) create_or_update(*args) rescue ActiveRecord::RecordInvalid false end def

    save(*) if status = super changes_applied end status end def save(*) #:nodoc: rollback_active_record_state! do with_transaction_returning_status { super } end end def save(options={}) perform_validations(options) ? super : false end
  82. ✨(2.2) Method#super_method Feature #9781 Returns a Method object that will

    be called via `super` Proposed by @schneems Implemented by nobu
  83. For debugging purpose “I believe adding Method#super_method, or exposing this

    same information somewhere else, could greatly help developers to debug large systems easily.” - @schneems
  84. Let’s Get Back to AMC vs. Module#prepend Module#prepend does not

    define *_without_* method So, There's No Way Calling save_without_validation for Prepended Method?
  85. No. You Can Call the super_method! class Record def save()

    p 'Saved.' end end module Validation def save p 'Validated.' and super end end Record.prepend Validation record = Record.new record.method(:save).super_method.call #=> "Saved."
  86. You Can Call super_method’s super_method class Record def save() p

    'Saved.' end end module Validation def save() p 'Validated.' and super ennd module Callback def save() super and p 'Calling back...' ennd Record.prepend Callback, Validation method = Record.new.method(:save) method.super_method.super_method.call #=> "Saved."
  87. Method#owner class Record def save() p 'Saved.' end end module

    Validation def save() p 'Validated.' and super ennd module Callback def save super and p 'Calling back...' ennd module Transaction def save p 'Transaction start.' and super and p 'Transaction end.' ennd Record.prepend Callback, Transaction, Validation method = Record.new.method(:save) p method.owner p method.super_method.owner #=> Callback #=> Transaction
  88. What if We Have n Modules Prepended? ... Record.prepend Callback,

    Transaction, Validation method = Record.new.method(:save) until method.owner == Record method = method.super_method end method.call #=> "Saved."
  89. Generalizing a Little Bit More class Method def call_without(mod, *args,

    &block) if mod == owner super_method.call(*args, &block) else super_method.call_without(mod, *args, &block) end end end ... Record.prepend Callback, Transaction, Validation method = Record.new.method(:save) method.call_without(Validation) #=> "Saved."
  90. Module#prepend + UnboundMethod#super_method class C def a() end end module

    M def a() end end C.prepend M p C.instance_method(:a).super_method #=> nil # This should be #<UnboundMethod: C#a>
  91. Module#prepend + Method#super_method’s super_method class C def a() end end

    module M def a() end end C.prepend M p C.new.method(:a).super_method #=> #<Method: Object(C)#a> p C.new.method(:a).super_method.super_method #=> #<Method: Object(M)#a> p C.new.method(:a).super_method.super_method.super_method #=> #<Method: Object(C)#a> (and this loops...) # This should be nil
  92. Module#prepend Is a Great Monkey-patching Tool Less polluting Only When

    Overriding an Existing Method But that's not Always the Case
  93. Consider this Case What if we want to include a

    Module that has internal methods, and we don't want to expose them to the users?
  94. Some Methods from
 the End Users class Framework::Base end module

    MyMonkeypatch # We want to reveal this to the end users public def foo ... end # We want to hide this from the end users private def bar ... end end Framework::Base.include MyMonkeypatch
  95. ✨ (2.0) Refinements Proposed and implemented by shugo (@shugomaeda) The

    initial implementation was more powerful and useful See my talk “Ruby 2.0 on Rails” at RubyConf 2012 https://speakerdeck.com/a_matsuda/ruby-2-dot-0-on-rails Then diminished to the current specification
  96. Basic Usage of Refinements # amplifier.rb module Amplifier refine String

    do def shout() p self + '!' end end end # another_file.rb require_relative 'amplifier' using Amplifier 'hello'.shout #=> "hello!"
  97. refining Rails’ Core Class # lib/action_args/params_handler.rb module ActionArgs module ParamsHandler

    refine AbstractController::Base do def extract_method_arguments_from_params(method_name) ... end def strengthen_params!(method_name) ... ennnnd
  98. Then using it from Another File # lib/action_args/abstract_controller.rb require_relative 'params_handler'

    using ActionArgs::ParamsHandler module ActionArgs module AbstractControllerMethods def send_action(method_name, *args) ... strengthen_params! method_name values = extract_method_arguments_from_params method_name super method_name, *values ennd ... end
  99. I Would Call this “super private” The methods defined inside

    the library are visible only inside the library Never exposed to the end-users (unless they explicitly call `using`) Thanks @_ksss_ for giving me an inspiration for this implementation!
  100. A Pitfall of Refinements Refined methods can’t be called via

    Kernel#send Because Kernel#send is not lexically scoped? But we often want to dynamically change the method to call, for instance in Rails
  101. I Think this Restriction Should Be Loosen So I raised

    a feature request Feature #11476 If this is allowed, we could implement something useful, maybe?
  102. Ruby’s Method is still evolving! ⤴⤴ Let’s play with Method!

    You can make it better! Let’s make it more fun! Summary
  103. end