(The solution in this article is probably not the best way to solve this exercise in Ioke, but read on if you want. A better way is probably something along the lines in this blog post.)I just reread a pdf describing extreme oop, you can read it
here, inspired by the Object Calestenetics chapter in The Thoughtworks Antology. I like the idea and decided to try it out using Ioke. To do this we need to modify the rules a bit. As originaly stated they read.
Extreme OOP Rules
1. Use only one level of indentation per method.
2. DonÃt use the else keyword.
3. Wrap all primitives and strings.
4. Use only one dot per line.
5. DonÃt abbreviate.
6. Keep all entities small.
7. DonÃt use any classes with more than two instance variables.
8. Use first-class collections.
9. DonÃt use any getters/setters/properties
My try at translating them to apply to the Ioke syntax yielded this.
Extreme OOP Rules for Ioke
1. Use only one level of indentation per method.
2. DonÃt use the else argument to the if method.
3. Wrap all primitives and strings.
4. Use only one method call per line.
5. DonÃt abbreviate.
6. Keep all entities small.
7. DonÃt use any kind with more than two inactivable cells (cells that are not methods).
8. Use first-class collections.
9. DonÃt access another objects inactivable cells directly or via dumb getter/setters.
The exercise in the pdf is to write a simple interpreter for (something similar to) Commodore 64 BASIC. The requirements are given as stories, that can be translated nicely into ISpec. So lets start with the first story.
Story: An empty program produces no output. Acceptance:
input:
(empty)
output:
(empty)
To start with we won't bother with the ui. I first translated this into the following ISpec code.
describe(Interpreter,
it("should give no output given an empty program",
Interpreter input("") should == ""
)
)
But then I remembered that all Strings and primitives should be wraped. So we need a class that represent programs. Lets try this instead.
use("ispec")
describe(Interpreter,
it("should give no output given an empty program",
Interpreter input(Interpreter TextProgram fromText("")) should == ""
)
)
It fails because there is no Interpreter kind, so lets create the Interpreter and TextProgram kinds and the input method.
Interpreter = Origin mimic do (
input = method(textProgram,
"" ; for now we just return an empty string
)
TextProgram = Origin mimic do(
fromText = method(text,
self ; just return self
)
)
)
If we add a use clause for this new file to the iSpec test file the test passes, so lets move on to the next story.
Story: A bare "print" statement produces a single newline. Acceptance:
input:
PRINT
output:
(a single newline)
Translated to ISpec.
it("should produce a single newling given a bare \"print\" statement",
Interpreter input(Interpreter TextProgram fromText("PRINT")) should == "\n"
)
The specification fails and to make it pass we need to save the program in TextProgram and update the input method. To begin with we just check if the program is empty or not and let all non-empty programs return "\n".
Interpreter = Origin mimic do (
input = method(textProgram,
if(textProgram text == "",
return "")
"\n"
)
TextProgram = Origin mimic do(
initialize = method(text,
@text = text
)
fromText = method(text,
self mimic(text)
)
)
)
As we are not allowed to use the else keyword, we use an if as a guard clause for empty inputs and return "\n" for all other cases. The test passes, but we have broken the rule that we shuld not access an objects cells directly, i.e. the text cell in the textProgram object. So lets move the check if the program is empty into the TextProgram kind.
TextProgram = Origin mimic do(
initialize = method(text,
@text = text
)
fromText = method(text,
self mimic(text)
)
empty? = method(@text empty?)
)
And use this method from the input method.
input = method(textProgram,
if(textProgram empty?,
return "")
"\n"
)
The test still passes, and there is no real need to refactor the code, so lets see where the next story brings us.
Story: A "print" statement can have a constant string as an argument. The output is the constant
string. Acceptance:
input:
PRINT "Hello, World!"
output:
Hello, World!
Wich translates to the following ISpec code.
it("should output the content of a given constant string passed to print",
Interpreter input(Interpreter TextProgram fromText("PRINT \"Hello, World!\"")) should == "Hello, World!\n"
)
To pass this test we need to modify the input method again. Lets start by making the test pass without worrying about the rules.
input = method(textProgram,
if(textProgram empty?,
return "")
if(textProgram text == "PRINT",
return "\n")
"Hello, World!\n"
)
This passes all the tests, but wat we really need is some function that can handle the print statement. Lets create a Print kind to handle print statements.
Print = Origin mimic do (
initialize = method(argument,
@argument = argument
)
execute = method(@argument asText + "\n")
)
We added an initialize method that saves the argument in the argument cell and an execute method that we can call to get the result of executing the statement. To extract the argument to the Print statement we add a argument method to the TextProgram kind and wrap the argument to Print in a new Argument kind.
TextProgram = Origin mimic do (
...
argument = method(
Interpreter Argument mimic(@text split rest join(" "))
)
)
Argument = Origin mimic do(
initialize = method(text,
@text = text
)
asText = method(
@text[1...-1] ; remove the "" surrounding text arguments
)
)
To be able to handle all return values from input as objects with an execute method we also create a base kind Program to handle the empty program.
Program = Origin mimic do (
execute = method("")
)
With these new kinds in place, and a "first" method on TextProgram we can change the input method.
input = method(text,
program = Interpreter Program mimic
if (text first == "PRINT",
program = Interpreter Print mimic(Interpreter Argument mimic(text rest)))
program execute
)
TextProgram = Origin mimic do(
initialize = method(text,
@text = text
)
fromText = method(text,
self mimic(text)
)
first = method(
@text split[0]
)
rest = method(
@text split rest join(" ")
)
)
As you can see we also replaced the argument method in TextProgram with a rest method, so that it is more in alignment with the first method. Now the input method is responsible for creating objects for our program and their argument, and the TextProgram kind only deals with text processing.
The test still passes and I don't feel the need to refactor the code any more right now. So lets move on to the next story.
Story: Two or more statements in a sequence are executed one after the other
PRINT "Hi"
PRINT "There"
PRINT "!"
output:
Hi
There
!
Translated to an ISpec specification.
it("should execute consequetive statements one after the other",
Interpreter input(Interpreter TextProgram fromText("PRINT \"Hi\""),
Interpreter TextProgram fromText("PRINT \"There\""),
Interpreter TextProgram fromText("PRINT \"!\"")) should == "Hi\nThere\n!\n"
)
The test fails. To make it pass all we need to do is invoke our current logic in a loop, collect the result and return it as a string.
input = method(+textPrograms,
textPrograms map(text,
program = Interpreter Program mimic
if (text first == "PRINT",
program = Interpreter Print mimic(Interpreter Argument mimic(text rest)))
program execute
) join
)
But this introduces another level of indentation in the input method and breaks the first rule "Use only one level of indentation per method." So we need to refactor this code. Lets simply introduce a handleLine method that we use from the original input method.
input = method(+textPrograms,
textPrograms map(text,
handleLine(text)
) join
)
handleLine = method(text,
program = Interpreter Program mimic
if (text first == "PRINT",
program = Interpreter Print mimic(Interpreter Argument mimic(text rest)))
program execute
)
The next story introduces numbers as argument to PRINT.
Story: The "print" statement can output number constants.
PRINT 123
output:
123
PRINT -3
output:
-3
Translated to ISpec we get.
it("should output numbers passed as argument to PRINT",
Interpreter input(Interpreter TextProgram fromText("PRINT 123"),
Interpreter TextProgram fromText("PRINT -3")) should == "123\n-3\n"
)
As expected the test fails. To handle this case we need to change the way we handle the argument to the Print statement. More specifically we need to change the asText method in the Argument kind so that it removes the quotation marks surrounding text and leave numbers untouched.
Argument = Origin mimic do (
...
asText = method(
if(@text[0..0] == "\"", ; a text argument
return @text[1...-1]) ; remove the "" surrounding text arguments
return @text
)
)
This makes the test pass, and we are still following all the rules so lets move on to the next story.
Story: A single letter is a variable. The print statement can print its value. The default value for a
variable is 0
PRINT A
output:
0
Translated to ISpec.
it("should treat single letters as variables with 0 as default value",
Interpreter input(Interpreter TextProgram fromText("PRINT A")) should == "0\n"
)
The test fails. All we have to do to make this story pass is to replace single letters outside of strings with 0. We don't need to worry about remembering variable values, yet. So lets just parse the argument and replace single letters outside of strings with 0.
Print = Program mimic do (
...
execute = method(
@argument = @argument evaluate
@argument asText + "\n"
)
)
In Argument we create the evaluate method and add a textArgument? method.
Argument = Origin mimic do(
...
textArgument? = method(
@text[0..0] == "\""
)
evaluate = method(
if(@textArgument?,
return @)
res = @text split map(expr,
res = #/-?\d+/ =~ expr
if(res,
return expr)
return 0
) join
Interpreter Argument mimic(res)
)
)
This implementation makes all the test pass but it doesn't look very nice, and we are breaking the rule about not having more than 1 method call per line with the line "res = @text split map(expr,". To clean up the code we start with creating kinds for the different types of arguments we accept to Print. This also means that we have to change the creation logic for Arguments, instead of mimic:ing Arguments with a text arguments it feels more appropriate to create a fromText method in Arguments that we can call from the handleLine method.
handleLine = method(text,
program = Interpreter Program mimic
if (text first == "PRINT",
program = Interpreter Print mimic(Interpreter Argument fromText(text rest)))
program execute
)
The Argument kind.
Argument = Origin mimic do (
initialize = method(value, @value = value)
fromText = method(value,
cond(
value empty?, Interpreter Argument mimic(value),
textArgument?(value), Interpreter TextArgument mimic(value),
numberArgument?(value), Interpreter NumberArgument mimic(value),
Interpreter VariableArgument mimic(value)
)
)
textArgument? = method(value, value[0..0] == "\"")
numberArgument? = method(value, #/-?\d+/ =~ value)
evaluate = method(@value)
)
TextArgument = Argument mimic("") do (
evaluate = method(@value[1...-1])
)
VariableArgument = Argument mimic("") do (
evaluate = method(0)
)
NumberArgument = Argument mimic("") do (
)
In the argument kinds mimic:ing Argument we pass in a default value of "". When one of the sub kinds are mimic:ed with an argument the initialize method in Argument will be called and the value cell is set to the passed in value.
iik> Interpreter NumberArgument
+> Interpreter NumberArgument_0x114080D:
kind = "Interpreter NumberArgument"
value = ""
iik> Interpreter NumberArgument mimic(3)
+> Interpreter NumberArgument_0x10A1F17:
value = "3"
The code looks better and the tests still pass. The main problem now is that the cond expression in the fromText method in Argument breaks the rule that we should not use else statements. We need to somehow determine what kind of argument we are dealing with. One way to do this without using a cond would be to use a Dict with regular expressions we want to match against and the kind we want to create if the expression matches the passed in text value.
Argument = Origin mimic do(
...
typeDict = {}(#/^$/ => Interpreter Argument mimic(""),
#/^"[^"]+"$/ => Interpreter TextArgument mimic(""),
#/^[-+]?\d+$/ => Interpreter NumberArgument mimic(""),
#/^\w$/ => Interpreter VariableArgument mimic(""))
fromText = method(text,
argType = typeDict find(entry,
entry key =~ text
) value
argType mimic(text)
)
)
This is better but not quite there yet. Rule 8 states "Use first-class collections. In other words, any class that contains a collection should contain no other member variables" So the typeDict collection should be contained in its own class. So lets create an ArgumentTypeMatcher kind.
ArgumentTypeMatcher = Origin mimic do(
typeDict = {}(#/^$/ => Interpreter Argument mimic(""),
#/^"[^"]+"$/ => Interpreter TextArgument mimic(""),
#/^[-+]?\d+$/ => Interpreter NumberArgument mimic(""),
#/^\w$/ => Interpreter VariableArgument mimic(""))
match = method(text,
entryMatch = typeDict find(entry,
entry key =~ text
)
entryMatch value
)
)
Argument = Origin mimic do (
...
fromText = method(text,
argumentType = ArgumentTypeMatcher match(text)
argumentType mimic(text)
)
)
The tests still passes so lets try the next story.
Story: An assignment statement binds a value to a variable.
input:
A=12
PRINT A
output:
12
Translated to ISpec.
it("should accept an assignment statement and bind the passed in value to a variables",
Interpreter input(Interpreter TextProgram fromText("A=12"),
Interpreter TextProgram fromText("PRINT A")) should == "12\n"
)
The test fails and to make it pass we need some means to remember values for variables. A Dict feels like a natural fit for variable and their values. Assignment statements differs from all the previous statements that we have seen so far. This statement does not start with the string "PRINT" and produces no output. So we need to change the handleLine method. We will need to do a similar match operation as with the Argument type to PRINT. handleLine currently looks like this.
Interpreter = Origin mimic do (
...
handleLine = method(textProgram,
program = Interpreter Program mimic
if (textProgram printStatement?,
argument = Interpreter Argument fromText(textProgram rest)
program = Interpreter Print mimic(argument))
program execute
)
)
Lets add a ProgramTypeMatcher kind that determines if this is a PRINT statement or a variable assignment.
ProgramTypeMatcher = Origin mimic do (
typeDict = {}(#/^PRINT.+$/ => Interpreter Print mimic(""),
#/^\w=[-+]?\d+$/ => Interpreter Assignment mimic("", ""),
#/^$/ => Interpreter Program mimic("")
)
match = method(text,
entryMatch = typeDict find(entry,
entry key =~ text
)
entryMatch value
)
)
The handleLine method becomes.
handleLine = method(textProgram,
progromType = ProgramTypeMatcher match(textProgram)
program = programType fromTextProgram(textProgram)
program execute
)
From this we can see that we need a fromTextProgram method in all Program types and a new Assignment kind for assignments.
Program = Origin mimic do (
fromTextProgram = method(textProgram, self mimic)
...
)
Print = Origin mimic do (
initialize = method(argument, @argument = argument)
fromTextProgram = method(textProgram,
argument = Interpreter Argument fromText(textProgram rest)
self mimic(argument)
)
...
)
Assignment = Origin mimic do (
initialize = method(variable, value,
@variable = variable
@value = value
)
fromTextProgram = method(textProgram,
match = #/^({variable}\w)=({value}[-+]?\d+)$/ =~ textProgram asText
variable = match variable
value = match value
self mimic(variable, value)
)
)
And we need a execute method in the Assignment kind and a kind to store the variable values in, lets call it Variables.
Variables = Origin mimic do (
variableDict = Dict withDefault(0)
setValue = method(variable, value,
variableDict[variable] = value
)
getValue = method(variable,
variableDict[variable]
)
)
Assignment = Origin mimic do (
...
execute = method(
Interpreter Variables setValue(@variable, @value)
""
)
)
Finally we need to change the evaluate method in the VariableArgument kind, to get the value from the Variables kind instead.
VariableArgument = Argument mimic("") do(
evaluate = method(Interpreter Variables getValue(@value))
)
All the test passes with this code, but the ProgramTypeMatcher and ArgumentTypeMatcher are almost identical so lets merge them into one kind.
TypeMatcher = Origin mimic do (
initialize = method(typeDict, @typeDict = typeDict)
withTypeDict = method(typeDict,
Interpreter TypeMatcher mimic(typeDict)
)
match = method(text,
entryMatch = typeDict find(entry,
entry key =~ text
)
entryMatch value
)
)
And initialize it with the typeDict that we want to use.
This article is growing a bit large and if anyone is still reading I will spare you the rest of the details. Anyway, I continued with the remaining stories, you can find my final solution on
github.
ConclusionIt was pretty easy to solve this problem in Ioke and follow the extreme oop rules. After a while I learned the rules and could feel when code I was writing was breaking one of them. I created far more small kinds than I would have otherwise and was able to keep the methods pretty small and focused one one task. I liked the exercise and will try it again in some other language and with another program exercise. I could probably have written more ISpec tests for certain parts of the system. Maybe I could have used the stories in the pdf as a kind of acceptance tests and then written unit tests as I tried to make the acceptance test pass.
Instead of all the code we wrote above you could use the fact that the statements we want to support are valid Ioke statements, except for print. So we can simple use the doText method on the Ioke Message kind to solve the exercise. With similar methods as in the above solution.
Interpreter = Origin mimic do (
input = method(+textPrograms,
textPrograms map(text,
text evaluate
) join
)
print = method(arg, Origin mimic)
TextProgram = Origin mimic do(
initialize = method(text, @text = text)
evaluate = method(Message doText(text))
)
)
And all the tests passes.