Fun with turtles

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.

This entry was posted in F sharp love, Syntax Puzzles and tagged , , , . Bookmark the permalink.

Comments are closed.