Разработка кода для программы в Схеме, связанной с пользовательским вводом - PullRequest
1 голос
/ 03 октября 2019

Я пытаюсь написать небольшую программу, древнюю игру Хамураби на схеме (хитрость, если быть точным). Я хочу узнать о предпочтительном подходе к «дизайну» такой программы, активно занимаясь пользовательским вводом. Например, используя циклы, изменяемые или неизменяемые переменные и т. Д.

У меня есть несколько рабочих вариантов, которые мне не нравятся. Я считаю, что мне не хватает лучшего подхода. Ниже приведены подробности. Извините за длинные объяснения.

Сама игра представляет собой простой симулятор "экономики" - у нас есть 3 значения, для населения нашего королевства, площади земли и количества зерна (также служащего валютой),Игрок определяет правила в течение нескольких лет, выбирая каждый год последовательно:

  • сколько земли купить или продать за зерно
  • , а затем сколько зерна использовать для кормления людей
  • наконец, сколько зерна использовать для посева

Итак, у нас есть внешний цикл с итерациями, представляющими годы. Внутри у нас есть три шага. Сначала изменяется количество площади и зерна. Вторая меняет количество зерна и населения. Третье - это изменение количества зерна (по отношению к имеющейся земле и людям, занимающимся полями). Четвертый шаг (без ввода данных пользователем) определяет, сколько новых культур мы собрали и что съели крысы (то есть увеличилось количество зерна).

Это легко сделать с помощью глобальных переменных и форм (set! ...). Однако мне интересно найти способ кодировать это в более «функциональном стиле». Кажется, тогда мне нужно использовать несколько взаимно рекурсивных (оптимизированных хвостом) функций для представления шагов. И передайте измененные значения в качестве параметров каждый раз. Вот суть этого подхода, реализованного только с шагом покупки / продажи земли . И это работает так:

You have 100 people, 700 acres of land and 9600 bushels of grain.
Land trades at 24 bushels of grain for acre
How many acres to buy? -100
You have 100 people, 600 acres of land and 12000 bushels of grain.
Land trades at 21 bushels of grain for acre
How many acres to buy? 200

Это не очень удобно, так как будет много маленьких функций, и большинству из них нужны все переменные, даже если некоторые из них пропущены. И кроме pop, area и grain нам нужны некоторые аккумуляторы (например, общее количество людей, умерших от голода).

Поэтому я создал две функции для поддержания неизменной структуры ключ-значение, например

(list (cons 'pop 100) (cons 'area 1000) (cons 'grain 2800))

И использовать их как state, передаваемый каждой функции. prop-get извлекает значение по ключу из состояния, в то время как prop-set возвращает измененную копию (я подозреваю, что в библиотеке уже реализована похожая структура).

(load "props.scm")

(define (one-year state)
    (map display
        (list "You have "
            (prop-get state 'pop) " people, "
            (prop-get state 'area) " acres of land and "
            (prop-get state 'grain) " bushels of grain."))
    (newline)
    (let ((state-upd (buy-land state)))
        (step-2 state-upd)))

(define (buy-land state)
    (let ((price (+ (random 10) 17))
            (area (prop-get state 'area))
            (grain (prop-get state 'grain)))
        (map display
            (list "Land trades at " price " bushels of grain for acre"))
        (newline)
        (display "How many acres to buy? ")
        (let ((b (read)))
            (prop-set (prop-set state 'area (+ area b)) 'grain (- grain (* price b))))))

Пожалуйста, вот полный код в другой сущности .

Это несколько лучше, но все же полный код немного многословен со всеми этими проп-получателями, позволяет и взаимная рекурсия.

Какие еще варианты могут быть здесь? Я думаю, что существует «промежуточное» решение между изменяемыми глобальными переменными и неизменяемыми с хвостовой рекурсией - например, использование именованного let для внешнего цикла и некоторой изменяемой структуры для хранения состояния в локальной переменной. Но я чувствую, что могу упустить что-то более простое и элегантное.

1 Ответ

2 голосов
/ 05 октября 2019

Если вы хотите использовать чисто функциональный подход, вам нужно передать переменную состояния из одного места в другое. Мы можем использовать список ассоциаций для хранения элементов, и поскольку в 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))
...