[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Наша задача --- нарисовать график, показывающий число определений функций различной длины в исходных текстах на Emacs Lisp.
Обычно на практике, если вам надо создать график, то вы вероятно
воспользуетесь для этого какой-нибудь специализированной программой
например, gnuplot
. (В Emacs имеется неплохой интерфейс к
gnuplot
). Однако в нашем случае, мы создадим такую программу с
нуля, и процессе работы, мы воспользуемся всеми нашими знаниями и
изучим еще кое-что.
В этой главе мы сначала создадим простую функцию рисующую график. Это первая попытка будет прототипом, то есть, быстро написанной функцией, которая позволит нам вторгнуться на незнакомую територрию создания графиков. Мы можем обнаружить скрытые мели и рифы и выяснить, что все намного проще. После предварительной разведки, мы будем чувствовать себя уверенней и расширим нашу функцию для автоматического отображения меток и осей.
Отображение колонок графика 15.1 Функция graph-body-print
How to print the body of a graph. 15.2 Функция recursive-graph-body-print
15.3 Need for Printed Axes 15.4 Упражнения
Поскольку при создании Emacs разрабатывался гибким и настраиваемым, то
он должен работать со всеми терминалами, включая и чисто текстовые,
поэтому график надо будет делать с помощью каких-нибудь `печатных'
символов. Звездочка *
--- это то что надо; когда мы будем
улучшать функцию печатающую график, то мы можем сделать выбор символа
настраиваемой опцией.
Можно назвать эту функцию graph-body-print
; она будет принимать
единственный аргумент --- numbers-list
(список чисел). На
этом этапе мы не будем рисовать координатные оси, мы только отобразим
тело самого графика.
Функция graph-body-print
будет вставлять вертикальную колонку
составленную из звездочек для каждого элемента из numbers-list
.
Высота каждой колонки будет определятся значением этого элемента из
numbers-list
.
Вставка колонок --- это итеративный процесс; это значит, что такая
функция может быть написана с помощью цикла while
или рекурсии.
Наша первоочередная задача --- выяснить, как печатать колонку звездочек. Обычно в Emacs мы печатаем символы на экране горизонтально, строка за строкой. Сейчас мы можем пойти двумя путями: создать собственную функцию для вставки колонок или найти какую-нибудь подобную функцию уже существующую в Emacs.
Для поиска существующей функции мы можем воспользовать командой
M-x apropos. Эта команда действует подобно команде C-h a
(apropos-command), но apropos-command
производит поиск только
среди интерактивных функций. А функция M-x apropos перечисляет
все символы, которые совпадают с заданным регулярным выражением,
включая и неинтерактивные функции.
Мы хотим найти какую-нибудь команду, которая печатает или вставляет
колонки. Весьма вероятно, что в названии функции будет использоваться
или слово `print', или слово `insert' или слово `column'.
Следовательно мы можем просто набрать M-x apropos RET
print\|insert\|column RET и изучить результат. На моей машине
выполнение это команды заняло некоторое время, после чего был выдан
список из 79 функций и переменных. Внимательно изучив список (на что
ушло гораздо больше времени) я обнаружил, что единственная функция,
которая мне подходит --- это insert-rectangle
. И в самом
деле документация на нее гласит:
insert-rectangle: Вставить текст из RECTANGLE верхний левый угол у точки. первая строка RECTANGLE вставляется у точки, вторая строка вставляется вертикально вниз у точки и т.д. RECTANGLE должен быть списком строк. |
Можно запустить быстрый тест, чтобы убедиться, что это нужная нам функция.
Ниже результат вычисления представленного выражения в буфере
`*scratch*', скопируйте выражение с insert-rectangle
туда
и наберите C-u C-x C-e (eval-last-sexp
). Функция вставит
строки `"first"', `"second"', и `"third"' ниже точки.
Она также вернет значение nil
.
(insert-rectangle '("first" "second" "third"))first second third |
Конечно мы сами не будем вставлять текст выражения
insert-rectangle
в тот буфер в который мы будем выводить
график, вместо этого мы вызовем функцию из нашей программы. Однако мы
должны убедиться, что точка в буфере располагается как раз на том
месте, куда функция insert-rectangle
вставит колонку строк.
Если вы читаете это в системе Info, то вы легко можете проверить, как
это работает, переключившись в другой буфер, например, в буфер
`*scratch*', расположив точку где-нибудь в этом буфере и нажав
ESC :, после этого ввести в минибуфере выражение
insert-rectangle
и нажать RET. Это заставит Emacs
вычислить выражение из минибуфера, но использовать значение точки из
буфера `*scratch*'. (ESC : --- связана с командой
eval-expression
).
После выполнения этого теста мы обнаружим, что точка перемещается в конец последней вставленной строки --- то есть, эта функция в качестве побочного эффекта перемещает точку. Если мы повторим эту команду, то следующая вставка переместит точку ниже и правее предыдущего вставленного текста. Но нам этого не надо! Если мы собираемся отображать график, то колонки должны быть рядом друг с другом.
Таким образом мы выяснили, что каждый цикл while
вставляющий
колонку должен позиционировать точку в нужное место, и это место
должно быть вверху, а не внизу колонки. Более того, мы же еще помним,
что нам надо печатать график, и мы не хотим, чтобы все колонки были
одинаковой длинны. Это значит, что вершины различных колонок могут
быть разной высоты. Мы не можем просто переместить точку в ту же
самую строку каждый раз, а должны переместится в право --- или все
таки можем....
Мы планируем создавать колонки нашего графика из символов `*'
(звездочка). Число звездочек к колонке соответствует текущему элементу
в numbers-list
. Нам нужно создать список из звездочек при
каждом из вызове insert-rectangle
. Если этот список состоит
только из звездочек, то нам необходимо будет указать нужное число
строк выше базовой линии графика, чтобы правильно отобразить его. Это
может быть тяжелым.
В качестве альтернативы, если мы можем определить некоторый способ передачи
функции insert-rectangle
списка той же самой длины при каждом
из вызовов, то мы можем помещать точку на ту же самую строку при
каждом вызове, но перемещаться на одну колонку вправо. Если мы сделаем
это, то некоторые части списка должны будут заполнены пробелами, а не
звездочками. Например, если максимальная высота графика равна 5, но
высота колонки равна 3, то для функции insert-rectangle
аргумент будет выглядеть вот так:
(" " " " "*" "*" "*") |
Это последнее ограничение не будет таким серьезным, поскольку мы можем
определить высоту колонки. Для нас существует два способа указания
высоты колонки: мы можем выбрать ее произвольно, что будет работать
хорошо для графиков с такой высотой; или мы можем произвести поиск
максимального числа в списке и использовать его как максимальную
высоту графика. Если последняя операция кажется тяжелой, то первая
будет самой простой, но в Emacs существуют встроенные функции
определения максимального значения аргументов. Мы можем использовать
эту функцию. Функция называется max
и возвращает самое большое
значение среди заданных аргументов, которые должны быть
числами. Например,
(max 3 4 6 5 7 3) |
возвратит 7. (Соответствующая функция с именем min
возвращает
наименьшее число среди заданных аргументов).
Однако мы не можем просто вызвать функцию max
для numbers-list
;
функция max
ожидает, что ее аргументами будут числа, а не список
чисел. Так что следующее выражение,
(max '(3 4 6 5 7 3)) |
приведет к выдаче такого сообщения об ошибке.
Wrong type of argument: integer-or-marker-p, (3 4 6 5 7 3) |
Нам нужна функция, которая передает функции список аргументов. Эта
функция называется apply
. Эта функция `применяет' свой первый
аргумент (функцию) ко всем оставшимся аргументам, последний из которых
может быть списком.
Например,
(apply 'max 3 4 7 3 '(4 8 5)) |
возвратит 8.
(Кстати, я не знаю как вы могли бы изучить эту функцию без книги,
такой как эта. Можно узнать функции, такие как search-forward
или insert-rectangle
, путем угадания частей их имен в сочетании
с использованием команды apropos
. Даже, хотя основа ясна
--- `apply' применяет свой первый аргумент к оставшимся --- я все
равно сомневаюсь, что новичок догадается использовать такое слово,
даже используя команду apropos
или другую помощь. Конечно, я
могу быть неправ; как никак, функция была названа тем человеком,
который ее придумал).
Второй и последующие аргументы функции apply
являются
необязательными, так что мы можем использовать apply
для вызова
функции и передачи ей элементов списка, как вот здесь, что возвращает
8:
(apply 'max '(4 8 5)) |
Вот как мы будем использовать apply
. Функция
recursive-lengths-list-many-files
возвращает список чисел, к
которым мы будем применять функцию max
(мы также можем применить
max
к отсортированному списку чисел; нет никакой разницы между
отсортированным и несортированным списоком).
Таким образом, операция по нахождению максимальной высоты графика будет выглядеть следующим образом:
(setq max-graph-height (apply 'max numbers-list)) |
Теперь мы можем вернуться к вопросу о том, как создать список строк для
колонок графика. По заданной высоте графика и количеству звездочек,
которые должны появиться в колонке, функция должна возвратить список
строк для вставки командой insert-rectangle
.
Каждая колонка создается из звездочек и пробелов. Поскольку функции передается высота графика и число звездочек в колонке, то число пробелов может быть найдено вычитанием количества звездочек из высоты колонки. Полученное число пробелов и число звездочек может быть использовано для создания списка:
;;; Первая версия. (defun column-of-graph (max-graph-height actual-height) "Возвращает список строк, описывающих колонку графика." (let ((insert-list nil) (number-of-top-blanks (- max-graph-height actual-height))) ;; Заполняем звездочками. (while (> actual-height 0) (setq insert-list (cons "*" insert-list)) (setq actual-height (1- actual-height))) ;; Заполняем пробелами. (while (> number-of-top-blanks 0) (setq insert-list (cons " " insert-list)) (setq number-of-top-blanks (1- number-of-top-blanks))) ;; Возвращаем полный список. insert-list)) |
Если вы установите эту функцию, а затем вычислите следующее выражение, то вы увидите, что возвратиться требуемый список:
(column-of-graph 5 3) |
возвратит
(" " " " "*" "*" "*") |
Как видно, column-of-graph
содержит большую ошибку ---
символы для пустых и помеченных ячеек жестко забиты как пробел и
звездочка. Это удобно для прототипа, но вы, в качестве другого
пользователя, можете захотеть использовать доугие символы. Например,
при тестировании функции построения графика, вы можете захотеть
использовать точку, вместо пробела, для того, чтобы убедиться, что
точка позиционируется правильно при вызове функции
insert-rectangle
; или вы можете захотеть использовать знак
`+' или другой символ вместо знака звездочка. Вы даже можете
захотеть сделать колонку графика шире чем один символ. Программа
должны быть более гибкая. Для того, чтобы сделать это, нам нужно
заменить знаки пробела и звездочки на две переменных, которые мы будем
называть graph-blank
и graph-symbol
, и определим эти две
переменных раздельно.
Также документация написана не совсем хорошо. Эти соображения ведут нас ко второй версии функции:
(defvar graph-symbol "*" "Строка, используемая в качестве символа в графике, обычно -- звездочка.") (defvar graph-blank " " "Строка, используемая в качестве пробела в графике, обычно -- пробел. graph-blank должны быть той же длины, что и graph-symbol.") |
(Для разъяснения по поводу defvar
, смотрите раздел Инициализации переменных с помощью defvar
.)
;;; Вторая версия. (defun column-of-graph (max-graph-height actual-height) "Возвращает список MAX-GRAPH-HEIGHT строк; ACTUAL-HEIGHT определяет количество graph-symbol. graph-symbol являются смежными записями в конце списка. Список будет вставлен как одна колонка графика. Строки являются либо graph-blank либо graph-symbol." (let ((insert-list nil) (number-of-top-blanks (- max-graph-height actual-height))) ;; Заполняем |
Если мы хотим, то мы можем переписать функцию column-of-graph
в
третий раз, для того, чтобы можно было создавать строчные графики,
вместе с колоночными. Это не так тяжело сделать. Можно рассматривать
линейный график как столбчатый график в котором часть каждого столбца,
расположенная ниже верхней части, состоит из пробелов. Для создания
колонки для линейного графика функция сначал должна создать список из
пробелов, который на единицу меньше чем значение, затем использовать
функцию cons
для присоединения символа графика, а затем снова
использовать cons
для присоединения верхних пробельных символов
к списку.
Легко увидеть как можно написать такую функцию, но поскольку мы не
нуждаемся в ней, то мы не будем ее писать. Но работа могла быть
сделана, и если она была сделана, то она могла быть сделана с помощью
функции column-of-graph
. Даже более важно, что ее ценой
является лишь несколько изменений, которые должны быть сделаны в
другом месте. Такие расширения, если мы даже захотим их сделать, очень
просты.
В заключении, мы придем к нашей первой настоящей функции выдачи
графика. Она печатает тело графика, но не метки для вертикальных и
горизонтальных осей, так что мы назовем ее graph-body-print
.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
graph-body-print
После наших приготовлений в предыдущем разделе, функция
graph-body-print
является очень простой. Функция будет выдавать
колонку за колонкой, используя элементы списка чисел для указания
числа звездочек в каждой из колонок. Это повторяющееся действие, что
означает, что для этой работы мы будем использовать уменьшающийся цикл
while
или рекурсивную функцию. В этом разделе мы создадим
определение функции с помощью цикла while
.
Функция column-of-graph
требует в качестве аргумента высоту
графика, так что мы должны определить и сохранить ее в качестве
локальной переменной.
Это ведет нас к следующему шаблону для версии с циклом while
:
(defun graph-body-print (numbers-list) "documentation..." (let ((height ... ...)) (while numbers-list insert-columns-and-reposition-point (setq numbers-list (cdr numbers-list))))) |
Нам нужно заполнить части шаблона.
Несомненно, мы можем использовать выражение (apply 'max
numbers-list)
для определения высоты графика.
Цикл while
будет пробегаться по элементам списка
numbers-list
. По мере того, как он будет сокращаться с помощью
выражения (setq numbers-list (cdr numbers-list))
, то поле
CAR каждого из вариантов списка будет значением аргумента для
функции column-of-graph
.
На каждой итерации цикла while
, функция insert-rectangle
вставляет список возвращенный функцией
column-of-graph
. Поскольку функция insert-rectangle
перемещает точку в нижний правый угол вставленного прямоугольника, то
нам нужно сохранить расположение точки перед вставкой прямоугольника,
затем после вставки прямоугольника переместить точку в сохраненную
позицию и горизонтально переместиться в следующую позицию, из которой
функция insert-rectangle
была вызвана.
Если вставленные колонки шириной более одного символа, если бы
использовалось по одному символу пробела и звездочки, то команда
перемещения будет просто равна (forward-char 1)
; однако, ширина
колонки может быть более одного символа. Это означает что, команда
перемещения может быть записана как (forward-char
symbol-width)
. Само значение symbol-width
будет равно длине
graph-blank
и может быть найдено с использованием кода
(length graph-blank)
. Лучшим местом для связывания переменной
symbol-width
со значением ширины колонки, является список
переменных в выражении let
.
Это ведет к следующему определению функции:
(defun graph-body-print (numbers-list) "Выдает столбцовый график на основе NUMBERS-LIST. Список чисел состоит из значений по оси Y." (let ((height (apply 'max numbers-list)) (symbol-width (length graph-blank)) from-position) (while numbers-list (setq from-position (point)) (insert-rectangle (column-of-graph height (car numbers-list))) (goto-char from-position) (forward-char symbol-width) ;; Отображение колонки за колонкой. (sit-for 0) (setq numbers-list (cdr numbers-list))) ;; Поместить точки для меток по оси X. (forward-line height) (insert "\n") )) |
Одним из неожиданных для нас выражений является выражение
(sit-for 0)
в цикле while
. Это выражение делает
операцию выврла графика более интересной, чем она могла бы быть. Это
вырадение заставляет Emacs ничего не делать нулевой отрезок времени,
а затем перерисовать экран. Поскольку это выражение помещено тут, то
оно заставляет Emacs перерисовать экран колонка за колонкой. Без этого
кода, Emacs не перерисует экран до тех пор, пока мы не выйдем из
функции.
Мы можем протестировать работу graph-body-print
с коротким
списком чисел.
graph-symbol
, graph-blank
,
column-of-graph
и graph-body-print
.
(graph-body-print '(1 2 3 4 6 4 3 5 7 6 5 2 3)) |
eval-expression
).
graph-body-print
в минибуфер с помощью
команды C-y (yank)
.
graph-body-print
.
Emacs выдаст график, подобный следующему:
* * ** * **** *** **** ********* * ************ ************* |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
recursive-graph-body-print
Функция graph-body-print
также может быть переписана для
использования рекурсии. В этом случае, она будет разделена на две
части: внешнюю `обвязку (wrapper)', которая использует выражение
let
для определения значения нескольких переменных, которые
нужно вычистить лишь раз, таких как максимальная высота графика, и
внутреннюю функцию, которая вызывается рекурсивно для выдачи графика.
`Обвязка' не сложна:
(defun recursive-graph-body-print (numbers-list) "Выдает график из списка чисел NUMBERS-LIST. Список чисел состоит из значений по оси Y." (let ((height (apply 'max numbers-list)) (symbol-width (length graph-blank)) from-position) (recursive-graph-body-print-internal numbers-list height symbol-width))) |
Рекурсивная функция немного более трудная. Она состоит из четырех
частей: `do-again-test', печатающей код, рекурсивного вызова и
`next-step-expression'. Часть `do-again-test' является выражением
if
, которое определяет есть ли еще элементы в списке
numbers-list
; Если тест выполняется, то функция печатает одну
колонку графика и заново вызывает себя. Функция вызовет себя снова в
соответствии со значением выданным `next-step-expression', который
заставляет работать с укороченной версией списка numbers-list
.
(defun recursive-graph-body-print-internal (numbers-list height symbol-width) "Печатает график. Используется внутри функции recursive-graph-body-print." (if numbers-list (progn (setq from-position (point)) (insert-rectangle (column-of-graph height (car numbers-list))) (goto-char from-position) (forward-char symbol-width) (sit-for 0) ; Отображает график колонка за колонкой. (recursive-graph-body-print-internal (cdr numbers-list) height symbol-width)))) |
После установки, мы можем протестировать это выражение; вот пример:
(recursive-graph-body-print '(3 2 5 6 7 5 3 4 6 4 3 2 1)) |
Вот что выдаст recursive-graph-body-print
:
* ** * **** * **** *** * ********* ************ ************* |
Любая из двух функций, graph-body-print
или
recursive-graph-body-print
, создает тело графика.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Для графика нужны оси, так что вы можете сориентироваться сами. Для одноразоового проекта вы можете нарисовать оси вручную, используя режим Picture в составе Emacs; но функция рисования графика может быть использована более одного раза.
По этой причине, я написал расширения для базовой функции
print-graph-body
, которые автоматически печатают метки для
горизонтальной и вертикальной осей. Поскольку функции выдачи меток не
содержат нового материала, то я поместил их описание в приложение.
@xref{Full Graph, , График с подписанными осями}.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Напишите версию для выдачи линейных графиков.
[ << ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |