Ein Block ist ein Stück Code, das an eine Methode übergeben werden kann. In anderen Programmiersprachen nennt man das eine anonyme Funktion oder ein Lambda. Eine Methode kann einen Block in einen Proc umwandeln, der einen Codeblock darstellt.
def my_method(&block)
1 + yield
end
my_method { 2 }
# => 3
Blöcke und Procs werden in Crystal sehr häufig verwendet, etwa für Iterationen und Callbacks. Aber auch, um DSLs (domänenspezifische Sprachen) zu erstellen. Beispiele für DSLs sind das Web-Framework Kemal oder das Test-Framework Spec (mit dem deine Crystal-Lösungen getestet werden).
Um eine Methode zu erstellen, die einen Block annehmen kann, musst du ein Ampersand (&) vor das letzte Argument setzen.
Dadurch wird der Block in einen Proc umgewandelt und dem Argument zugewiesen.
Mit yield führst du dann den Block aus, der der Methode übergeben wurde.
def my_method(&block)
1 + yield
end
Um einen Block zu übergeben, gibt es zwei Möglichkeiten: entweder mit den Schlüsselwörtern do ... end oder mit geschweiften Klammern ({}).
Der wichtigste Unterschied ist der Vorrang: do ... end unterstützt mehrzeilige Blöcke, geschweifte Klammern ({}) nicht.
my_method do
2
end
# => 3
my_method { 2 }
# => 3
yield ist ein Schlüsselwort, das den Block ausführt, der der Methode übergeben wurde.
Das bedeutet, dass der Block nie ausgeführt wird, wenn yield nicht aufgerufen wird.
Ihm können auch Argumente übergeben werden, die dann an den Block weitergereicht werden.
Es gibt keine Begrenzung für die Anzahl der Argumente, die an den Block übergeben werden können.
Um die Argumente im Block zu verwenden, benutzt du die Syntax |x, y, ...|.
Dabei sind x, y, ... die Namen der Argumente.
def my_method(&block)
1 + yield 2
end
# With curly braces:
my_method { |x| x * 3 }
# => 7
# With do ... end:
my_method do |x|
x * 3
end
# => 7
Im obigen Beispiel bekommt der Block das Argument 2, multipliziert es mit 3 und gibt 6 zurück.
Die Methode addiert dann 1 zum Ergebnis und gibt 7 zurück.
Wenn die Anzahl der an yield übergebenen Argumente größer ist als die Anzahl der Argumente, die der Block erwartet, wird ein Fehler ausgelöst.
def my_method(&block)
1 + yield
end
my_method { |x| x * 3 }
# Error: too many block parameters (given 1, expected maximum 0)
Blöcke können explizit oder implizit geschrieben werden. Implizite Blöcke sind am häufigsten und kommen in den obigen Beispielen vor. Du kannst dem Block sogar Argumente unterschiedlicher Typen geben, und der Compiler leitet die Typen ab und erstellt einen Union-Typ.
def my_method(&block)
yield "a"
yield 1
end
my_method do |x|
p typeof(x)
end
# output: (String | Int32)
Bei expliziten Blöcken kannst du die Typen der Argumente und den Rückgabetyp angeben.
Das machst du mit der Syntax Arg1Type, Arg2Type, ... -> ResultType.
Dabei werden die Argumente durch Kommas (,) getrennt, und der Ergebnistyp wird durch -> getrennt.
def my_method(&block : Int32, Int32 -> Int32)
1 + yield(2, 3)
end
my_method { |x, y| x * y }
# => 7
Wie bei anderen expliziten Typen erzwingt der Compiler auch hier, dass die Typen den Erwartungen entsprechen. Wenn der Block nicht zum Typ passt, der der Methode übergeben wurde, wird ein Fehler ausgelöst.
Proc hat eine sehr enge Verbindung zu Blöcken.
Der entscheidende Unterschied ist, dass ein Proc ein Typ ist und ein Block nicht.
Ein Proc repräsentiert einen Funktionszeiger und kann als Variable weitergegeben werden.
Um einen Proc zu definieren, benutzt du die Syntax des Proc-Literals ->(args) { ... }.
Die Argumente werden durch Kommas (,) getrennt, und der Block wird durch { ... } abgegrenzt.
Um einen Proc aufzurufen, benutzt du die Methode call und übergibst ihr die Argumente.
Die Typen der Parameter sind verpflichtend.
Der Rückgabetyp wird aus dem Code des Procs abgeleitet, kann aber auch explizit angegeben werden.
my_proc = ->(x : Int32, y : Int32) : Int32 { x * y }
my_proc.call(2, 3)
# => 6
Blöcke erfassen ist eine Möglichkeit, einen Proc aus einem Block zu erstellen.
Dazu benutzt du den Operator & vor dem letzten Argument einer Methode.
Dadurch wird der Block in einen Proc umgewandelt und dem Argument zugewiesen.
def my_method(&block)
block.call
end
my_method { 1 + 2 }
# => 3
Procs können an Methoden übergeben werden, die einen Block erwarten. Das ist praktisch, wenn du je nach Bedingung unterschiedliche Blöcke übergeben möchtest.
def my_method(&block)
yield 5
end
my_proc = -> (x) { x * 2 }
my_method(&my_proc)
# => 10
Beim Deklarieren einer Variablen kannst du die Syntax : Type verwenden, um den Typ der Variablen anzugeben.
Für Procs benutzt du die Syntax Proc((ArgType, ...), ResultType).
Der letzte Typ ist immer der Rückgabetyp und die übrigen sind die Argumenttypen.
Das heißt, wenn es nur einen Typ gibt, ist dieser der Rückgabetyp und der Proc nimmt keine Argumente entgegen.
my_proc : Proc(Int32, Int32, Int32) = ->(x, y) { x * y }
other_proc : Proc(Int32) = -> { 1 }
Du entwickelst ein Content-Management-System für eine Website, auf der Artikel, Blogbeiträge und andere Textinhalte gehostet werden. Du sollst einen Task-Handler implementieren, der Aufgaben in einer Prioritätswarteschlange verarbeitet. Der Task-Handler empfängt Aufgaben und entscheidet anhand der Auslastung des Servers und der Priorität der Aufgabe, welche Aufgabe als Nächstes ausgeführt wird. Ist der Server stark ausgelastet, muss die Prioritätszahl höher sein, damit die Aufgabe ausgeführt wird. Der Server passt die Auslastung dynamisch je nach Last an.
TaskHandler initialisierenDer TaskHandler muss mit einer Warteschlange und einem Standardwert für load_level initialisiert werden.
Implementiere den Initialisierer der Klasse TaskHandler, der ein Array aus Strings mit den Namen der Aufgaben als Argument entgegennimmt und in einer Instanzvariablen namens @tasks speichert.
Der Initialisierer soll außerdem die Instanzvariable @task_condition_logic anlegen, die ein Proc speichert, das ein Int32 entgegennimmt und wahr zurückgibt, wenn das Argument gleich 0 oder größer ist, und falsch, wenn es kleiner als 0 ist.
task_handler = TaskHandler.new(["task1", "task2", "task3"])
# => #<TaskHandler:0x7fd8d4cfbf00 @tasks=["task1", "task2", "task3"], @task_condition_logic=#<Proc(Int32, Bool):0x562536a4a330>>
Der Server muss die Aufgaben-Bedingungslogik abhängig von seiner Auslastung aktualisieren können.
Implementiere die Methode TaskHandler#update_task_condition_logic, die einen Block als Argument entgegennimmt; dieser Block soll in der Instanzvariablen @task_condition_logic gespeichert werden.
task_handler = TaskHandler.new(["task1", "task2", "task3"])
task_handler.update_task_condition_logic { |load| load > 1 }
# => #<TaskHandler:0x7f068118cf00 @tasks=["task1", "task2", "task3"], @task_condition_logic=#<Proc(Int32, Bool):0x562df99d0370>>
Der Server muss die nächste Aufgabe in der Warteschlange ausführen können.
Die erste Aufgabe im @tasks-Array ist die nächste Aufgabe, die ausgeführt wird.
Die Aufgabe soll ausgeführt werden, wenn die Prioritätsstufe der Aufgabe an das Proc @task_condition_logic übergeben wird und dieses wahr zurückgibt.
Je nachdem, ob die Aufgabe ausgeführt wurde oder nicht, soll sie eine Meldung zurückgeben.
Wurde die Aufgabe ausgeführt, soll sie zurückgeben: "Completed <task>".
Wurde die Aufgabe nicht ausgeführt, soll sie zurückgeben: "Could not complete <task>".
Wurde die Aufgabe ausgeführt, soll sie aus dem @tasks-Array entfernt werden.
Implementiere die Methode TaskHandler#execute_next_task, die die Prioritätsstufe der nächsten Aufgabe als Int32 als Argument entgegennimmt und abhängig davon, ob die Aufgabe ausgeführt wurde oder nicht, einen String zurückgibt.
task_handler = TaskHandler.new(["task1", "task2", "task3"])
task_handler.update_task_condition_logic { |load| load > 1 }
task_handler.execute_next_task(2)
# => "Completed task1"
task_handler.execute_next_task(1)
# => "Could not complete task2"
Melde dich bei Exercism an, um Crystal mit 26 Konzepte142 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.