Interpreter (Interpreter Pattern)

Od wzorca Komenda jest już blisko do Interpretera.

Note

Interpreter wykonuje polecenia, działając na globalnym kontekście (danych)

Wspomniany powyżej kontekst to zmienne, do których mogą odwoływać się polecenia. Wydzielmy zatem klasę Turtle, która będzie owym kontekstem - obiektem na który wpływać będą komendy:

 1from math import sin, cos, pi
 2
 3
 4class Turtle:
 5    def __init__(self):
 6        """Default turtle constructor"""
 7        self.x, self.y, self.a, self.pen, self.speed = 0, 0, 0, True, 1
 8
 9    def __str__(self): return "%d %d" % (self.x, self.y)
10

Żółw zna swoje położenie, umie też podać swoją reprezentację jako napis. Teraz powinniśmy zmodyfikować komendy (czyli klasy R, L itp tak, aby metoda __call__() przyjmowała żółwia (obiekt klasy Turtle) jako argument. Zmiany te trzeba oczywiście wprowadzić najpierw do klasy bazowej:

 1class TurtleCommand:
 2    """Base class for a turtle command"""
 3
 4    def __str__(self):
 5        raise NotImplemented
 6
 7    def __call__(self, turtle, drawing):
 8        raise NotImplemented
 9
10
11class R(TurtleCommand):
12
13    def __init__(self, angle):
14        """Turn right by a given angle"""
15        self.angle = angle
16
17    def __call__(self, turtle, drawing):
18        turtle.a -= self.angle
19
20    def __str__(self):
21        return "R " + str(self.angle)
22
23
24class L(TurtleCommand):
25
26    def __init__(self, angle):
27        """Turn left by a given angle"""

Analogiczne zmiany wprowadzono do klas F i B. Ponieważ komendy są obiektami, możemy przechowywać je w liście, a nawet zdediniować komendę, która zawiera w sobie inne komendy (klasa Procedure):

 1class F(TurtleCommand):
 2
 3    def __init__(self, n_steps=1):
 4        """Go forward command"""
 5        self.n_steps = n_steps
 6
 7    def __call__(self, turtle, drawing):
 8        turtle.y += self.n_steps * sin(turtle.a / 180.0 * pi) * turtle.speed
 9        turtle.x += self.n_steps * cos(turtle.a / 180.0 * pi) * turtle.speed
10        if turtle.pen:
11            drawing.line_to(turtle.x, turtle.y)
12        else:
13            drawing.move_to(turtle.x, turtle.y)
14
15    def __str__(self):
16        return "F " + str(self.n_steps) + "" if self.n_steps != 1 else "F"
17
18
19class J(TurtleCommand):
20    def __init__(self, params):

Samo wykorzystanie interpretera przebiega podobnie, jak poprzednio (linie 3-7). Dodatkowo w linii 1 definiowana jest procedura rysująca kwadrat. Odpowiednie komendy są wstawiane do tej procedury w linii 2.

1    def __call__(self, turtle, drawing):
2        turtle.x, turtle.y = self.xy[0], self.xy[1]
3        drawing.move_to(turtle.x, turtle.y)
4
5    def __str__(self):
6        return "J " + str(self.xy[0]) + " " + str(self.xy[1])
7

Dodatkowo dla każdej komendy zdefiniowano metodę __str__(), co umożliwia np. nagranie programu na dysk lub wypisanie na ekranie. Kompletny program znajduje się poniżej:

(rozwiń kod programu TurtleCommands.py)/summary>
  1from math import sin, cos, pi
  2
  3
  4class Turtle:
  5    def __init__(self):
  6        """Default turtle constructor"""
  7        self.x, self.y, self.a, self.pen, self.speed = 0, 0, 0, True, 1
  8
  9    def __str__(self): return "%d %d" % (self.x, self.y)
 10
 11
 12class TurtleCommand:
 13    """Base class for a turtle command"""
 14
 15    def __str__(self):
 16        raise NotImplemented
 17
 18    def __call__(self, turtle, drawing):
 19        raise NotImplemented
 20
 21
 22class R(TurtleCommand):
 23
 24    def __init__(self, angle):
 25        """Turn right by a given angle"""
 26        self.angle = angle
 27
 28    def __call__(self, turtle, drawing):
 29        turtle.a -= self.angle
 30
 31    def __str__(self):
 32        return "R " + str(self.angle)
 33
 34
 35class L(TurtleCommand):
 36
 37    def __init__(self, angle):
 38        """Turn left by a given angle"""
 39        self.angle = angle
 40
 41    def __call__(self, turtle, drawing):
 42        turtle.a += self.angle
 43
 44    def __str__(self):
 45        return "L " + str(self.angle)
 46
 47
 48class A(TurtleCommand):
 49
 50    def __init__(self, angle):
 51        """Set a given angle (in degrees)"""
 52        self.angle = angle
 53
 54    def __call__(self, turtle, drawing):
 55        turtle.a = self.angle
 56
 57    def __str__(self):
 58        return "A " + str(self.angle)
 59
 60class U(TurtleCommand):
 61
 62    def __call__(self, turtle, drawing):
 63        turtle.pen = False
 64
 65    def __str__(self):
 66        return "U"
 67
 68
 69class D(TurtleCommand):
 70
 71    def __call__(self, turtle, drawing):
 72        turtle.pen = True
 73
 74    def __str__(self):
 75        return "D"
 76
 77
 78class F(TurtleCommand):
 79
 80    def __init__(self, n_steps=1):
 81        """Go forward command"""
 82        self.n_steps = n_steps
 83
 84    def __call__(self, turtle, drawing):
 85        turtle.y += self.n_steps * sin(turtle.a / 180.0 * pi) * turtle.speed
 86        turtle.x += self.n_steps * cos(turtle.a / 180.0 * pi) * turtle.speed
 87        if turtle.pen:
 88            drawing.line_to(turtle.x, turtle.y)
 89        else:
 90            drawing.move_to(turtle.x, turtle.y)
 91
 92    def __str__(self):
 93        return "F " + str(self.n_steps) + "" if self.n_steps != 1 else "F"
 94
 95
 96class J(TurtleCommand):
 97    def __init__(self, params):
 98        """Turtle jumps to an arbitrary position given in absolute coordinates"""
 99        self.xy = [float(token) for token in params.split()]
100
101    def __call__(self, turtle, drawing):
102        turtle.x, turtle.y = self.xy[0], self.xy[1]
103        drawing.move_to(turtle.x, turtle.y)
104
105    def __str__(self):
106        return "J " + str(self.xy[0]) + " " + str(self.xy[1])
107
108
109class Faster(TurtleCommand):
110
111    def __init__(self, f):
112        """Increase turtle speed by a given value"""
113        self.f = f
114
115    def __call__(self, turtle, drawing):
116        turtle.speed += self.f
117
118    def __str__(self):
119        return "FASTER " + str(self.f)
120
121
122class Slower(TurtleCommand):
123
124    def __init__(self, f):
125        """Decrease turtle speed by a given value"""
126        self.f = f
127
128    def __call__(self, turtle, drawing):
129        turtle.speed += self.f
130
131    def __str__(self):
132        return "SLOWER " + str(self.f)
133
134
135class Procedure(TurtleCommand):
136
137    def __init__(self, name):
138        """Procedure is a group of commands"""
139        self.__commands = []
140        self.__name = name
141
142    def do_next(self, next_command):
143        self.__commands.append(next_command)
144        return self
145
146    def __call__(self, turtle, drawing):
147        for cmd in self.__commands:
148            cmd(turtle, drawing)
149
150    def __str__(self):
151        out = self.__name+" := ["
152        for cmd in self.__commands:
153            out += str(cmd)+" "
154        return out + "]"
155
156
157class Repeat(TurtleCommand):
158
159    def __init__(self, n, cmd):
160        """Repeats a command multiple times"""
161        self.__command = cmd
162        self.__n = n
163
164    @property
165    def n(self):
166        return self.__n
167
168    def __call__(self, turtle, drawing):
169        for i in range(self.__n):
170            self.__command(turtle, drawing)
171
172    def __str__(self):
173        return "REPEAT " + str(self.__n) + " " + str(self.__command)
174
175
176class GraphicDevice:
177    def line_to(self, x, y):
178        raise NotImplemented
179
180    def move_to(self, x, y):
181        raise NotImplemented
182
183
184class EchoDevice(GraphicDevice):
185    def line_to(self, x, y):
186        print("line to %.1f %.1f" %( x, y))
187
188    def move_to(self, x, y):
189        print("move to %.1f %.1f" %( x, y))
190
191
192class VisualifeBridge(GraphicDevice):
193    def __init__(self, viewport):
194        self.__viewport = viewport
195        self.__x, self.__y = 0, 0
196        self.__n_lines = 0
197
198    def line_to(self, x, y):
199        self.__viewport.line("line-%d" % self.__n_lines, self.__x, self.__y, x, y)
200        self.__n_lines += 1
201        self.__x, self.__y = x, y
202
203    def move_to(self, x, y):
204        self.__x, self.__y = x, y
205
206
207class Logo:
208
209    def __init__(self, device=EchoDevice()):
210        self.__makers = {}
211        self.__turtle = Turtle()
212        self.__device = device
213        self.add_command("R", lambda angle: R(float(angle)))
214        self.add_command("L", lambda angle: L(float(angle)))
215        self.add_command("A", lambda angle: A(float(angle)))
216        self.add_command("F", lambda steps: F(float(steps)))
217        self.add_command("FASTER", lambda value: Faster(float(value)))
218        self.add_command("SLOWER", lambda value: Slower(float(value)))
219        self.add_command("U", U())
220        self.add_command("D", D())
221        self.add_command("J", lambda value: J(value))
222        self.add_command("JUMP", lambda value: J(value))
223        self.add_command("DEF", self.make_procedure)
224        self.add_command("REPEAT", self.make_repeat)
225        self.__loop_cnt = 0
226
227    @property
228    def turtle(self): return self.__turtle
229
230    def draw(self, *cmds):
231        for ci in cmds:
232            if isinstance(ci, str):                     # ---------- If it's a string, use factory to produce a command
233                cmd = self.produce_command(ci)
234                if not ci.startswith("DEF") and cmd:    # ---------- Execute, if it's not a definition
235                    cmd(self.__turtle, self.__device)
236            elif ci:                                    # ---------- Execute a command
237                if ci: ci(self.__turtle, self.__device)
238
239    def add_command(self, name, cmd_maker):
240        self.__makers[name] = cmd_maker
241
242    def make_procedure(self, procedure_string):
243        name = procedure_string.split(maxsplit=1)[0]
244        p = Procedure(name)
245        cmnds = procedure_string[procedure_string.find('[')+1:procedure_string.find(']')].split(";")
246        for cmd in cmnds:
247            p.do_next(self.produce_command(cmd))
248
249        self.__makers[name] = p             # ---------- here we register the new procedure in this factory
250        return p
251
252    def make_repeat(self, repeat_string):
253        tokens = repeat_string.split(maxsplit=1)
254        if tokens[1].find('[') >= 0:
255            tokens[1] = "DEF loop_" + str(self.__loop_cnt) + " := " + tokens[1]
256            self.__loop_cnt += 1
257        return Repeat(int(tokens[0]), self.produce_command(tokens[1]))
258
259    def produce_command(self, a_string):
260        if a_string.strip() == "": return None
261        tokens = a_string.split(maxsplit=1)
262        if isinstance(self.__makers[tokens[0]], TurtleCommand):
263            return self.__makers[tokens[0].strip()]
264        else:
265            return self.__makers[tokens[0].strip()](*tokens[1:])
266
267squares = """
268JUMP 3 3
269REPEAT 4 [F 194; R -90]
270JUMP 80 80
271DEF one_side := [F 10; R 90]
272DEF square := [REPEAT 4 one_side]
273REPEAT 36 [R 10; square]
274A 80
275F 100
276A -75
277F 110
278SLOWER 0.1
279REPEAT 36 [R 10; square]
280JUMP 10 180
281FASTER 0.2
282DEF diamond := [F 3; L 20; F 3; L 160; F 3; L 20; F 3]
283REPEAT 20 [A -130; diamond; A -80; diamond; A 0; F 7]
284"""
285
286if __name__ == "__main__":
287    square = Procedure("square")
288    square.do_next(F(10)).do_next(R(90)).do_next(F(10)).do_next(R(90)).do_next(F(10)).do_next(R(90)).do_next(F(10)).do_next(R(90))
289    path = [R(90), F(10), L(0), F(20), square]
290
291    import sys
292    sys.path.append("/Users/dgront/src.git/visualife/")
293    from visualife.core import SvgViewport
294
295    v = SvgViewport("logo.svg", 0, 0, 200, 200)
296    v.style = "stroke:black; stroke-width: 0.25;"
297    logo = Logo(VisualifeBridge(v))
298    commands = squares.splitlines()
299    logo.draw(*commands)
300    v.close()