How could metaprogramming be used to reduce redundancy in this Ruby code? - ruby

class Device
def initialize(device_id, data_resource)
#id = device_id
#data_resource = data_resource
end
def display_device
mode = #data_resource.get_display_device_mode(#id)
presets = #data_resource.get_display_device_presets(#id)
summary = "display_device: #{mode} ($#{presets})"
return "* #{summary}" if presets == "XTC909"
summary
end
def chip
mode = #data_resource.get_chip_mode(#id)
presets = #data_resource.get_chip_presets(#id)
summary = "chip: #{mode} ($#{presets})"
return "* #{summary}" if presets == "XTC909"
summary
end
def input_device
mode = #data_resource.get_input_device_mode(#id)
presets = #data_resource.get_input_device_presets(#id)
summary = "input_device: #{mode} ($#{presets})"
return "* #{summary}" if presets == "XTC909"
summary
end
end
As you can see from the code above, there is quite a bit of redundancy within the methods. Regardless of whether metaprogramming is the best way to reduce this redundancy, I am hoping to learn how to use metaprogramming in Ruby to reduce some of the repetitiveness here if someone could provide some suggestions.

Here's a version that uses metaprogramming, though I'd also remove the duplication by putting it in a method where it belongs.
class Device
def initialize(device_id, data_resource)
#id = device_id
#data_resource = data_resource
end
def resource_summary(resource_name)
mode = #data_resource.send("get_#{resource_name}_mode", #id)
presets = #data_resource.send("get_#{resource_name}_presets", #id)
summary = "#{resource_name}: #{mode} ($#{presets})"
return "* #{summary}" if presets == "XTC909"
summary
end
def self.resource_accessor(*names)
names.each {|resource| define_method(resource) {resource_summary resource}}
end
resource_accessor :display_device, :chip, :input_device
end
If you really didn't want to make a method for that functionality, you could just replace the resource_summary method call with the body of the resource_summary method.

Something like this could work so you can define 'components' (or whatever they are) declaratively. This is overkill for this sort of example, but you can use it when you need to define dozens/hundreds of these things, or you're putting it as part of some framework (like rails does).
The component class level method would usually live in some other module that gets included into the class rather than declaring it inline where it's used like this.
class Device
class << self
def component(component_name)
define_method(component_name) do
mode = #data_resource.send("get_#{component_name}_mode", #id)
presets = #data_resource.send("get_#{component_name}_presets", #id)
summary = "#{component_name} : #{mode} ($#{presets})"
presets == "XTC909" ? "* #{summary}" : summary
end
end
end
component :display_device
component :chip
component :input_device
def initialize(device_id, data_resource)
#id = device_id
#data_resource = data_resource
end
end
You can drive it with something like:
class DataResource
def method_missing(method, *args)
# puts "called #{method} with:#{args.inspect}"
"#{method}-#{args.join(':')}"
end
end
device = Device.new("ID123", DataResource.new)
puts device.display_device
puts device.chip
puts device.input_device

Obviously, some names should change...
def display_device
i_heart_meta_programming("display_device")
end
def chip
i_heart_meta_programming("chip")
end
def input_device
i_heart_meta_programming("input_device")
end
def i_heart_meta_programming(what_to_get)
mode = #data_resource.send("get_#{what_to_get}_mode", #id)
mode = #data_resource.send("get_#{what_to_get}_presets", #id)
summary = "#{what_to_get}: #{mode} ($#{presets})"
return "* #{summary}" if presets == "XTC909"
summary
end

Are you sure that you need to reduce redundancy here at all? It's certainly possible, but anything you do will just make the code harder to understand and will not necessarily be a net win.

I guess u probably solve this alreaday, anyway this is my alternative:
class Device
def initialize(device_id, data_resource)
#id,#data_resource = device_id, data_resource
end
%w{display_device chip input_device}.each do |met|
define_method met do
mode = #data_resource.send("get_#{met}_mode", #id)
presets = #data_resource.send("get_#{met}_presets",#id)
summary = "#{met}: #{mode} ($#{presets})"
return "* #{summary}" if presets == "XTC909"
summary
end
end
end

Can you come up with a better example?
As I said your previous version of this, metaprogramming is hardly needed here. Basic encapsulation of functionality in methods would work.
Any examples that people give would be contrived and not really representative of real world usage of metaprogramming.

Related

Metrics/AbcSize Too High: How do I decrease the ABC in this method?

I have recently started using Rubocop to "standardise" my code, and it has helped me optimise a lot of my code, as well as help me learn a lot of Ruby "tricks". I understand that I should use my own judgement and disable Cops where necessary, but I have found myself quite stuck with the below code:
def index
if params[:filters].present?
if params[:filters][:deleted].blank? || params[:filters][:deleted] == "false"
# if owned is true, then we don't need to filter by admin
params[:filters][:admin] = nil if params[:filters][:admin].present? && params[:filters][:owned] == "true"
# if admin is true, then must not filter by owned if false
params[:filters][:owned] = nil if params[:filters][:owned].present? && params[:filters][:admin] == "false"
companies_list =
case params[:filters][:admin]&.to_b
when true
current_user.admin_companies
when false
current_user.non_admin_companies
end
if params[:filters][:owned].present?
companies_list ||= current_user.companies
if params[:filters][:owned].to_b
companies_list = companies_list.where(owner: current_user)
else
companies_list = companies_list.where.not(owner: current_user)
end
end
else
# Filters for deleted companies
companies_list = {}
end
end
companies_list ||= current_user.companies
response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
json_response(response)
end
Among others, the error that I'm getting is the following:
C: Metrics/AbcSize: Assignment Branch Condition size for index is too high. [<13, 57, 16> 60.61/15]
I understand the maths behind it, but I don't know how to simplify this code to achieve the same result.
Could someone please give me some guidance on this?
Thanks in advance.
Well first and foremost, is this code fully tested, including all the myriad conditions? It's so complex that refactoring will surely be disastrous unless the test suite is rigorous. So, write a comprehensive test suite if you don't already have one. If there's already a test suite, make sure it tests all the conditions.
Second, apply the "fat model skinny controller" paradigm. So move all the complexity into a model, let's call it CompanyFilter
def index
companies_list = CompanyFilter.new(current_user, params).list
response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
json_response(response)
end
and move all those if/then/else statements into the CompanyFilter#list method
tests still pass? great, you'll still get the Rubocop warnings, but related to the CompanyFilter class.
Now you need to untangle all the conditions. It's a bit hard for me to understand what's going on, but it looks as if it should be reducible to a single case statement, with 5 possible outcomes. So the CompanyFilter class might look something like this:
class CompanyFilter
attr_accessors :current_user, :params
def initialize(current_user, params)
#current_user = current_user
#params = params
end
def list
case
when no_filter_specified
{}
when user_is_admin
#current_user.admin_companies
when user_is_owned
# etc
when # other condition
# etc
end
end
private
def no_filter_specified
#params[:filter].blank?
end
def user_is_admin
# returns boolean based on params hash
end
def user_is_owned
# returns boolean based on params hash
end
end
tests still passing? perfect! [Edit] Now you can move most of your controller tests into a model test for the CompanyFilter class.
Finally I would define all the different companies_list queries as scopes on the Company model, e.g.
class Company < ApplicationRecord
# some examples, I don't know what's appropriate in this app
scope :for_user, ->(user){ where("...") }
scope :administered_by, ->(user){ where("...") }
end
When composing database scopes ActiveRecord::SpawnMethods#merge is your friend.
Post.where(title: 'How to use .merge')
.merge(Post.where(published: true))
While it doesn't look like much it lets you programatically compose scopes without overelying on mutating assignment and if/else trees. You can for example compose an array of conditions and merge them together into a single ActiveRecord::Relation object with Array#reduce:
[Post.where(title: 'foo'), Post.where(author: 'bar')].reduce(&:merge)
# => SELECT "posts".* FROM "posts" WHERE "posts"."title" = $1 AND "posts"."author" = $2 LIMIT $3
So lets combine that with a skinny controllers approach where you handle filtering in a seperate object:
class ApplicationFilter
include ActiveModel::Attributes
include ActiveModel::AttributeAssignment
attr_accessor :user
def initialize(**attributes)
super()
assign_attributes(attributes)
end
# A convenience method to both instanciate and apply the filters
def self.call(user, params, scope: model_class.all)
return scope unless params[:filters].present?
scope.merge(
new(
permit_params(params).merge(user: user)
).to_scope
)
end
def to_scope
filters.map { |filter| apply_filter(filter) }
.compact
.select {|f| f.respond_to?(:merge) }
.reduce(&:merge)
end
private
# calls a filter_by_foo method if present or
# defaults to where(key => value)
def apply_filter(attribute)
if respond_to? "filter_by_#{attribute}"
send("filter_by_#{attribute}")
else
self.class.model_class.where(
attribute => send(attribute)
)
end
end
# Convention over Configuration is sexy.
def self.model_class
name.chomp("Filter").constantize
end
# filters the incoming params hash based on the attributes of this filter class
def self.permit_params
params.permit(filters).reject{ |k,v| v.blank? }
end
# provided for modularity
def self.filters
attribute_names
end
end
This uses some of the goodness provided by Rails to setup objects with attributes that will dynamically handle filtering attributes. It looks at the list of attributes you have declared and then slices those off the params and applies a method for that filter if present.
We can then write a concrete implementation:
class CompanyFilter < ApplicationFilter
attribute :admin, :boolean, default: false
attribute :owned, :boolean
private
def filter_by_admin
if admin
user.admin_companies
else
user.non_admin_companies
end
end
# this should be refactored to use an assocation on User
def filter_by_owned
case owned
when nil
nil
when true
Company.where(owner: user)
when false
Company.where.not(owner: user)
end
end
end
And you can call it with:
# scope is optional
#companies = CompanyFilter.call(current_user, params), scope: current_user.companies)

Find which widget has focus in Ruby 2.3.3

In Python, I use:
who = widget.focus_get()
and in Perl:
$who = $widget->focusCurrent;
to tell me which widget has the focus. So:
What is the equivalent code in Ruby under Linux?
Is there a good book or article about low-level Ruby Tk? All the articles I have seen only cover the simplistic stuff.
[T]o tell me which widget has the focus[...w]hat is the equivalent code in Ruby under Linux?
Fairly equivalently, if you have three Tk objects (for example), then you can say (in Ruby):
array = [tk_object_1, tk_object_2, tk_object_3]
path = Tk.focus.path
index = array.find_index {|e| e.path == path}
object = array.at index
EDIT: Or, more simply:
object = Tk.focus
Here's how to determine which object (of your many Tk objects) this is:
array = [tk_object_1, tk_object_2, tk_object_3]
index = array.find_index {|e| e == object}
(End of edit.)
I managed to locate documentation for the focus method (in both the
Ruby wrapper
and the original
Tcl)
and for the path method (in the
Ruby wrapper).
I wrote a program which demonstrates setting and getting the focus:
require 'tk'
def focus_array
#focus_array ||= [
f_content, e_content,
to_one, e_one,
to_two, e_two,
]
end
def focus_array_class_width_max
#focus_array_class_width_max ||= focus_array.map {|e| e.class.to_s.length}.max
end
def focus_delay
milliseconds = 1000
Tk.after milliseconds, lambda_focus_rotate
nil
end
def focus_force(object)
force = true
Tk.focus_to object, force
nil
end
def focus_print
path = Tk.focus.path
index = focus_array.find_index {|e| e.path == path}
s = klass_justified index
puts "Item #{index}'s class and path are: #{s} #{path}"
nil
end
def focus_rotate
#counter += 1
index = #counter % focus_array.length
puts '' if 0 == index
focus_force focus_array.at index
nil
end
def klass_justified(index)
focus_array.at(index).class.to_s.ljust focus_array_class_width_max
end
def lambda_focus_rotate
#lambda_focus_rotate ||= Kernel.lambda do
focus_rotate
focus_print
focus_delay
end
end
def main
root.title = 'Root'
objects_create
#counter = -1 # Start before first.
focus_delay
Tk.mainloop
nil
end
def objects_create
# Keep order:
e_content
e_one
e_two
nil
end
def spacing_set(object)
object.width 7
object.grid padx: 40
end
#-------------
# Tk objects:
def e_content
#e_content ||= begin
e = Tk::Tile::Entry.new f_content
spacing_set e
end
end
def e_one
#e_one ||= begin
e = Tk::Tile::Entry.new to_one
spacing_set e
end
end
def e_two
#e_two ||= begin
e = Tk::Tile::Entry.new to_two
spacing_set e
end
end
def f_content
$f_content ||= begin
f = Tk::Tile::Frame.new root
f.grid
end
end
def root
$root ||= TkRoot.new
end
def to_one
#to_one ||= TkToplevel.new f_content
end
def to_two
#to_two ||= TkToplevel.new f_content
end
main
It works for me using Ruby 2.2.5 (with Tk 8.5.12) on Windows 7.
Is there a good book or article about low-level Ruby Tk? All the articles I have seen only cover the simplistic stuff.
To my knowledge, no one has written a thorough description of Ruby's wrapper for Tk (at least, not in English). However, we have the wrapper's
RDoc
documentation, and we can read books describing how to use Tk from other languages. I think the best one (for Ruby purposes) is Learning Perk/Tk by Nancy Walsh (1999). Here are its links at
Amazon,
Alibris
and
O'Reilly (along with its example code).
Also we can grep the wrapper's library source code (since it's installed on our computers).
Doing so for the constant TkToplevel (for example) leads to the file /Ruby/lib/ruby/2.2.0/tk/toplevel.rb (or wherever that file resides on your system).
In that file, searching (for the string TkToplevel) reveals that the constant seems to be defined as an alias for class Tk::Toplevel, which is documented
here.
Ultimately it takes fairly difficult effort to investigate how to do unusual things in Ruby with Tk.
Good results regarding particular issues have been obtained by asking on the
Ruby forum.
The
TkDocs
tutorial is very helpful, along with the Tcl-language
Tk commands
reference documentation.

How to make sure each Minitest unit test is fast enough?

I have a large amount of Minitest unit tests (methods), over 300. They all take some time, from a few milliseconds to a few seconds. Some of them hang up, sporadically. I can't understand which one and when.
I want to apply Timeout to each of them, to make sure anyone fails if it takes longer than, say, 5 seconds. Is it achievable?
For example:
class FooTest < Minitest::Test
def test_calculates_something
# Something potentially too slow
end
end
You can use the Minitest PLugin loader to load a plugin. This is, by far, the cleanest solution. The plugin system is not very well documented, though.
Luckily, Adam Sanderson wrote an article on the plugin system.
The best news is that this article explains the plugin system by writing a sample plugin that reports slow tests. Try out minitest-snail, it is probably almost what you want.
With a little modification we can use the Reporter to mark a test as failed if it is too slow, like so (untested):
File minitest/snail_reporter.rb:
module Minitest
class SnailReporter < Reporter
attr_reader :max_duration
def self.options
#default_options ||= {
:max_duration => 2
}
end
def self.enable!(options = {})
#enabled = true
self.options.merge!(options)
end
def self.enabled?
#enabled ||= false
end
def initialize(io = STDOUT, options = self.class.options)
super
#max_duration = options.fetch(:max_duration)
end
def record result
#passed = result.time < max_duration
slow_tests << result if !#passed
end
def passed?
#passed
end
def report
return if slow_tests.empty?
slow_tests.sort_by!{|r| -r.time}
io.puts
io.puts "#{slow_tests.length} slow tests."
slow_tests.each_with_index do |result, i|
io.puts "%3d) %s: %.2f s" % [i+1, result.location, result.time]
end
end
end
end
File minitest/snail_plugin.rb:
require_relative './snail_reporter'
module Minitest
def self.plugin_snail_options(opts, options)
opts.on "--max-duration TIME", "Report tests that take longer than TIME seconds." do |max_duration|
SnailReporter.enable! :max_duration => max_duration.to_f
end
end
def self.plugin_snail_init(options)
if SnailReporter.enabled?
io = options[:io]
Minitest.reporter.reporters << SnailReporter.new(io)
end
end
end

Assert_equal undefined local variable LRTHW ex52

Hi I made it to the lase exercise os Learn Ruby The Hard Way, and I come at the wall...
Here is the test code:
def test_gothon_map()
assert_equal(START.go('shoot!'), generic_death)
assert_equal(START.go('dodge!'), generic_death)
room = START.go("tell a joke")
assert_equal(room, laser_weapon_armory)
end
And here is the code of the file it should test:
class Room
attr_accessor :name, :description, :paths
def initialize(name, description)
#name = name
#description = description
#paths = {}
end
def ==(other)
self.name==other.name&&self.description==other.description&&self.paths==other.paths
end
def go(direction)
#paths[direction]
end
def add_paths(paths)
#paths.update(paths)
end
end
generic_death = Room.new("death", "You died.")
And when I try to launch the test file I get an error:
generic_death = Room.new("death", "You died.")
I tried to set the "generic_death = Room.new("death", "You died.")" in test_gothon_map method and it worked but the problem is that description of the next object is extremely long, so my questions are:
why assertion doesn't not respond to defined object?
can it be done different way then by putting whole object to testing method, since description of the next object is extremely long...
The nature of local variable is that they are, well, local. This means that they are not available outside the scope they were defined.
That's why ruby does not know what generic_death means in your test.
You can solve this in a couple of ways:
define rooms as constants in the Room class:
class Room
# ...
GENERIC_DEATH = Room.new("death", "You died.")
LASER_WEAPON_ARMORY = Room.new(...)
end
def test_gothon_map()
assert_equal(Room::START.go('shoot!'), Room::GENERIC_DEATH)
assert_equal(Room::START.go('dodge!'), Room::GENERIC_DEATH)
room = Room::START.go("tell a joke")
assert_equal(room, Room::LASER_WEAPON_ARMORY)
end
assert the room by its name, or some other identifier:
def test_gothon_map()
assert_equal(START.go('shoot!').name, "death")
assert_equal(START.go('dodge!').name, "death")
room = START.go("tell a joke")
assert_equal(room.name, "laser weapon armory")
end

Ruby - Method call to object in array

I'm working with a Ruby project for school, and have sadly not been able to find an answer to this question in my literature.
I have an array of camping lots, each containing a guest. I initialize the lots like this:
lots = Array.new
for i in (1..36)
lots[i] = Lot.new(i)
end
Further down I create a Guest object, initialize it, and now I want to add the Guest to my Lot. The method in the class Lot looks like this:
def AddGuest(guest)
#guest = guest
end
The problem comes when I want to call the method, as the Lot is in an Array.
lots[lotnumber].AddGuest(guest)
This call gives me the error:
undefined method `+#' for #<Guest:0x2c1ff14> (NoMethodError)
I have used require, so the classes know about each other. I've had quite a hard time understanding Ruby, could my error be that I try to access the AddGuest method in the Array class? I'm used to doing things like this in C++.
Below is the full source (the relevant parts at least).
Entire Lot class:
class Lot
def initialize(number)
#gauge = rand(2000) + 2000
#number = number
#guest = false
end
def Occupied()
return #guest
end
def AddGuest(guest)
#guest = guest
end
def RemoveGuest()
#guest = false
end
end
Parts of main.rb
#includes
require 'guest'
require 'lot'
#initiate comparison variables
userInput = "0"
numberOfGuests = 0
foundLot = false
guests = Array.new
lots = Array.new
#initialize lot list
for i in (1..36)
lots[i] = Lot.new(i)
end
Player input omitted
#make sure lot is not taken
while foundLot == false do
lotnumber = rand(35)+1
if lots[lotnumber].Occupied() == false then
foundLot = "true"
end
end
foundLot = false
guest = Guest.new(firstName, lastName, adress, phone, arrival, lotnumber)
guests.insert(numberOfGuests, guest)
numberOfGuests++
lots[lotnumber].AddGuest(guest) #this is where error hits
end
end
end
The error appears to be related to your use of the ++ operator, which is, quite naturally, supported in C++, but is not supported in Ruby.
The equivalent is:
numberOfGuests += 1
A couple little tips...
[1]
A slightly more idiomatic way to write this...
for i in (1..36)
lots[i] = Lot.new(i)
end
would be...
(1..36).each { |i| lots[i] << Lot.new(i) }
[2]
To remove a Guest from a Lot, you might want to set it to nil rather than false. This would be my suggestion...
class Lot
def initialize(number)
#gauge = rand(2000) + 2000
#number = number
# Don't need to set #guest -- it's nil by default.
end
# In Ruby, methods that return a boolean often have a "?".
# Makes it "read better" when you call the method. (See
# usage sample.)
def occupied?
! #guest.nil?
end
# There's a more commonplace way to do this. See below...
def add_guest(guest)
#guest = guest
end
def remove_guest()
#guest = nil
end
end
Example of usage:
>> lot = Lot.new(2)
=> #<Lot:0x1300920 #number=2, #gauge=3444>
>> lot.occupied
=> false
>> lot.add_guest('A guest')
=> "A guest"
>> lot.occupied?
=> true
>> lot.remove_guest
=> nil
>> lot.occupied?
=> false
Take two...
It's conventional to use attr_accessor methods in your class definition. They automatically add getter and setter methods to your class. You could do that instead of add_guest and remove_guest if you wanted to follow the common Ruby pattern...
class Lot
attr_accessor :number, :gauge, :guest
def initialize(number)
#gauge = rand(2000) + 2000
#number = number
end
def occupied?
! #guest.nil?
end
end
Usage...
irb(main):017:0> lot = Lot.new(3)
=> #<Lot:0xb7f7fca8 #gauge=3186, #number=3>
Set the Guest of a Lot (like add_guest)...
irb(main):019:0> lot.guest = 'A guest'
=> "A guest"
irb(main):020:0> lot.occupied?
=> true
Get the Guest for a Lot...
irb(main):025:0> lot.guest
=> "A guest"
Remove the Guest...
irb(main):021:0> lot.guest = nil
=> nil
irb(main):023:0> lot.occupied?
=> false
Generally Ruby method names are not capitalized. The convention are simply: ClassName, CONSTANT, method_name.
Since you have an Array of Lot objects, the following should be true:
lots.class # => Array
lots[1].class # => Lot
The method called should be defined for Lot.

Resources