|
| 1 | +--- |
| 2 | +layout: recipe |
| 3 | +title: Interpreter Pattern |
| 4 | +chapter: Design Patterns |
| 5 | +--- |
| 6 | + |
| 7 | +h2. Problem |
| 8 | + |
| 9 | +Someone else needs to run parts of your code in a controlled fashion. Alternately, your language of choice cannot express the problem domain in a concise fashion. |
| 10 | + |
| 11 | +h2. Solution |
| 12 | + |
| 13 | +Use the Interpreter pattern to create a domain-specific language that you translate into specific code. |
| 14 | + |
| 15 | +Assume, for example, that the user wants to perform math inside of your application. You could let them forward code to _eval_ but that would let them run arbitrary code. Instead, you can provide a miniature "stack calculator" language that you parse separately in order to only run mathematical operations while reporting more useful error messages. |
| 16 | + |
| 17 | +{% highlight coffeescript %} |
| 18 | +class StackCalculator |
| 19 | + parseString: (string) -> |
| 20 | + @stack = [ ] |
| 21 | + for token in string.split /\s+/ |
| 22 | + @parseToken token |
| 23 | + |
| 24 | + if @stack.length > 1 |
| 25 | + throw "Not enough operators: numbers left over" |
| 26 | + else |
| 27 | + @stack[0] |
| 28 | + |
| 29 | + parseToken: (token, lastNumber) -> |
| 30 | + if isNaN parseFloat(token) # Assume that anything other than a number is an operator |
| 31 | + @parseOperator token |
| 32 | + else |
| 33 | + @stack.push parseFloat(token) |
| 34 | + |
| 35 | + parseOperator: (operator) -> |
| 36 | + if @stack.length < 2 |
| 37 | + throw "Can't operate on a stack without at least 2 items" |
| 38 | + |
| 39 | + right = @stack.pop() |
| 40 | + left = @stack.pop() |
| 41 | + |
| 42 | + result = switch operator |
| 43 | + when "+" then left + right |
| 44 | + when "-" then left - right |
| 45 | + when "*" then left * right |
| 46 | + when "/" |
| 47 | + if right is 0 |
| 48 | + throw "Can't divide by 0" |
| 49 | + else |
| 50 | + left / right |
| 51 | + else |
| 52 | + throw "Unrecognized operator: #{operator}" |
| 53 | + |
| 54 | + @stack.push result |
| 55 | + |
| 56 | +calc = new StackCalculator |
| 57 | + |
| 58 | +calc.parseString "5 5 +" # => { result: 10 } |
| 59 | + |
| 60 | +calc.parseString "4.0 5.5 +" # => { result: 9.5 } |
| 61 | + |
| 62 | +calc.parseString "5 5 + 5 5 + *" # => { result: 100 } |
| 63 | + |
| 64 | +try |
| 65 | + calc.parseString "5 0 /" |
| 66 | +catch error |
| 67 | + error # => "Can't divide by 0" |
| 68 | + |
| 69 | +try |
| 70 | + calc.parseString "5 -" |
| 71 | +catch error |
| 72 | + error # => "Can't operate on a stack without at least 2 items" |
| 73 | + |
| 74 | +try |
| 75 | + calc.parseString "5 5 5 -" |
| 76 | +catch error |
| 77 | + error # => "Not enough operators: numbers left over" |
| 78 | + |
| 79 | +try |
| 80 | + calc.parseString "5 5 5 foo" |
| 81 | +catch error |
| 82 | + error # => "Unrecognized operator: foo" |
| 83 | +{% endhighlight %} |
| 84 | + |
| 85 | +h2. Discussion |
| 86 | + |
| 87 | +As an alternative to writing our own interpreter, you can co-op the existing CoffeeScript interpreter in a such a way that its normal syntax makes for more natural (and therefore more comprehensible) expressions of your algorithm. |
| 88 | + |
| 89 | +{% highlight coffeescript %} |
| 90 | +class Sandwich |
| 91 | + constructor: (@customer, @bread='white', @toppings=[], @toasted=false)-> |
| 92 | + |
| 93 | +white = (sw) -> |
| 94 | + sw.bread = 'white' |
| 95 | + sw |
| 96 | + |
| 97 | +wheat = (sw) -> |
| 98 | + sw.bread = 'wheat' |
| 99 | + sw |
| 100 | + |
| 101 | +turkey = (sw) -> |
| 102 | + sw.toppings.push 'turkey' |
| 103 | + sw |
| 104 | + |
| 105 | +ham = (sw) -> |
| 106 | + sw.toppings.push 'ham' |
| 107 | + sw |
| 108 | + |
| 109 | +swiss = (sw) -> |
| 110 | + sw.toppings.push 'swiss' |
| 111 | + sw |
| 112 | + |
| 113 | +mayo = (sw) -> |
| 114 | + sw.toppings.push 'mayo' |
| 115 | + sw |
| 116 | + |
| 117 | +toasted = (sw) -> |
| 118 | + sw.toasted = true |
| 119 | + sw |
| 120 | + |
| 121 | +sandwich = (customer) -> |
| 122 | + new Sandwich customer |
| 123 | + |
| 124 | +to = (customer) -> |
| 125 | + customer |
| 126 | + |
| 127 | +send = (sw) -> |
| 128 | + toastedState = sw.toasted and 'a toasted' or 'an untoasted' |
| 129 | + |
| 130 | + toppingState = '' |
| 131 | + if sw.toppings.length > 0 |
| 132 | + if sw.toppings.length > 1 |
| 133 | + toppingState = " with #{sw.toppings[0..sw.toppings.length-2].join ', '} and #{sw.toppings[sw.toppings.length-1]}" |
| 134 | + else |
| 135 | + toppingState = " with #{sw.toppings[0]}" |
| 136 | + "#{sw.customer} requested #{toastedState}, #{sw.bread} bread sandwich#{toppingState}" |
| 137 | + |
| 138 | +send sandwich to 'Charlie' # => "Charlie requested an untoasted, white bread sandwich" |
| 139 | +send turkey sandwich to 'Judy' # => "Judy requested an untoasted, white bread sandwich with turkey" |
| 140 | +send toasted ham turkey sandwich to 'Rachel' # => "Rachel requested a toasted, white bread sandwich with turkey and ham" |
| 141 | +send toasted turkey ham swiss sandwich to 'Matt' # => "Matt requested a toasted, white bread sandwich with swiss, ham and turkey" |
| 142 | +{% endhighlight %} |
| 143 | + |
| 144 | +This example allows for layers of functions by how it returns the modified object so that outer functions can modify it in turn. By borrowing a very and the particle _to_, the example lends natural grammar to the construction and ends up reading like an actual sentence when used correctly. This way, both your CoffeeScript skills and your existing language skills can help catch code problems. |
0 commit comments