How to convert any method to infix operator in ruby

后端 未结 4 1304
陌清茗
陌清茗 2020-12-20 03:40

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 ac

4条回答
  •  执念已碎
    2020-12-20 04:41

    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 <> b, a -op- b, a >op> b and a 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}
    

提交回复
热议问题