By-Name-Parameter zur Auswertung von Parametern je nach Bedarf

Scala Bytes: Sugartime

Arno Haase

Wenn man eine Methode aufruft, werden ihr die Werte ihrer Parameter übergeben. Und das ist gut so, oder?

Sehen wir uns eine extrem vereinfachte Logger-Implementierung an. Sie hat ein Flag, mit dem man das Debug-Logging ein- und ausschalten kann:

object Logger {
  var isDebugEnabled = true
  def debug(msg: AnyRef) {
    if (isDebugEnabled) {
      println(msg)
    }
  }
}

Damit kann man beliebige Nachrichten loggen. Wenn das Erzeugen der Nachricht aber teuer ist – z. B. die Anzahl der Einträge in einem Dateiverzeichnis beinhaltet – kann es sinnvoll sein, diese Kosten zu vermeiden, wenn das Logging gerade abgeschaltet ist. Man kann dazu den Aufruf in ein if verpacken:

if (Logger.isDebugEnabled) {
  Logger.debug("Anzahl Dateien: " + new File(".").list().size)
}

Das funktioniert und ist in Java gängige Praxis, macht das Logging aber komplizierter. Hier wäre es nützlich, wenn der Logger selbst entscheiden könnte, ob der Parameter überhaupt evaluiert wird. Genau für solche Situationen, in denen man einen Wert übergeben will, der Aufrufer aber die Kontrolle über die Auswertung der übergebenen Expression haben soll, gibt es in Scala By-Name-Parameter. Dazu stellt man einem Parametertyp => voran:

def debug(msg: => AnyRef) {
  if (isDebugEnabled) {
    println(msg)
  }

Eine While-Schleife als Beispiel

Ein By-Name-Parameter ist intern eine vollwertige Funktion. Sie wird jedes Mal ausgewertet, wenn der Parameter verwendet wird. Das eröffnet vielfältige Möglichkeiten zur Gestaltung von Syntax. Die folgende Implementierung einer While-Schleife (in Anlehnung an [1]) soll das illustrieren. Sie tut in dieser Form nichts anders als das eingebaute while. Aber wir können uns ja vorstellen, dass sie zusätzlich Dinge wie Protokollierung, Transaktionsbehandlung o. ä. erledigt.

@tailrec
def myWhile(cond: => Boolean)(body: => Unit): Unit = {
  if(cond) {
    body
    myWhile(cond)(body)
  }
}

Diese Methode hat zwei Parameter, die beide by-name sind, nämlich die Bedingung und den Body der Schleife. Die Implementierung ist rekursiv, um nicht platt das eingebaute while-Konstrukt von Scala zu verwenden. Sie ist sogar tail-rekursiv [2], sodass diese Implementierung keinen Performancenachteil gegenüber iterativem Code hat.
Die Methode wertet die Bedingung aus, und im Erfolgsfall führt sie den Body aus und ruft sich selbst rekursiv mit denselben Parametern auf. Auf beide Parameter wird also potenziell mehrmals zugegriffen, und Scala wertet sie jedes Mal neu aus. Wir können unsere neue Schleife jetzt genau so verwenden wie das eingebaute while-Konstrukt:

var i=0
myWhile (i
println(i)
i += 1
}

Behandlung als Funktion

Ein By-Name-Parameter ist nichts anderes als eine Funktion ohne Argumente, allerdings mit besonderer Syntax. Insbesondere wird sie bei jeder Verwendung ausgewertet. Im Allgemeinen ist das auch genau das, was man sich wünscht. Aber manchmal ist es nützlich, einen By-Name-Parameter auch formal als Funktion zu behandeln. Nehmen wir zum Beispiel an, wir wollen die Evaluierung der Bedingung unserer myWhile-Schleife profilen und dazu generischen, vorhandenen Performance-Logging-Code verwenden. Sinnvolles Performance-Logging ist nicht trivial, aber die folgende Minimalfunktion soll hier als Skizze dienen:

def perfLog[T](f: () => T) = {
  val start = System.currentTimeMillis
  val res = f()
  println (System.currentTimeMillis - start)
  res
}

Insbesondere die Signatur der perfLog-Methode ist hier relevant: Sie erwartet eine Funktion als Parameter. Der naive Aufruf perfLog(cond) funktioniert leider nicht. Erstens würde cond schon vor dem Aufruf ausgewertet werden, und zweitens hat es den Typ Boolean, während perfLog eine Funktion () => Boolean erwartet. Die Lösung besteht darin, einen Unterstrich hinter den By-Name-Parameter zu schreiben:

if (perfLog (cond _)) {
  ...
}

Das sagt dem Scala-Compiler, dass hier nicht der Aufruf der Funktion, sondern die Funktion selbst gemeint ist. Es ist dieselbe Syntax, mit der man auch eine Methode als Funktion behandeln kann.

Fazit

By-Name-Parameter sind syntaktischer Zucker, um Funktionen ohne Argumente als Parameter zu übergeben. Insbesondere Aufrufe solcher Methoden können lesbarer sein, als wenn man explizit Funktionsliterale übergeben würde.

Arno Haase ist selbstständiger Softwarearchitekt aus Braunschweig. Neben seiner Beratungstätigkeit krempelt er gerne die Ärmel hoch und schreibt selbst Software, sowohl in Kundenprojekten als auch Open Source. Er ist eines der Gründungsmitglieder von se-radio.net, dem Software Engineering Podcast. Außerdem spricht er regelmäßig auf einschlägigen Konferenzen und ist Autor diverser Artikel und Patterns sowie Koautor von Büchern über JPA und modellgetriebene Softwareentwicklung.
Geschrieben von
Arno Haase
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.