Если вы хотите использовать чисто функциональный подход, вам нужно передать переменную состояния из одного места в другое. Мы можем использовать список ассоциаций для хранения элементов, и поскольку в Hammurabi игра заканчивается за 10 шагов, мы можем легко использовать переменную состояния в качестве журнала, журнала всех событий, которые произошли в игре.
Ассоциациясписки имеют свойство, что сопоставления могут встречаться несколько раз, но возвращается только первое совпадение. Таким образом, в основном, если состояние ((population . 100) (population . 30))
, то это означает, что текущая популяция равна 100, а на предыдущем ходу было 30. Мы храним все значения в слотах, что означает, что мы можем отображать статистику по полученной игре столько, сколько мыwant.
Например, начальное состояние:
(define initial-state '((population . 100)
(acres . 1000)
(grain . 3000)
(year . 0)))
Мы можем скрыть конкретные детали реализации за вспомогательными функциями доступа:
(define (value state slot)
(cdr (assoc slot state)))
А также мыможет использовать полезный синтаксис для добавления нескольких элементов одновременно в состоянии:
(define (extend0 state key/values)
(if (null? key/values)
state
(let ((key (car key/values))
(val (cadr key/values))
(tail (cddr key/values)))
(extend0 (acons key val state) tail))))
(define (extend state . key/values)
(extend0 state key/values))
Так, например, вы можете сделать:
(extend initial-state 'grain 1000 'population 200)
$1 = ((population . 200) (grain . 1000) (population . 100) (acres . 1000) (grain . 3000) (year . 0))
Мы также можем определить средства доступа для общих слотов:
(define (getter slot)
(lambda (state)
(value state slot)))
(define (setter slot)
(lambda (state value)
(acons slot value state)))
(define population (getter 'population))
(define set-population (setter 'population))
(define acres (getter 'acres))
(define set-acres (setter 'acres))
(define grain (getter 'grain))
(define set-grain (setter 'grain))
(define price (getter 'price))
(define set-price (setter 'price))
(define year (getter 'year))
(define set-year (setter 'year))
Вы также можете сократить это с помощью макроса. Подход здесь заключается в разработке небольших вспомогательных функций, чтобы гарантировать, что реальный код, который мы пишем, настолько выразителен, насколько мы хотим.
Кроме того, тестируйте часто и изолированно, что проще сделатькогда внутреннее состояние не задействовано.
Определите также смарт-объектный принтер:
(define (echo items state)
(if (list? items)
(map (lambda (u)
(cond
((null? u) (newline))
((symbol? u) (display (value state u)))
((procedure? u) (display (u)))
(else (display u))))
items)
(begin (display items) (newline)))
state)
... и универсальное приглашение:
(define (prompt state message tester setter)
(echo message state)
(let ((value (read)))
(if (tester value)
(setter state value)
(prompt state message tester setter))))
Один раз весь словарьна месте, вот как вы могли бы написать buy-land
:
(define (buy-land state)
(let ((max-acres (floor/ (grain state) (price state))))
(if (zero? max-acres)
(echo "You cannot buy any acre." state)
(prompt state
`("Land trades at " price " bushels of grain for acre." ()
"You have " grain " bushel(s) of grain." ()
"How many acres to buy (0-" ,max-acres ")? ")
(lambda (v) (and (integer? v) (<= 0 v max-acres)))
(lambda (state buy)
(extend state
'buy buy
'acres (+ (acres state) buy)
'grain (- (grain state)
(* buy (price state)))))))))
Вы можете разделить функции на маленькие, которые делают меньше вещей, но сочиняют лучше:
(define (random-events state)
(let ((starve (random 20)))
(extend state
'starve starve
'price (+ 17 (random 10))
'population (max 0 (- (population state) starve)))))
(define (game-step state)
(if (= (year state) 10)
(end-game state)
(let ((state (set-year state (+ 1 (year state)))))
(display-new-year-text state)
(let ((state (random-events state)))
(game-step (buy-land state))))))
(define hammurabi
(game-step initial-state))