Загрузка данных
Лабораторная работа № 2. Типы данных. Модульное тестирование
Дубанов А.В., Коновалов А.В.
4 сентября 2019
Цели работы
На практике ознакомиться с системой типов языка Scheme.
На практике ознакомиться с юнит-тестированием.
Разработать свои средства отладки программ на языке Scheme.
На практике ознакомиться со средствами метапрограммирования языка Scheme.
Задания
Реализуйте макрос trace для трассировки. Трассировка — способ отладки, при котором отслеживаются значения переменных или выражений на каждом шаге выполнения программы. Необходимость и вывести значение в консоль, и вернуть его в программу нередко требует существенной модификации кода, что может стать источником дополнительных ошибок. Реализуйте макрос, который позволяет ценой небольшой вставки, не нарушающей декларативность кода, выполнить и вывод значения в консоль с комментарием в виде текста выражения, которое было вычислено, и возврат его значения в программу.
Код без трассировки:
(define (zip . xss)
(if (or (null? xss)
(null? (car xss))) ; Надо отслеживать значение (car xss) здесь...
'()
(cons (map car xss)
(apply zip (map cdr xss))))) ; ...и значение xss здесь.
Код с трассировкой:
(load "trace.scm")
(define (zip . xss)
(if (or (null? xss)
(null? (trace (car xss)))) ; Здесь...
'()
(cons (map car xss)
(apply zip (map cdr (trace xss)))))) ; ... и здесь
(define counter
(let ((n 0))
(lambda ()
(set! n (+ n 1))
n)))
Консоль:
> (zip '(1 2 3) '(one two three))
(car xss) => (1 2 3)
xss => ((1 2 3) (one two three))
(car xss) => (2 3)
xss => ((2 3) (two three))
(car xss) => (3)
xss => ((3) (three))
(car xss) => ()
((1 one) (2 two) (3 three))
> (+ (trace (counter))
(trace (counter)))
(counter) => 1
(counter) => 2
3
Вычисление значения выражения осуществляется после вывода цитаты этого выражения в консоль. Таким образом, в случае аварийного завершения программы из-за невозможности вычисления значения, вы всегда сможете определить, в каком выражении возникает ошибка.
Проследите, чтобы выражение вычислялось ровно один раз, в противном случае можно получить неверный результат работы программы.
В дальнейшем используйте этот макрос при отладке своих программ на языке Scheme.
Указание. Для вывода на экран используются следующие процедуры:
(write e) — вывод выражения e в однозначной, «машиночитаемой» форме: строки при выводе будут заключены в кавычки и управляющие символы в них будут записаны escape-последовательностями, литеры предварены #\, её следует использовать для вывода цитаты выражения,
(display e) — вывод выражения e в «человекочитаемой» форме — строки и символы выводятся буквально, её следует использовать для вывода =>,
(newline) — печатает перевод на следующую строку (аналогично (display "\n") или (display #\newline)).
Модульное тестирование (юнит-тестирование) — способ проверки корректности отдельных относительно независимых частей программы. При таком подходе для каждой функции (процедуры) пишется набор тестов — пар «выражение — значение, которое должно получиться». Процесс тестирования заключается в вычислении выражений тестов и автоматизированном сопоставлении результата вычислений с ожидаемым результатом. При несовпадении выдается сообщение об ошибках.
Реализуйте свой каркас для модульного тестирования. Пусть каркас включает следующие компоненты:
Макрос test — конструктор теста вида (выражение ожидаемый-результат).
Процедуру run-test, выполняющую отдельный тест. Если вычисленный результат совпадает с ожидаемым, то в консоль выводятся выражение и признак того, что тест пройден. В противном случае выводится выражение, признак того, что тест не пройден, а также ожидаемый и фактический результаты. Функция возвращает #t, если тест пройден и #f в противном случае. Вывод цитаты выражения в консоль должен выполняться до вычисления его значения, чтобы при аварийном завершении программы последним в консоль было бы выведено выражение, в котором произошла ошибка.
Процедуру run-tests, выполняющую серию тестов, переданную ей в виде списка. Эта процедура должна выполнять все тесты в списке и возвращает #t, если все они были успешными, в противном случае процедура возвращает #f.
Какой предикат вы будете использовать для сравнения ожидаемого результата с фактическим? Почему?
Пример:
;; Пример процедуры с ошибкой
;;
(define (signum x)
(cond
((< x 0) -1)
((= x 0) 1) ; Ошибка здесь!
(else 1)))
;; Пример процедуры с побочным эффектом
;;
(define counter
(let ((n 0))
(lambda ()
(set! n (+ n 1))
n)))
;; Загружаем каркас
;;
(load "unit-test.scm")
; Определяем список тестов
;;
(define the-tests
(list (test (signum -2) -1)
(test (signum 0) 0)
(test (signum 2) 1)
(test (counter) 1)
(test (counter) 77) ; ошибка в тесте, должно быть 2
(test (counter) 3)
;; тесты, демонстрирующие разницу
;; между write и display
(test (* 7 7) "49")
(test (string-append "4" "9") 49)))
;; Выполняем тесты
;;
(run-tests the-tests)
Пример результата в консоли:
(signum -2) ok
(signum 0) FAIL
Expected: 0
Returned: 1
(signum 2) ok
(counter) ok
(counter) FAIL
Expected: 77
Returned: 2
(counter) ok
(* 7 7) FAIL
Expected: "49"
Returned: 49
(string-append "4" "9") FAIL
Expected: 49
Returned: "49"
#f
Если перепутать write и display, последний пример будет показывать странный результат.
Используйте разработанные вами средства отладки для выполнения следующих заданий этой лабораторной работы и последующих домашних заданий.
Обратите внимание, что тесты при создании не должны выполняться. Т.е. следующая строка не должна приводить к ошибке:
(define bad-test (test (/ 10 0) 100))
Однако, попытка запуска при помощи run-test должна приводить к ошибке деления на ноль:
> (run-test bad-test)
(/ 10 0)
‹СООБЩЕНИЕ ОБ ОШИБКЕ›
Выполните задание 1 из индивидуального варианта.
Ачивка. Постарайтесь избежать построения избыточных промежуточных структур данных и дублирования кода.
Выполните задание 2 из индивидуального варианта.
Индивидуальный вариант
Реализуйте процедуру (reverse! seq) деструктивного обращения последовательности (правильного списка, вектора или строки).
(define xs '(a b c d e))
(define ys (vector 1 2 3 4 5))
;; Синтаксис #(…) может создавать неизменяемый вектор
(define zs (string #\h #\e #\l #\l #\o))
(reverse! xs)
(reverse! ys)
(reverse! zs)
xs ⇒ (e d c b a)
ys ⇒ #(5 4 3 2 1)
zs ⇒ "olleh"
Добавьте в процедуру reverse! необязательный параметр-приёмник — если он задан, то исходная последовательность не меняется, результат её обращения записывается в приёмник:
(define xs (list 'a 'b 'c 'd 'e))
(define ys (vector 1 2 3 4 5))
(reverse! xs ys)
xs ⇒ (a b c d e)
ys ⇒ #(e d c b a)
(define zs (string #\h #\e #\l #\l #\o))
(reverse! zs xs)
zs ⇒ "hello"
xs ⇒ (#\o #\l #\l #\e #\h)
(define as '(a b c))
(define bs (append '(x y z) as))
bs ⇒ (x y z a b c)
;; Последние три элемента bs - список as
(set-car! as 1)
bs ⇒ (x y z 1 b c)
(reverse! bs)
bs ⇒ (c b 1 z y x)
as ⇒ (z y x)
;; Изменяться должно каждое звено списка!
(reverse! (cdr bs))
bs ⇒ (c x y z 1 b)
Можно считать, что размер источника и приёмника всегда должны совпадать.
Таким образом, применение процедуры с одним аргументом (reverse! seq) должно быть эквивалентно (reverse! seq seq).
Попробуйте предусмотреть все возможные варианты.
Совет. При написании юнит-тестов используйте let.
Примечание. Результатом выполнения задания должно быть одно определение процедуры reverse!. Алгоритм её работы должен определяться числом аргументов и их типами.
Подсказка. Процедура, заполняющая другой список:
(define xs '(a b c))
(define ys (append '(x y z) xs))
(define (fill src dest)
(if (not (or (null? src) (null? dest)))
(begin (set-car! dest (car src))
(fill (cdr src) (cdr dest)))))
Запуск:
> (fill '(1 2 3 4 5) ys)
> ys
(1 2 3 4 5 c)
> xs
(4 5 c)
Разработайте наборы юнит-тестов и используйте эти тесты для разработки процедуры, выполняющей разложение на множители.
Реализуйте процедуру factorize, выполняющую разложение многочленов вида a2 − b2, a3 − b3 и a3 + b3 по формулам.
Пусть процедура принимает единственный аргумент — выражение на языке Scheme, которое следует разложить на множители, и возвращает преобразованное выражение. Возведение в степень в исходных выражениях пусть будет реализовано с помощью встроенной процедуры expt. Получаемое выражение должно быть пригодно для выполнения в среде интерпретатора с помощью встроенной процедуры eval. Упрощение выражений не требуется.
Примеры применения процедуры:
(factorize '(- (expt x 2) (expt y 2)))
⇒ (* (- x y) (+ x y))
(factorize '(- (expt (+ first 1) 2) (expt (- second 1) 2)))
⇒ (* (- (+ first 1) (- second 1))
(+ (+ first 1) (- second 1)))
(eval (list (list 'lambda
'(x y)
(factorize '(- (expt x 2) (expt y 2))))
1 2)
(interaction-environment))
⇒ -3
Вопросы для защиты лабораторной работы
Суметь объяснить логику работы написанных определений и обосновать выбор используемых языковых конструкций.
Отчёт по лабораторной работе № 2
Отчёт должен содержать примеры, предложенные преподавателем и задания из индивидуального варианта, и то, и другое — с тестированием.
Отчёт выполняется в разметке Markdown по следующему шаблону:
% Лабораторная работа № 2. Типы данных. Модульное тестирование
% <лабораторная ещё не сдана>
% Кирилл Меньшиков, ИУ9-12/6
# Цель работы
…переписываете цель работы из задания…
# Индивидуальный вариант
…названия процедур, которые требуется реализовать…
# Реализация
```scheme
(define (hello name)
(display "Привет, ")
(display name)
(display "!\n"))
```
# Тестирование
```
Welcome to DrRacket, version 6.10.1 [3m].
Language: R5RS; memory limit: 128 MB.
> (hello "Вася")
Привет, Вася!
> (hello "Пупкин")
Привет, Пупкин!
```
# Вывод
…пишете, чему научились…
В отчёте приведён лишь необходимый минимум. Можно писать больше и интереснее — интересные и вдумчивые отчёты поощряются дополнительным баллом.
Ваш отчёт будет конвертирован в PDF при помощи pandoc следующей командой:
pandoc \
--pdf-engine=xelatex \
-V 'mainfont:Liberation Serif' \
-V 'monofont:Liberation Mono' \
"$SOURCE" -o "$PDF"