Friday, May 22, 2009

Ioke and ISpec

This is the second post about the Ioke programming language, you can read the first part hear A Look at Ioke.

Ioke comes with the ISpec framework that lets you do bdd in Ioke, the syntax is very similar to that of Ruby's RSpec. There is a short introduction to ISpec on the Ioke wiki. To try it out I will use a problem from the TDD problems site. The mission is to convert a dollar amount to text. Let's start with a simple spec that says that 0 should be spelled out as 'zero'.
use("ispec")

describe(DollarToTextConverter,
it("should convert 0 to zero",
dttc = DollarToTextConverter mimic
dttc convert(0) should == "zero"
)
)

If we run this specification through Ioke we get the following output.
*** - couldn't find cell 'DollarToTextConverter' on 'Ground' (Condition Error No
SuchCell)

DollarToTextConverter [bddTest.ik:3:9]

So let's create the DollarToTextConverter kind. Because Ioke is a prototype based language there is no concept of a class, instead all objects are created as prototypes of already existing objects. New objects are created with the keyword mimic. Base is the top most object in Ioke but is not meant to be used to create new objects from. Instead we will use the Origin kind to create our object from.
DollarToTextConverter = Origin mimic

We save this to a dollarToTextConverter.ik file and add a use("dollarToTextConverter") statment to our ISpec test file. Running the specification again we get.
F

1)
Condition Error NoSuchCell in 'DollarToTextConverter should convert 0 to zero'
couldn't find cell 'convert' on 'DollarToTextConverter_0x1123BE5' (Condition Err
or NoSuchCell)

dttc convert(0) should ==("zero") [bddTest.ik:8:7]
bddTest.ik:7:2
bddTest.ik:7:2

Finished in 0.16 seconds

1 example, 1 failure

This tells us that there is no convert method on DollarToTextConverter, let's create it.
DollarToTextConverter = Origin mimic
DollarToTextConverter convert = method(amount, "zero")

Running our specification again we get.
.

Finished in 0.0 seconds

1 example, 0 failures

Our first passing specification in Ioke. The next test will be to make sure that 1 returns 'one'.
use("ispec")
describe(DollarToTextConverter,
it("should convert 0 to zero",
dttc = DollarToTextConverter mimic
dttc convert(0) should == "zero"
)

it("should convert 1 to one",
dttc = DollarToTextConverter mimic
dttc convert(1) should == "one"
)

Of course it fails when we run it as our convert functions always return 'zero'.
'DollarToTextConverter should convert 1 to one' FAILED
expected "zero" to == "one" (ISpec ExpectationNotMet)


bddTest.ik:16:18

Finished in 0.16 seconds

2 examples, 1 failure

Let's change our convert method to look at the incoming amount parameter and return an appropriate text representation.
DollarToTextConverter = Origin mimic
DollarToTextConverter convert = method(amount,
if (amount == 0,
"zero",
"one"
)
)

This will make our specification pass.
..

Finished in 0.15 seconds

2 examples, 0 failures

Time for some refactoring. Both test cases creates a new mimic of DollarToTextConverter so lets move this out of the it method calls.
describe(DollarToTextConverter,
dtcc = DollarToTextconverter mimic

it("should convert 0 to zero",
dttc convert(0) should == "zero"
)

it("should convert 1 to one",
dttc convert(1) should == "one"
)

The tests still passes. Let's consider what to test next. We want all one digit numbers to convert to the corresponding english text representation. So let's write a new test that tests all one digit numbers are converted to the correct text.
describe(DollarToTextConverter,
dtcc = DollarToTextconverter mimic

it("should convert to the correct text for a single digit amount",
(0..9) map(num, dttc convert(num)) should == ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
)

it("should convert 0 to zero",
dttc convert(0) should == "zero"
)

it("should convert 1 to one",
dttc convert(1) should == "one"
)

The specification fails so let's write some code to try to make it pass. A dict with numbers as keys and texts as values seems like a natural fit at this point. The literal syntax to create a dict in Ioke is {}(). In order to only refactor our code when the test cases are green we begin without the new specification and only refactor our code while keeping the orignal tests passing. The do method is a way to add cells to an object.

DollarToTextConverter = Origin mimic
DollarToTextConverter do (
numberDict = {}(0 => "zero", 1 => "one")
convert = method(amount,
numberDict[amount]
)
)

The tests passes so we can safely introduce our new test. We leave the previous tests until we are sure this new one works and covers the same cases. The new specification fails. But it is easy to fix by adding the numbers from 2 to 9 in our numberDict.
DollarToTextConverter do (
numberDict = {}(0 => "zero", 1 => "one", 2 => "two", 3 => "three", 4 => "four",
5 => "five", 6 => "six", 7 => "seven", 8 => "eight", 9 => "nine")
convert = method(amount,
numberDict[amount]
)
)

This makes all the tests pass, so we can safely remove the original two test cases.
Lets move on to two digit amounts. The numbers from 10 to 19 are similar to the single digit amounts and are easily converted by adding them to the numberDict. What about the numbers between 20 and 99? The special cases are the numbers dividable by 10, i.e 20 30 40 etc. We can just add them to our numberDict. The numbers between the 10-multiples are combinations of the corresponding 10-multiple and singel digit number, i.e. 21 => twenty-one. So let's write a new specification for the numbers up to 99.
describe(DollarToTextConverter,
dtcc = DollarToTextconverter mimic

it("should convert numbers between 0 and 19 to the correct text",
(0..19) map(num, dttc convert(num)) should == ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixtee", "seventeen", "eighteen", "nineteen"]
)

it("should convert multiples of 10 below 100 to the correct text",
[20, 30, 40, 50, 60, 70, 80, 90] map(num, dttc convert(num)) should == ["twenty", "thirty", "fourty", "fifty", "sixty", "seventy", "eighty", "ninty"]
)

it ("should convert two digit numbers to the correct text",
[21, 32, 43, 44, 55, 66, 67, 78, 88, 99] map(num, dttc convert(num)) should == ["twenty-one", "thirty-two", "fourty-three", "fourty-four", "fifty-five", "sixty-six", "seventy-eight", "eighty-eight", "ninty-nine"]
)

)

We just need a simple adjustment to our convert method to handle the new possible amount values.
DollarToTextConverter do (
numberDict = {}(0 => "zero", ... ) ;; lots of numbers left out
convert = method(amount,
if(numberDict[amount],
numberDict[amount], ;; if there is a direct mapping for our number return it
;; otherwise we construct a compound text
"#{numberDict[amount - amount % 10]}-#{numberDict[amount % 10]}"
)
)
)

The rules for multiples of 100 are combinations of the single digit and the 'hundred' suffix. The pattern for numbers below a thousand is then repeated for the 1000 part up to a million, but with a thousand suffix. Let's start with numbers between 100 and 999.
describe(DollarToTextConverter,
dtcc = DollarToTextconverter mimic

it("should convert numbers between 100 and 999 to the correct text",
dttc = DollarToTextConverter mimic
[100, 298, 321, 400, 555, 654, 777, 890, 901] map(num, dttc convert(num)) should == ["one hundred", "two hundred ninety-eight", "three hundred twenty-one", "four hundred", "five hundred fifty-five", "six hundred fifty-four", "seven hundred seventy-seven", "eight hundred ninety", "nine hundred one"]
)
)

Before inserting this test we need to refactor our code. We start by creating a new method convertTens.
convert = method(amount,
textArray = convertTens(amount)
)

convertTens = method(amount,
if(numberDict[amount],
numberDict[amount],
"#{numberDict[amount - amount % 10]}-#{numberDict[amount % 10]}"
)
)

The tests still passes so let's move on, we add our new test case for numbers between 100 and 999. To convert numbers between 100 and 999, we need to first convert the leftmost digit and add a "hundred", then convert the two rightmost digits if they are > 0. To do this we create a method convertHundreds, and a helper method to be able to get at the leftmost digit.
convertHundreds = method(amount,
result = []
hundreds = decimalShiftRight(amount, 2)
if(hundreds > 0,
result << convertTens(hundreds) << "hundred")
result << convertTens(amount%100)
)

decimalShiftRight = method(amount, places,
(amount - amount%(10**places)) / (10**places)
)


As you can see we have also chosen to start to store the intermediate result in an array. We change convertTens so that it also returns an array. And join the array with " " in the convert method.
convert = method(amount,
textArray = convertHundreds(amount) flatten
textArray join(" ") trim
)

convertTens = method(amount,
if(amount == 0,
[],
if(numberDict[amount],
numberDict[amount],
"#{numberDict[amount - amount % 10]}-#{numberDict[amount % 10]}"
)
)
)

This makes the new test pass. So let's add a test case for numbers above 1000. The conversion of numbers above 999 will be uniform and can be handled by a new method convertWithModifier.
modifier = ["", "thousand", "million"]
convertWithModifier = method(amount, modifierIndex,
if (amount == 0,
[],
convertWithModifier(decimalShiftRight(amount,3), modifierIndex + 1) <<
convertHundreds(amount%1000) << modifier[modifierIndex]
)
)

Here we convert the total amount in groups of three digits at a time. We call convertHundreds on the rightmost three digits we currently have, add an appropriate modifier, e.g. "thousand", and recursively call ourself with the same number decimal-shifted to the right 3 places. Our final code looks like this.

DollarToTextConverter = Origin mimic
DollarToTextConverter do (
numberDict = {}(1 => "one", 2 => "two", 3 => "three", 4 => "four",
5 => "five", 6 => "six", 7 => "seven", 8 => "eight", 9 => "nine", 10 => "ten",
11 => "eleven", 12 => "twelve", 13 => "thirteen", 14 => "fourteen",
15 => "fifteen", 16 => "sixtee", 17 => "seventeen", 18 => "eighteen", 19 => "nineteen",
20 => "twenty", 30 => "thirty", 40 => "fourty", 50 => "fifty", 60 => "sixty",
70 => "seventy", 80 => "eighty", 90 => "ninety")
modifier = ["", "thousand", "million"]

convert = method(amount,
textArray = convertWithModifier(amount, 0) flatten
textArray join(" ") trim
)

convertWithModifier = method(amount, modifierIndex,
if (amount == 0,
[],
convertWithModifier(decimalShiftRight(amount,3), modifierIndex + 1) <<
convertHundreds(amount%1000) << modifier[modifierIndex]
)
)

convertHundreds = method(amount,
result = []
hundreds = decimalShiftRight(amount, 2)
if(hundreds > 0,
result << convertTens(hundreds) << "hundred")
result << convertTens(amount%100)
)

decimalShiftRight = method(amount, places,
(amount - amount%(10**places)) / (10**places)
)

convertTens = method(amount,
if(amount == 0,
[],
if(numberDict[amount],
numberDict[amount],
"#{numberDict[amount - amount % 10]}-#{numberDict[amount % 10]}"
)
)
)
)


And the ISpec tests.
use("ispec")
use("dollarToTextConverter")

describe(DollarToTextConverter,
dttc = DollarToTextConverter mimic
it("should convert numbers between 0 and 19 to the correct text",
(1..19) map(num, dttc convert(num)) should == ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixtee", "seventeen", "eighteen", "nineteen"]
)

it("should convert multiples of 10 below 100 to the correct text",
[20, 30, 40, 50, 60, 70, 80, 90] map(num, dttc convert(num)) should == ["twenty", "thirty", "fourty", "fifty", "sixty", "seventy", "eighty", "ninety"]
)

it ("should convert two digit numbers to the correct text",
[21, 32, 43, 44, 55, 66, 67, 78, 88, 99] map(num, dttc convert(num)) should == ["twenty-one", "thirty-two", "fourty-three", "fourty-four", "fifty-five", "sixty-six", "sixty-seven", "seventy-eight", "eighty-eight", "ninety-nine"]
)

it("should convert multiples of 100 to the correct text",
[100, 298, 321, 400, 555, 654, 777, 890, 901] map(num, dttc convert(num)) should == ["one hundred", "two hundred ninety-eight", "three hundred twenty-one", "four hundred", "five hundred fifty-five", "six hundred fifty-four", "seven hundred seventy-seven", "eight hundred ninety", "nine hundred one"]
)

it("should convert amounts larger than 1000 to the correct text",
[1000, 12000, 231000, 567000, 999999, 123456789] map(num, dttc convert(num)) should ==
["one thousand", "twelve thousand", "two hundred thirty-one thousand", "five hundred sixty-seven thousand", "nine hundred ninety-nine thousand nine hundred ninety-nine", "one hundred twenty-three million four hundred fifty-six thousand seven hundred eighty-nine"]
)
)

A look at Ioke

This will be a short look at Ioke (ioke.org) a programming language created by Ola Bini. Ioke code is read from left to right. You can read about the execution model in Ioke here. The first symbol implicit on each line is called Origin. "kind" is a method that tells you the kind of the symbol it is evaluated on.
iik> kind
+> "Origin"

iik> 4 kind
+> "Number Integer"

Addition works as you would expect.
iik> 1 + 1
+> 2

+ is actually a method defined in the kind Number Integer so we can also call it as
iik> 1 +(1)
+> 2

Create variables by assigning a value to a new name. What you actually are doing is creating a cell in the receiving object, which is Origin in this case.
iik> x = 1

You can see what cells are defined on Origin withe the cells method. This returns a Dict with names and values. To get a nicer view we can use the each method and tell it to apply println to each pair in the Dict
iik> cells each(println)

You will see x as the last cell in the list of cells on Origin.

Ioke does some operator shuffling between readable syntax as above and a canonical form. So the above assignment can also be written as.
iik> =(x, 1)

Which is the form Ioke will print parsed code in. With this syntax it becomes clear that = is just another function that takes two arguments. You define methods with the method keyword. To see this in practice we create a method with an assignment in it.
iik> a = method(x=1) 
+> a:method(=(x, 1))

The value returned from a function is the last evaluated expression.
iik> myFunction = method("Hello there")
+> myFunction:method("Hello there")

iik> myFunction println
Hello there
+> nil

To define a method that takes some arguments:
iik> myAdd = method(num1, num2, num1 + num2)

iik> myAdd(3, 4)
+> 7

We can also choose to add our new function to the Ioke kind Number Integer.
iik> Number Integer add = method(num, num + self)
+> add:method(num, num + self)

iik> 3 add(2)
+> 5

self (or @) refers to the current object, like this in Java. We are also free to redefine + on Number Integer if we want to.
iik> Number Integer + = method(num, self - num)

iik> 3 + 2
+> 1

Let's try some conditions in our function. The if function will evaluate its second argument if the first argument is true and the third argument otherwise. One or both of the branches can be left out. There are also conditional functions called unless, cond and case. Go look them up in the ioke reference.
iik> Number Integer add2IfTrue = method(boolean, if(boolean, self + 2, self))
+> add2IfTrue:method(boolean, if(boolean, self +(2), self))

iik> 2 add2IfTrue(4>3)
+> 4

iik> 2 add2IfTrue(3>3)
+> 2

Looping over stuff can be done with loop, while, until, times, each and some other. Here are some examples from the ioke guide.
iik> loop("hello" println) ;; An infinite loop

iik> x = 0
iik> loop(if(x > 10, break) . x++ println)
iik> 3 times("hello" println)

iik> [1, 2, 3] each(println)

[] creates arrays and {} creates Dict's or hashes.
iik> nums = {1 => "one", 2 => "two"}
+> {1 => "one", 2 => "two"}

iik> nums[1]
+> "one"