F# is a great language to build internal DSLs, but I had no idea that you could go as far as what I’m going to present here…
Let’s start from the start: my goal was to design some way for me to explain my daughter what programming is about and how it works. There are some nice graphical tools for that, like Scratch or more recently Hour of code, however I wanted to show something which is closer to what I actually do on a daily basis: write (and read) code. There are some nice educational programming languages, some of them are even usable in a localized way (French, in my case). LOGO is a good example of this, and I could have used an existing tool, but where is the fun if you don’t build your own?
It appears that building an internal LOGO-like DSL is surprisingly easy, and requires almost no code! What you need is just to define the basic types to describe your actions:
type Distance_Unit = STEPS type Rotation_Unit = GRADATIONS type Rotation_Direction = | LEFT | RIGHT let STEP = STEPS let GRADATION = GRADATIONS type Color = | RED | GREEN | BLUE type Action = | Walk of int * Distance_Unit | Turn of int * Rotation_Unit * Rotation_Direction | LiftPenUp | PutPenDown | PickColor of Color type Turtle = Action seq
And then a computation expression to do the trick of transforming sentences to sequences of actions:
type AS_word = AS type TO_word = TO type THE_word = THE type PEN_word = PEN type UP_word = UP type DOWN_word = DOWN type TIMES_word = TIMES type WHAT_word = WHAT type DOES_word = DOES type TurtleBuilder() = member x.Yield(()) = Seq.empty [<CustomOperation("WALK", MaintainsVariableSpace = true)>] member x.Walk(source:Turtle, nb, unit:Distance_Unit) = Seq.append source [Walk(nb, unit)] [<CustomOperation("TURN", MaintainsVariableSpace = true)>] member x.Turn(source:Turtle, nb, unit:Rotation_Unit, to_word:TO_word, the_word:THE_word, direction:Rotation_Direction) = Seq.append source [Turn(nb, unit, direction)] [<CustomOperation("LIFT", MaintainsVariableSpace = true)>] member x.LiftPenUp(source:Turtle, the_word:THE_word, pen_word:PEN_word, up_word:UP_word) = Seq.append source [LiftPenUp] [<CustomOperation("PUT", MaintainsVariableSpace = true)>] member x.PutPenDown(source:Turtle, the_word:THE_word, pen_word:PEN_word, down_word:DOWN_word) = Seq.append source [PutPenDown] [<CustomOperation("PICK", MaintainsVariableSpace = true)>] member x.PickColor(source:Turtle, the_word:THE_word, color:Color, pen_word:PEN_word) = Seq.append source [PickColor color] [<CustomOperation("DO", MaintainsVariableSpace = true)>] member x.Do(source:Turtle, as_word:AS_word, turtle:Turtle) = Seq.append source turtle [<CustomOperation("REPEAT", MaintainsVariableSpace = true)>] member x.Repeat(source:Turtle, nb:int, times_word:TIMES_word, what_word:WHAT_word, turtle:Turtle, does_word:DOES_word) = Seq.append source (List.replicate nb turtle |> Seq.collect id) let turtle = new TurtleBuilder()
And with nothing more, you can now write this kind of plain English instructions:
turtle { LIFT THE PEN UP WALK 4 STEPS TURN 3 GRADATIONS TO THE RIGHT PICK THE GREEN PEN PUT THE PEN DOWN WALK 4 STEPS }
Now, this doesn’t solve my initial problem, I want a French DSL. But I just need to define another builder, and a few translation functions:
type Tortue = Turtle type Unite_De_Distance = PAS with member x.enAnglais = match x with | PAS -> STEP type Unite_De_Rotation = | CRANS with member x.enAnglais = match x with | CRANS -> GRADATION let CRAN = CRANS type Sens_De_Rotation = | GAUCHE | DROITE with member x.enAnglais = match x with | GAUCHE -> LEFT | DROITE -> RIGHT type Couleur = | ROUGE | VERT | BLEU with member x.enAnglais = match x with | ROUGE -> RED | VERT -> GREEN | BLEU -> BLUE type Mot_A = A type Mot_DE = DE type Mot_LE = LE type Mot_STYLO = STYLO type Mot_COMME = COMME type Mot_FOIS = FOIS type Mot_CE = CE type Mot_QUE = QUE type Mot_FAIT = FAIT type TortueBuilder() = member x.Yield(()) = Seq.empty member x.For(_) = Seq.empty [<CustomOperation("AVANCE", MaintainsVariableSpace = true)>] member x.Avance(source:Tortue, de:Mot_DE, nb, unite:Unite_De_Distance) = Seq.append source [Walk(nb, unite.enAnglais)] [<CustomOperation("TOURNE", MaintainsVariableSpace = true)>] member x.Tourne(source:Tortue, de:Mot_DE, nb, unite:Unite_De_Rotation, a:Mot_A, sens:Sens_De_Rotation) = Seq.append source [Turn(nb, unite.enAnglais, sens.enAnglais)] [<CustomOperation("LEVE", MaintainsVariableSpace = true)>] member x.Leve(source:Tortue, le:Mot_LE, stylo:Mot_STYLO) = Seq.append source [LiftPenUp] [<CustomOperation("POSE", MaintainsVariableSpace = true)>] member x.Pose(source:Tortue, le:Mot_LE, stylo:Mot_STYLO) = Seq.append source [PutPenDown] [<CustomOperation("PRENDS", MaintainsVariableSpace = true)>] member x.Prends(source:Tortue, le:Mot_LE, stylo:Mot_STYLO, couleur:Couleur) = Seq.append source [PickColor couleur.enAnglais] [<CustomOperation("FAIS", MaintainsVariableSpace = true)>] member x.Fais(source:Tortue, comme:Mot_COMME, tortue:Tortue) = Seq.append source tortue [<CustomOperation("REPETE", MaintainsVariableSpace = true)>] member x.Repete(source:Tortue, nb:int, fois:Mot_FOIS, ce:Mot_CE, que:Mot_QUE, tortue:Tortue, fait:Mot_FAIT) = Seq.append source (List.replicate nb tortue |> Seq.collect id) let tortue = new TortueBuilder()
And I can write my instructions in French!
tortue { AVANCE DE 5 PAS TOURNE DE 6 CRANS A DROITE AVANCE DE 5 PAS TOURNE DE 6 CRANS A DROITE AVANCE DE 5 PAS TOURNE DE 6 CRANS A DROITE AVANCE DE 10 PAS }
The next steps involved:
- Writing a Windows Forms client to actually see the turtle draw things
- Making it possible to send actions to the turtle using FSI
- Hand-coding every letter of the alphabet
- Adding a new keyword to my turtle builders
And please welcome my “Hello world” sample:
turtle { WRITE "MR T. SAYS:\nHELLO WORLD!\n\n" }
As usual, all the code is available on my github.