I have a set of nested functions that each take an arbitrary list of arguments:
def foo *args
bar args
end
def bar *args
baz args
end
def baz *args
end
Calling foo with a set of args like :a => :foo, :b => :bar gives us a single element array after the splat:
[{:a => :foo, :b => :bar}]
And then passing that along to the nested function, and again through a splat, makes for this:
[[{:a => :foo, :b => :bar}]]
Is it appropriate to pass args[0] along to the nested function, or is there some kind of reverse splat that I should be using instead?
If you want to relay splatted arguments to another function, just splat them again (the operator behaves the opposite way when used in a method call (vs. method definition))
def foo(*args)
bar *args
end
Related
I want to define a method that takes keyword arguments. I would like it to raise when keyword arguments are not provided, and I can code this myself - but ideally I would like to let Ruby do that for me. Also I would like to be able to inspect the freshly defined method using Method#parameters. If I use a shorthand double-splat (like **kwargs) the actual structure I expect is not visible to parameters.
I can of course do this:
define_method(:foo) do | foo:, bar: |
# ...
end
which achieves the desired result:
method(:foo).parameters
# => [[:keyreq, :foo], [:keyreq, :bar]]
but I cannot pass those arguments programmatically, they have to be literally placed in the code. Is there a way I could bypass this?
You have to use eval to define arguments dynamically (not just keyword arguments), e.g. using class_eval:
class MyClass
name = :foo
args = [:bar, :baz]
class_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{name}(#{args.map { |a| "#{a}:" }.join(', ')}) # def foo(bar:, baz:)
[#{args.join(', ')}] # [bar, baz]
end # end
METHOD
end
MyClass.new.foo(bar: 1, baz: 2)
#=> [1, 2]
MyClass.instance_method(:foo).parameters
#=> [[:keyreq, :bar], [:keyreq, :baz]]
Having this method which can dynamically send args to an object:
module DSL
def update object, *args, &block
updated_object = object.send *args
# then, some stuff with the updated object and the block
end
end
the following code could be used such as:
include DSL
# equivalent to: Array.new 3, :foo
update Array, :new, 3, :foo do
# updated object => [:foo, :foo, :foo]
# some stuff
end
or such as:
# equivalent to: [:foo, :foo, :foo].size
update [:foo, :foo, :foo], :size do
# updated object => 3
# some stuff
end
But how could we update the content of this update method in order to handle blocks, like here:
[:foo, :bar].select {|a| a == :foo }
I thought about converting the block into a proc, such as:
update [:foo, :bar], :select, &:foo.method(:==) do
# ...
end
But then, because a same method can not handle more than one block, this exception is raised:
SyntaxError: both block arg and actual block given
Is there an elegant way to solve this?
I think that you're going over the top with this, but anyway, here's an idea:
You could pass a regular proc to your update method and then have special handling for last argument, which is a proc.
module DSL
def update object, *args, &block
updated_object = if args.last.is_a? Proc
proc = args.pop
object.send *args, &proc
else
object.send *args
end
yield updated_object
end
end
extend DSL
update [:foo, :bar], :select, ->(a) { a == :foo } do |obj|
puts "selected object: #{obj}"
end
# >> selected object: [:foo]
def foo(bar)
'return value'
end
foo 'bar' # => "return value"
def foo=(bar)
'return value'
end
foo = 'bar' # => "bar"
send :foo=, 'bar' # => "return value"
I want foo = 'bar' to return "return value" but not to use send for this purpose. How can I do this?
Update
I need a desired behavior in my gem. Here is an example:
car = Car.new
car.gear # => :first
car.next_gear # => :second
car.gear # => :second
car.gear = :fourth # => false
car.gear # => :second
car.gear = :third # => :third
car.gear # => :third
Assignments always return the right hand side of an assignment.
Have a look at the ruby documentation for details:
Methods that end with an equals sign indicate an assignment method.
For assignment methods the return value is ignored, the arguments are
returned instead.
Having said that, foo = bar also assigns to a local variable foo instead of using the foo= method. Again, this is defined in the ruby docs:
When using method assignment you must always have a receiver. If you
do not have a receiver Ruby assumes you are assigning to a local
variable
You can test that by running
local_variables #=> []
def foo=(bar);end
foo = 42
local_variables #=> [:foo]
You see that the local variable foo was created. Better use self.foo = 'bar'.
To address your specific problem with your gem: Follow Neil's advice and use an extra method like change_gear for what you want to do. He gave you good council in his comments.
It's a Ruby gotcha: the return value of accessor methods get ignored.
This code will make it more clear what is actually happening:
#!/usr/bin/env ruby
def foo(bar)
p "called :foo w/ #{bar.inspect}"
end
def foo=(bar)
p "called :foo= with #{bar.inspect}"
end
ret = (foo :bar1) # calls foo(bar)
p "ret: #{ret}" # "ret: called :foo w/ :bar1"
ret = (foo = :bar2) # assigns a local variable foo = 'bar2'
p "ret: #{ret}" # "ret: bar2"
ret = (send :foo=, :bar3) # calls foo=(bar), returns what p returns
p "ret: #{ret}" # "ret: called :foo= with :bar3"
ret = (self.foo = :bar4) # calls foo=(bar), returns ???
p "ret: #{ret}" # "ret: bar4"
Basically, the Ruby parser (in 2.1 at least) behaves as if self.foo= was calling an accessor method (even if it actually isn't assigning anything), and will always return the value passed to it irrespective of what you sent it, rather than the accessor's return value.
Demonstration:
#!/usr/bin/env ruby
class << self
attr_accessor :foo
def foo=(bar)
p "called :foo= with #{bar.inspect}"
#foo = :baz
end
end
ret = (self.foo = :bar)
p "ret: #{ret} vs #foo: #{#foo.inspect}"
Outputs:
"called :foo= with :bar"
"ret: bar vs #foo: :baz"
Edit: hat #tessi for the reference:
Methods that end with an equals sign indicate an assignment method. For assignment methods the return value is ignored, the arguments are returned instead.
I think the reason why it's failing is that local variable names take precedence over method names when they are defined.
So you need to use send so that self knows it's looking for a method instead of a variable.
You need to do this:
self.foo = 'bar'
I have a method with a lengthy list of optional arguments, such as:
def foo(foo = nil, bar = nil, baz = nil, qux = nil)
# no-op
end
I thought that calling the method and passing a split hash as a parameter would map the hash items to parameters by matching the key with the method parameter:
params = { bar: 'bar', foo: 'foo' }
foo(*params)
Unfortunately, when I examine the local variables after calling the method with a split hash, I get exactly what I'd expect if I passed in a split array, but it's not what I was hoping for:
foo == [:bar, 'bar'] # hoped: foo == 'foo'
bar == [:foo, 'foo'] # hoped: bar == 'bar'
What am I lacking here?
(this answer refers to the version of Ruby that was current when the question was asked. See edit for the situation today.)
Ruby does not support passing arguments by name. The splat operator (*) expands an arbitrary enumerable by calling to_ary on it and splices the result into the argument list. In your case, the enumerable you pass in is a hash, which gets transformed into an array of key-value pairs:
[2] pry(main)> params.to_a
=> [[:bar, "bar"], [:foo, "foo"]]
So the first two arguments of the function will be the values [:bar, "bar"] and [:foo, "foo"] (regardless of their parameter name!).
If you want to have something similar to keyword arguments in Ruby, you can make use of the fact that you don't need the braces when passing a hash to a function as the last argument:
def foo(opts = {})
bar = opts[:bar] || <default>
foo = opts[:foo] || <default>
# or with a lot of parameters:
opts = { :bar => <default>, :foo => <default>, ... }.merge(opts)
end
foo(foo: 3) # equivalent to foo({ foo: 3 })
EDIT:
As of version 2.0, Ruby now supports named arguments with a dedicated syntax. Thanks to user jgoyon for pointing that out.
This problem well known as named parameters. Because Ruby doesn't have named parameters so here's classical way to deal with it:
def foo(options = {})
options = {:foo => nil, :bar => nil, :baz => nil, qux => nil}.merge(options)
..
end
or using ActiveSupport::CoreExtensions::Hash::ReverseMerge from Rails
def foo(options = {})
options.reverse_merge!{:foo => nil, :bar => nil, :baz => nil, qux => nil}
..
end
In future version Ruby 2.0 will possible to use named parameters
def foo(foo: nil, bar: nil, baz: nil, qux: nil)
puts "foo is #{foo}, bar is #{bar}, baz is #{baz}, ..."
..
end
In ruby, I often find myself writing the following:
class Foo
def initialize(bar, baz)
#bar = bar
#baz = baz
end
<< more stuff >>
end
or even
class Foo
attr_accessor :bar, :baz
def initialize(bar, baz)
#bar = bar
#baz = baz
end
<< more stuff >>
end
I'm always keen to minimise boilerplate as much as possible - so is there a more idiomatic way of creating objects in ruby?
One option is that you can inherit your class definition from Struct:
class Foo < Struct.new(:bar, :baz)
# << more stuff >>
end
f = Foo.new("bar value","baz value")
f.bar #=> "bar value"
f.baz #=> "baz value"
I asked a duplicate question, and suggested my own answer there, expecting for a better one, but a satisfactory one did not appear. I will post my own one.
Define a class method like the following along the spirit of attr_accessor, attr_reader, attr_writer methods.
class Class
def attr_constructor *vars
define_method("initialize") do |*vals|
vars.zip(vals){|var, val| instance_variable_set("##{var}", val)}
end
end
end
Then, you can use it like this:
class Foo
attr_constructor :foo, :bar, :buz
end
p Foo.new('a', 'b', 'c') # => #<Foo:0x93f3e4c #foo="a", #bar="b", #buz="c">
p Foo.new('a', 'b', 'c', 'd') # => #<Foo:0x93f3e4d #foo="a", #bar="b", #buz="c">
p Foo.new('a', 'b') # => #<Foo:0x93f3e4e #foo="a", #bar="b", #buz=nil>
Struct
Struct object's are classes which do almost what you want. The only difference is, the initialize method has nil as default value for all it's arguments. You use it like this
A= Struct.new(:a, :b, :c)
or
class A < Struc.new(:a, :b, :c)
end
Struct has one big drawback. You can not inherit from another class.
Write your own attribute specifier
You could write your own method to specify attributes
def attributes(*attr)
self.class_eval do
attr.each { |a| attr_accessor a }
class_variable_set(:##attributes, attr)
def self.get_attributes
class_variable_get(:##attributes)
end
def initialize(*vars)
attr= self.class.get_attributes
raise ArgumentError unless vars.size == attr.size
attr.each_with_index { |a, ind| send(:"#{a}=", vars[ind]) }
super()
end
end
end
class A
end
class B < A
attributes :a, :b, :c
end
Now your class can inherit from other classes. The only drawback here is, you can not get the number of arguments for initialize. This is the same for Struct.
B.method(:initialize).arity # => -1
You could use Virtus, I don't think it's the idiomatic way to do so but it does all the boiler plate for you.
require 'Virtus'
class Foo
include 'Virtus'
attribute :bar, Object
attribute :baz, Object
end
Then you can do things like
foo = Foo.new(:bar => "bar")
foo.bar # => bar
If you don't like to pass an hash to the initializer then add :
def initialize(bar, baz)
super(:bar => bar, :baz => baz)
end
If you don't think it's DRY enough, you can also do
def initialize(*args)
super(self.class.attributes.map(&:name).zip(args)])
end
I sometimes do
#bar, #baz = bar, baz
Still boilerplate, but it only takes up one line.
I guess you could also do
["bar", "baz"].each do |variable_name|
instance_variable_set(:"##{variable_name}", eval(variable_name))
end
(I'm sure there's a less dangerous way to do that, mind you)
https://bugs.ruby-lang.org/issues/5825 is a proposal to make the boilerplate less verbose.
You could use an object as param.
class Foo
attr_accessor :param
def initialize(p)
#param = p
end
end
f = Foo.new
f.param.bar = 1
f.param.bax = 2
This does not save much lines in this case but it will if your class has to handle a large number of param. You could also implement a set_param and get_param method if you want to keep your #param var private.