Un bloc est un morceau de code qui peut être passé à une méthode. Dans d'autres langages de programmation, on parle de fonction anonyme ou lambda. Une méthode peut convertir un bloc en un Proc, qui représente un bloc de code.
def my_method(&block)
1 + yield
end
my_method { 2 }
# => 3
Les blocs et les Procs sont très couramment utilisés en Crystal, notamment pour l'itération et les callbacks. Ils servent aussi à créer des DSL (langages dédiés) ; quelques exemples de DSL sont le framework web Kemal ou le framework de test Spec (celui utilisé pour tester tes solutions Crystal).
Pour créer une méthode qui peut accepter un bloc, il faut ajouter une esperluette (&) avant le dernier argument.
Cela convertira le bloc en un Proc et l'affectera à l'argument.
Ensuite, yield exécute le bloc donné à la méthode.
def my_method(&block)
1 + yield
end
Pour passer un bloc, il y a deux façons : soit avec les mots-clés do ... end, soit avec des accolades ({}).
La principale différence entre les deux est la précédence : do ... end prend en charge les blocs multilignes, tandis que les accolades ({}) ne le font pas.
my_method do
2
end
# => 3
my_method { 2 }
# => 3
yield est un mot-clé qui exécute le bloc donné à la méthode.
Cela signifie que le bloc ne sera jamais exécuté si yield n'est pas appelé.
On peut aussi lui donner des arguments, qui seront passés au bloc.
Il n'y a pas de limite au nombre d'arguments qui peuvent être passés au bloc.
Pour utiliser les arguments dans le bloc, on utilise la syntaxe |x, y, ...|.
Ici, x, y, ... sont les noms des arguments.
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
Dans l'exemple ci-dessus, le bloc reçoit l'argument 2, le multiplie par 3 et renvoie 6.
Ensuite, la méthode ajoute 1 au résultat et renvoie 7.
Si le nombre d'arguments donnés à yield est supérieur au nombre d'arguments attendus par le bloc, une erreur est levée.
def my_method(&block)
1 + yield
end
my_method { |x| x * 3 }
# Error: too many block parameters (given 1, expected maximum 0)
Les blocs peuvent être écrits de manière explicite ou implicite. Les blocs implicites sont les plus courants, et ce sont ceux montrés dans les exemples ci-dessus. Tu peux même donner au bloc différents types d'arguments, et le compilateur inférera les types et créera un type union.
def my_method(&block)
yield "a"
yield 1
end
my_method do |x|
p typeof(x)
end
# output: (String | Int32)
Lorsque tu écris des blocs explicites, tu peux spécifier les types des arguments et le type de retour.
Cela se fait avec la syntaxe Arg1Type, Arg2Type, ... -> ResultType.
Les arguments y sont séparés par des virgules (,), et le type de résultat est séparé par ->.
def my_method(&block : Int32, Int32 -> Int32)
1 + yield(2, 3)
end
my_method { |x, y| x * y }
# => 7
Comme pour les autres types explicites, le compilateur vérifiera que les types correspondent à ce qui est attendu. Si le bloc ne correspond pas au type donné à la méthode, une erreur sera levée.
Proc a un lien très fort avec les blocs.
La différence essentielle est qu'un Proc est un type, alors qu'un bloc n'en est pas un.
Un Proc représente un pointeur de fonction et peut être transmis comme une variable.
Pour définir un Proc, on utilise la syntaxe du littéral de proc ->(args) { ... }.
Les arguments sont séparés par des virgules (,), et le bloc est délimité par { ... }.
Pour invoquer un Proc, on utilise la méthode call en lui passant les arguments.
Les types des paramètres sont obligatoires.
Le type de retour est inféré à partir du corps du proc, mais peut aussi être fourni explicitement.
my_proc = ->(x : Int32, y : Int32) : Int32 { x * y }
my_proc.call(2, 3)
# => 6
La capture de blocs est une façon de créer un Proc à partir d'un bloc.
Cela se fait en utilisant l'opérateur & avant le dernier argument d'une méthode.
Cela convertira le bloc en un Proc et l'affectera à l'argument.
def my_method(&block)
block.call
end
my_method { 1 + 2 }
# => 3
Les procs peuvent être passés aux méthodes qui attendent un bloc. C'est pratique lorsqu'on veut pouvoir passer différents blocs selon une condition.
def my_method(&block)
yield 5
end
my_proc = -> (x) { x * 2 }
my_method(&my_proc)
# => 10
Lorsque tu déclares une variable, tu peux utiliser la syntaxe : Type pour déclarer le type de la variable.
Pour les procs, tu utilises la syntaxe Proc((ArgType, ...), ResultType).
Le dernier type sera toujours le type de retour, et les autres seront les types des arguments.
Autrement dit, s'il n'y a qu'un seul type, ce sera le type de retour et le proc ne prendra aucun argument.
my_proc : Proc(Int32, Int32, Int32) = ->(x, y) { x * y }
other_proc : Proc(Int32) = -> { 1 }
Tu développes un système de gestion de contenu pour un site web qui héberge des articles, des billets de blog et d'autres contenus écrits. Ta mission est d'implémenter un gestionnaire de tâches qui traitera les tâches d'une file de priorité. Le gestionnaire de tâches reçoit des tâches et, en fonction de la charge du serveur et de leur priorité, décide laquelle exécuter ensuite. Si le serveur est fortement sollicité, le numéro de priorité doit être plus élevé pour que la tâche soit exécutée. Le serveur fait varier dynamiquement la charge en fonction de la charge.
TaskHandler
Le TaskHandler doit être initialisé avec une file d'attente et un load_level par défaut.
Implémente l'initialiseur de la classe TaskHandler, qui prend en argument un Array de Strings contenant les noms des tâches et le stocke dans une variable d'instance appelée @tasks.
L'initialiseur doit aussi créer la variable d'instance @task_condition_logic, qui stocke un Proc prenant un Int32 et renvoyant vrai si l'argument est supérieur ou égal à 0, et faux s'il est inférieur à 0.
task_handler = TaskHandler.new(["task1", "task2", "task3"])
# => #<TaskHandler:0x7fd8d4cfbf00 @tasks=["task1", "task2", "task3"], @task_condition_logic=#<Proc(Int32, Bool):0x562536a4a330>>
Le serveur doit pouvoir mettre à jour la logique de condition des tâches en fonction de la charge du serveur.
Implémente la méthode TaskHandler#update_task_condition_logic, qui prend un bloc en argument ; ce bloc doit être stocké dans la variable d'instance @task_condition_logic.
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>>
Le serveur doit pouvoir exécuter la tâche suivante dans la file.
La première tâche du tableau @tasks est la prochaine tâche à exécuter.
La tâche doit être exécutée si le niveau de priorité de la tâche, passé au proc @task_condition_logic, renvoie vrai.
Elle doit renvoyer des messages selon que la tâche a été exécutée ou non.
Si la tâche a été exécutée, elle doit renvoyer : "Completed <task>".
Si la tâche n'a pas été exécutée, elle doit renvoyer : "Could not complete <task>".
Si la tâche a été exécutée, elle doit être retirée du tableau @tasks.
Implémente la méthode TaskHandler#execute_next_task, qui prend en argument le niveau de priorité de la tâche suivante sous forme d'Int32 et renvoie une String selon que la tâche a été exécutée ou non.
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"
Inscris-toi sur Exercism pour apprendre et maîtriser Crystal avec 26 concepts142 exercices, et un vrai mentorat humain, le tout gratuitement.