How can I cleanly define "antonym" or "opposite" methods in Ruby / Rails? - ruby-on-rails

I'm pretty often defining methods and their antonyms in the code I'm writing, as in:
def happy?
#happiness > 3
end
def sad?
!happy?
end
Which is fine, but I'm a little surprised that Ruby or ActiveSupport doesn't give me something like:
def happy?
#happiness > 3
end
alias_opposite :sad? :happy?
Or am I just looking in the wrong place?

There is no such method in popular libraries, but there is how this could be implemented
class Module
def alias_opposite(a, b)
define_method(a) { !self.send(b) }
end
end
Usage
class A < Struct.new(:happiness)
def happy?
happiness > 3
end
alias_opposite :sad?, :happy?
end
p A.new(1).sad? # => true
p A.new(5).sad? # => false

I suspect this pattern is not as common in ruby because the unless keyword often does the trick:
# ...
clap_your_hands if happy?
stomp_your_feet unless happy?
# ...
Of course, its simple to roll your own:
module Antonymator
def define_antonym(as, of)
define_method(as.to_sym) do |*args|
return !(send(of.to_sym, *args))
end
end
end
# Usage Example
class AreThey
extend Antonymator
define_antonym :uneql?, :eql?
define_antonym :nonconsecutive?, :consecutive?
def eql?(a, b)
a == b
end
def consecutive?(a, b)
a.next == b
end
end
are_they = AreThey.new
puts are_they.uneql? 1, 2 # true
puts are_they.nonconsecutive? 1, 2 # false

If your methods return a Boolean, you can always include the positive method in the negative method.
def drinking_age?(age)
age > #restricted_age
end
def not_drinking_age?(age)
!drinking_age?(age)
end
#restricted_age = 20
Hope that helps.
I guess it depends on what 'opposite' means in the context.

Related

Is it possible to call super to invoke alias method in a method that is being aliased

Let's say in the activerecord model car there are two boolean fields: suv and blue
If there is a method in the car model defined as such
def suv
something_true? ? super : false
end
alias :blue :suv
Now if something_true? is true, the "super" works if i invoke car.suv. However, it does not work if i invoke car.blue, then the car.blue instead returns the value of suv stored in the database. Is there anyway to make this work?
It's a clever idea but I don't think it will work. Even if it's accessed through an alias, calling super inside the method suv will only call suv. You can use metaprogramming though:
class A
def a; 1; end
def b; 2; end
end
class B < A
def initialize(condition)
#condition = condition
end
%i{a b}.each do |fn|
define_method(fn) do
#condition ? super() : "default"
end
end
end
puts B.new(false).a # => "default"
puts B.new(false).b # => "default"
puts B.new(true).a # => 1
puts B.new(true).b # => 2

Neat way to get and set keys of json column in Rails

I have a model/table with a json column in it as follows
t.json :options, default: {}
The column can contain many keys within it, something like this
options = {"details" : {key1: "Value1", key2: "Value2"}}
I want to set and get these values easily. So i have made getters and setters for the same.
def key1
options['details']&.[]('key1')
end
def key1=(value)
options['details'] ||= {}
options['details']['key1'] ||=0
options['details']['key1'] += value
end
But this just adds lines to my code, and it does not scale when more details are added. Can you please suggest a clean and neat way of doing this?
Use dynamic method creation:
options['details'].default_proc = ->(_,_) {{}}
ALLOWED_KEYS = %i[key1 key2 key3]
ALLOWED_KEYS.each do |key|
define_method key do
options['details'][key] if options['details'].key?(key)
end
define_method "#{key}=" do |value|
(options['details'][key] ||= 0) += value
end
end
You can just pass the key as a parameter as well right?
def get_key key=:key1
options['details']&.[](key)
end
def set_key= value, key=:key1
options['details'] ||= {}
options['details'][key] ||=0
options['details'][key] += value
end
Simple & Short
Depending on re-usability you can choose different options. The short option is to simply define the methods using a loop in combination with #define_method.
class SomeModel < ApplicationRecord
option_accessors = ['key1', 'key2']
option_accessors.map(&:to_s).each do |accessor_name|
# ^ in case you provide symbols in option_accessors
# this can be left out if know this is not the case
define_method accessor_name do
options.dig('details', accessor_name)
end
define_method "#{accessor_name}=" do |value|
details = options['details'] ||= {}
details[accessor_name] ||= 0
details[accessor_name] += value
end
end
end
Writing a Module
Alternatively you could write a module that provide the above as helpers. A simple module could look something like this:
# app/model_helpers/option_details_attribute_accessors.rb
module OptionDetailsAttributeAccessors
def option_details_attr_reader(*accessors)
accessors.map(&:to_s).each do |accessor|
define_method accessor do
options.dig('details', accessor)
end
end
end
def option_details_attr_writer(*accessors)
accessors.map(&:to_s).each do |accessor|
define_method "#{accessor}=" do |value|
details = options['details'] ||= {}
details[accessor] ||= 0
details[accessor] += value
end
end
end
def option_details_attr_accessor(*accessors)
option_details_attr_reader(*accessors)
option_details_attr_writer(*accessors)
end
end
Now you can simply extend your class with these helpers and easily add readers/writers.
class SomeModel < ApplicationRecord
extend OptionDetailsAttributeAccessors
option_details_attr_accessor :key1, :key2
end
If anything is unclear simply ask away in the comments.

extract `.for()` into a method

I have a class like this one:
class House
def bricks
Brick.for(#house_plan).where(size: 5)
end
def wood
Wood.for(#house_plan).where(size: 5)
end
end
My goal is to extract the call for(self).where(size: 5):
What I tried first was:
class House
def bricks
Brick.match_material
end
def wood
Wood.match_material
end
def match_material
for(#house_plan).where(size: 5)
end
end
But then I got this error:
syntax error, unexpected '\n', expecting :: or '[' or '.'
Then I changed my code to:
def match_material
.for(#house_plan).where(size: 5)
end
And now when I do:
house = House.new(HousePlan.new)
house.bricks
I get this error:
formal argument cannot be an instance variable
In this line: for(#house_plan).where(size: 5)
What do I wrong?
Your approach isn't right, remember match_material method will always be called in the context of your self. I would do it this way:
def bricks
match_material(Brick)
end
def wood
match_material(Wood)
end
def match_material(klass)
klass.for(#house_plan).where(size: 5)
end
Just out of curiosity:
def bricks
klazz = Kernel.const_get(__callee__[0...-1].capitalize)
klazz.for(#house_plan).where(size: 5)
end
alias :woods :bricks
NB: In this approach aliased methods are to be named consistently(bricks, woods.) Please don’t use it in production unless you understand what you are doing.

Ruby/Rails: Prepend, append code to all methods

I wrote a small benchmarking Class for testing my code doing development. At the moment I have to add the Class to the beginning and end of every method. Is it posible to prepend, append on the fly, so that I don't have to clutter my code?
class ApplicationController
before_filter :init_perf
after_filter :write_perf_results_to_log!
def init_perf
#perf ||= Perf.new
end
def write_perf_results_to_log!
#perf.results
end
end
class Products < ApplicationsController
def foo
#perf.log(__methond__.to_s)
caculation = 5 *4
#perf.write!
end
def bar
#perf.log(__methond__.to_s)
caculation = 1 / 5
#perf.write!
end
end
This is the Perf class. It is located in the services folder.
class Perf
def initialize
#results = []
end
def log(note)
#start = Time.now
#note = note
end
def write!
if #results.find {|h| h[:note] == #note } # Update :sec method exists in results
#results.select { |h| h["note"] == #note; h[":sec"] = (Time.now - #start).round(3) }
else # Add new Hash to results
#results << { :note => #note, :sec => (Time.now - #start).round(3) }
end
end
def results
content = "
PERFORMANCE STATISTICS!
"
#results.each do |r|
content += r[:note] + " " + r[:sec].to_s + "
"
end
content += "
"
Rails.logger.info content
end
end
In general computing terms what you want to do is called code instrumentation. There are several ways to accomplish this, however here's one (crude) example using some metaprogramming:
First define a new method that we will use for injecting our instrumentation code:
class ApplicationController
def self.instrument_methods(*methods)
methods.each { |m|
# Rename original method
self.send(:alias_method, "#{m}_orig", m)
# Redefine old method with instrumentation code added
define_method m do
puts "Perf log #{m}"
self.send "#{m}_orig"
puts "Perf write"
end
}
end
end
How to use it:
class Product < ApplicationController
def foo
puts "Foo"
end
def bar
puts "Bar"
end
# This has to be called last, once the original methods are defined
instrument_methods :foo, :bar
end
Then:
p = Product.new
p.foo
p.bar
Will output:
Perf log foo
Foo
Perf write
Perf log bar
Bar
Perf write
Here are some other ways to instrument ruby code and measure performance:
http://ruby-prof.rubyforge.org/
http://www.igvita.com/2009/06/13/profiling-ruby-with-googles-perftools/
There is better solution.
class ApplicationController
def self.inherited(klass)
def klass.method_added(name)
return if #_not_new
#_not_new = true
original = "original #{name}"
alias_method original, name
define_method(name) do |*args, &block|
puts "==> called #{name} with args: #{args.inspect}"
result = send original, *args, &block
puts "<== result is #{result}"
result
end
#_not_new = false
end
end
end
class Product < ApplicationController
def meth(a1, a2)
a1 + a2
end
end
product = Product.new
puts product.meth(2,3)
And the result:
==> called meth with args: [2, 3]
<== result is 5
5
The source & explanation are here: http://pragprog.com/screencasts/v-dtrubyom/the-ruby-object-model-and-metaprogramming. I recommend to spend not a big money to get this course.
I'm the author of aspector gem. Thanks to dimuch for mentioning it.
I've come up with a solution using aspector. Here are the high level steps:
Create an aspect as a subclass of Aspector::Base
Inside the aspect, define advices (before/after/around are the primary types of advices)
Apply the aspect on target class (or module/object)
The full code can be found in this gist. Please feel free to let me know if you have questions or the solution doesn't do what you intend to.
class PerfAspect < Aspector::Base
around options[:action_methods] do |proxy|
#perf ||= Perf.new
proxy.call
#perf.results
end
around options[:other_methods], :method_arg => true do |method, proxy, *args, &block|
#perf.log(method)
result = proxy.call *args, &block
#perf.write!
result
end
end
action_methods = [:action]
other_methods = Products.instance_methods(false) - action_methods
PerfAspect.apply(Products, :action_methods => action_methods, :other_methods => other_methods)
Guess aspector gem can help. It's not well documented but has useful examples.

Difference between &param and :&param in ruby methods

I have functions with the definition def test(&param) and def test(:&param). What is the difference between both?
The difference is that def test(:&param) causes a syntax error and def test(&param) does not.
def test(&block) ...
means that our method accepts a block:
def test(number, &block)
yield number
# same as
# block.call number
end
test(10) {|a| a+a}
#=> 20
# or
block = proc{|a| a*a}
test 10, &block
#=> 100
While def test(:&param) will throw an error.
Also you can call something like method(&:operator):
[1,2,3].inject(&:+)
#=> 6
That is the same as
[1,2,3].inject{|sum, i| sum+i }

Resources