如何在 Pharo Smalltalk 中实现开关

How to implement a switch in Pharo Smalltalk

我正在尝试解析一个命令和一个 int 以在棋盘上进行 "turtle" 移动。我有点不知所措,因为它没有抛出异常,而且我什至不知道如何在没有调试器的情况下打开调试器。

我的代码:

"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 ] ] ] ] ].

您可以按原样进行,并且可以通过在代码中插入 self halt 语句来打开调试器。

通常,if 以及大小写 if 是一个不好的迹象。所以你可以做的是将功能分解成 class 类,如 DownMoveLeftMove 等等,然后每个 class 将在你调用时实现自己的功能,因为例如,execute: 方法将完全执行命令所需的操作。但是就您而言,这种方法很麻烦;而且你的动作很琐碎

您可以使用带有定义的字典。因此,假设您定义了一个实例变量或 class 变量:

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

然后在您的代码中,您执行:(moveActions at: bo) value: valInt。此代码段将为您提供字符串(键)的块(值),然后您使用整数评估该块。

另一方面,由于操作模式是相同的,只有消息发生变化,您可以只映射字典中的消息名称:

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

然后你可以让你的乌龟执行一个由字符串动态给出的消息:

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

另外,如果你想依赖你读取的命令和你发送给海龟的消息之间的相似性,你可以动态地组成消息字符串:

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

请注意,在您的情况下不建议这样做,因为首先,您将用户输入和您的代码结合在一起,即如果您决定将用户命令从 downmoveDown,您必须将方法名称从 down: 更改为 moveDown:。此外,这种方法允许用户将 "inject" 错误代码插入您的系统,因为他可以编写 become 42 之类的命令,这将导致代码:

`turtle perform: #become: with: 42`

这将在 turtle 对象和 42 之间交换指针。或者你可以考虑更糟糕的情况。但我希望这次元游览对您有好处。 :)

在 Smalltalk 中,您不使用 switch 语句。相反,您使用 "case methods"(我认为术语是由 Ken Beck 引入的,但我不确定)。

在你的情况下,它会是这样的:

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'.

如您所见,这更加清晰,您无需应用 "smalltalk magic" 即可获得高效的设计。这还有一个优点,即清晰、执行速度快,并且易于由编译器 JIT 优化 :)

仅举另一种可能性:不要自己编写解析器,而是使用 Smalltalk 中提供的一个 ParserGenerator(PetitParser、OMeta、Smacc、Xtreams,...)

这是 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)。

您首先在字符串中定义语法:

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

然后定义一个移动海龟的解释器:

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

使用一些方法将解释器连接到 aTurtle,并通过所谓的 pragmas(注释)将动作连接到上面的语法规则:

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

然后您只需创建一个解析器并将其连接到此解释器:

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

我让你发现如何指定空格、换行符或命令序列,但你会看到你的代码在这样的框架下是多么解耦和容易扩展:一个新命令 = 语法描述中的一行 + 中的一个方法用于连接到 Turtle 操作的解释器...