C. График с координатными осями
Координатные оси помогают воспринимать график, ведь обычно на них наносится масштаб. В предыдущих главах мы создали функции для печати тела графика, сейчас мы напишем функции для создания вертикальной и горизонтальной оси вместе с самим графиком.
Пример графика с координатными осями C.1 Список переменных print-graph C.2 Функция print-Y-axis Градуируем вертикальную ось. C.3 Функция print-X-axis Печатаем горизонтальную ось. C.4 Печать полного графикаПример графика с координатными осями
Так как вставляемые символы заполняют буфер вправо и вниз наша новая функция должна вначале печатать вертикальную ось (ось Y), затем тело графика и наконец горизонтальную ось (ось X). Работу будущей функции можно представить следующим образом:
-
Инициализация необходимых переменных.
Вот как будет выглядеть наш график:
На этом графике координатные оси пронумерованы числами, однако иногда по горизонтальной оси откладывается время и тогда лучше градуировать единицами времени, например, месяцами, как в следующем графике:
На самом деле, если мы проявим немного фантазии, то сможем придумать множество возможных способов градуировки координатных осей, но это только усложнит задачу. Вместо этого лучше вначале, для первой попытки выбрать простую схему, а впоследствии ее можно заменить или модифицировать.
Все эти рассуждения приводят нас к следующему шаблону для будущей функции print-graph :
Теперь можно по очереди реализовывать каждую часть определения функции print-graph .
C.1 Список переменных print-graph
При создании функции print-graph первая задача придумать список локальных переменных для выражения let . (Пока мы не будем задумываться о документировании функции и о том будет ли она интерактивной или нет.)
Нам понадобится несколько локальных переменных. Ясно, что верхняя метка для вертикальной оси должна быть по крайней мере равна высоте графика, значит нам нужна переменная, которая будет содержать эту величину. Отметим, что функции print-graph-body так же требуется знать высоту графика. Нет никаких оснований вычислять ее в двух разных местах, лучше немного изменить предыдущую версию print-graph-body для того чтобы воспользоваться появившимися преимуществами.
Аналогично и функции для печати оси X и функции print-graph-body необходимо знать ширину каждого символа, поэтому лучше произвести вычисление ширины символа при создании списка аргументов и так же немного изменить предыдущее определение print-graph-body .
Последняя метка откладываемая по горизонтали должна быть по крайней мере такой же длины как и график, однако эта информация используется только в функции для печати горизонтальной оси, поэтому мы можем не вычислять ее в списке переменных выражения let .
Все это приводит нас к следующей форме списка переменных выражения let для функции print-graph :
В последствии мы увидим, что это выражение не совсем правильно.
C.2 Функция print-Y-axis
Задача функции print-Y-axis пронумеровать вертикальную ось, что будет выглядеть примерно следующим образом:
В эту функцию надо передать высоту графика, для того чтобы она должным образом напечатала соответствующие номера и метки.
Впрочем легко сказать и даже нарисовать, как должна выглядеть ось Y; но создать функцию это совсем другое дело. Будет не совсем правильно, если мы скажем, что нам нужен номер и метка каждые пять строк: ведь между `1' и `5' только три строки (строки 2, 3 и 4), а между `5' и 10 уже четыре строки (строки 6, 7, 8, 9). Лучше скажем, что нам нужна метка и номер в начале оси и потом метка с номером каждые следующие пять строк.
Теперь, надо обдумать чему будет равна последняя метка. Предположим, что максимальная высота колонки графика равна 7. Какой должна быть верхняя отметка по оси Y --- `5 -' , но тогда график будет выше отметки? Или верхняя метка должна быть равна `7 -' , чтобы мы знали чему равна вершина графика? А может быть верхняя метка должна быть равна 10 - , ведь 10 делится на пять, но тогда верхняя метка будет расположена выше чем пик графика? Лучше всего реализовать последний случай. В большинстве случаев координатные оси пронумерованы дискретно с шагом 5 --- 5, 10, 15, и так далее. Но как только мы решаем использовать дискретный шаг для вертикальной оси, мы обнаруживаем, что простое выражение для списка переменных вычисляющее высоту неправильно. Выражение (apply 'max numbers-list) , вернет максимальную высоту столбца, а нам необходимо значение округленное к ближайшему большему числу кратному пяти. Значит необходимо придумать более сложное выражение.
Как обычно сложная задача станет проще, если ее разделить на несколько простых задач.
Во первых может получится, что высота графика это число кратное пяти, например --- 5, 10, 15 или какое-нибудь большее число. В этом случая мы можем прямо использовать это значение как высоту оси Y.
Самый простой способ определить кратность числа пяти это разделить число на пять и проанализировать остаток. Если остаток равен 0, значит число кратно пяти. Например семь при делении на пять дает в остатке 2, значит семь не кратно пяти. Можно сказать совсем просто, как обьясняют в школе, пять входит в семь один раз с остатком два. Однако пять входит в десять дважды без остатка: значит десять кратно пяти.
C.2.1 Обходной путь: Вычисление остатка C.2.2 Создание элементов оси Y C.2.3 Создание колонки оси Y C.2.4 Окончательная версия print-Y-axis Печать веритикальной оси. C.2.1 Обходной путь: Вычисление остаткаВ Лиспе есть функция для вычисления остатка это % . Эта функция возвращает остаток от деления первого аргумента на второй. Так случилось, что % функция Emacs Lisp, которую вы не сможете обнаружить с помощью apropos : поиск M-x apropos RET remainder RET ничего не выдаст. Единственный способ узнать о существовании % прочесть об этом в каком-нибудь учебнике или изучив исходные тексты Emacs Lisp. Функция % используется в определении функции rotate-yank-pointer , об этом можно прочесть в приложении (See section Тело функции rotate-yank-pointer .)
Вы можете понять работу функцию % , вычислив следующие два выражения:
Первое выражение вернет 2, а второе выражение вернет 0.
Для проверки того, равно ли значение, возвращаемое функцией % нулю можно использовать функцию zerop . Эта функция вернет t , если ее аргумент, который должен быть числом, ноль.
Таким образом следующее выражение вернет t , если высота графика кратна пяти:
(Значение height , мы найдем с помощью выражения (apply 'max numbers-list) .)
С другой стороны, если значение height не кратно пяти, мы должны установить его в большее значение, которое кратно пяти. Это достаточно простое арифметическое действие, которое вполне можно выполнить уже знакомыми нам функциями. Сначала мы разделим значение height на пять для того чтобы определить сколько раз пять входит в это число. Например пять два раза входит в двенадцать, если мы добавим к частному единицу и умножим его на пять, то мы получим большее число кратное пяти. Пять два раза входит в двенадцать, добавив к двум один и умножив на пять мы получим пятнадцать, число кратное пяти и большее чем двенадцать. Соответствующее Лисп выражение приведено ниже:
После вычисления следующего выражения вы получите 15:
Во время наших рассуждений мы использовали `пять', как значение дискретного шага по оси Y; но мы вполне можем выбрать и какое-нибудь другое значение, лучше всего, если мы заменим `пять' переменной, которой мы сможем присвоить значение. Самое лучшее имя, которое я смог придумать это Y-axis-label-spacing . Используя новоизобретенную переменную и выражение if , получаем следующее выражение:
Это выражение возвращает значение height , если высота кратна значению Y-axis-label-spacing или вычисляет и присваивает переменной height следующее большее число, кратное Y-axis-label-spacing .
Мы можем теперь включить это выражение в список переменных формы let для функции print-graph (конечно после того, как присвоим значение переменной Y-axis-label-spacing ):
(Стоит отметить, что здесь мы используем функцию let* : ведь вначале вычисляется значение height с помощью (apply 'max numbers-list) и после этого получившееся значение используется для вычисления окончательного результата. See section The let* expression, за дополнительной информацией о let* .)
C.2.2 Создание элементов оси YКогда мы печатаем вертикальные оси, мы вставляем метки, такие как `5 -' и `10 - ' каждые пять строк. Кроме того хотелось бы, чтобы вертикальные метки находились на одной линии, так что более короткие номера придется выравнивать ведущими пробелами. Если некоторая метка это число, состоящее из двух цифр, тогда метка с числом, состоящим из одной цифры должна быть выровнена с помощью дополнительного пробела, расположенного перед числом.
Для того чтобы найти количество цифр, входящих в число можно использовать функцию length . Но так как функция length работает только со строками, а не с числами, число необходимо предварительно превратить в строку. Это можно сделать с помощью функции int-to-string . Например,
Кроме этого в каждой метке за числом следует строка ` - ' , которую мы будем называть маркером и назовем ее Y-axis-tic . Давайте зададим эту переменную с помощью defvar :
Длина метки по оси Y это сумма длин Y-axis-tic и длины наибольшего числа отложенного по оси Y.
Это значение будет вычисляться в списке локальных переменных функции print-graph и присваиваться переменной full-Y-label-width . (Стоит отметить, что вначале мы и не думали что появится эта переменная.)
Для того чтобы полностью напечатать метку по вертикальной оси, строку Y-axis-tic надо обьединить с номером; а перед ними может потребоваться вставить один или два пробела, в зависимости от того, какой длины текущее число. Таким образом координатная метка будет состоять из трех частей: (необязательные) ведущие пробелы, число и строка Y-axis-tic (в нашем случае это символ тире). В функцию, создающую координатную метку по оси Y передается число для текущей строки и значение ширины верхней метки, (ведь она получится самой широкой), которое вычисляется при создании локальных переменных в функции print-graph .
В функции Y-axis-element обьединяются вместе пробелы, которые могут и отсутствовать; число, предварительно превращенное в строку; и строка Y-axis-tic в нашем случае это знак тире.
Для того чтобы вычислить, сколько потребуется дополнительных пробелов в функции производится вычитание текущей длины метки, то есть длины числа плюс длины символа Y-axis-tic из максимально допустимой ширины метки.
Пробелы вставляются с помощью функции make-string . Этой функции требуются два аргумента: первый задает, какой длины должна быть строка, а второй--- это символ, который нужно вставить, заданный в особом формате. В нашем случае формат это знак вопроса за которым следует пробел, например `? ' . See section `Character Type' in The GNU Emacs Lisp Reference Manual , за подробным описанием синтаксиса для символов.
Функция int-to-string используется в выражении concat при создании результирующей строки, для того, чтобы конвертировать число в строку, которая будет слита с пробелами и знаком тире.
C.2.3 Создание колонки оси YТеперь мы полностью готовы к созданию функции, которая выдаст нам список пронумерованных и пустых меток, который будет представлять собой вертикальную координатную ось:
В этой функции в конце каждой итерации цикла while из начального значения height последовательно вычитается единица. Перед каждым вычитанием проверяется, является ли значение height кратным Y-axis-label-spacing , если да, то с помощью функции Y-axis-element создается нумерованная метка; если же текущее значение height не кратно Y-axis-label-spacing , то с помощью функции make-string создается пустая метка.
В конце функции создается начальная метка для вертикальной оси, ранее мы условились что она будет пронумерована единицей.
C.2.4 Окончательная версия print-Y-axisСписок созданный Y-axis-column передается в функцию print-Y-axis , которая и напечатает его с помощью insert-rectangle .
Функция print-Y-axis использует insert-rectangle для вставки оси Y, которая создана с помощью функции Y-axis-column . Кроме этого, она устанавливает точку в ту позицию откуда мы начнем печатать график.
Давайте проверим работу print-Y-axis :
В буфере `*scratch*' будет напечатана следующая ось Y:
Emacs напечатал метки вертикально, верхняя метка равна `10 -' . (Функция print-graph передаст более правильное значение, как вершину графика height-of-top-line , которое в этом случае должно быть равно 15.)
C.3 Функция print-X-axis
Метки по оси X похожы на метки по оси Y, кроме того что маркеры распологаются над номерами. Метки должны выглядеть следующим образом:
Первый маркер под первым столбцом графика, перед ним несколько пробелов, это резервирует пространство для оси Y. Второй, третий, четвертый и последующие маркеры все расположены на одинаковом расстоянии друг от друга, согласно значению X-axis-label-spacing .
Вторая строка оси X состоит из номеров, которые также разделены согласно значению переменной X-axis-label-spacing .
Значение переменной X-axis-label-spacing надо задавать кратным значению переменной symbol-width , ведь вполне возможно что вы захотите изменить ширину символа, который мы используем для создания тела графика и тогда ось X автоматически воспримет это изменение.
Функция print-X-axis будет очень похожа на функцию print-Y-axis , разве что она будет печатать две строки: строку маркеров и строку номеров. Мы создадим две разные функции для печати каждой строки и затем обьединим их в общей функции print-X-axis .
Получается трехшаговый процесс:
-
Создать функцию для печати маркеров по оси X print-X-axis-tic-line .
Первая функция должна напечатать маркеры для оси X. Нам надо будет задать сами маркеры и разделяющее их пространство.
(Напомним, что значение graph-blank инициализируется тоже с помощью выражения defvar . Предикат boundp проверяет инициализировани ли его аргумент; если нет то boundp возвращает nil . Если бы символ graph-blank был бы еще неинициализирован и мы бы не использовали условную конструкцию if , тогда бы мы получили следующее сообщение об ошибке `Symbol's value as variable is void' .)
Наша цель напечатать строку, которая выглядит следующим образом:
Первый маркер выровнен так, чтобы находиться под первой колонкой графика и освобождать место для оси Y.
Один полный элемент маркера состоит из пробелов, которые продолжаются от одной метки до другой, плюс символ маркера. Необходимое число пробелов можно определить зная ширину символа маркера и X-axis-label-spacing .
Соответствующий фрагмент кода выглядит следующим образом:
Теперь давайте определим сколько пробелов понадобится для того, чтобы правильно выровнять первую метку с первым столбцом графика. Здесь мы будем использовать значение full-Y-label-width вычисляемое в функции print-graph .
Соответствующий код выглядит следующим образом:
Также нам надо определить длину горизонтальной оси, которая равна длине списка номеров и количеству меток на горизонтальной оси:
Теперь все готово для создания функции, которая напечатает строку координатных маркеров по оси X:
Вставка строки номеров очень похожа:
Вначале мы создаем пронумерованный элемент с необходимым числом пробелов перед каждым номером:
Теперь можно написать функцию, которая напечатает строку номеров для координатной оси, при этом под первым столбцом графика мы напечатаем "1":
Наконец нам надо создать функцию print-X-axis , в которой мы будем использовать и print-X-axis-tic-line и print-X-axis-numbered-line .
Эта функция должна определить локальные значения переменных, используемые и print-X-axis-tic-line и print-X-axis-numbered-line , и вызвать эти две функции. Также она должна напечатать символ перевода строки, которая должна разделять строку маркеров и строку номеров.
Функция состоит из списка переменных, который описывает пять локальных переменных, и вызовов каждой из двух печатающих различные строки функций:
Давайте проверим print-X-axis :
-
Установите X-axis-tic-symbol , X-axis-label-spacing , print-X-axis-tic-line , так же как X-axis-element , print-X-axis-numbered-line , и print-X-axis .
Скопируйте следующее выражение:
Emacs должен напечатать следующую горизонтальную ось:
C.4 Печать полного графика
Сейчас мы почти готовы создать функцию для печати всего графика.
Функция для печати графика с координатными осями будет в целом соответствовать шаблону, который мы уже создали ранее, (see section Пример графика с пометками) но с небольшими добавлениями.
Последняя версия будет отличаться от более ранней двумя нюансами: во-первых, она содержит дополнительных локальные переменные; а во-вторых мы предусмотрим опцию, которая будет задавать масштаб меток по оси. Это может оказаться очень критичным; в противном случае в графике может быть больше строк чем способно уместиться на дисплее или на листе бумаги.
Эта новая возможность требует незначительной модификации функции Y-axis-column , надо добавить необязательный параметр vertical-step . Функция выглядит следующим образом:
Значения для максимальной высоты графика и ширины символа вычисляются функцией print-graph в выражении let ; поэтому функцию graph-body-print надо немного изменить.
Наконец код функции print-graph :
C.4.1 Проверка функции print-graph Быстрая проверка. C.4.2 График количества слов в функциях C.4.3 Окончательная печать графика C.4.1 Проверка функции print-graphМы можем вначале проверить функцию print-graph с помощью короткого списка чисел
-
Установите последнии версии Y-axis-column , graph-body-print и print-graph (а также весь остальной код.)
Emacs напечатает следующий график:
С другой стороны, если вы передадите в print-graph значение 2 для аргумента vertical-step , вычислив следующее выражение:
График будет выглядеть следующим образом:
C.4.2 График количества слов в функцияхТеперь мы сможем наконец полностью напечатать график, который покажет сколько определений функций содержит менее 10 слов и символов, сколько между 10 и 19, сколько между 20 и 29 и так далее.
Это многошаговый процесс. Сначала убедитесь, что вы загрузили весь требуемый код.
Неплохая мысль заново установить top-of-ranges в случае если вы уже установили ее в другое значение. Вы можете вычислить следующее:
Затем надо создать список номеров слов и символов в каждом диапазоне.
На моей машине это заняло около одного часа. Необходимо было просмотреть все 303 файла в моей версии Emacs. После всех вычислений переменная list-for-graph имела следующее значение:
Все это значило, что в моей версии Emacs было 537 определений функции, в которых было менее 10 слов, 1027 определений функции, в которых от 10 до 19 слов, 955 определений функции, в которых 20 от 29 слов, и так далее.
Ясно, что одного взгляда на этот список достаточно, чтобы понять что наибольшее число функции содержат от десяти до тридцати слов.
Теперь о печати. Мы не хотим напечатать график в котором около тысячи строк . . Вместо этого нам бы хотелось напечатать график в котором менее 25 строк. Это число строк можно отобразить почти на любом мониторе.
Это означает, что каждое значение в списке list-for-graph надо уменьшить в пятьдесят раз.
Ниже коротенькая функция, которая сможет сделать это правда с помощью двух еще незнакомых нам функций, mapcar и lambda .
Выражение lambda Создание безымянной функции. Функция mapcar Запуск функции для каждого элемента списка. Последняя ошибка . самая коварная Последняя ошибка обычно самая коварная. Выражение lambdalambda это символ, обозначающий безымянную функцию, то есть функцию, которой не присвоено имя. Каждый раз когда вы используете безымянную функцию вам нужно включать все ее определение.
это определение функции, котороя возвращает результат от деления переданного ее аргумента arg на 50.
В одной из ранних глав мы создали функцию умножить-на-семь , которая, как вы сразу же вспомнили умножала, переданный в нее аргумент на семь. Безымянная функция, выполняющая ту же работу, будет выглядеть следующим образом:
если нам понадобиться умножить 3 на 7, мы можем написать:
Это выражение вернет 21.
С помощью безымянной функции, надо написать следующее:
Если нам надо разделить 100 на 50, тогда мы запишем:
Это выражение вернет 2. В функцию передан аргумент 100, который будет разделен на 50.
Дополнительную информацию о безымянных функция можно найти как всегда в справочном руководстве по Emacs Lisp See section `Lambda Expressions' in The GNU Emacs Lisp Reference Manual .
Функция mapcarmapcar это функция, которая по очередно вызывает свой первый аргумент (функцию) с каждым элементом своего второго аргумента. Второй аргумент должен быть списком.
Функция 1+ , добавляющая единицу к своему аргументу выполняется для каждого элемента списка, в результате чего возвращается новый список.
Сравните действие функции mapcar с функцией apply , которая применяет свой первый аргумент ко всем оставшимся сразу. (See section Изготовляем график, где описано использование apply .)
В определении one-fiftieth первый аргумент это безымянная функция:
а второй аргумент это full-range , который будет связан с list-for-graph .
Таким образом все выражение будет выглядеть следующим образом:
See section `Mapping Functions' in The GNU Emacs Lisp Reference Manual , можно найти дополнительную информацию о mapcar .
С помощью функции one-fiftieth мы можем создать список в котором каждый элемент будет в пятьдесят раз меньше чем соответствующий элемент в списке list-for-graph .
Результирующий список выглядит следующим образом:
Вот этот список мы почти готовы напечатать!
Последняя ошибка . самая коварнаяЯ сказал `почти готовы печатать'! Конечно же есть ошибка в функции print-graph . . В ней есть опция vertical-step , но нет опции horizontal-step . Переменная top-of-range принимает значения от 10 до 300 с шагом 10. Но функция print-graph напечатает только первые значения.
Это класический пример ошибки, которые некоторые считают наиболее коварным типом ошибки, ошибка упущения. Эту ошибку вы не сможете найти изучая программу, так как ее нет там; это упущенная возможность. Лучше всего если вы будете чаще переписывать свою программу; и попробуете писать программу, так чтобы ее было легче понимать и изменять. Попытайтесь осознать, очень простой факт, все что вы написали рано или поздно надо будет изменить. Это основной закон практического программирования.
Основному изменению подвергнется функция print-X-axis-numbered-line , после этого надо будет немного подправить функции print-X-axis и print-graph . Не так уж много, но есть одна тонкость числа должны быть под символами маркера. Придется немного подумать.
Ниже скорректированная функция print-X-axis-numbered-line :
Если вы читаете этот документ в Инфо, вы можете увидеть полностью новые версии функций print-X-axis print-graph и вычислить их. Если вы читаете книгу, вы увидите только измененные строки (не хочется печатать слишком много лишнего).
C.4.3 Окончательная печать графикаТеперь когда вы все исправили и заново установили можно наконец вызвать функцию print-graph следующим образом:
А вот и долгожданный график:
Из графика видно, что больше всего функций, которые содержат от 10 до 19 слов.