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.
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 variableStdin
is used to actually handle the task of reading the user input. That's why we needed theimport "io" for Stdin
clause.Regarding
promptYesNo
: it internally callspromptString
to delegate to it the job of reading user input. Also, note thatFiber.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 necessarilynew
)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()
andQuestion.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 thevar
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 namedask
. In our case those fields hold references to instances of eitherQuestion
orAnimal
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 toAnimal
instances. Then, as the game progresses and new animals are added by the user, those fields are reassigned to hold references to other instances ofQuestion
, and instances ofAnimal
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.
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:
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: