Infrastructures programmatiques (3/4) : Kotlin en action

34 views
Skip to first unread message

Laurent Caillette

unread,
Feb 11, 2016, 2:24:23 AM2/11/16
to tec...@googlegroups.com
Nous nous étions arrêtés à Kable, un idiome de Kotlin. Avec Kable on
définit un ``Playbook`` enchaînant des ``Task``, lesquelles, avant de
s'exécuter pour de bon, vont peupler un ``ExecutionPlan``.

En standard, Kotlin ne supporte pas la syntaxe de Kable. Mais on peut
utiliser Kotlin de façon à supporter une telle syntaxe, sans renoncer
au typage statique, ni se livrer à des acrobaties avec des macros ou
des extensions du compilateur.


=== Méthodes d'extension et blocs

On va repartir d'un exemple minimal :

<<<
val playbook = Playbook() {

- RunProcess( "echo 'Hello Kotlin!' " )

}
>>>

La déclaration du ``Playbook`` ressemble à une déclaration de classe
Java, du fait des accolades. Mais les parenthèses indiquent
l'instanciation d'un objet ``Playbook``. Le bloc entre accolades est
un paramètre d'entrée. Ainsi commence la classe ``Playbook`` (dérivée
de ``Playlist``) :

<<<
open class Playbook( block : Playlist.() -> Unit ) : Playlist() {

init {
this.block()
}

// ...
>>>

Kotlin considère un bloc de code entre accolades comme une fonction
anonyme sans paramètres d'entrée ni de sortie. Lorsque le dernier
paramètre d'une fonction est de ce type (fonction sans paramètres
d'entrée ni de sortie), alors on peut le passer en-dehors de la
fonction. C'est ce qui permet de passer le paramètre ``block`` au
constructeur par défaut (identifié par le nom réservé ``init``). Le
fait d'exécuter le bloc (avec ``this.block()``) va provoquer
l'enregistrement des ``Task``.

``this.block()`` laisse penser qu'il s'agit d'une méthode de
``Playbook`` (en fait, de ``Playlist``) alors que ce n'est pas le cas.
Sauf que si, grâce à la déclaration d'une méthode d'extension de
``Playlist`` avec ``block : Playlist.() -> Unit``. Les méthodes
d'extension ("extension methods") sont utilisées pour ajouter des
comportement à des classes existantes. Elles sont résolues
statiquement, et leur portée est limitée à celle de la déclaration.
Dans notre cas, la fonction nommée ``block`` ne sera considérée comme
une méthode de ``Playbook`` qu'à l'intérieur du constructeur.


=== Surcharge d'opérateurs

Mais ce n'est pas tout. Définir ``block`` comme méthode d'extension
donne un accès immédiat à toutes les méthodes de ``Playbook`` à
l'intérieur de ``block``. Regardons un peu plus loin dans ``Playbook``
(ou plutôt dans ``Playlist``), on trouve une méthode pour ajouter les
objets ``Task`` grâce au caractère ``-``, considéré comme un opérateur
unaire de négation :

<<<
operator fun< RESULT > Task< RESULT >.unaryMinus() : Deferred< RESULT > {
_actions.add( this )
// ...
return Deferred()
}
>>>

L'opérateur unaire de négation est détourné pour ressembler au ``-``
par lequel YAML symbolise les éléments de liste. À dessein, il n'est
pas possible de l'appeler sur une ``Task`` en-dehors du bloc passé au
constructeur. Chaque ``Task`` définit le type de sa valeur de retour.
Le type est alors propagé à la valeur retournée, c'est à dire que
l'objet ``Deferred`` reçoit le paramètre de type attendu (le bon type
est inféré lors de la création à la fin de la méthode).

Considérons un cas où on veut capturer la valeur de retour d'une ``Task`` :

<<<
Playbook() {

val result =
- RunProcess( "echo 'Hello Kotlin!' " )

}
>>>

Sachant que ``RunProcess`` est du type ``Task<
Task.Result.ProcessExecution >``, alors le type de ``result`` est bien
``Deferred< Task.Result.ProcessExecution >``.

On peut redéfinir le même opérateur pour autant de types qu'on veut.
Par exemple en ajoutant cette définition dans ``Playlist`` :

<<<
operator fun String.unaryMinus() {

_actions.add( ConsoleMessage( this ) )

}
>>>

Il devient possible d'ajouter une instance de ``ConsoleMessage`` avec
cette syntaxe allégée :

<<<
Playbook() {

- "This message goes to the console."

}
>>>


=== Propagation de type, fonctions infixes

Dans l'exemple de l'installation du JRE, on a vu passer un
modificateur d'exécution ``unless( ... )``. C'est une fonction qui
modifie la dernière tâche ajoutée, de façon à ce qu'elle ne s'exécute
que si la valeur s'évalue à ``true`` au tout dernier moment. (Si on
utilise un ``if`` standard, ça influera juste sur la création de
l'``ExecutionPlan``.)

Mais on peut aussi définir des blocs :

<<<
Playbook() {

val echo =
- RunProcess( "echo 'Hello Kotlin!' " )

If( echo, { it.stdout.contains( "Hello" ) } ) {
- "The echo command did run as expected."
} Else {
- "This is unlikely."
- "Should we do something?"
}

>>>

Le ``If`` (avec une majuscule) est une fonction qui prend comme
paramètres le ``Deferred`` sur lequel porte l'évaluation, et la
fonction d'évaluation. Pour celle-ci, l'exemple utilise la syntaxe
abrégée avec ``it`` comme nom standard s'il n'y a qu'un seul paramètre
d'entrée dans la fonction. La variable ``echo`` est du type
``Task.Result.ProcessExecution`` et possède une propriété ``stdout``
de type ``String`` (tout comme Ansible). La fonction ``If`` prend un
troisième paramètre sous forme de fonction sans paramètres -- juste un
bloc de code -- comme le constructeur de ``Playbook``.

Mais comment chaîner avec une clause ``Else`` optionnelle ? En
renvoyant un objet avec une méthode ``Else``. Cette méthode est
déclarée ``infix``, c'est à dire qu'il n'y a pas besoin de notation
pointée.

Le prototype de ``If`` ressemble à ça :

<<<
fun< T > If(
deferred : Deferred< T >,
predicate : ( T ) -> Boolean,
block : Playlist.() -> Unit
) : ElseClauseAcceptor {
// ...
}

interface ElseClauseAcceptor {
infix fun Else( block : Playlist.() -> Unit )
}
>>>

Arrivé là on n'a peut-être pas couvert tous les cas, mais on voit
qu'il est assez simple de bâtir un langage d'orchestration en pur
Kotlin, une fois qu'on a assimilé ses impressionnantes possibilités
d'extension.

La présentation de PalletOps se termine par ces mots :

<<
Sometimes we wished we had static typing...
>>

C'est intéressant de noter que les regrets vont à cette
caractéristique essentielle de Clojure. Il n'est pas question des
temps de démarrage de la JVM (désormais en-dessous de la seconde) ou
de l'ambition de tout ramener à un problème de programmation. Bon ben
les gars, le typage statique de feu, maintenant vous l'avez.

Rendez-vous très bientôt pour la conclusion.
Reply all
Reply to author
Forward
0 new messages