如何在 ruby 中将任何方法转换为中缀运算符
How to convert any method to infix operator in ruby
在某些语言中,例如 Haskell,it is possible 将任何带有两个参数的函数用作中缀运算符。
我觉得这个符号很有趣,想在 ruby 中实现同样的效果。
给出一个虚构的方法or_if_familiar
我希望能够写出类似 "omg" or_if_familiar "oh!"
而不是 or_if_familiar("omg", "oh!")
的东西
如何在 ruby 中创建这样的符号(不修改 ruby 本身)?
在Ruby中,运算符是前缀还是中缀是由解析器固定的。运算符优先级也是固定的。除了修改解析器之外,没有办法改变这些东西。
但是您可以为您的对象实现内置运算符
虽然您不能更改内置运算符的固定性或优先级,但您可以通过定义方法为您的对象实现运算符。那是因为 Ruby 将运算符转换为方法调用。例如,这个表达式:
a + b
翻译成:
a.+(b)
因此,您可以通过定义 + 方法为任意对象实现 + 运算符:
def +(rhs)
...
end
前缀运算符 -
调用方法 @-
,因此要实现前缀 - 您可以这样做:
def @-
..
end
你也可以使用方法
您可以将自己的中缀运算符实现为普通方法。这将需要与您想要的略有不同的语法。你想要:
"omg" or_if_familiar "oh!"
这是你不能拥有的。你可以拥有的是:
"omg".or_if_familiar "oh!"
之所以可行,是因为在 Ruby 中,方法参数上的括号通常会被省略。以上相当于:
"omg".or_if_familiar("oh!")
在这个例子中,我们将通过猴子修补字符串 class:
来实现
class String
def or_ir_familiar(rhs)
...
end
end
Ruby 没有中缀方法语法,除了一组固定和预定义的运算符。并且 Ruby 不允许用户代码更改语言语法。所以,你想要的不可能。
根据 Wayne Conrad 的回答,我可以编写以下代码,适用于 ruby 顶级中定义的任何方法:
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
允许写入
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
谢谢韦恩!
关于同一主题的有趣参考资料:
- Defining a new logical operator in Ruby
聚会有点晚了,但我一直在玩弄它,你可以使用运算符重载来创建中缀运算符,就像在 python 中一样(但需要更多的工作),语法变成a |op| b
,方法如下:
首先是一个快速而肮脏的复制粘贴来玩中缀:
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
#
好的
第 1 步:创建 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
第 2 步:修复所有没有 |
方法的 classes 和三种特殊情况(true
、false
和 nil
)(注意:您可以在此处添加任何 class,它可能会正常工作)
[ 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}}
第 3 步:以 5 种方式之一定义您的运算符
# 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
第 4 步:使用它们(间距无关紧要):
2 |pow| 3 # => 8
9|mod|4 # => 1
3| avg |6 # => 4.5
0 | pick | 1 # => 0 or 1 (randomly chosen)
你甚至可以有点咖喱:
(这只适用于第一个操作数)
diff_from_3 = 3 |diff
diff_from_3| 2 # => 1
diff_from_3| 4 # => 1
diff_from_3| -3 # => 6
作为奖励,这个小方法允许您在不使用 .new
:
的情况下定义中缀(或任何对象)
def Infix *args, &block
Infix.new *args, &block
end
pow = Infix -> (x, y) {x ** y} # and so on
剩下要做的就是将它包装在一个模块中
希望对您有所帮助
P.S。您可以与运算符一起使用 a <<op>> b
、a -op- b
、a >op> b
和 a <op<b
来表示方向性,a **op** b
来表示优先级以及您想要的任何其他组合但在使用 true
、false
和 nil
作为带有逻辑运算符(|
、&&
、not
等)的第一个操作数时要小心因为它们倾向于 return 在调用中缀运算符之前。
例如:false |equivalent_of_or| 5 # => true
如果你不更正。
最后,运行 这将检查所有内置 classes 的一堆情况作为第一个和第二个操作数:
# 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}
在某些语言中,例如 Haskell,it is possible 将任何带有两个参数的函数用作中缀运算符。
我觉得这个符号很有趣,想在 ruby 中实现同样的效果。
给出一个虚构的方法or_if_familiar
我希望能够写出类似 "omg" or_if_familiar "oh!"
而不是 or_if_familiar("omg", "oh!")
如何在 ruby 中创建这样的符号(不修改 ruby 本身)?
在Ruby中,运算符是前缀还是中缀是由解析器固定的。运算符优先级也是固定的。除了修改解析器之外,没有办法改变这些东西。
但是您可以为您的对象实现内置运算符
虽然您不能更改内置运算符的固定性或优先级,但您可以通过定义方法为您的对象实现运算符。那是因为 Ruby 将运算符转换为方法调用。例如,这个表达式:
a + b
翻译成:
a.+(b)
因此,您可以通过定义 + 方法为任意对象实现 + 运算符:
def +(rhs)
...
end
前缀运算符 -
调用方法 @-
,因此要实现前缀 - 您可以这样做:
def @-
..
end
你也可以使用方法
您可以将自己的中缀运算符实现为普通方法。这将需要与您想要的略有不同的语法。你想要:
"omg" or_if_familiar "oh!"
这是你不能拥有的。你可以拥有的是:
"omg".or_if_familiar "oh!"
之所以可行,是因为在 Ruby 中,方法参数上的括号通常会被省略。以上相当于:
"omg".or_if_familiar("oh!")
在这个例子中,我们将通过猴子修补字符串 class:
来实现class String
def or_ir_familiar(rhs)
...
end
end
Ruby 没有中缀方法语法,除了一组固定和预定义的运算符。并且 Ruby 不允许用户代码更改语言语法。所以,你想要的不可能。
根据 Wayne Conrad 的回答,我可以编写以下代码,适用于 ruby 顶级中定义的任何方法:
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
允许写入
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
谢谢韦恩!
关于同一主题的有趣参考资料:
- Defining a new logical operator in Ruby
聚会有点晚了,但我一直在玩弄它,你可以使用运算符重载来创建中缀运算符,就像在 python 中一样(但需要更多的工作),语法变成a |op| b
,方法如下:
首先是一个快速而肮脏的复制粘贴来玩中缀:
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
#
好的
第 1 步:创建 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
第 2 步:修复所有没有 |
方法的 classes 和三种特殊情况(true
、false
和 nil
)(注意:您可以在此处添加任何 class,它可能会正常工作)
[ 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}}
第 3 步:以 5 种方式之一定义您的运算符
# 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
第 4 步:使用它们(间距无关紧要):
2 |pow| 3 # => 8
9|mod|4 # => 1
3| avg |6 # => 4.5
0 | pick | 1 # => 0 or 1 (randomly chosen)
你甚至可以有点咖喱: (这只适用于第一个操作数)
diff_from_3 = 3 |diff
diff_from_3| 2 # => 1
diff_from_3| 4 # => 1
diff_from_3| -3 # => 6
作为奖励,这个小方法允许您在不使用 .new
:
def Infix *args, &block
Infix.new *args, &block
end
pow = Infix -> (x, y) {x ** y} # and so on
剩下要做的就是将它包装在一个模块中
希望对您有所帮助
P.S。您可以与运算符一起使用 a <<op>> b
、a -op- b
、a >op> b
和 a <op<b
来表示方向性,a **op** b
来表示优先级以及您想要的任何其他组合但在使用 true
、false
和 nil
作为带有逻辑运算符(|
、&&
、not
等)的第一个操作数时要小心因为它们倾向于 return 在调用中缀运算符之前。
例如:false |equivalent_of_or| 5 # => true
如果你不更正。
最后,运行 这将检查所有内置 classes 的一堆情况作为第一个和第二个操作数:
# 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}