Практическая схема программирования
Прошло несколько месяцев с тех пор, как я коснулся Scheme и решил внедрить разделитель дохода командной строки с помощью Scheme.
Моя первоначальная реализация использовала простую рекурсию над продолжением, но я подумал, что продолжение будет более подходящим для этого типа программ. Я был бы признателен, если бы кто-нибудь (более опытный в Схеме, чем я) мог взглянуть на это и предложить улучшения. Я, что несколько (display...
Линии - это идеальная возможность использовать макрос (я еще не разбирался в макросах).
(define (ab-income)
(call/cc
(lambda (cc)
(let
((out (display "Income: "))
(income (string->number (read-line))))
(cond
((<= income 600)
(display (format "Please enter an amount greater than $600.00~n~n"))
(cc (ab-income)))
(else
(let
((bills (* (/ 30 100) income))
(taxes (* (/ 20 100) income))
(savings (* (/ 10 100) income))
(checking (* (/ 40 100) income)))
(display (format "~nDeduct for bills:---------------------- $~a~n" (real->decimal-string bills 2)))
(display (format "Deduct for taxes:---------------------- $~a~n" (real->decimal-string taxes 2)))
(display (format "Deduct for savings:-------------------- $~a~n" (real->decimal-string savings 2)))
(display (format "Remainder for checking:---------------- $~a~n" (real->decimal-string checking 2))))))))))
Вызов (ab-income)
запрашивает ввод, и если что-то ниже 600 предоставляется, это (из моего понимания) возвращает (ab-income)
на current-continuation
, Моя первая реализация (как я уже говорил ранее) использовала рекурсию в стиле простой Джейн. Это было совсем не плохо, но я решил, что каждый ответный звонок (ab-income)
если значение было меньше 600, продолжал расширять функцию.
(Пожалуйста, поправьте меня, если это опасение неверно!)
1 ответ
Прежде всего, вам не нужно продолжение. Согласно стандарту, Схема всегда будет выполнять оптимизацию оконечного вызова. Конечный вызов - это вызов функции, который находится в конечной позиции в функции; после того, как этот вызов выполнен, больше ничего не произойдет. В этой ситуации нам не нужно сохранять запись активации, в которой мы сейчас находимся; как только функция, которую мы вызываем, вернется, мы ее просто вытолкнем. Следовательно, хвостовой вызов повторно использует текущую запись активации. В качестве примера рассмотрим это:
(define (some-function x y)
(preprocess x)
(combine (modified x) y))
(some-function alpha beta)
Когда мы звоним some-function
мы выделяем место для его записи активации в стеке: локальные переменные, параметры и т. д. Затем мы вызываем (preprocess x)
, Так как нам нужно вернуться к some-function
и продолжать обработку, мы должны сохранить some-function
запись активации, и поэтому мы выдвигаем новую запись активации для preprocess
, Как только это вернется, мы поп preprocess
Стоп кадр и продолжай. Далее нам нужно оценить modified
; то же самое должно произойти, и когда modified
возвращается, его результат передается combine
, Казалось бы, нам нужно создать новую запись активации, запустить combine
, а затем верните это some-function
-но some-function
не нужно ничего делать с этим результатом, но вернуть его! Таким образом, мы перезаписываем текущую запись активации, но оставляем адрес возврата в покое; когда combine
возвращает, тогда он будет возвращать свое значение точно к тому, что его ожидало. Вот, (combine (modified x) y)
является хвостовым вызовом, и для его оценки не требуется дополнительная запись активации.
Вот как вы можете реализовать циклы в Scheme, например:
(define (my-while cond body)
(when (cond)
(body)
(my-while cond body)))
(let ((i 0))
(my-while (lambda () (< i 10))
(lambda () (display i) (newline) (set! i (+ i 1)))))
Без оптимизации хвостовых вызовов это было бы неэффективно и потенциально могло бы привести к переполнению с помощью продолжительного цикла, создающего множество вызовов для my-while
, Однако, благодаря оптимизации хвостового вызова, рекурсивный вызов my-while cond body
это прыжок, и не выделяет памяти, что делает его столь же эффективным, как итерация.
Во-вторых, здесь вам не нужны макросы. Хотя вы можете абстрагироваться от display
блок, вы можете сделать это с простой функцией. Макросы позволяют вам на некотором уровне изменять синтаксис языка - добавьте свой собственный вид define
реализовать некоторую конструкцию case-case, которая не оценивает все ее ветви и т. д. Конечно, это все еще s-выражения, но семантика больше не просто "оценивает аргументы и вызывает функцию". Здесь, однако, семантика вызова функций - это все, что вам нужно.
С учетом сказанного, я думаю, что именно так я бы реализовал ваш код:
(require (lib "string.ss"))
(define (print-report width . nvs)
(if (null? nvs)
(void)
(let ((name (car nvs))
(value (cadr nvs)))
(display (format "~a:~a $~a~n"
name
(make-string (- width (string-length name) 2) #\-)
(real->decimal-string value 2)))
(apply print-report width (cddr nvs)))))
(define (ab-income)
(display "Income: ")
(let ((income (string->number (read-line))))
(if (or (not income) (<= income 600))
(begin (display "Please enter an amount greater than $600.00\n\n")
(ab-income))
(begin (newline)
(print-report 40 "Deduct for bills" (* 3/10 income)
"Deduct for taxes" (* 2/10 income)
"Deduct for savings" (* 1/10 income)
"Remainder for checking" (* 4/10 income))))))
Во-первых, по крайней мере, в моей версии mzscheme
Мне нужна была (require (lib "string.ss"))
строка для импорта real->decimal-string
, Далее я абстрагировал display
заблокировать вы говорили Мы видим, что каждая строка хочет напечатать деньги в том же формате в 40-м столбце, напечатав имя тега и строку тире перед ним. Следовательно, я написал print-report
, Первый аргумент - начальная ширина; в этом случае, 40
, Остальные аргументы являются парами поле-значение. Длина каждого поля (плюс два для двоеточия и пробела) вычитается из ширины, и мы генерируем строку, состоящую из такого количества тире. Мы используем format
расставить поля в правильном порядке, и display
распечатать строку. Функция рекурсивно обрабатывает все пары (используя хвостовую рекурсию, поэтому мы не будем перебирать стек).
В основной функции я переместил (display "Income: ")
до того, как let
; вы игнорируете его результат, так зачем присваивать его переменной? Затем я добавил if
условие для проверки, если input
ложно, что происходит, когда string->number
не могу разобрать ввод. Наконец, я удалил ваши локальные переменные, так как все, что вы делаете, это печатаете их и использовал синтаксис дроби Scheme вместо деления. (И конечно, я использую print-report
вместо display
с и format
с.)
Я думаю, что это все; если у вас есть другие вопросы о том, что я сделал, не стесняйтесь спрашивать.