DSL en Kotlin : comment créer votre propre langage de programmation en une soirée

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Qu'est-ce qu'un DSL et pourquoi en avez-vous besoin ?

\n\n

Imaginez que vous n'écrivez pas du code, mais une instruction dans un langage compréhensible qui se transforme elle-même en un programme fonctionnel. C'est exactement ainsi que fonctionnent les langages dédiés (DSL — Domain Specific Language). Contrairement aux langages universels (Java, Python, C#), un DSL est créé pour résoudre une tâche spécifique — qu'il s'agisse de configuration de serveur, de description de règles métier ou de construction de requêtes SQL complexes.

\n\n

Kotlin est le candidat idéal pour créer des DSL. Grâce aux lambdas avec récepteur, aux extensions et aux type-safe builders, vous pouvez écrire du code qui ressemble à une syntaxe native. Dans cet article, nous n'allons pas seulement aborder la théorie, mais créer un véritable DSL pour un framework de test — et vous verrez à quel point c'est simple et passionnant.

\n\n

Pourquoi Kotlin est-il le roi des DSL ?

\n\n

Kotlin a conquis le cœur des développeurs non seulement grâce à sa compatibilité avec Java. Ses capacités syntaxiques permettent de créer des DSL internes — des langages qui vivent à l'intérieur du langage hôte et utilisent sa syntaxe. Voici les trois piliers sur lesquels repose tout DSL Kotlin :

\n\n
    \n
  • Lambdas avec récepteur — permettent d'accéder aux méthodes d'un objet sans mention explicite de son nom. C'est la base du builder pattern.
  • \n
  • Extensions — ajoutent de nouvelles fonctions à n'importe quelle classe, même si vous ne pouvez pas la modifier.
  • \n
  • Fonctions infixes — permettent d'écrire du code dans un style de langage naturel : should be equalTo 5.
  • \n
\n\n

Ces capacités transforment l'écriture de DSL en un jeu de construction Lego : vous prenez des blocs prêts à l'emploi — lambdas, fonctions, propriétés — et vous assemblez un langage clair et sécurisé.

\n\n

Créons un DSL simple pour les tests : étape par étape

\n\n

Écrivons un DSL pour écrire des tests. L'objectif est que les tests ressemblent à une description lisible, et non à un ensemble d'appels assert :

\n\n
test("La calculatrice doit additionner les nombres") {\n    val result = Calculator.add(2, 3)\n    result should beEqualTo 5\n    result shouldNot beNull\n}
\n\n

Reconnaissez-le, c'est presque de l'anglais. Maintenant, voyons comment cela fonctionne.

\n\n

Étape 1. Créons le contexte de test

\n\n

Nous avons besoin d'une fonction test qui prend un nom et un bloc de code. Le bloc s'exécute dans un contexte où should et shouldNot sont disponibles :

\n\n
class TestContext {\n    fun Any.should(matcher: Matcher) {\n        if (!matcher.matches(this)) {\n            throw AssertionError("Attendu ${matcher.expected}, mais obtenu $this")\n        }\n    }\n\n    fun Any.shouldNot(matcher: Matcher) {\n        if (matcher.matches(this)) {\n            throw AssertionError("Attendu pas ${matcher.expected}, mais obtenu $this")\n        }\n    }\n}\n\nfun test(name: String, block: TestContext.() -> Unit) {\n    println("Exécution du test : $name")\n    try {\n        TestContext().block()\n        println("✓ Réussi")\n    } catch (e: AssertionError) {\n        println("✗ Échoué : ${e.message}")\n    }\n}
\n\n

Point clé : TestContext.() -> Unit est une lambda avec récepteur. À l'intérieur du bloc, this fait référence à l'objet TestContext, donc les méthodes should et shouldNot deviennent implicitement disponibles.

\n\n

Étape 2. Implémentons les matchers

\n\n

Créons maintenant des matchers simples. beEqualTo et beNull sont des fonctions qui retournent des objets Matcher :

\n\n
interface Matcher {\n    val expected: String\n    fun matches(value: Any?): Boolean\n}\n\nfun beEqualTo(expected: Any) = object : Matcher {\n    override val expected = "égal à $expected"\n    override fun matches(value: Any?) = value == expected\n}\n\nval beNull = object : Matcher {\n    override val expected = "null"\n    override fun matches(value: Any?) = value == null\n}
\n\n

Blogs

Book Recommendations