[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

15. Изготовляем график

Наша задача --- нарисовать график, наглядно показывающий число определений функций различной длины в исходных текстах на 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 должен быть
списком строк.  После выполнения этой команды метка расположена в
верхнем левом углу а точка в правом нижнем углу 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 '("first" "second" "third")) и нажать 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)

Нам нужна функция, которая способна передать в другую функцию список аргументов. В Emacs есть такая функция и она называется apply. Эта функция `применяет' (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 к отсортированному списку чисел; для функции 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)))

    ;; Заполняем graph-symbols.
    (while (> actual-height 0)                
      (setq insert-list (cons graph-symbol insert-list))
      (setq actual-height (1- actual-height)))

    ;; Заполняем graph-blanks.
    (while (> number-of-top-blanks 0) 
      (setq insert-list (cons graph-blank insert-list))
      (setq number-of-top-blanks
            (1- number-of-top-blanks)))

    ;; Возвращаем полный список.
    insert-list))

Вполне, возможно переписать функцию column-of-graph в третий раз, для того, чтобы можно было создавать строчные графики, вместе с колоночными. Это не так тяжело сделать. Можно рассматривать линейный график как столбчатый график в котором часть каждого столбца, расположенная ниже верхней части, состоит из пробелов. Для создания колонки для линейного графика функция сначал должна создать список из пробелов, который на единицу меньше чем значение, затем использовать функцию cons для присоединения символа графика, а затем снова использовать cons для присоединения верхних пробельных символов к списку.

Легко увидеть как можно написать такую функцию, но поскольку мы не нуждаемся в ней, то мы не будем ее писать. Но работа могла быть сделана, и если она была сделана, то она могла быть сделана с помощью функции column-of-graph. Даже более важно, что ее ценой является лишь несколько изменений, которые должны быть сделаны в другом месте. Такие расширения, если мы даже захотим их сделать, очень просты.

Наконец, мы вплотную подошли к созданию первой настоящей функции выдачи графика. Она будет печатать тело графика, но без меток для вертикальных и горизонтальных осей, так что мы можем назвать ее graph-body-print.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

15.1 Функция 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 ничего не делать нулевой отрезок времени, а затем перерисовать экран. Поскольку это выражение помещено в теле цикла while, то оно заставляет Emacs перерисовать экран колонка за колонкой. Без этого кода, Emacs не перерисует экран до тех пор, пока мы не выйдем из функции.

Давайте протестируем работу graph-body-print с коротким списком чисел.

  1. Установите graph-symbol, graph-blank, column-of-graph и graph-body-print.

  2. Скопируйте следующее выражение:

     
    (graph-body-print '(1 2 3 4 6 4 3 5 7 6 5 2 3))
    

  3. Переключитесь в буфер `*scratch*' и поместите курсор туда, откуда вы хотите начать печать графика.

  4. Наберите M-: (eval-expression).

  5. Вставьте выражение (graph-body-print '(1 2 3 4 6 4 3 5 7 6 5 2 3)) в минибуфер с помощью команды C-y (yank).

  6. Нажмите клавишу RET для оценки выражения graph-body-print.

Emacs выдаст график, подобный следующему:

 
                    *    
                *   **   
                *  ****  
               *** ****  
              ********* *
             ************
            *************


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

15.2 Функция recursive-graph-body-print

Функцию graph-body-print возможно переписать с помощью рекурсии. В этом случае, ее лучше разделить на две части: внешнюю функцию-обертку, использующую выражение 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)))

Рекурсивная функция немного сложней. Она состоит из четырех частей: `рекурсивную-проверку', код печатающий колонки графика, рекурсивный вызов функции и `выражение-следующего-вызова'. Часть `рекурсивная проверка' является выражением if, которое определяет есть ли еще элементы в списке numbers-list; Если тест выполняется, то функция печатает одну колонку графика и заново вызывает себя. Функция вызовет себя снова в соответствии со значением выданным `выражением-следующего-вызова', который заставляет функцию работать с укороченной версией списка 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] [ ? ]

15.3 Need for Printed Axes

Для лучшего восприятия графика необходимы координатные оси. Для одноразового проекта вы можете нарисовать оси вручную, используя режим Picture в составе Emacs; но функция создания графика может быть использована более одного раза.

По этой причине, я написал расширения для базовой функции print-graph-body, которые автоматически печатают метки для горизонтальной и вертикальной осей. Поскольку функции выдачи меток не содержат нового материала, то я поместил их описание в приложение. See section График с подписанными осями.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

15.4 Упражнения

Создайте строчную версию печати тела графика.


[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

This document was generated on March, 10 2004 using texi2html