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

14. Считаем слова в defun

Наш следующий проект --- сосчитать количество слов в определении функции. Конечно это можно сделать и с помощью какого-нибудь варианта count-word-region. See section Считаем слова: повторения и регулярные выражения. Если нам надо только лишь узнать сколько слов содержится в определении функции, то мы можем пометить определение функции, нажав C-M-h (mark-defun), и после этого вызвать count-word-region. Однако, давайте будем более амбициозны: попробуем сосчитать число слов и символов в каждом определении функции во всех файлах входящих в дистрибутив Emacs и затем нарисуем график, который покажет сколько функций какой длины: сколько функций содержат от 40 до 49 слов, сколько от 50 до 59, и так далее. Мне всегда было любопытно узнать какова средняя длина обычной функции, и скоро мы об этом узнаем.

Разделяй и властвуй  
14.1 Что считать?  
14.2 Что входит в состав слов или символов?  
14.3 Функция count-words-in-defun  Очень похожа на count-words.
14.4 Обработка нескольких функций в одном файле  
14.5 Открытие файла  
14.6 Подробности о lengths-list-file  Список длин многих функций.
14.7 Считаем слова в функциях из нескольких файлов  
14.8 Рекурсивная обработка нескольких файлов  
14.9 Подготовка данных для вывода в виде графика  

Разделяй и властвуй

Изложенный в одном предложении наш проект, где надо будет создать график-гистограмму выглядит довольно устрашающе; но если разбить его на несколько отдельных задач, каждую из которых можно сделать отдельно, то проект становиться совсем не сложным. Попробуем представить какими должны быть эти шаги:

Получился весьма значительный проект! Но если мы не спеша выполним каждый этап, то и весь проект окажется довольно простым.


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

14.1 Что считать?

Если мы начнем раздумывать о том, как сосчитать слова в определении функции, то первый вопрос (он обязательно должен появиться) --- что мы собираемся считать? Когда мы говорим `слова' по отношению к определению функции на Лиспе, то на самом деле, мы в большинстве случаев подразумеваем `символы'. Например, следующая функция умножить-на-семь содержит пять символов defun, умножить-на-семь, number, *, и 7. Кроме этого, в строке документации содержится еще четыре слова: `Multiply', `NUMBER', `by', и `seven'. Символ `number' повторяется, поэтому все определение содержит всего десять слов и символов.

 
(defun умножить-на-семь (number)
  "Multiply NUMBER by seven."
  (* 7 number))

Однако, если мы пометим определение функции умножить-на-семь нажав C-M-h (mark-defun), и затем вызовем функцию count-words-region для выделенной области, то функция count-words-region сообщит нам, что в области содержится 11 слов! Что-то неправильно!

Произошло следующее: функция count-words-region не посчитала `*' как слово, и сосчитала один символ умножить-на-семь, как три слова. Дефисы были обработаны таким образом, как будто они были разделителями слов, а не соединителями: функция `умножить-на-семь' посчитала так, как будто было написано `multiply by seven'.

Причина такой путаницы кроится в функции поиска по регулярному выражению, в определении count-words-region, которое перемещает курсор вперед слово за словом. В первоначальной версии count-words-region, регулярное выражение было следующим:

 
"\\w+\\W*"

Это регулярное выражение --- шаблон определяющий один или более символов входящих в состав слова, за которыми могут следовать один или более символов не входящих в состав слова. Обьяснение того, что означает `символ входящий в состав слова' подводит нас к теме синтаксиса, которая заслуживает отдельного обсуждения.


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

14.2 Что входит в состав слов или символов?

Emacs обрабатывает разные символы, как принадлежащие к разным синтаксическим категориям. Например, регулярное выражение `\\w+' --- это образец описывающий один или более символов составляющих слово. Символы составляющие слово являются членами одной синтаксической категории. Другие синтаксические категории включают класс символов пунктуации, таких как запятая и точка, и класс пробельных символов, таких как пробел и табуляция. (Для получения дополнительной информации смотрите разделы section `The Syntax Table' in The GNU Emacs Manual и section `Syntax Tables' in The GNU Emacs Lisp Reference Manual.)

Таблицы синтаксиса описывают то, какие символы принадлежат к каким категориям. Обычно дефис не считается `символом входящим в состав слова'. Вместо этого он принадлежит к `классу символов входящих в состав имен символов, но не слов'. Это означает, что функция count-words-region обрабатывает его таким же образом как и разделяющие слово пробелы. Вот почему count-words-region сосчитала `умножить-на-семь' как три слова.

Существуют два способа заставить Emacs воспринимать `умножить-на-семь', как один символ --- изменить синтаксическую таблицу или изменить регулярное выражение.

Мы можем переопределить дефис, как символ входящий в состав слова, изменив таблицу синтаксиса, которую Emacs создает для каждого буфера. В принципе именно этого мы и добиваемся, но оказывается, что дефис далеко не единственный символ, который считается не входящим в состав слова, но входит в состав имен символов; есть и другие, например `*'.

Пойдя другим путем, мы можем создать другое регулярное выражение, которое будет использоваться в определении count-words для подсчета символов. Правда оказывается что это не такая уж простая задача.

Первая часть регулярного выражения достаточна проста: шаблон должен совпадать "по крайней мере с одним символом который входит в состав слова или символа Лиспа". То есть:

 
\\(\\w\\|\\s_\\)+

`\\(' --- это первая часть группирующей конструкции, которая включает альтернативу `\\w' и `\\s_', разделяемые строкой `\\|'. `\\w' совпадает с любым символом входящем в состав слова, а `\\s_' совпадает с любым символом входящем в состав символа Лиспа, но не входящим в состав слова. Знак `+', следующий за всей конструкцией означает, что символ входящий в состав слова или символа Лиспа должен совпасть по крайней мере один раз.

Однако сконструировать вторую часть регулярного выражения значительно сложнее. Мы хотим, чтобы за первой частью следовала "необязательная часть состоящая из одного или нескольких символов не входящих в состав слова или символа Лиспа". Вначале мне казалось, что это можно сделать следующим образом:

 
\\(\\W\\|\\S_\\)*"

Знаки в верхнем регистре `W' и `S' совпадают с символами, которые не входят в состав слова или символа Лиспа. К несчастью выяснилось, что это выражение совпадает с любым символом, который не входит в состав слова или не входит в состав символа. Это совпадает с любым символом!

Потом я обратил внимание, что за каждым словом или символом в моей области следовал пробельный символ (пробел, табуляция, или символ новой строки). Поэтому я попробовал шаблон совпадающий с одним или более пробелом, которые следуют за одним или более символом составляющих слово или символ Лиспа. Это тоже было неудачной попыткой. Слова и символы Лиспа часто разделяются пробелами, но в реальном коде за символами Лиспа могут следовать круглые скобки, а за словами могут следовать знаки пунктуации. В конце концов я пришел, к образцу, в котором за одним или более символом входящем в состав слова или символа Лиспа необязательно следуют не пробельные символы за которыми следуют необязательные пробелы.

Ниже получившееся регулярное выражение:

 
"\\(\\w\\|\\s_\\)+[^ \t\n]*[ \t\n]*"


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

14.3 Функция count-words-in-defun

Мы уже видели несколько способов создания функции count-word-region. Чтобы написать функцию count-words-in-defun нам надо только немного изменить одну из этих версий.

Та версия, в которой используется цикл while является наиболее простой, поэтому я предлагаю использовать именно ее. Поскольку count-words-in-defun будет вызываться из более сложной программы, то ее не обязательно делать интерактивной и она не должна отображать сообщение в эхо-области; достаточно только лишь вернуть результат. Все это немного упрощает определение функции.

С другой стороны, функция count-words-in-defun будет использоваться в буфере, который содержит несколько определений функций. Соответственно появляется вопрос: как функция будет работать, как она будет определять, что точка находится в пределах функции и возвращать число символов и слов для этого определения функции. Все это конечно добавляет сложностей в опредление функции, но впоследствии нам не надо будет передавать в эту функцию аргументы.

Все вышеизложенное ведет к следующему шаблону будущей функции:

 
(defun count-words-in-defun ()
  "документация..."
  (установки...
     (цикл while...)
   возвращаем count)

Как обычно наша работа только лишь заполнить части этого шаблона.

В начале рассмотрим подготовительные установки.

Мы предполагаем, что эта функция будет вызвана из буфера содержащего несколько определений функций. Курсор будет находиться или внутри определения функции или вне определения. Для того, чтобы count-words-in-defun начала работать надо переместить точку в начало определения, счетчик слов установить в ноль и цикл, в котором будет происходить счет, должен остановиться, когда точка достигнет конца определения функции.

Функция beginning-of-defun производит поиск в обратном направлении открывающей скобки, такой как `(', находящейся в начале строки и перемещает точку на эту позицию или к границе поиска. На практике это означает, что beginning-of-defun перемещает точку к началу ближайшего определения функции или к началу буфера. Мы можем использовать beginning-of-defun для того чтобы переместить точку туда, откуда мы начнем отсчет.

В цикле while нам также потребуется счетчик для того, чтобы хранить число уже посчитанных слов и символов. Для создания и инициализации нулем соответствующей переменной, которая может быть локальной, мы будем использовать выражение let.

Функция end-of-defun работает аналогично функции beginning-of-defun, кроме того, что она перемещает точку в конец определения функции. Функцию end-of-defun можно использовать как часть выражения, которое находит положение конца определения анализируемой функции.

Инициализация и начальные установки для count-words-in-defun быстро принимают форму: сначала мы перемещаем точку в начало определения, затем мы создаем локальную переменную для хранения результатов подсчета, и, наконец, мы находим позицию конца определения, так чтобы цикл while знал когда нужно прекращать поиск.

Эта часть функции выглядит следующим образом:

 
(beginning-of-defun)
(let ((count 0)
      (end (save-excursion (end-of-defun) (point))))

Все довольно просто. Единственная сложность касается установки локальной переменной end: она инициализируется значением, которое возвращает выражение save-excursion, когда курсор временно перемещается к конец определения и с помощью функции point, возвращает значение точки. После этого курсор возвращается в начало определения функции.

Вторая часть функции count-words-in-defun --- это цикл while.

Этот цикл должен содержать выражение, которое будет перемещать курсор вперед через слово или символ Лиспа, и выражение, которое будет подсчитывать число перемещений. Проверка-истина-ложь цикла while должна возвращать истинное значение до тех пор, пока курсор находится внутри определения функции и ложное значение, если он достиг конца определения. Мы уже создали подходящее регулярное выражение для этого поиска (see section 14.2 Что входит в состав слов или символов?), поэтому структура цикла ясна:

 
(while (and (< (point) end)
            (re-search-forward 
             "\\(\\w\\|\\s_\\)+[^ \t\n]*[ \t\n]*" end t)
  (setq count (1+ count)))

В третей части функции нужно всего лишь вернуть количество подсчитанных слов и символов Лиспа. Эта часть является последним выражением в теле верхнего выражения let, и может быть просто локальной переменной count, которое после вычисления возвратит результаты подсчетов.

Если сложить все вышеизложенные части вместе, то получится следующее определение функции count-words-in-defun:

 
(defun count-words-in-defun ()
  "Возвращает число слов и символов Лиспа в функции."
  (beginning-of-defun)
  (let ((count 0)
        (end (save-excursion (end-of-defun) (point))))
    (while
        (and (< (point) end)
             (re-search-forward 
              "\\(\\w\\|\\s_\\)+[^ \t\n]*[ \t\n]*"
              end t))
      (setq count (1+ count)))
    count))

Как нам проверить эту функцию? Хотя сама функция не интерактивна, но можно легко создать функцию-обертку (wrapper), которая будет интерактивной и вызовет эту функцию; мы можем использовать почти тот же самый код, что и в рекурсивной версии count-words-region:

 
;;; Интерактивная версия.
(defun count-words-defun ()     
  "Число слов и символов Лиспа в определении функции."
  (interactive)
  (message
   "Считаем слова и символы Лиспа в определении функции...")
  (let ((count (count-words-in-defun)))
    (cond
    ((zerop count)
       (message
	"Область НЕ содержит слов."))
    ((= 1 count)
       (message
	"Область содержит 1 слово."))
    ((or (< 1 count) (> 5 count))
       (message
	"В области содержиться %d словa." count))
      (t
       (message
	"Область содержит %d слов." count)))))

Для удобства давайте свяжем эту функцию с C-c =:

 
(global-set-key "\C-c=" 'count-words-defun)

Сейчас мы можем проверить count-words-defun: установите обе функции count-words-in-defun и count-words-defun, вычислите выражение global-set-key, скопируйте определение функции умножить-на-семь в буфер `*scratch*' так чтобы открывающая определение скобка находилась в начале строки и поместив курсор в тело определения функции нажмите C-c=:

 
(defun умножить-на-семь (number)
  "Multiply NUMBER by seven."
  (* 7 number))
     => 10

Получилось! Определение содержит 10 слов и символов Лиспа.

Следующая задача --- подсчитать количество слов и символов Лиспа в нескольких определениях функций в одном файле.


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

14.4 Обработка нескольких функций в одном файле

В таком файле как `simple.el' может находиться более 80 определений различных функций. Конечная цель нашего проекта --- статистическая обработка определений функций во многих файлах, но, как первых шаг на пути к этой цели, давайте проанализируем только один файл.

Информация будет представлена в виде серии чисел, где каждое число --- количество слов и символов Лиспа в определении функции. Мы можем хранить эти числа в списке.

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

Команды для посчета слов содержат одно выражение, которое перемещает курсор вперед через слово, а также выражение подсчитывающее число перемещений. Определение функции для посчета длин может быть создано подобным же образом, где одно выражение будет перемещать курсор вперед через определение, а второе выражение будет составлять список длин.

Подобная аналогия делает создание такой функции почти элементарным. Ясно, что мы должны начать отсчет с начала файла, поэтому первой командой будет (goto-char (point-min)). Затем мы запустим цикл while; где проверка-истина-ложь для этого цикла может быть регулярным выражением для поиска следующего определения функции --- пока поиск успешен, точка перемещается вперед и вычисляется тело цикла. В теле цикла нам понадобится выражение, которое будет конструировать список длин. Для этого вполне подойдет основная команда для создания списков --- cons. В принципе это почти все.

Ниже фрагмент получившейся функции:

 
(goto-char (point-min))
(while (re-search-forward "^(defun" nil t)
  (setq lengths-list
        (cons (count-words-in-defun) lengths-list)))

Что мы пока пропустили --- так это механизм, для открытия файла, в котором содержатся определения функций.

В предыдущих примерах, мы использовали или систему Info или переключались на время в другой буфер; в основном в буфер `*scratch*'.

Открытие (finding) файла --- эта новая и еще не обсуждавшаяся нами тема.


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

14.5 Открытие файла

Для того чтобы открыть файл в Emacs вы обычно используете команду C-x C-f (find-file). Эта команда делает то, что надо, но все же она не совсем подходит для нашей задачи определения длин функций.

Давайте взглянем на исходный код find-file (для этого вы можете использовать команду find-tag(11)).

 
(defun find-file (filename)
  "Edit file FILENAME.
Switch to a buffer visiting file FILENAME,
creating one if none already exists."
  (interactive "FFind file: ")
  (switch-to-buffer (find-file-noselect filename)))

В это определение входит короткая, но полная документация и спецификация для интерактивного использования функции, откуда видно, что Emacs запросит у вас имя файла, если вы будете использовать эту функцию интерактивно. В теле определения используется всего лишь две функции find-file-noselect и switch-to-buffer.

Если с помощью C-h f (команда describe-function) вызвать документацию для функции find-file-noselect, то выяснится, что эта функция считывает заданный файл в буфер и возвращает буфер. Однако этот буфер не становится выбранным. То есть внимание Emacs на него не переключается (значит и ваше тоже). Для переключения в буфер используется функция switch-to-buffer, и она отображает его содержимое в текущем окне. Мы уже обсуждали тему переключения буферов. (See section 2.3 Смена буфера.)

В нашем проекте создания гистограммы нам не нужно отображать каждый файл на экране, в то время, когда программа находит длины функций в этом файле. Вместо использования switch-to-buffer мы вполне можем использовать функцию set-buffer --- эта функция производит переключение в другие буфера, но не отображает их на экране. Поэтому, вместо того, чтобы пользоваться функцией find-file, для открытия файла, нам придется создать свое собственное выражение. Но мы к этому вполне готовы, можно просто воспользоваться функциями find-file-noselect и set-buffer.


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

14.6 Подробности о lengths-list-file

Ядро функции lengths-list-file --- это цикл while, в котором содержится код перемещения курсора вперед, в поисках регулярного выражения "^defun" и функция, которая будет подсчитывать число слов и символов Лиспа в каждом из определений. Это ядро должно быть окружено функциями, которые выполняют различные вспомогательные задачи: открывают файл, перемещают точку к началу файла. Получившиеся определение функции выглядит следующим образом:

 
(defun lengths-list-file (filename)
  "Возвращает список длин функций содержащихся в ФАЙЛЕ.
Возвращаемый список --- это список чисел.
Каждое число --- количество слов или символов Лиспа
в одном определении функции."
  (message "Анализируем `%s' ... " filename)
  (save-excursion
    (let ((buffer (find-file-noselect filename))
          (lengths-list))
      (set-buffer buffer)
      (setq buffer-read-only t)
      (widen)   
      (goto-char (point-min))
      (while (re-search-forward "^.defun" nil t)
        (setq lengths-list
              (cons (count-words-in-defun) lengths-list)))
      (kill-buffer buffer)
      lengths-list)))

Функция принимает один аргумент --- имя файла, с которым она и будет работать. Затем идут четыре строки описывающие работу функции, но выражение interactive отсутствует. Поскольку обычно люди думают что компьютер сломался, если они не видят результатов происходящей работы, то в первой строке тела функции мы отображаем информационное сообщение.

Следующая строка --- это выражение save-excursion, которое возвращает внимание Emacs к текущему буферу, когда функция завершит свою работу. Это полезно в том случаe, если вы будете использовать эту функцию из другой, которая полагает, что курсор остается в текущем буфере.

В списке переменных выражения let Emacs открывает файл и связывает локальную переменную buffer с буфером, в котором содержится открытый файл. Помимо этого Emacs также создает еще одну локальную переменную --- lengths-list.

После этого Emacs переключается в новый буфер.

Затем Emacs защищает буфер от случайного изменения, устанавливая для него режим только для чтения. Вообще-то это совершенно не обязательно. Ни одна из функций для подсчета слов и символов Лиспа в определении функции не будет изменять содержимое буфера. Кроме этого буфер никто не собирается сохранять, даже если он будет изменен. Эта строка лишь привычка следовать пословице "Береженого бог бережет". Причина такой излишней осторожности кроется в том, что эта функция, и те которые будут ее использовать работают с исходными текстами самого Emacs и будет очень неудобно, если мы случайно изменим какой-то из файлов. Ладно признаюсь, сначала я и сам думал, что эта строка не нужна, до тех пор пока мои экспериментальные функции не начали изменять исходные тексты Emacs ....

После этого мы вызываем функцию widen, на тот случай, если в буфер включено сужение. Обычно это также не требуется --- Emacs создает новый буфер, если его еще не существовало; но если этот файл уже был открыт, то Emacs возвращает уже существующий буфер. В таком случае в нем теоретически может быть включено сужение, и для начала работы его необходимо выключить. Если бы мы хотели быть совсем `дружелюбными к пользователю', то мы бы окружили бы это все выражением save-restriction, для того чтобы востановить сужение и местоположение точки, но мы не хотим этого делать.

Следующее выражение, (goto-char (point-min)), перемещает курсор в начало буфера.

Затем идет цикл while, в котором и выполняется `основная работа' функции. В цикле Emacs определяет длину каждой функции и создает список, в котором и сохраняет результаты работы.

После того, как содержимое файла будет проанализировано, Emacs уничтожает буфер. Это сделано для экономичного использования оперативной памяти. В моей версии Emacs 19 содержится более 300 различных файлов. Другая функция будет использовать lengths-list-file на каждом из них. Если Emacs откроет все эти файлы, и не будет их закрывать, то на моем компьютере может не хватить виртуальной памяти.

Последнее выражение в теле функции и одновременно в выражении let --- это переменная lengths-list; ее значение будет возвращено, как значение всей функции.

Мы можем испробовать эту функцию установив ее как обычно. Теперь расположите курсор за следующим выражением и нажмите C-x C-e (eval-last-sexp).

 
(lengths-list-file "../lisp/debug.el")

(Может быть вам понадобиться изменить путевое имя файла; в приведенном примере предполагается, что файлы Info и исходные тексты Emacs расположены по соседству, например, в /usr/local/emacs/info и /usr/local/emacs/lisp.) Вы можете изменить это выражение скопировав его в буфер `*scratch*' и вычислив его там.

Моей версии Emacs потребовалось несколько секунд, чтобы выдать список длин функций для файла `debug.el'. Он получился следующим:

 
(75 41 80 62 20 45 44 68 45 12 34 235)

Обратите внимание, что длина последней функции в этом файле, в списке расположена первой.


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

14.7 Считаем слова в функциях из нескольких файлов

В предыдущем разделе мы создали функцию, которая возвращает список длин каждого из определений функций в файле. Сейчас нам надо будет сконструировать функцию, которая возвращает большой список длин функций находящихся во многих файлах, эта функция будет работать со списком файлов.

Работа на каждом файле из списка --- это повторяющийся процесс, поэтому мы можем использовать либо цикл while, либо рекурсию.

Создать цикл while --- для нас уже совсем не сложно. Аргумент передаваемый в функцию --- это список файлов. Как мы уже видели раньше (see section 11. Циклы и рекурсия), вы можете написать такой цикл while тело которого будет вычисляться до тех пор, пока в списке еще содержатся какие-нибудь элементы, но как только список станет пуст, то выполнение цикла прекращается. Для того, чтобы эта конструкция работала правильно в теле цикла должно содержаться выражение, которое укорачивает список, каждый раз когда вычисляется тело цикла, так что постепенно список опустошается. Обычно для этой цели используют функцию cdr, и с ее помощью при каждой итерации цикла связывают значение списка с CDR этого же списка.

Соответствующий шаблон выглядит следующим образом:

 
(while проверить-пуст-ли-список
  тело...
  присвоить-списку-cdr-списка)

Также мы помним, что цикл while всегда возвращает nil после вычисления (результат вычисления проверки-истина-ложь), а не результат какого-нибудь выражения вычисляемого в теле цикла. (Все вычисления в теле цикла выполняются только ради их побочного эффекта). Однако, нам хотелось бы вернуть результат вычисления выражения, в котором создается список длин. Для того чтобы выполнить это мы должны вложить цикл while в выражение let, и сделать так, чтобы последним элементом выражения let была переменная в которой содержится список длин. (See section Циклы и рекурсия.)

Все это ведет нас к следующему определению функции:

 
;;; Используя цикл while.
(defun lengths-list-many-files (list-of-files) 
  "Возвращает список длин функций в LIST-OF-FILES."
  (let (lengths-list)

;;; проверка-истина-ложь
    (while list-of-files        
      (setq lengths-list
            (append
             lengths-list

;;; Генерируем список длин функций.
             (lengths-list-file
              (expand-file-name (car list-of-files)))))

;;; Укорачиваем список анализируемых файлов.
      (setq list-of-files (cdr list-of-files))) 

;;; Возвращает результат: значение списка длин функций.
    lengths-list))              

expand-file-name --- это встроенная функция, которая преобразует относительное имя файла в его абсолютную, полную форму. То есть,

 
debug.el

становится

 
/usr/local/emacs/lisp/debug.el

Еще один новый для нас элемент в этом определении функции --- это незнакомая нам пока функция append, которая заслуживает краткого рассмотрения.

14.7.1 Функция append  Добавление одного списка к другому.


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

14.7.1 Функция append

Функция append присоединяет один список к другому. Так,

 
(append '(1 2 3 4) '(5 6 7 8))

вернет после вычисления

 
(1 2 3 4 5 6 7 8)

Именно так мы и хотим соединять друг с другом списки длин, которые возвращает функция lengths-list-file в теле цикла while. Принцип работы append отличен от принципа работы cons.

 
(cons '(1 2 3 4) '(5 6 7 8))

сконструирует новый список, в котором первый аргумент функции cons станет первый элементом результирующего списка:

 
((1 2 3 4) 5 6 7 8)


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

14.8 Рекурсивная обработка нескольких файлов

Кроме использования цикла while мы можем обработать список файлов с помощью рекурсии. Рекурсивная версия функции lengths-list-many-files получится короче и проще.

Рекурсивная функция содержит обычные части: `рекурсивную-проверку', `выражение-следующего-вызова', и сам рекурсивный вызов. `Рекурсивная-проверка' определяет нужно ли снова выполнять рекурсивный вызов, что необходимо в том случае, если list-of-files содержит еще какие-то элементы; `выражение-следующего-вызова' укорачивает list-of-files устанавливая его в CDR самого себя, поэтому постепенно список опустошается. Вообще то сама функция получилась короче чем это абзац!

 
(defun recursive-lengths-list-many-files (list-of-files) 
  "Возвращает список длин функций из LIST-OF-FILES."
  (if list-of-files                     ; рекурсивная-проверка
      (append
       (lengths-list-file
        (expand-file-name (car list-of-files)))
       (recursive-lengths-list-many-files
        (cdr list-of-files)))))

В одном предложении, функция возвращает список длин для первого файла из list-of-files к которому добавляется результат рекурсивного вызова этой же функции, когда аргумент будет остаток list-of-files, то есть cdr списка.

Ниже представлены результаты проверки функции recursive-lengths-list-many-files вместе с результатами запуска функции lengths-list-file на каждом из файлов по отдельности.

Установите обе эти функции, и recursive-lengths-list-many-files, и lengths-list-file, а затем вычислите следующие выражения. Возможно вам понадобится изменить путевые имена файлов; те которые представлены здесь работают, если файлы Info и исходные тексты Emacs расположены рядом. Если вам понадобится изменить эти выражения, то вы можете сначала скопировать их в буфер `*scratch*' и внеся необходимые изменения вычислить их.

Результаты показаны после `=>' (Эти результаты для файлов входящих в Emacs версии 18.57; если вы работает с другой версией Emacs, то результаты могут отличаться.)

 
(lengths-list-file 
 "../lisp/macros.el")
     => (176 154 86)

(lengths-list-file
 "../lisp/mailalias.el")
     => (116 122 265)

(lengths-list-file
 "../lisp/makesum.el")
     => (85 179)

(recursive-lengths-list-many-files
 '("../lisp/macros.el"
   "../lisp/mailalias.el"
   "../lisp/makesum.el"))
       => (176 154 86 116 122 265 85 179)

Функция recursive-lengths-list-many-files возвращает результат, который мы и ожидали.

Следующий шаг --- обработать данные таким образом, чтобы их легче было отобразить в виде графика.


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

14.9 Подготовка данных для вывода в виде графика

Функция recursive-lengths-list-many-files возвращает список чисел. Каждое число означает длину определения функции в словах и символах Лиспа. Сейчас нам надо будет преобразовать эти данные в список чисел, который будет удобнее отобразить в виде графика. Из нового списка должно быть ясно, сколько определений функций содержат меньше 10 слов, сколько содержат от 10 до 19 слов, сколько содержат от 20 до 29 слов и так далее.

Короче говоря, нам надо будет обработать список длин, который возвращает функция recursive-lengths-list-many-files, сосчитать число функций попадающих в определенные диапазоны длин, и составить список этих чисел.

Исходя из того, что мы уже научились делать, мы вполне можем предсказать, что не должно быть слишком трудным написать функцию, которая будет укорачивать (с помощью cdr) список длин, анализировать каждый элемент списка, определять к какому диапазону он принадлежит и увеличивать счетчик для этого диапазона.

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

14.9.1 Сортировка списков  
14.9.2 Создание списка файлов  


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

14.9.1 Сортировка списков

В Emacs имеется функция для сортировки списков, которая называется (как вы уже наверное догадались) sort. Функция sort принимает два аргумента --- список, который нужно отсортировать и предикат, который определяет какой из двух элементов списка "меньше".

Как мы уже говорили раньше (see section Использование объекта неправильного типа в качестве аргумента), предикат --- это функция, которая определяет истинно ли некоторое свойство или нет. Функция sort будет изменять список согласно свойству, которое анализирует наш предикат; это значит, что sort можно использовать для сортировки не только списков, состоящих из чисел, но и любых других, если использовать предикат для нечислового критерия --- например, это может быть алфавитный критерий.

Предикатом подходящим для сортировки чисел может быть функция <. Например,

 
(sort '(4 8 21 17 33 7 21 7) '<)

produces this:

 
(4 7 7 8 17 21 21 33)

(Обратите внимание, что в этом примере оба аргумента маскированы с помощью апострофа, чтобы интерпретатор Лиспа не вычислил их до того, как передать функции sort в виде аргументов).

Отсортировать список возвращаемый функцией recursive-lengths-list-many-files очень просто:

 
(sort
 (recursive-lengths-list-many-files
  '("../lisp/macros.el"
    "../lisp/mailalias.el"
    "../lisp/makesum.el"))
 '<)

в моем случае вывод был следующим:

 
(85 86 116 122 154 176 179 265)

(Обратите внимания, что в этом примере первый аргумент не маскирован, поэтому это выражение будет вычислено, а результирующий список будет передан в функцию sort).


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

14.9.2 Создание списка файлов

Функции recursive-lengths-list-many-files в качестве аргумента требуется список файлов. Для наших тестовых примеров мы составляли такой список вручную; но в дистрибутив Emacs входит очень много файлов с библиотеками Emacs Lisp. Поэтому, для того, чтобы создать список файлов с библиотеками Emacs Lisp входящих в дистрибутив Emacs мы воспользуемся функцией directory-files.

Функции directory-files требуется три аргумента: первый аргумент --- имя каталога, строка; если второй аргумент не равен nil, то функция возвратит список абсолютных имен файлов; третий аргумент задает шаблон поиска. Если третим аргументом задано регулярное выражение, то будут выбраны только имена файлов, которые соответствуют этому регулярному выражению.

Например, на моем компьютере,

 
(length
 (directory-files "../lisp" t "\\.el$"))

сообщила мне что моя версия 19.25 Emacs содержит 307 файлов с исходными текстами на Emacs Lisp.

Выражение, которое будет сортировать список возвращаемый функцией recursive-lengths-list-many-files выглядит следующим образом:

 
(sort
 (recursive-lengths-list-many-files
  (directory-files "../lisp" t "\\.el$"))
 '<)

Наша следующая задача --- создать список, который скажет нам сколько определений функций содержат менее 10 слов, сколько содержат от 10 до 19 слов, сколько содержат от 20 до 29 слов, и так далее. С отсортированным списком чисел эта задача становится намного проще: сосчитать сколько элементов списка меньше 10, после этого отбросив только что сосчитанные числа посчитать сколько меньше 20, теперь сосчитать сколько элементов списка меньше 30 и так далее. Каждое из этих чисел 10, 20, 30, 40, и им подобные на единицу больше чем самое большое число диапазона. Мы можем назвать список таких чисел top-of-ranges.

Если бы мы захотели мы могли бы произвести этот список автоматически, но проще создать его вручную. Ниже требуемый список:

 
(defvar top-of-ranges
 '(10  20  30  40  50
   60  70  80  90 100
  110 120 130 140 150
  160 170 180 190 200
  210 220 230 240 250
  260 270 280 290 300)
 "Список задающий диапазоны для `defuns-per-range'.")

Для того, чтобы изменить диапазоны мы должны будем изменить этот список.

Теперь нам надо написать функцию, создающую список, в котором каждое число --- количество определений функций, чья длина попадает в соответствующий диапазон. Ясно, что аргументами этой функции должны быть списки sorted-lengths и top-of-ranges.

Функция defuns-per-range должна выполнять две задачи снова и снова: она должна подсчитывать количество определений функций чья длина попадает в диапазон, заданый текущим верхним значением; и она должна сдвигаться к следующему значению в списке top-of-ranges после подсчета количества определений в текущем диапазоне. Поскольку каждое из этих действий является повторяющимся, то мы можем использовать циклы while. Один из циклов будет подсчитывать число определений в диапазоне, определенном текущим значением top-of-range, а другой цикл будет последовательно выбирать значения top-of-range.

Для каждого диапазона могут подходить несколько элементов sorted-lengths; это означает, что цикл обрабатывающий список sorted-lengths должен быть вложен в цикл работающий со списком top-of-ranges, как маленькая матрешка спрятана в большую.

Внутренний цикл будет подсчитывать число определений функции входящих в соответствующий диапазон. Это простой цикл, с которым мы уже хорошо знакомы (See section Цикл с увеличивающимся счетчиком. Проверка-истина-ложь для этого цикла должна проверять меньше ли значение из списка sorted-lengths, чем текущее значение верхней границы диапазона. Если да, то функция должна увеличивать счетчик и переходить к обработке следующего значения из списка sorted-lengths.

Внутренний цикл выглядит следующим образом:

 
(while длина-элемента-меньше-чем-граница-диапазона
  (setq number-within-range (1+ number-within-range))     
  (setq sorted-lengths (cdr sorted-lengths)))

Внешний цикл должнен начать работу с наименьшего значения из списка top-of-ranges, и после этого последовательно перебирать заданные значения. Это можно сделать следующим образом:

 
(while top-of-ranges
  тело-цикла...
  (setq top-of-ranges (cdr top-of-ranges)))

Если эти два цикла обьединить, то получится следующее:

 
(while top-of-ranges

  ;; Сосчитать число элементов входящих в текущий диапазон.
  (while длина-элемента-меньше-чем-граница-диапазона
    (setq number-within-range (1+ number-within-range))     
    (setq sorted-lengths (cdr sorted-lengths)))

  ;; Перейти в следующий диапазон.
  (setq top-of-ranges (cdr top-of-ranges)))

Кроме этого, при каждой итерации цикла, надо записывать количество функций, чья длина входит в этот диапазон (значение number-within-range) в список. Для этой цели мы можем использовать функцию cons. (See section cons.)

Функция cons выполнит работу как надо, разве что список, который она составит, будет содержать число самых длиных функций в начале списка, а число самых коротких функций в конце. Это происходит из-за того, что функция cons присоединяет новый элемент в начало списка, а поскольку мы будем обрабатывать список длин функций от коротких к длинным, то функция defuns-per-range-list закончит работу с самыми длиными функциями. Но на графике нам хотелось бы отобразить сначала наименьшие значения, а потом наибольшие. Для этого надо будет "перернуть" список defuns-per-range-list. Это можно выполнить с помощью функции nreverse, которая изменяет порядок элементов списка.

Например,

 
(nreverse '(1 2 3 4))

выдаст:

 
(4 3 2 1)

Следует отметить, что функция nreverse --- "деструктивная", то есть она изменяет список, который передан ей как аргумент; этим она отличается от функций car и cdr, которые не являются деструктивными. В нашем случае первоначальный список defuns-per-range-list нам не нужен, поэтому мы не возражаем против изменения этого списка. (Функция reverse наоборот возвращает перевернутую копию списка, оставляя первоначальный список без изменений).

Если сложить эти части вместе, то функция defuns-per-range будет выглядеть следующим образом:

 
(defun defuns-per-range (sorted-lengths top-of-ranges)
  "Число функций в SORTED-LENGTHS в каждом диапазоне TOP-OF-RANGES."
  (let ((top-of-range (car top-of-ranges))
        (number-within-range 0)
        defuns-per-range-list)

    ;; Внешний цикл.
    (while top-of-ranges

      ;; Внутренний цикл.
      (while (and 
              ;; Для проверки необходимо числовое значение.
              (car sorted-lengths) 
              (< (car sorted-lengths) top-of-range))

        ;; Найти число функций чья длина попадает в текущий диапазон.
        (setq number-within-range (1+ number-within-range))
        (setq sorted-lengths (cdr sorted-lengths)))

      ;; Выход из внутреннего цикла, но остаемся во внешнем.

      (setq defuns-per-range-list
            (cons number-within-range defuns-per-range-list))
      (setq number-within-range 0)      ; Сбросить счетчик в ноль.

      ;; Переходим в следующий диапазон.
      (setq top-of-ranges (cdr top-of-ranges))
      ;; Задать следующую верхнюю границу диапазона.
      (setq top-of-range (car top-of-ranges)))

    ;; Выход из внешнего цикла и сосчитать число функций чья длинна больше
    ;;   границе наибольшего диапазона из top-of-range.
    (setq defuns-per-range-list
          (cons
           (length sorted-lengths)
           defuns-per-range-list))

    ;; Вернуть список, где каждое число это количество функций, чья длина
    ;;  входит в диапазон от наименьшего к наибольшему.
    (nreverse defuns-per-range-list)))

Функция достаточно понятна, надо только обьяснить одну тонкость. Проверка-истина-ложь внутреннего цикла выглядит следующим образом:

 
(and (car sorted-lengths) 
     (< (car sorted-lengths) top-of-range))

хотя ожидалось следующее:

 
(< (car sorted-lengths) top-of-range)

Цель проверки --- определить меньше ли первый элемент списка sorted-lengths, чем значение для верхней границы диапазона.

Простая версия проверки работает превосходно, до тех пор, пока значение списка sorted-lengths не станет равным nil. В этом случае выражение (car sorted-lengths) возвращает значение nil. Функция < не может сравнить число и пустой список nil, поэтому Emacs сообщит об ошибке и остановит выполнение функции.

Список sorted-lengths всегда становится пустым, когда счетчик достигает конца списка. Это значит, что любая попытка использования функции defuns-per-range с простой проверкой-истина-ложь обречена на провал.

Мы решили эту проблему, используя выражение (car sorted-lengths) вместе с выражением and. Выражение (car sorted-lengths) возвращает значение не равное nil до тех пор, пока список содержит по крайней мере один элемент, если список пуст, то будет возвращено значение nil. Выражение and сначала вычисляет выражение (car sorted-lengths), и если в результате вычислений возвращается nil, то все выражение and возвращает ложь, не вычисляя выражения <. Но если выражение (car sorted-lengths) возвращает значение не равное nil, то выражение and вычисляется выражение <, и результат вычисления этого выражения будет результатом всего выражения and.

Таким путем мы исправляем возможную ошибку. See section 12.4 forward-paragraph: сокровищница функций, для получения дополнительной информации о функции and.

Давайте сразу проверим функцию defuns-per-range. Сначала вычислите выражения, которые связывают (короткий) список top-of-ranges со списком проверяемых значений, и инициализируют список sorted-lengths, после этого вычислите функцию defuns-per-range.

 
;; (В дальнейшем мы будем работать с большими списками.)
(setq top-of-ranges        
 '(110 120 130 140 150
   160 170 180 190 200))

(setq sorted-lengths
      '(85 86 110 116 122 129 154 176 179 200 265 300 300))

(defuns-per-range sorted-lengths top-of-ranges)

После вычисления вернется следующий список:

 
(2 2 2 0 0 1 0 2 0 0 4)

И в самом деле --- два элемента списка sorted-lengths меньше 110, два элемента принадлежат диапазону от 110 до 119, два элемента попадают в диапазон от 120 до 129, и так далее. Четыре элемента имеет значение большее 200.

Наша следующая задача превратить список этих чисел в график.


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

This document was generated on March, 10 2004 using texi2html