Union of filters in Ruby

2019-09-07 05:56发布

问题:

I've designed a class NumberSet which contains an array on which I have to implement certain filters, which I've designed as classes. The code is:

class NumberSet
include Enumerable
 def initialize
    @arr=[]
 end
 def each (&block)
    @arr.each do |member|
        block.call (member)
    end
 end
 def << number
    @arr<<number unless  @arr.include?(number)
 end
 def [] (sito)
    @arr.select{|number| sito.filter(number)}
 end
end

class Filter
 attr_reader :filter
 def initialize &block
   @filter = block
 end
 def filter number
  @filter.call(number)
 end
end

class SignFilter
 def initialize(sign)
  @sign = sign
 end
 def filter number
  return true if(@sign==:negative && number<0)
  return true if(@sign==:positive && number>0)
  return true if(@sign==:nonnegative && number >= 0)
  return true if(@sign==:nonpositive && number <= 0)
 end
end

class TypeFilter
 def initialize(sign)
  @sign = sign
 end
 def filter number
  return true if (@sign==:complex && number.is_a?(Complex) == true)
  return true if (@sign==:integer && number.is_a?(Integer) == true)
  return true if (@sign==:real && (number.is_a?(Rational) == true 
  || number.is_a?Float)   == true))
 end
end

All works well, but I have to also define the & and | operators so they work like intersection and union, meaning that & should get only numbers that satisfy all filters and | numbers that satisfy at least one of the filters.

Also, the syntax

numbers[SignFilter.new(:non_negative) & Filter.new { |number| number.even? }]

must be valid.

How do I define them so they work correctly?


To answer some of the comments, I am looking for a way to make the operators &, | work the way I want them. The problem is not how to write the syntax, but rather what should the code between def and end be.

回答1:

You can define them just like any other method. I just tried this as an example:

class Adder
    def &(number_to_add)
        number_to_add + 20
    end
end

a = Adder.new
a & 20
#=> 40


回答2:

Just as a note because this looks like smell:

def filter number
  return true if(@sign==:negative && number<0)
  return true if(@sign==:positive && number>0)
  return true if(@sign==:nonnegative && number >= 0)
  return true if(@sign==:nonpositive && number <= 0)
end

Why try this:

class SignFilter
  @@signs = {negative: "<", positive: ">", nonnegative: ">=", nonpositive: "<="}
  @@signs.each do |meth,sign|
     define_method meth do |number|
       number.send(sign,0)
     end
  end
  def initialize(sign)
    @sign = sign
  end
  def filter(number)
    self.public_send(@sign,number)
  end
end

You can do the same with TypeFilter (as a hint Numeric has a #real? method) or define the methods individually. I know it is not an answer to your actual question I just like clean code. :)



回答3:

Here's something to meditate on...

def filter number
  return true if(@sign==:negative && number<0)
  return true if(@sign==:positive && number>0)
  return true if(@sign==:nonnegative && number >= 0)
  return true if(@sign==:nonpositive && number <= 0)
end

can be written as:

def filter number
  ( @sign == :negative    && number <  0 ) ||
  ( @sign == :positive    && number >  0 ) ||
  ( @sign == :nonnegative && number >= 0 ) ||
  ( @sign == :nonpositive && number <= 0 )
end

Why? Because each of the tests returns either true or false. If one returns true, none of the others joined by || will be tested because || short-circuits, and the final value Ruby sees will be true, which will be returned. If the first fails, || will tell Ruby to check the next, and on down the chain.

This mimics the behavior of the original code, but there's a possible problem in either. In a successful result, true is returned. Failure is going to be nil. Is that what you want, or do you want false returned to make the method only return true/false? In Ruby nil is a "falsey" value so it'll work the same but for consistency you might want to only return true/false.

Also, as a coding-style recommendation, use whitespace between your operators.

@sign==:negative && number<0

is not as readable as

@sign == :negative && number < 0

Readability is very important when debugging, or after passing the code on to someone else for maintenance. People like to say they'll fix it later, but later comes, the code remains the same and gets pushed into production, where it waits to irritate some poor soul at 3AM.



回答4:

How about something like this?

class Filter
  def initialize(&filter_block)
    @filter_block = filter_block
  end
  def filter(number)
    @filter_block.call(number)
  end
  def &(other)
    CompositeFilterAND.new(self, other)
  end
  def |(other)
    CompositeFilterOR.new(self, other)
  end
end

class CompositeFilterAND
  attr_accessor :left, :right

  def initialize(left, right)
    @left, @right = left, right
  end

  def filter(number)
    @left.filter(number) && @right.filter(number)  # This is the magic.
  end
end

class CompositeFilterOR
  attr_accessor :left, :right

  def initialize(left, right)
    @left, @right = left, right
  end

  def filter(number)
    @left.filter(number) || @right.filter(number)  # And this.
  end
end