tutorials.de Buch-Aktion 05/2012
RSS-Feed anzeigen

Code that sings

Arbeiten auf Arrays mit Blockparametern

Bewerten
von Matthias Reitinger am 27.04.08 um 20:43 (1619 Hits)
Voher

Der Tutorilaner Darian wollte im Thema „Array elemente bearbeiten...“ wissen, wie man mit PHP am elegantesten jedes Element eines Arrays bearbeitet. Dabei ergaben sich zwei unterschiedliche Lösungen (im Thema ging es eigentlich um eine etwas komplexere Aktion, die ich hier aber aus Gründen der Vereinfachung durch ein strtoupper() ersetzt habe):

Lösung A
Code PHP:
1
2
3
4
5
6
7
<?php
 
foreach ($array as $idx => $elm) {
    $array[$idx] = strtoupper($elm);
}
 
?>
Hier wird das Array „zu Fuß“ mit einer foreach-Schleife durchlaufen und jedes Element überschrieben.

Lösung B
Code PHP:
1
2
3
4
5
6
7
8
9
<?php
 
function cb_strtoupper(&$elm, $key) {
    $elm = strtoupper($elm);
}
 
array_walk($array, 'cb_strtoupper');
 
?>
In dieser Variante (vorgeschlagen vom Benutzer Mairhofer) übernimmt array_walk() das Iterieren durch das Array. Für jedes Element wird die im zweiten Parameter angegebene Callback-Funktion aufgerufen, die sich um die Bearbeitung des Elements kümmert. Grundsätzlich ist das die elegantere Methode, die allerdings den Nachteil hat, dass man extra eine neue Funktion definieren muss. Dem kann man zwar durch create_function() aus dem Weg gehen – wirklich hübscher macht das den Quellcode aber auch nicht (eher im Gegenteil).


Nachher

Code Ruby:
1
array.map! { |elm| elm.upcase }


Was passiert?

Block-Parameter

In Ruby kann jeder Methode ein sogenannter „Blockparameter“ mitgegeben werden. Dieser stellt ein Stück Code dar, das Parameter entgegen nimmt und einen Wert zurückgibt (praktisch wie eine Methode). Ein solcher Block wird entweder von geschweiften Klammern (wie im Quellcode) oder von do … end umschlossen. Die Parameterliste folgt direkt auf die öffnende Klammer (bzw. das do) und wird durch senkrechten Striche (|…|) begrenzt.

In der Methode kann dieser Codeblock dann beliebig oft mit verschiedenen Parametern aufgerufen und der Rückgabewert weiterverarbeitet werden. Genau das geschieht auch in der Methode map! der Array-Klasse: jedes Element wird dem Codeblock in einem Aufruf als Parameter übergeben und durch den Rückgabewert ersetzt.

Als Randnotiz sei noch erwähnt, dass der Rückgabewert eines Blocks (oder einer Methode) der Wert des letzten ausgewerteten Ausdrucks ist. Man muss also nicht explizit return wert schreiben (was aber auch möglich wäre), sondern es genügt wert.

Das Modul Enumerable

Neben map! gibt es noch eine Vielzahl anderer Array-Methoden, die mit Blockparametern arbeiten. Viele davon werden vom Mixin-Modul Enumerable bereitgestellt. Eine kleine Auswahl möchte ich an dieser Stelle kurz vorstellen:
  • all?: Gibt true zurück, falls der Block für jedes Element true zurückgibt. Ansonsten false. Beispiel:
    Code Ruby:
    1
    2
    
    [1, 2, 3, 4].all? { |num| num < 5 } # => true
    [1, 2, 3, 4].all? { |num| num < 3 } # => false
  • select: Gibt ein Array der Elemente zurück, für die der Block true liefert. Beispiel:
    Code Ruby:
    1
    
    [1, 2, 3, 4].select { |num| num < 3 } # => [1, 2]
  • partition: Unterteilt die Elemente in zwei Gruppen je nach (boole'schem) Rückgabewert des Blocks. Beispiel:
    Code Ruby:
    1
    
    [1, 2, 3, 4].partition { |num| num < 3 } # => [[1, 2], [3, 4]]
  • sort_by: Sortiert die Elemente nach einer Eigenschaft, die der Block für jedes Element berechnet. Beispiel:
    Code Ruby:
    1
    2
    
    ["apple", "banana", "pear", "strawberry"].sort_by { |str| str.length }
    # => ["pear", "apple", "banana", "strawberry"]
Das Schöne daran ist nun, dass diese Methoden genau auf dieselbe Weise verwendet werden können, solange die Klasse Enumerable einbindet. Beispielsweise könnte man so später ohne Probleme ein Array durch ein Objekt der Klasse Set (zur Repräsentation von Mengen) austauschen. In PHP wäre hier ein aufwändigeres Refactoring nötig (da foreach nur mit Arrays funktioniert).


Was haben wir gewonnen?

Durch das Konzept der Blockparameter kann in Ruby das Callback-Muster ohne Definieren zusätzlicher Methoden umgesetzt werden. Besonders beim Bearbeiten von Arrays kann man dadurch oft sehr kompakten und dennoch aussagekräftigen Quellcode schreiben. Außerdem ermöglicht das Modul Enumerable, häufig benötigte Operationen auf iterierbaren Datenstrukturen unabhängig von der konkreten Implementierung zu formulieren.

Desweiteren...
Blockparameter sind übrigens keine Erfindung von Ruby. Das darunter liegende Konzept existiert in funktionalen Programmiersprachen schon seit vielen Jahrzehnten als fundamentaler Bestandteil. Daher ist Ruby auch für Freunde funktionaler Programmierung durchaus einen Blick wert.

Über Kommentare zu diesem Beitrag würde ich mich wie immer freuen. Auch tiefergehende Fragen sind natürlich erlaubt und erwünscht.

- Matthias

"Arbeiten auf Arrays mit Blockparametern" bei Twitter speichern "Arbeiten auf Arrays mit Blockparametern" bei Facebook speichern

Stichworte: arrays, blocks, ruby
Kategorien
Programming

Kommentare

  1. Avatar von Mairhofer
    Da ich ja schon erwähnt werde, muss ich direkt antworten.
    Sehr guter Beitrag auch wenn es mir Ruby nicht schmackhafter macht
    Ich könnte auch noch Lösung C in die Runde werfen
    PHP-Code:
    $array array_map('strtoupper'$array); 
    Natürlich kommt PHP meines Wissens nicht an den Komfort von Ruby, Python oder Perl ran, aber ich glaube das ist es, was PHP ausmacht.
    Okay, Fehler oder Missstände als Vorteil anzupreisen ist vielleicht auch nicht der richtige Weg

    Ruhig weiter so mit den guten Beiträgen. Es ist immer interessant, was in anderen Sprachen so machbar ist.

    Gruss
  2. Avatar von Matthias Reitinger
    Hallo Mairhofer,

    danke für deinen Kommentar.

    Diese Lösung hatte ich auch schon im Kopf. Sie hat gegenüber den anderen jedoch die nachteilige Eigenschaft, dass das alte Array bis zum nächsten Lauf des Garbage Collectors immer noch im Speicher bleibt. Deshalb habe ich sie nicht berücksichtigt.

    Außerdem hätte ich es mir dann sicher nicht verkneifen können, darauf hinzuweisen, wie inkonsistent array_walk() und array_map() doch bezüglich der Reihenfolge ihrer Parameter sind – und mit PHP-Bashing wollte ich mich in diesem Blog eigentlich weitestgehend zurückhalten

    Grüße,
    Matthias
  3. Avatar von Radhad
    Perl und komfort? *hust* - das war wohl eher ein Scherz, oder?
  4. Avatar von Thomas Darimont
    Hallo,

    interessanter Beitrag Weiter so

    Um das gezeigte in Python zu realisieren, kann man sich der Konstrukte
    map, filter, reduce und lambda bedienen. Mit lambda definiert man, ähnlich zu Blöcken in Ruby, "ad-hoc" Code Blöcke mit Anweisungen.

    Das schaut dann so aus:
    Code python:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    data = ('a','b','c')
     
    map(lambda x: x.upper(),data)
    -> ['A', 'B', 'C']
     
    map(ord,data)
    -> [97, 98, 99]
    (Die Funktion ord(...) gibt den Ascii Wert des Zeichens zurück)
     
     
    filter(lambda x: x < 'c',data)
    -> ('a', 'b')
     
    reduce(lambda x,y: x + y,data)
    -> 'abc'

    Insgesamt kann man sagen, dass man mit python so ziemlich jede
    Syntax Nettigkeit von ruby nachbauen kann, mal mehr mal weniger elegant.

    Gruß Tom
  5. Avatar von Matthias Reitinger
    Hallo Tom,

    sehr schön, danke für die Ergänzung! Python steht Ruby in Sachen Syntax-Zucker in der Tat in nichts nach. Gleiches gilt aber auch umgekehrt

    Um die Brücke zurück zu Ruby zu schlagen: auch hier gibt es das Schlüsselwort lambda (eigentlich ist es eine Methode). Es erzeugt aus einem Block ein Objekt der Klasse Proc:

    Code Ruby:
    1
    2
    3
    4
    5
    6
    7
    8
    
    >> data = %w{ a b c }
    => ["a", "b", "c"]
    >> prc = lambda { |x| x.upcase }
    => #<Proc:0xb7bcb828@(irb):2>
    >> prc["foobar"]
    => "FOOBAR"
    >> data.map(&prc)
    => ["A", "B", "C"]

    Wie man an diesem IRB-Ausschnitt sieht (IRB ist die interaktive Ruby-Shell), kann man ein solches Objekt wie eine Methode aufrufen (Zeile 5) und auch als Blockparameter verwenden, indem man es als Argument mit vorangestelltem & übergibt (Zeile 7). Die Ähnlichkeit zum Python-Code ist jetzt nicht mehr zu übersehen

    Grüße,
    Matthias