Reputation: 2489
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
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
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
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
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.
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 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