ruby-mine

exploring the mine

^^

von murphy am 30.04.2006 (19 Uhr)

Syntax-Sonntag! Heute:

Selbstdefinierte Operatoren - oder - Wie bringe ich Ruby zum Lachen

Man kann in Ruby sehr einfach die vordefinierten Operatoren überschreiben, und zwar so:

class String
  def + other  #-> warning: method redefined; discarding old +
    self.to_i + other.to_i
  end
end
p '5' + '7'  #-> 12

Was aber, wenn uns die vordefinierten Operatoren nicht reichen? Würden wir nicht gerne auch so etwas schreiben:

# Da geht sogar der Highlighter kaputt :/
6 over 49  # Lotto!
8! / 5!    # Fakultät
[3,4,5] x [1,2,0]  # Kreuzprodukt
pattern = !/[A-Z]/ && (/^/ + MY_OTHER_PATTERN)  # Regexp-Logik

Hier bleibt Ruby stur: Die Syntax, so dynamisch sie auch scheint, ist fest. Wir müssen mit dem auskommen, was der Parser ohne Ausführung des Codes versteht.

Ein Versuch

Als nicht ganz ernst gemeinten Vorschlag zur Definition beliebiger Infix-Operatoren stelle ich heute die Smiley-Methode vor. Ziel ist die Auswertung solcher Ausdrücke:

a ^:mod^ b  # ^ wird extrem selten verwendet

Sieht witzig aus, ist aber nur eine Kurzform für:

a.^(:mod).^(b)

Die Aufrufe sollen nun umgeletet werden auf:

a.mod(b)

Dafür definieren wir Object#^ so, dass ein Proxy generiert wird, der sich einfach das erste Objekt (a im Beispiel) und das Symbol (:mod) merkt.

class Object
  # Umleitung von ^ auf Proxy
  def ^ method
    Proxy.new self, method
  end
end

# Proxy mit zwei Attributen
# (Zu dieser Konstruktion siehe Link am Ende)
class Proxy < Struct.new :object, :method
  # ruft man proxy.^ auf, wird der eigentliche Aufruf erzeugt
  def ^ other
    object.send method, other
  end
end

Was passiert nun mit a ^:mod^ b? Schrittweise.

a ^:mod^ b
# für Ruby:
a.^(:mod).^(b)

# 1. Schritt
pr = a.^( :mod )  #-> Proxy.new a, :mod

# 2. Schritt
pr.^( b )         #-> Proxy bekommt zweiten Operanden

# 3. Schritt, in Proxy#^
a.send(:mod, b)   #-> a.mod b wird aufgerufen

Wir müssen also noch Fixnum#mod definieren. Hier steht uns noch im Wege, dass die Klasse Fixnum die Methode ^ bereits definiert. Wir müssen sie vorher löschen.

class Fixnum
  remove_method :^
  def mod base
    self % base
  end
end

Jetzt geht es:

a = 4
b = 2
p a ^:mod^ b  #-> 0

Für Fans

Im Prinzip haben wir nicht mehr getan, als eine neue Variante von send zu implementieren. Wir können jetzt jeden Methodenaufruf umleiten:

p Array ^:new^ 3  #-> [nil, nil, nil]
self ^:puts^ 'Hello, World!'  #-> Hello, World!

Operatoren und Methoden in Ruby

Noch ein kleiner Ausflug zur Begründung.
Es gibt eine Fülle überschreibbarer Operatoren (auch Methoden-Operatoren, siehe Pickaxe 2, Tabelle 22.4):

# Unär (ein Operand), Präfix
+ -  # Überschreiben mittels +@ und -@
~    # Komplement
``   # Halte ich auch für einen Operator

# Binär, Infix
+ - * / ** %  # Mathematik
& | ^         # Logik: und, oder, nicht, xor
<< >>         # append, shift
=~            # Mustervergleich
== === <=>    # Vergleichslogik
< <= > >=     # Abgeleitete Vergleiche

# Binär, "Umfix" (?)
[] []=     # Elementzugriff (fetch/store)

Nicht überschreiben kann man dagegen alle Operatoren, die den Programmablauf steuern oder die beim Lesen umgewandelt werden:

# Binär
!= !~  # werden intern umgewandelt
&& ||  # steuern die Auswertung
.. ... # Ranges

# Zuweisung
=

# Abkürzungen, werden umgewandelt
+= -= *= /= **= %=
&= |= ^=
<<= >>=
&&= ||=

# Ternär
? :  # if-then-else

# ...und alle Operatoren-Schlüsselwörter

Smooth Operators

Die meisten Methoden-Operatoren sind für solche Spielereien tabu, weil sie sehr häufig verwendet werden. Man stelle sich vor:

# + und - vertauschen
class Fixnum
  alias plus +
  alias minus -
  alias + minus
  alias - plus
end

p 4 + 2  #-> 2

Ein Absturz des Interpreters ist vorherzusehen.
Wenn man solche Überraschungen vermeiden will, muss man sich einschränken auf:

Wenn es um die Erweiterung eigener Klassen geht, besteht keine Gefahr.
Wenn man einer vorhandenen Klasse eine Methode hinzufügen will, die sie bisher nicht hatte (Facets definert zB Array#/), dann sollte das auch klappen.

Erwähnenswert sind außerdem noch die Ruby-typischen Kurzmethoden: p und y dürften ja bekannt sein.
Mehr davon!

Zugabe

# Deine Ruby-Version
(a=%q:ruby:)^:`^(a^:<<^' -v')^:[]^/\\d\\S+/^:display^$>

Weiterlesen

Japanische Smileys, wer sie noch nicht kennt o_O' Originalthread auf rubyforen.de class Foo < Struct.new :bar - erklärt Funny Symbols in Code auf rubygarden.org


Kommentar schreiben

Name (notwendig)

Mail (wird nicht veröffentlicht)

Webseite


Kommentare

  1. WoNáDo schrieb am 01.05.2006 (16 Uhr)

    Einfach super! Mit den Backticks gibt es aber leider Probleme - oder mir fehlt noch eine Idee. Ein Backtick ist zwar eine Methode in Object, aber irgendwie nichts Infix-artiges. Ich hab mit folgendem Grundschema mal rumgespielt.

    class Object
      alias xeq `
      def `(cmd, p2)
        self.method(cmd.to_sym).call(p2)
      end
    end
    p [1,2,3].`('concat', [4,5,6]) # => [1, 2, 3, 4, 5, 6]
    p [1,2,3].`(:concat, [4,5,6]) # => [1, 2, 3, 4, 5, 6]
    p "Hurra! ".`(:*, 3) # => "Hurra! Hurra! Hurra! "
    p "Hurra! ".`('*', 3) # => "Hurra! Hurra! Hurra! "
    # Leider geht nicht die Wunschform
    # [1,2,3] `concat` [4,5,6]
    Hier ist jetzt nur drin was geht. Wenn ich dagegen auch nur versuche den zweiten Parameter bei der `...`-Form unterzubringen, ernte ich bei jedem Versuch nur ein höhnisches "Syntax Error" ))-: Selbst wenn man das in den Griff bekommt, müsste `...` aber noch ein Infix-Operator sein, weil sonst "[1,2,3] `concat` [4,5,6]" syntaktisch falsch wäre, ausser man bekommt es gebacken, dass "[1,2,3]" eine Methode wird ... hmmm - mal überlegen..

  2. WoNáDo schrieb am 01.05.2006 (16 Uhr)

    Nicht dass es schön ist oder genau dem Wunsch entspricht oder angewandt werden sollte - es kommt auch 'eval' drin vor...

    class Object
      @@infixops = []
      alias xeq `
      def addinfix(operator)
        @@infixops << operator
      end
      def `(expression)
        @@infixops.each{|op|break if expression.match(/^(.*?) (#{op}) (.*)$/)}
        raise "unknown infix operator in expression: #{expression}" if $2 == nil
        eval($1).method($2.to_sym).call(eval($3))
      end
    end
    addinfix("concat")
    p `[1,2,3] concat [4,5,6]` # => [1, 2, 3, 4, 5, 6]

  3. WoNáDo schrieb am 01.05.2006 (16 Uhr)

    Irgendwie wird der Code nicht korrekt angenommen. Ich pack ihn noch mal ins Forum, Ihr könnt ihn ja dann hier reinkopieren.

  4. Murphy schrieb am 01.05.2006 (16 Uhr)

    da siehst du, warum wir WordPress ersetzen wollen.

  5. Murphy schrieb am 12.05.2006 (16 Uhr)

    =~ zu den definierbaren methoden hinzugefügt.

  6. Ruby-Mine &raquo; Blog Archive &raquo; Optionen aufladen mit Ruby schrieb am 16.05.2006 (02 Uhr)

    [...] Der letzte Syntax-Sonntag beschäftigte sich ebenfalls mit dem Thema Operatoren. [...]