Slide 1

Slide 1 text

IMMUTABLE PROGRAMMING WRITING FUNCTIONAL PYTHON Cale Pennington @vengefulpickle github.com/cpennington

Slide 2

Slide 2 text

COMPARE Python Mutable by default (mostly) Haskell Immutable by default

Slide 3

Slide 3 text

IMMUTABILITY ALLOWS LOCAL THINKING

Slide 4

Slide 4 text

IMMUTABILITY IN PYTHON # Attribute assignment x.foo = 1 # Item assignment x["foo"] = 3 # Methods that modify state x.add("foo") # Modifying an objects own attributes self.foo = 4

Slide 5

Slide 5 text

IMMUTABILITY IN PYTHON # Name assignment x = 1 # Reading attributes x = self.foo # Read-only methods x = y.items()

Slide 6

Slide 6 text

IMMUTABILITY IN PYTHON object() {"foo": 1} {"foo"} ["foo", "bar"] (i for i in range(3)) (object(), object())

Slide 7

Slide 7 text

IMMUTABILITY IN PYTHON 1 "bar" (1, 2, 3) ((1, 2), (2, 3)) frozenset(1, 2, 3)

Slide 8

Slide 8 text

TOOLS FOR LOCAL THINKING @property tuple (and namedtuple) Commands

Slide 9

Slide 9 text

THE SETUP GAME LOOP def main(): board = Board() while not board.is_finished: print(board) move = input(f"Player {board.player.value} move (x y)? ") x, y = move.split() board.do_move(int(x), int(y)) class Board(): def __init__(self): self.board = [[Player.NA]*3]*3] def do_move(self, x, y): if self.board[x][y] == Player.NA: self.board[x][y] = self.player

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

PROPERTY @property def player(self): plays = Counter(sum(self.board, [])) if plays[Player.O] < plays[Player.X]: return Player.O else: return Player.X

Slide 12

Slide 12 text

TESTS def test_game_end(self): self.assertFalse(self.board.is_finished) self.board.do_move(0, 0) self.assertFalse(self.board.is_finished)

Slide 13

Slide 13 text

TESTS def test_game_end(self): self.assertFalse(self.board.is_finished) self.board.do_move(0, 0) self.assertFalse(self.board.is_finished) ================================================================ FAIL: test_game_end (tictactoe_v4_properties.TestTicTacToe) ---------------------------------------------------------------- Traceback (most recent call last): File ".../tictactoe_v4_properties.py", line 93, in test_game_end self.assertFalse(self.game.is_finished) AssertionError: True is not false

Slide 14

Slide 14 text

TESTS def test_moves_made(self): # Store the state of the board before a move before = {(x, y, self.board.board[x][y]) for (x, y) in ALL_MOVES} # Make a single move self.board.do_move(0, 0) # Store the state of the board after the move after = {(x, y, self.board.board[x][y]) for (x, y) in ALL_MOVES} # Compare the state before and after self.assertEqual(after - before, {(0, 0, Player.X)}) self.assertEqual(before - after, {(0, 0, Player.NA)})

Slide 15

Slide 15 text

TESTS def test_moves_made(self): # Store the state of the board before a move before = {(x, y, self.board.board[x][y]) for (x, y) in ALL_MOVES} # Make a single move self.board.do_move(0, 0) # Store the state of the board after the move after = {(x, y, self.board.board[x][y]) for (x, y) in ALL_MOVES} # Compare the state before and after self.assertEqual(after - before, {(0, 0, Player.X)}) self.assertEqual(before - after, {(0, 0, Player.NA)})

Slide 16

Slide 16 text

TESTS def test_moves_made(self): # Store the state of the board before a move before = {(x, y, self.board.board[x][y]) for (x, y) in ALL_MOVES} # Make a single move self.board.do_move(0, 0) # Store the state of the board after the move after = {(x, y, self.board.board[x][y]) for (x, y) in ALL_MOVES} # Compare the state before and after self.assertEqual(after - before, {(0, 0, Player.X)}) self.assertEqual(before - after, {(0, 0, Player.NA)})

Slide 17

Slide 17 text

TESTS ============================================================= FAIL: test_moves_made (tictactoe_v4_properties.TestTicTacToe) ------------------------------------------------------------- Traceback (most recent call last): File ".../tictactoe_v4_properties.py", line 116, in test_moves_made self.assertEqual(after - before, {(0, 0, Player.X)}) AssertionError: Items in the first set but not the second: (1, 0, ) (2, 0, )

Slide 18

Slide 18 text

TESTS class Board(): def __init__(self): self.board = [[Player.NA]*3]*3] def __init__(self): self.board = [[Player.NA]*3 for _ in range(3)] Spooky action at a distance

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

TESTS class Board(): def __init__(self): self.board = [[Player.NA]*3]*3] def __init__(self): self.board = [[Player.NA]*3 for _ in range(3)] Saaad...

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

IMMUTABLE STORAGE class Board(): def __init__(self): self.board = ((Player.NA, )*3, )*3

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

TESTS def test_moves_made(self): # Store the state of the board before a move before = {(x, y, self.board.board[x][y]) for (x, y) in ALL_MOVES} # Make a single move self.board.do_move(0, 0) # Store the state of the board after the move after = {(x, y, self.board.board[x][y]) for (x, y) in ALL_MOVES} # Compare the state before and after self.assertEqual(after - before, {(0, 0, Player.X)}) self.assertEqual(before - after, {(0, 0, Player.NA)})

Slide 25

Slide 25 text

TESTS def test_moves_made(self): # Store the state of the board before a move before = BoardState() # Store the state of the board after the move after = before.do_move(0, 0) # Compare the state before and after self.assertEqual(after - before, {(0, 0, Player.X)}) self.assertEqual(before - after, {(0, 0, Player.NA)})

Slide 26

Slide 26 text

STORAGE class Board(): def __init__(self): self.board = ((Player.NA, )*3, )*3

Slide 27

Slide 27 text

STORAGE class BoardState(namedtuple('_BoardState', ['board'])): ... BoardState.__new__.__defaults__ = (((Player.NA, )*3, )*3, )

Slide 28

Slide 28 text

NAMEDTUPLE from collections import namedtuple Widgit = namedtuple('Widgit', ['height', 'weight']) x = Widgit(10, 20) x.height # 10 x.weight # 20 list(x) # [10, 20]

Slide 29

Slide 29 text

STORAGE class BoardState(namedtuple('_BoardState', ['board'])): ... BoardState.__new__.__defaults__ = (((Player.NA, )*3, )*3, )

Slide 30

Slide 30 text

ACTION def do_move(self, x, y): if self.board[x][y] == Player.NA: self.board[x][y] = self.player

Slide 31

Slide 31 text

ACTION def do_move(self, x, y): if self.board[x][y] == Player.NA: new_row = replace(self.board[x], y, self.player) new_board = replace(self.board, x, new_row) return BoardState(new_board) else: return self def replace(tpl, idx, value): return tpl[:idx] + (value, ) + tpl[idx+1:]

Slide 32

Slide 32 text

COMMANDS PLAYER def main(): states = [BoardState()] while not states[-1].is_finished: print(states[-1]) move = input(f"Player {states[-1].player.value}: " "x y to move, u to undo, " "gN to revert to move N)? ") if move == 'u': states.pop() elif move.startswith('g'): states = states[:int(move.replace('g','')) + 1] else: try: x, y = move.split() states.append(states[-1].do_move(int(x), int(y))) except: print("Invalid move")

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

PLAYER def move_human(state): while True: print(state) move = input(f"Player {state.player.value}: " "x y to move, u to undo, " "gN to revert to move N)? ") if move.startswith('u'): return Undo(1) elif move.startswith('g'): return RevertTo(int(move.replace('g', '')) + 1) else: try: x, y = move.split() return Move(int(x), int(y)) except: print("Invalid move")

Slide 35

Slide 35 text

class Undo(namedtuple('_Undo', ['count'])): def apply(self, board_states): return board_states[:-self.count] class Move(namedtuple('_Move', ['x', 'y'])): def apply(self, board_states): if board_states[-1].board[self.x][self.y] == Player.NA: return board_states + ( board_states[-1].do_move(self.x, self.y), ) else: return board_states class RevertTo(namedtuple('_RevertTo', ['idx'])): def apply(self, board_states): return board_states[:self.idx]

Slide 36

Slide 36 text

class Undo(namedtuple('_Undo', ['count'])): def apply(self, board_states): return board_states[:-self.count] class Move(namedtuple('_Move', ['x', 'y'])): def apply(self, board_states): if board_states[-1].board[self.x][self.y] == Player.NA: return board_states + ( board_states[-1].do_move(self.x, self.y), ) else: return board_states class RevertTo(namedtuple('_RevertTo', ['idx'])): def apply(self, board_states): return board_states[:self.idx]

Slide 37

Slide 37 text

class Undo(namedtuple('_Undo', ['count'])): def apply(self, board_states): return board_states[:-self.count] class Move(namedtuple('_Move', ['x', 'y'])): def apply(self, board_states): if board_states[-1].board[self.x][self.y] == Player.NA: return board_states + ( board_states[-1].do_move(self.x, self.y), ) else: return board_states class RevertTo(namedtuple('_RevertTo', ['idx'])): def apply(self, board_states): return board_states[:self.idx]

Slide 38

Slide 38 text

TESTS def test_revert(self): self.assertEqual( RevertTo(2).apply(self.states), self.states[:2] ) def test_inverse(self): start = (BoardState(), ) for x in range(3): for y in range(3): self.assertEqual( Undo(1).apply(Move(x, y).apply(start)), start )

Slide 39

Slide 39 text

LOOP player_types = [move_human, move_random] players = { Player.X: player_types[x_choice], Player.O: player_types[y_choice], } states = (BoardState(), ) while not states[-1].is_finished: move = players[states[-1].player](states[-1]) states = move.apply(states)

Slide 40

Slide 40 text

RANDOM def move_random(state): x = randrange(3) y = randrange(3) return Move(x, y)

Slide 41

Slide 41 text

ITERATION SEARCH def depth_first(board=None): if board is None: board = Board() yield board for x, y in ALL_MOVES: if board.board[x][y] != Player.NA: board.do_move(x, y) try: yield from depth_first(board) finally: board.board[x][y] = Player.NA Saaad...

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

SEARCH def depth_first(board=None): if board is None: board = Board() yield board for x, y in ALL_MOVES: if board.board[x][y] != Player.NA: old_board = [list(board.board[x]) for x in range(3)] board.do_move(x, y) try: yield from depth_first(board) finally: board.board = old_board

Slide 44

Slide 44 text

SEARCH def depth_first(board=None): if board is None: board = Board() yield board for x, y in ALL_MOVES: if board.board[x][y] != Player.NA: old_board = [list(board.board[x]) for x in range(3)] board.do_move(x, y) try: yield from depth_first(board) finally: board.board = old_board Saaad...

Slide 45

Slide 45 text

No content

Slide 46

Slide 46 text

SEARCH def depth_first(state=None): if state is None: state = BoardState() yield state for x, y in ALL_MOVES: next_state = state.do_move(x, y) if state != next_state: yield from depth_first(next_state)

Slide 47

Slide 47 text

FILTER def depth_first_filter(filter_fn, state=None): if state is None: state = BoardState() yield state next_states = (state.do_move(x, y) for x, y in ALL_MOVES) next_states = (next_state for next_state in next_states if state != next_state) next_states = filter_fn(state, next_states) for next_state in next_states: yield from depth_first_filter(filter_fn, next_state)

Slide 48

Slide 48 text

FILTER FUNCTION def filter_finished(state, next_states): if state.winner is not None: return else: yield from next_states

Slide 49

Slide 49 text

MAIN winning_states = { Player.X: [], Player.O: [], Player.NA: [], } for state in depth_first_filter(filter_finished): if state.winner is not None: winning_states[state.winner].append(state) print("O wins", len(winning_states[Player.O])) print("X wins", len(winning_states[Player.X])) print("Tie", len(winning_states[Player.NA]))

Slide 50

Slide 50 text

RESULTS > python tictactoe_v8_all_games.py O wins 77904 X wins 131184 Tie 46080

Slide 51

Slide 51 text

IMMUTABILITY ALLOWS LOCAL THINKING

Slide 52

Slide 52 text

TOOLS FOR LOCAL THINKING @property tuple (and namedtuple) Commands

Slide 53

Slide 53 text

QUESTIONS?

Slide 54

Slide 54 text

REFERENCES Talk: bit.ly/immutable-python-pres Source Code: bit.ly/immutable-python-src