Search code examples
rubymethodsparametersunary-operator

Ruby method with optional options and &block parameter


  1. Hey there

    Is it possible to have optional attributes and a block as parameters for a method call?

    Example: I have to call

    method(foo, foo: bar, -> { do_something }
    

    and tried it with

    def method(foo, *bar, &block)
    end
    

    As for my understanding the block always has to be at last position?

    After a bit of research I found out the unary(?) * seems to be for arrays. Since I try to pass a Hash I changed the code to

    def method(foo, bar={}, &block)
    end
    

    But this doesn't do the trick either. I guess its because he cant figure out where the bar ends and the block starts.

    Any ideas or suggestions? Thank you in advance

    Append: Just for the curious why I need this. We have a big json schema running and have a small DSL that builds the json from the model definitation. Without going to much into detail we wanted to implement exportable_scopes.

    class FooBar
      exportable_scope :some_scope, title: 'Some Scope', -> { rewhere archived: true }
    end
    

    On some initializer this is supposed to happens:

    def exportable_scope scope, attributes, &block
      scope scope block
      if attributes.any?
        attributes.each do |attribute|
          exportable_schema.scopes[scope] = attribute
        end
      else
        exportable_schema.scopes[scope] = {title: scope}
      end
    end
    

    So this is working fine, I just need a hint for the method parameters.


Solution

  • Yes, it is possible.

    When mixing different kinds of parameters, they have to be included in the method definition in a specific order:

    1. Positional parameters (required and optional) and a single splat parameter, in any order;
    2. Keyword parameters (required and optional), in any order;
    3. Double splat parameter;
    4. Block parameter (prefixed with &);

    The order above is somewhat flexible. We could define a method and begin the parameter list with a single splat argument, then a couple of optional positional arguments, and so on. Even though Ruby allows that, it's usually a very bad practice as the code would be hard to read and even harder to debug. It's usually best to use the following order:

    1. Required positional parameters;
    2. Optional positional parameters (with default values);
    3. Single splat parameter;
    4. Keyword parameters (required and optional, their order is irrelevant);
    5. Double splat parameter;
    6. Explicit block parameter (prefixed with &).

    Example:

    def meditate cushion, meditation="kinhin", *room_items, time: , posture: "kekkafuza", **periods, &b
        puts "We are practicing #{meditation}, for #{time} minutes, in the #{posture} posture (ouch, my knees!)."
        puts "Room items: #{room_items}"
        puts "Periods: #{periods}"
        b.call # Run the proc received through the &b parameter
    end
    
    meditate("zafu", "zazen", "zabuton", "incense", time: 40, period1: "morning", period2: "afternoon" ) { puts "Hello from inside the block" }
    
    # Output:
    We are practicing zazen, for 40 minutes, in the kekkafuza posture (ouch, my knees!).
    Room items: ["zabuton", "incense"]
    Periods: {:period1=>"morning", :period2=>"afternoon"}
    Hello from inside the block
    

    Notice that when calling the method, we have:

    • Provided the cushion mandatory positional argument;
    • Overwritten the default value of the meditation optional positional argument;
    • Passed a couple of extra positional arguments (zabuton and incense) through the *room_items parameter;
    • Provided the time mandatory keyword argument;
    • Omitted the posture optional keyword argument;
    • Passed a couple of extra keyword arguments (period1: "morning", period2: "afternoon") through the **periods parameter;
    • Passed the block { puts "Hello from inside the block" } through the &b parameter;

    Please note the example above servers only to illustrate the possibility of mixing different types of parameters. Building a method like this in real code would be a bad practice. If a method needs that many arguments, it's probably best to split it into smaller methods. If it's absolutely necessary to pass that much data to a single method, we should probably create a class to store the data in a more organized way, then pass an instance of that class to the method as a single argument.