How to implement a switch in Pharo Smalltalk
Asked Answered
R

3

9

I'm trying to parse a command and an int to make a "turtle" move on a board. I'm a bit overwhelmed, since it's not throwing an exception, and I can't even figure out how to open the debugger without one.

My code:

"only should begin parsing on new line"
endsWithNewLine:= aTurtleProgram endsWith: String cr.
endsWithNewLine ifTrue:[
    "splits commands based on new lines"
    commands := aTurtleProgram splitOn: String cr.
    commands do:[:com |
        "should split into command and value for command"
        i := com splitOn: ' '.
        "command"
        bo := ci at: 1.
        val := ci at: 2.
        "value for command"
        valInt := val asInteger.
        ^ bo = 'down' "attempted switch"
            ifTrue: [ turtle down: valInt ]
            ifFalse: [
              bo = 'left'
                  ifTrue: [ turtle left: valInt ]
                  ifFalse: [
                    bo = 'right'
                        ifTrue: [ turtle right: valInt ]
                        ifFalse: [
                          bo = 'up'
                              ifTrue: [ turtle up: valInt ]
                              ifFalse: [ self assert: false ] ] ] ] ].
Ruling answered 28/5, 2015 at 9:6 Comment(0)
H
10

You can do it the way you did it, and you can open a debugger by inserting a self halt statement into your code.

Usually, ifs, and moreover case-sytle ifs, are a bad sign. So what you can do is break the functionality into classes like DownMove, LeftMove and so on, and then each class will implement its own functionality when you call, for example, an execute: method that will do exactly what is needed by the command. But in your case, the approach would be cumbersome; moreover, you have very trivial actions.

You can use a dictionary with definitions. So imagine that you define an instance variable or class variable:

moveActions := {
  'down' -> [ :dist | turtle down: dist ] .
  'left' -> [ :dist | turtle left: dist ] .
  ... } asDictionary

Then in your code, you do: (moveActions at: bo) value: valInt. This snippet will give you a block (value) for a string (key), then you evaluate the block with your integer.

On the other hand, as the action pattern is the same and only message changes, you can map only the message names in the dictionary:

moveActions := {
  'down' -> #down: .
  'left' -> #left: .
  ... } asDictionary

Then you can ask your turtle to perform a message dynamically given by string:

`turtle perform: (moveActions at: bo) with: valInt`

Also, if you want to rely on the similarity between commands that you read and messages that you send to the turtle, you can dynamically compose the message string:

`turtle perform: (bo, ':') asSymbol with: valInt`

Please note that this is not recommended in your case, as, first of all, you are coupling user input and your code, i.e. if you decide to change the user command from down to moveDown, you will have to change your method name from down: to moveDown:. Also, this approach allows the user to "inject" bad code into your system, as he can write a command like become 42, which will result in the code:

`turtle perform: #become: with: 42`

which will swap pointers between the turtle object and 42. Or you can think about even worse cases. But I hope that this meta excursion was good for you. :)

Hellhole answered 28/5, 2015 at 9:50 Comment(6)
perform: needs a symbol as argument. So you can do either (bo, ':') asSymbol or bo asSymbol asMutatorExperience
Why would an approach based on polymorphism be cumbersome? Quite the opposite, it would've been the correct approach.. There's a reason Smalltalk doesn't have a switch statement; it's very easy to create a small hierarchy of objects and let them manage the conditional behaviour. It's what the GoF called the "State Pattern", and it's meant to make the code more maintainable than many conditionals. (Of course, the dictionary approach is still better than having a switch statement, though.)Dirty
@AmosM.Carpenter according to A. Riel: 3.9: Do not turn an operation into a class. Be suspicious of any class whose name is a verb or derived from a verb. Especially those which have only one piece of meaningful behavior (i.e. do not count sets, gets, and prints). Ask if that piece of meaningful behavior needs to be migrated to some existing or undiscovered class. Also to construct the classes from string commands you have to either do the same tricks I've described, or create a parser, which I think is cumbersome for the current case.Hellhole
@Uko: Interesting, hadn't heard of that guy. Googled a bit and assume you mean Arthur J Riel. Doesn't "3.9" directly contradict his "2.8 - A class should capture one and only one key abstraction", though, as well as many common Patterns, such as Decorator and Visitor? Wouldn't amalgamating those lovely classes with distinct purposes into one class just lead to more God Objects? Don't think I agree with Riel on many of those one-liners I've seen (he seems awfully fond of absolute words like "always" and "eliminate"), but I haven't read his book, maybe his reasons are better explained in there.Dirty
@AmosM.Carpenter as far as I can tell, there is no "perfect software design", and that's why the book is called "Object-Oriented Design Heuristics". Of course it makes sense to have different classes that handle the same message in their own way, but in the current situation, the solution of EstebanLM is short and simple, while maintaining a class for each possible command can be quite tough. On the other hand if you want to add new commands and manage them in an easy way, it definitely has to be done with classes.Hellhole
@Uko: Fair enough. If there was one perfect way, everybody'd be doing it that way. :-) In languages other than Smalltalk, I'd probably agree that many little class hierarchies for such things can be cumbersome (e.g. in Java, a solution using enums might be easier).Dirty
B
7

In Smalltalk you do not use switch statements. Instead, you use "case methods" (I think terminology was introduced by Ken Beck, but I'm not sure).

In your case, it would be something like this:

method1
    "only should begin parsing on new line"
    endsWithNewLine:= aTurtleProgram endsWith: String cr.
    endsWithNewLine ifTrue:[
    "splits commands based on new lines"
    commands := aTurtleProgram splitOn: String cr.
    commands do:[ :eachCommand | 
        | tuple direction value |

        tuple := eachCommand splitOn: ' '.
        direction := tuple first.
        value := tuple second asInteger.
        self moveTurtleDirection: direction value: value ].

moveTurtleDirection: direction value: value
    direction = 'down'  ifTrue: [ ^turtle down: value ].
    direction = 'left'  ifTrue: [ ^turtle left: value ].
    direction = 'right' ifTrue: [ ^turtle right: value ].
    direction = 'up'    ifTrue: [ ^turtle up: value ].

    self error: 'Invalid direction'.

As you can see, this is a lot more clear and you do not need to apply "smalltalk magic" to have an efficient design. This has also the advantage of being clear, fast on execution and easily optimisable by the compiler and the JIT :)

Betsybetta answered 29/5, 2015 at 6:34 Comment(0)
C
3

Just to cite another possibility: instead of writing the Parser by yourself, rather use one of the ParserGenerator available in Smalltalk (PetitParser, OMeta, Smacc, Xtreams, ...)

Here is an example with Xtreams https://code.google.com/p/xtreams/wiki/Parsing (the link is likely to die soon, but I haven't anything newer...) which can parse the PEG format (http://en.wikipedia.org/wiki/Parsing_expression_grammar).

You first define your grammar in a String:

grammar := '
    Number   <- [0-9]+
    Up  <- "up" Number
    Down    <- "down" Number
    Left    <- "left" Number
    Right   <- "right" Number
    Command <- Up / Down / Left / Right
'.

Then you define an interpreter for moving the turtle:

PEGActor subclass: #TurtleInterpreter
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Test-Parser'.

with a few methods to connect the interpreter to aTurtle, and connect an action to the grammar rules above via so called pragmas (annotations):

turtle: aTurtle
    turtle := aTurtle

Number: digits
    <action: 'Number'>
    ^digits inject: 0 into: [ :total :digit | total * 10 + ('0123456789' indexOf: digit) - 1 ]

Up: aNumber
    <action: 'Up'>
    turtle moveUp: aNumber

Down: aNumber
    <action: 'Down'>
    turtle moveDown: aNumber

Left: aNumber
    <action: 'Left'>
    turtle moveLeft: aNumber

Right: aNumber
    <action: 'Right'>
    turtle moveRight: aNumber

Then you just create a parser and connect it to this interpreter:

parser := PEGParser parserPEG
    parse: 'Grammar'
    stream: grammar
    actor: PEGParserParser new.
interpreter := TurtleInterpreter new turtle: Turtle new.    
parser parse: 'Command' stream: 'left24' actor: interpreter.

I let you discover how to specify spaces, newlines or sequence of commands, but you see how decoupled and easily extensible your code can be with such framework: one new command = one line in the grammar description + one method in the interpreter for connecting to a Turtle action...

Chuppah answered 29/5, 2015 at 22:6 Comment(2)
This is kind of hilarious that a person asks about switch statement and gets one answer that actually answers his question, another one that tells how to use meta programming with indirect messages, and a short guide on writing parsers :). We have an awesome communityHellhole
@uko yes, the answers are rather how to not write a switch in Smalltalk ;)Chuppah

© 2022 - 2024 — McMap. All rights reserved.