Wren in action. A quick guide through a non-trivial Wren example.

in #eos8 years ago (edited)

This is a walk-through of a small script written in Wren which implements the classic Animals guessing game. It's very short and simple, yet it's a good way to see in action some of the non-trivial features of Wren.

I assume here that you have skimmed through the documentation or at least have some familiarity with its concepts. Also, I assume that you've been able to compile and run Wren VM.

anima.jpg

The source code originates from examples created by Bob Nystrom. Please note that I modified the code a bit (to be specific: the implementation of promptString method), as I had some issues with it on Windows.

Here is what the program is intended to do:

The user thinks of an animal. The program asks a series of yes/no questions to try to guess the animal they are thinking of. If the program fails, it asks the user for a new question and adds the animal to its knowledge base.

And here is the basic idea of implementation:

Internally, the program's brain is stored as a binary tree. Leaf nodes are animals. Internal nodes are yes/no questions that choose which branch to explore.

Let's go through the code step by step:

Import

Since we will be needing a way to collect a user's input from the console, we import an existing module which handles this task:

import "io" for Stdin

The import clause executes the code supplied by the imported module (in our case it's named io), whereas the for keyword creates and initializes a variable Stdin defined in the imported module which will be available in the current module.

Classes

We will be needing three classes: the basic class named Node, and two classes (named Animal and Question), both of which extend the Node class. Here is how you declare them:

class Node {

}

class Animal is Node {

}

class Question is Node {

}

Class Node

The Node class has two methods:

  • promptString which writes a prompt and reads a string of input supplied by the user and loops until the input is not empty.

  • promptYesNo which reads a yes or no (or something approximating those) and returns true or false, depending if yes or no was entered.

class Node {
   promptString(prompt) {
      var response = null
      while (response == null) {
         System.write("%(prompt)\n")
         response = Stdin.readLine()
         if (response.count >= 2) {
            response = response[0..(response.count - 2)]
         } else {
           response = null
         }
    }
    return response
  }
  promptYesNo(prompt) {
    while (true) {
      var line = promptString(prompt)
      if (line.startsWith("y") || line.startsWith("Y")) return true
      if (line.startsWith("n") || line.startsWith("N")) return false
      if (line.startsWith("q") || line.startsWith("Q")) Fiber.yield()
    }
  }
}

Things worth noting:

  • Regarding promptString: the imported variable Stdin is used to actually handle the task of reading the user input. That's why we needed the import "io" for Stdin clause.

  • Regarding promptYesNo: it internally calls promptString to delegate to it the job of reading user input. Also, note that Fiber.yield() will cause the current fiber to quit, thus transfer control to the main fiber, which will terminate the program, as there are no further commands in the main fiber.

Class Animal

It extends the Node class and introduces a constructor (named new) with a parameter and one method (named ask) which makes use of two methods inherited from Node: promptString and promptYesNo.

class Animal is Node {
   construct new(name) {
       _name = name
   }

  ask() {
    // Hit a leaf, so see if we guessed it.
    if (promptYesNo("Is it a %(_name)?")) {
      System.print("I won! Let's play again!")
      return null
    }

    // Nope, so add a new animal and turn this node into a branch.
    var name = promptString(
        "I lost! What was your animal?")
    var question = promptString(
        "What question would distinguish a %(_name) from a %(name)?")
    var isYes = promptYesNo(
        "Is the answer to the question 'yes' for a %(name)?")
    
    System.print("I'll remember that. Let's play again!")

    var animal = Animal.new(name)
    return Question.new(question, isYes ? animal : this, isYes ? this : animal)
  }
}

Things worth noting:

  • Constructors require the construct keyword before their name (and unlike most other languages their name can be anything, not necessarily new)

  • New instances of a class are created by calling the contractor name (e.g. new) on a class name (e.g. Animal). We have two examples of those in the above code: Animal.new() and Question.new()

  • Class fields are differentiated from other variables by using the underscore prefix (e.g. _name). Also, they are not declared explicitly (as other variables by using the var keyword) but instead you bring them into existence just by initializing them (e.g. _name = name).

  • If you use a percent sign (%) followed by a parenthesized expression, the expression is evaluated when building string literals, e.g. if _name equals "dog" then "Is it a %(_name)?" will evaluate to "Is it a dog?".

Class Question

It extends the Node class and introduces a constructor (named new) with three parameters and one method (named ask) which makes recursive calls to either itself or the ask method defined in the Animal class.

class Question is Node {
   construct new(question, ifYes, ifNo) {
      _question = question
      _ifYes = ifYes
      _ifNo = ifNo
   }

   ask() {
      // Recurse into the branches.
      if (promptYesNo(_question)) {
          var result = _ifYes.ask()
          if (result != null) _ifYes = result
      } else {
          var result = _ifNo.ask()
          if (result != null) _ifNo = result
      }
      return null
   }
}

Things worth noting:

  • As Wren is dynamically typed, the fields _ifYes and _ifNo can be anything - all that is required is implementing a method named ask. In our case those fields hold references to instances of either Question or Animal classes, depending if we are dealing with a branch or a leaf of the tree.

  • Initially (i.e. when the constructor is invoked), the fields _ifYes and _ifNo hold reference to Animal instances. Then, as the game progresses and new animals are added by the user, those fields are reassigned to hold references to other instances of Question, and instances of Animal are pushed further down the tree hierarchie.

The main loop

Up till now all we did was define three classes and their methods. Now it's time to put them in action. We do it by initializing the first instance of the Question class (assigned to a variable named root) and then creating a new fiber (consisting of an infinite while loop) and then starting this fiber (by invoking its call method).

var root = Question.new("Does it live in the water?",
    Animal.new("frog"), Animal.new("goat"))

// Play games until the user quits.
Fiber.new {
  while (true) root.ask()
}.call()

The entire source code can be found here.

Sort:  

Thanks! I did a quick post on Wren but didn't quite get into fibers. So what makes a fiber different than an instance? Is it more like an execution of the ask method on it until a terminating condition in this case? (Not sure I asked that very well...)

Fibers resemble threads in Java, with one important exception: they do not race against each other so their behavior is deterministic.

This is how Wren's creator explains it:

Fibers are a bit like threads except they are cooperatively scheduled. That means Wren doesn’t pause one fiber and switch to another until you tell it to.

More details can be found here.

My understanding is that at a given time only one fiber is active and all other fibers are waiting until the current one yields control. But I might be wrong about it, as the chapter describing fibers is titled Concurrency, which actually means the opposite:

Concurrence: the fact of two or more events or circumstances happening or existing at the same time.