Methoden und Blöcke in Ruby

Thomas Stratmann
Labor e.V., 16. März 2011

Creative Commons License

Übersicht

Ruby 1.9 besitzt eine mächtigere Syntax als 1.8. Ich zeige meist nur, was in 1.8 geht!

Terminologie

                receiver.method(arguments) { ...block... }
            

Außer dem Methodennamen ist alles optional!

Es kann höchstens ein Block übergeben werden

Moment mal...

Receiver optional??

            class A
              def fun1
                ...
              end

              def fun2
                fun1
              end
            end
          

Wenn keiner angegeben, ist self implizit der Receiver!

puts("Hello world")

Frage:

Was ist hier der receiver, und wieso ist Kernel#puts hier aufrufbar?

Gib mir komische Zeichen (1)

Methodennamen können auf ! oder ? enden.

Die Bedeutungen sind reine Konvention!

Prädikate

Methoden mit ? am Ende liefern true oder false zurück:

"abc".instance_of?(String) # => true

Gib mir komische Zeichen (2)

Bang-Methoden

                a = [[0, 1]]
                a.flatten
                a.length # => 1
              
                a = [[0, 1]]
                a.flatten!
                a.length # => 2
              

Gib mir komische Zeichen (3)

            class MyClass
              def ==(other)
                ...
              end
            end

            a = A.new
            a == 1
            a.== 1
          

Blöcke übergeben

Hierzu bietet die Syntax zwei äquivalente Möglichkeiten:

            object.method(args) { |a1, a2| a1 + a2 }

            object.method(args) do |a1, a2|
              a1 + a2
            end
          

Konvention: do ... end wenn der Block über mehrere Zeilen geht

Achtung: return im Block beendet umgebende Methode, nicht den Block!

Klammern sind optional

            irb(main):019:0> call_with 4 { |n| puts n }
            SyntaxError: compile error
          
            puts html_escape("here & there")
          

Methoden definieren

                class MyClass
                  def fun(a1, a2)
                    a1 + a2 + @ivar
                  end
                end
              
Rückgabewert: Wert des letzten Ausdrucks
Achtung: Leak-Gefahr!
                class YourClass
                  def self.fun(a1, a2)
                    return a1 + a2
                  end
                end
              
Rückgabewert: Argument von return

Übergebene Blöcke (1)

            def fun(arg)
              return yield(arg + 1) * 2
            end

            fun(3) { |a| a.to_s } # => "44"
          

Der übergebene Block wird mit yield aufgerufen, Parameter und Rückgabe sind möglich.

            irb(main):023:0> fun 0
            LocalJumpError: no block given
          

Mit block_given? lässt sich prüfen, ob ein Block übergeben wurde.

            if block_given?
              ...
            else
              ...
            end
          

oder so:

            raise ArgumentError, "fun expects a block parameter" unless block_given?
          

Übergebene Blöcke (2)

            def fun(arg, &block)
              raise ArgumentError, "fun expects a block parameter" if block.nil?
              return 2 * block.call(arg + 1)
            end
          

Der übergebene Block wird in ein Proc-Objekt verwandelt.

Dieses lässt sich wegspeichern und an ganz anderer Stelle aufrufen!

            def takes_block(arg, &block)
              @block = block
            end

            def invokes_block
              @block.call
            end

            def fun
              takes_block(0) { return }
            end

            fun
            invokes_block
          

Frage:

Was passiert, wenn return aufgerufen wird?

Von der anderen Seite betrachtet:

Statt eine Proc-Objekt (hier in my_block gespeichert) mit

            my_block.call(3)
          

direkt aufzurufen, kann es auch anstelle eines Block-Arguments übergeben werden:

            call_with(3, &my_block)
          

Achtung: Ohne das & würde einfach das Proc-Objekt selbst übergeben! (auch manchmal sinnvoll)

Flexible Argumente (1)

Default-Parameter

            def divide(arg, by = 2, allow_0 = false)
              ...
            end
          
            def obj.fun(arg = @ivar)
              ...
            end
          

Flexible Argumente (2)

Splat-Parameter

            def announce(channel, *items)
              items.each { |i| channel << i }
              return items.length
            end

            announce(broadcast) # => 0
            announce(broadcast, "gaga") # => 1
            announce(secure, "secret", "token") # => 2
          

Alle Parameter ab der Stelle werden in einem Array gesammelt und übergeben.

            #Ab Ruby 1.9 moeglich:
            def fun(arg1, *splat, arg2)
          

Methoden-"Varianten" mittels Splats

            def fun(*arguments)
              case arguments.length
              when 1
                name = arguments[0]
                ...
              when 2
                name, value = arguments
                ...
              else
                raise ArgumentError, "detailed complaint"
              end
            end
          

Man kann auch auf den "Typ" der Argumente prüfen.

Von der anderen Seite betrachtet:

Arrays unsplatten

            fun("abc", 0, 1, 2)
            #macht das gleiche wie
            fun("abc", *[0, 1, 2])
          

Flexible Argumente (3)

Hash-Argumente am Schluß

Statt

            notify("x", { :async => false, :count => 3 })
          

geht auch, dank syntaktischem Zucker:

            notify("x", :async => false, :count => 3)
          

oder, da Klammern optional sind:

            notify "x", :async => false, :count => 3
          

Der Hash muss als letztes übergeben werden

Dies ermöglicht benannte Parameter!

Best practice dabei:

            def fun(arg, option_hsh = {})
              ...
            end
          

Nun ist das Hash-Argument optional.

Zusammenfassung:

Parameter und Argumente

Und von der Aufrufseite kommt noch hinzu

Methoden und Objektdesign (1)

Access control

                class MyClass
                  ...
                private

                  def fun
                  end
                end
              
                class MyClass
                  def fun1
                  end

                  private :fun
                end
              

Genauso: public, protected

private

private Methoden können nicht mit explizitem Receiver aufgerufen werden.

Zugriff bleibt innerhalb der Instanz.

protected

protected Methoden können nur aus Objekten der gleichen Klasse (oder einer abgeleiteten) aufgerufen werden.

Zugriff bleibt innerhalb der Familie.

Methoden und Objektdesign (2)

__send__

            object.__send__(:fun, arg1, arg2) { ... }
          

M & O (3)

super

Überladende Methoden können die zuletzt gültige Definition aufrufen:

            class FilledRectangle < Rectangle
              def paint
                super
                fill
              end
            private
            
              def fill
                ...
              end
            end
          

Achtung: Ohne explizite Argumente an super werden die Argumente der überladenden Methode weitergereicht!

            class Base
              def fun(arg)
                arg * 2
              end
            end

            class Derived < Base
              def fun(arg)
                super
              end
            end

            Derived.new.fun(3) # => 6
          

Methoden und Objektdesign (4)

method_missing

            class MyClass
              def method_missing(method, *args, &block)
                case method
                when :fun1
                  ...
                when :fun2
                  ...
                else
                  super #raises NoMethodError
                end
              end
            end
          

Blöcke zu Procs

            def fun
              @p_proc = Proc.new { |args| ... }
              @p_lambda = lambda { |args| ... }
            end
          

speichert zwei Proc-Objekte. Unterschiede:

lambda-Procs mehr wie Funktionen!

Methoden zu Procs (1)

            p = object.method(:fun)

            p.call(arg) #macht jetzt das Gleiche wie
            object.fun(arg)
          

Methoden zu Procs (2)

            p = :fun.to_proc

            p.call(obj, arg) #macht jetzt das Gleiche wie
            obj.fun(arg)
          

Dies ist nützlich für dieses Idiom mit sehr funktionalem Geschmack:

            irb(main):001:0> ["a", "b", "c"].reduce(&:+)
            => "abc"
            irb(main):002:0> ["a", "b", "c"].reduce { |memo, nxt| memo + nxt }
            => "abc"
          
(Verwendung von & ruft implizit to_proc auf)

Fragen?