kamaradclimber
kamaradclimber

Reputation: 2489

How to convert any method to infix operator in ruby

In some language such as Haskell, it is possible to use any function taking two arguments as an infix operator.

I find this notation interesting and would like to achieve the same in ruby.

Given a imaginary method or_if_familiar I'd like to be able to write something like "omg" or_if_familiar "oh!" instead of or_if_familiar("omg", "oh!")

How one would create such a notation in ruby (without modifying ruby itself)?

Upvotes: 2

Views: 1742

Answers (4)

Asone Tuhid
Asone Tuhid

Reputation: 549

A bit late to the party but I've been toying around with it and you can use operator overloading to create Infix operators just like in python (but with a bit more work), the syntax becomes a |op| b, here's how:

First a quick and dirty copy-paste to play around with Infix:

class Infix def initialize*a,&b;raise'arguments size mismatch'if a.length<0||a.length>3;raise'both method and b passed'if a.length!=0&&b;raise'no arguments passed'if a.length==0&&!b;@m=a.length>0? a[0].class==Symbol ? method(a[0]):a[0]:b;if a.length==3;@c=a[1];@s=a[2]end end;def|o;if@c;o.class==Infix ? self:@m.(@s,o)else;raise'missing first operand'end end;def coerce o;[Infix.new(@m,true,o),self]end;def v o;Infix.new(@m,true,o)end end;[NilClass,FalseClass,TrueClass,Object,Array].each{|c|c.prepend Module.new{def|o;o.class==Infix ? o.v(self):super end}};def Infix*a,&b;Infix.new *a,&b end
#

Ok

Step 1: create the Infix class

class Infix
  def initialize *args, &block
    raise 'error: arguments size mismatch' if args.length < 0 or args.length > 3
    raise 'error: both method and block passed' if args.length != 0 and block
    raise 'error: no arguments passed' if args.length == 0 and not block
    @method = args.length > 0 ? args[0].class == Symbol ? method(args[0]) : args[0] : block
    if args.length == 3; @coerced = args[1]; @stored_operand = args[2] end
  end
  def | other
    if @coerced
      other.class == Infix ? self : @method.call(@stored_operand, other)
    else
      raise 'error: missing first operand'
    end
  end
  def coerce other
    [Infix.new(@method, true, other), self]
  end
  def convert other
    Infix.new(@method, true, other)
  end
end

Step 2: fix all the classes that don't have a | method and the three special cases (true, false, and nil) (note: you can add any class in here and it will probably work fine)

[ NilClass, FalseClass, TrueClass,
  Float, Symbol, String, Rational,
  Complex, Hash, Array, Range, Regexp
].each {|c| c.prepend Module.new {
  def | other
    other.class == Infix ? other.convert(self) : super
  end}}

Step 3: define your operators in one of 5 ways

# Lambda
pow = Infix.new -> (x, y) {x ** y}
# Block
mod = Infix.new {|x, y| x % y}
# Proc
avg = Infix.new Proc.new {|x, y| (x + y) / 2.0}
# Defining a method on the spot (the method stays)
pick = Infix.new def pick_method x, y
  [x, y][rand 2]
end
# Based on an existing method
def diff_method x, y
  (x - y).abs
end
diff = Infix.new :diff_method

Step 4: use them (spacing doesn't matter):

2 |pow| 3      # => 8
9|mod|4        # => 1
3| avg |6      # => 4.5
0 | pick | 1   # => 0 or 1 (randomly chosen)

You can even kinda sorta curry: (This only works with the first operand)

diff_from_3 = 3 |diff

diff_from_3| 2    # => 1
diff_from_3| 4    # => 1
diff_from_3| -3   # => 6

As a bonus, this little method allows you to define Infixes (or any object really) without using .new:

def Infix *args, &block
  Infix.new *args, &block
end

pow = Infix -> (x, y) {x ** y} # and so on

All that's left to do is wrap it up in a module

Hope this helped

P.S. You can muck about with the operators to have something like a <<op>> b, a -op- b, a >op> b and a <op<b for directionality, a **op** b for precedence and any other combination you want but beware when using true, false and nil as the first operand with logical operators (|, &&, not, etc.) as they tend to return before the infix operator is called.

For example: false |equivalent_of_or| 5 # => true if you don't correct.

FINALLY, run this to check a bunch of cases of all the builtin classes as both the first and second operand:

# pp prints both inputs
pp = Infix -> (x, y) {"x: #{x}\ny: #{y}\n\n"}

[ true, false, nil, 0, 3, -5, 1.5, -3.7, :e, :'3%4s', 'to',
  /no/, /(?: [^A-g7-9]\s)(\w{2,3})*?/,
  Rational(3), Rational(-9.5), Complex(1), Complex(0.2, -4.6),
  {}, {e: 4, :u => 'h', 12 => [2, 3]},
  [], [5, 't', :o, 2.2, -Rational(3)], (1..2), (7...9)
].each {|i| puts i.class; puts i |pp| i}

Upvotes: 7

kamaradclimber
kamaradclimber

Reputation: 2489

Based on Wayne Conrad's answer, I can write the following code that would work for any method defined in ruby top-level:

class Object
  def method_missing(method, *args)
    return super if args.size != 1
    # only work if "method" method is defined in ruby top level
    self.send(method, self, *args)
  end
end

which allows to write

def much_greater_than(a,b)
  a >= b * 10
end

"A very long sentence that say nothing really but should be long enough".much_greater_than "blah"

# or

42.much_greater_than 2

Thanks Wayne!

Interesting reference on the same subject:

Upvotes: 0

J&#246;rg W Mittag
J&#246;rg W Mittag

Reputation: 369458

Ruby does not have infix method syntax, except for a fixed and predefined set of operators. And Ruby does not allow user code to change the language syntax. Ergo, what you want is not possible.

Upvotes: 2

Wayne Conrad
Wayne Conrad

Reputation: 107989

In Ruby, whether the operator is prefix or infix is fixed by the parser. Operator precedence is also fixed. There is no way, short of modifying the parser, of changing these things.

But you can implement the built-in operators for your objects

Although you may not change the fix-ness or precedence of a built-in operator, you may implement operators for your objects by defining methods. That is because Ruby translates operators into method calls. For example, this expression:

a + b

is translated into:

a.+(b)

Therefore, you may implement the + operator for an arbitrary object by defining the + method:

def +(rhs)
  ...
end

The prefix operator - causes a call to method @-, so to implement prefix - you do this:

def @-
  ..
end

You may also use methods

You may implement your own infix operators as plain methods. This will require a slightly different syntax than what you want. You want:

"omg" or_if_familiar "oh!"

Which you cannot have. What you can have is:

"omg".or_if_familiar "oh!"

This works because, in Ruby, the parentheses on method arguments may often be omitted. The above is equivalent to:

"omg".or_if_familiar("oh!")

In this example, we would implement this by monkey-patching the String class:

class String
  def or_ir_familiar(rhs)
    ...
  end
end

Upvotes: 3

Related Questions