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

13. Подсчеты: Повторения и Регулярные выражения

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

Считаем слова  
13.1 Функция count-words-region  Используем regexp, но появляется проблема.
13.2 Рекурсивный подсчет слов  
13.3 Упражнения: Сколько знаков пунктуации  

Считаем слова

В стандартном дистрибутиве Emacs имеется функция для подсчета числа строк в области буфера. Однако, соответствующей функции для посчета слов нет.

Иногда в процессе работы требуется посчитать количество слов. Например, если вы пишете эссе, то вы можете поставить верхний предел в 800 слов; если вы создаете роман, можно заставить себя писать в день не меньше 1000 слов. Мне кажеться странным, что в Emacs нет такой функции. Возможно большинство людей, которые используют редактор пишут программы или создают документацию, в общем делают то, что не требует подсчета слов; или может быть всех удовлетворяет существующая в операционной системе команда wc, которая может подсчитать слова. Кроме этого возможно многие следуют соглашению издательств и находят число слов в документе путем деления общего числа символов на пять. В любом случае мы все равно создадим команду для посчета слов.


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

13.1 Функция count-words-region

Функция считающая слова может находить число слов в строке, параграфе, области или буфере. Что нам подойдет больше всего? Мы могли бы создать функцию, которая считает слова во всем буфере. Однако, в традициях Emacs принято создавать многоцелевые функции --- вам может потребоваться сосчитать слова не во всей книге, а только в одной главе. Поэтому будет лучше, если наша функция будет находить число слов в области. Как только мы напишем функцию count-words-region мы сможем при желании сосчитать количество слов во всем буфере, пометив его как область, набрав C-x h (mark-whole-buffer).

Ясно, что подсчет слов это циклический процесс: сначала мы прибавляем первое слово области, затем второе, потом третье, и так далее, до тех пор, пока мы не достигнем конца области. Это значит, что подсчет слов идеально подходит для рекурсивного или циклического решения с использованием while.

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

Как обычно шаблон для определения интерактивной функции следующий:

 
(defun имя-функции (список-аргументов)
  "документация..."
  (выражение-interactive...)
  тело...)

Все что надо --- это заполнить соответствующие места.

Имя функции должно быть запоминающимся и лучше, если оно будет походить на имя существующей функции count-lines-region. Так пользователю будет легче запомнить название функции. Неплохой выбор --- count-words-region.

Функция подсчитывает количество слов в области. Это значит, что список аргументов должен содержать символы, которые будут связаны с положением двух позиций --- началом и концом области. Эти символы можно назвать соответственно `beginning' и `end'. Также первая строка документации должна быть завершенным предложением, поскольку некоторые команды, такие как apropos, отображают только ее. Выражение, делающее функцию интерактивной командой, это хорошо знакомое нам выражение `(interactive "r")', которое сообщает Emacs, что начало и конец области надо связать с символами в списке аргументов нашей функции.

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

Когда пользователь вызывает функцию count-words-region курсор может быть в начале или конце области. Однако подсчет слов лучше производить с начала области. Это значит, что мы должны переместить курсор в начало области, вдруг при вызове функции он был в конце или середине области. Для верности мы воспользуемся функцией (goto-char beginning). Конечно же после того, как мы закончим работу курсор необходимо вернуть на прежнюю позицию. Поэтому, тело функции должно быть вложено в особую форму save-excursion.

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

Мы могли бы воспользоваться выражением (forward-word 1) для перемещения курсора вперед слово за словом, но лучше понять, что же, в редакторе GNU Emacs считается `словом' и воспользоваться функцией поиска регулярных выражений.

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

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

То есть нам понадобится регулярное выражение, которое будет совпадать с текстом, когда за одним или более символами составляющими слово может следовать один или несколько или ни одного символа не входящих в состав слов. Соответствующее регулярное выражение приведено ниже:

 
\w+\W*

Таблица синтаксиса текущего буфера определяет, какие символы могут входить в состав слова, а какие нет. (See section Что входит в состав слова или символа?, для получения дополнительной информации о синтаксисе. Также смотри section `The Syntax Table' in The GNU Emacs Manual, и, section `Syntax Tables' in The GNU Emacs Lisp Reference Manual.)

Выражение поиска в таком случает выглядит так:

 
(re-search-forward "\\w+\\W*")

(Обратите внимание на удвоенные обратные слэши перед `w' and `W'. Одиночный обратный слэш имеет специальное значение для интерпретатора Лиспа. Он означает, что следующий за ним символ должен обрабатываться особым образом. Например, два символа `\n' означают символ `новой строки', а не только обратный слэш за которым идет `n'. Для интерпретатора Lisp два обратных слэша подряд означают один `не специальный' обратный слэш).

Также нам понадобится счетчик, в котором мы будет хранить количество найденных слов; эту переменную мы вначале проинициализируем значением 0 а затем будем увеличивать каждый раз, когда Emacs вычисляет тело цикла while. Увеличивающее выражение очень простое:

 
(setq count (1+ count))

Наконец, мы должны будем сообщить пользователю сколько всего слов в области. Как раз для этой цели и предназначена функция message. Но надо чтобы эта функция отображала правильно составленную фразу не зависимо от того сколько в области слов: мы же не говорим "в области 1 слов". Грамматику надо знать и единственное со множественным числом не путать. Эту проблему можно решить используя условное выражение, которое будет возвращать при вычислении различные фразы в зависимости от того сколько в области слов. Существует четыре варианта: в области нет ни одного слова, в области только одно слово, в области от двух до четырех слов, и в области больше четырех слов. Для этого хорошо подходит особая форма cond.

Таким образом получается следующее определению функции:

 
;;; Первая версия; с ошибками!
(defun count-words-region (beginning end)  
  "Сообщает число слов в области.
Словом считается текст в котором за хотя бы одним символом входящим в
состав слова следует хотя бы один символ не входящий в состав слова.
Таблица синтаксиса текущего буфера определяет, какими должны быть эти
символы."
  (interactive "r")
  (message "Подсчет слов в области ... ")

;;; 1. Соответствующая инициализация.
  (save-excursion
    (goto-char beginning)
    (let ((count 0))

;;; 2. Запуск цикла while.
      (while (< (point) end)
        (re-search-forward "\\w+\\W*")
        (setq count (1+ count)))

;;; 3. Сообщаем о результатах пользователю.
      (cond ((zerop count)
             (message 
              "Область НЕ содержит слов."))
            ((= 1 count) 
             (message 
              "В области содержится 1 слово."))
       	    ((and (< 1 count) (> 5 count))
	     (message 
              "В области содержиться %d словa." count))
            (t 
             (message 
              "В области содержится %d слов." count))))))

Эта функция работает, но не всегда правильно.

13.1.1 Ошибка в count-words-region  


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

13.1.1 Ошибка в count-words-region

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

 
Search failed: "\\w+\\W*"

Если вы читаете это в Info, то вы сами можете убедиться в наличии этих ошибок.

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

 
;;; Первая версия; с ошибками!
(defun count-words-region (beginning end)  
  "Сообщает число слов в области.
Словом считается текст в котором за хотя мы одним символом входящим в
состав слова следует хотя бы один символ не входящий в состав слов.
Таблица синтаксиса текущего буфера определяет, какими должны быть эти
символы."
  (interactive "r")
  (message "Подсчет слов в области ... ")

;;; 1. Соответствующая инициализация.
  (save-excursion
    (goto-char beginning)
    (let ((count 0))

;;; 2. Запуск цикла while.
      (while (< (point) end)
        (re-search-forward "\\w+\\W*")
        (setq count (1+ count)))

;;; 3. Сообщаем о результатах пользователю.
      (cond ((zerop count)
             (message 
              "Область НЕ содержит слов."))
            ((= 1 count) 
             (message 
              "В области содержится 1 слово."))
       	    ((and (< 1 count) (> 5 count))
	     (message 
              "В области содержиться %d словa." count))
            (t 
             (message 
              "В области содержится %d слов." count))))))

Если хотите, то вы можете также для удобства связать эту функцию с какой-нибудь клавишей:

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

Для первой проверки, поставьте метку в начале, а курсор в конце следующей строки и наберите C-c = (или M-x count-words-region, если вы не связали команду с клавишами C-c =)

 
    раз   два  три

Emacs выдаст вам ожидаемое сообщение:

 
В области содержиться 3 словa.

Повторим этот тест, но на этот раз поставьте метку в начале строки, а курсор расположите как раз перед словом `раз'. Снова выполните команду C-c = (или M-x count-words-region). Вроде бы Emacs должен сообщить нам что в области не содержится слов --- ведь там находятся только пробелы. Но вместо этого Emacs скажет нам что в области содержиться одно слово!

Перед третьей проверкой скопируйте тестируемую строку в конец буфера `*scratch*' и после нее напечатайте несколько пробелов. Поставьте метку сразу за словом `три', а точку в конце строки. (Конец строки должен быть и концом буфера). И снова наберите C-c = (или M-x count-words-region). Мы опять ожидаем, что Emacs сообщит нам об отсутствии слов в области, поскольку она содержит только пробелы. Вместо этого в эхо-области появляется сообщение `Search failed'.

Эти две ошибки следствие одной и той же проблемы.

Рассмотрим первое проявление ошибки, когда область содержит только пробелы, а затем идет какой-то текст. Происходит следующее: команда M-x count-words-region перемещает точку в начало области. В цикле while проверяется меньше ли значение точки, чем значение end, сначала это верно. Теперь функция поиска по регулярному выражению ищет и находит первое слово. Она оставляет курсор после найденного слова. Переменной count присваивается значение 1. Цикл while повторяется; но теперь значение точки больше чем значение end и поэтому цикл завершается, функция отображает сообщение, где говорится что область содержит одно слово, ведь значение count равно 1. Короче говоря, функция поиска регулярного выражения ищет и находит слово даже если оно находится вне помеченной области.

При втором проявлении ошибки, область состоит из одних пробелов в конце буфера. Emacs сообщает, что `Search failed'. Это происходит в результате выполненения все той же функции поиска. Поскольку теперь в буфере нет слов, то поиск завершается неудачей.

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

Решение напрашивается само собой --- ограничить поиск заданой областью, но если немного подумать, то все получается не совсем так просто.

Как мы знаем функция re-search-forward принимает первый аргумент, в виде шаблона текста, который необходимо найти. Но кроме первого обязательного аргумента, функция может принимать еще три необязательных параметра. Вторым аргументом задается граница поиска. Третий аргумент, если назначить ему значение t заставляет функцию вернуть значение nil в случае неудачного поиска, а не сообщать об ошибке. Четвертый аргумент задает число повторов поиска. (В Emacs, вы можете вызвать описание любой функций нажав C-h f, и набрав имя функции в минибуфере).

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

 
(re-search-forward "\\w+\\W*" end)

Однако, если вы сделаете только это изменение в определении count-words-region и снова проверите работу функции на области состоящей из пробелов, то вы снова получите сообщение об ошибке `Search failed'.

Причина ясна --- поиск ограничен областью, и заканчивается неудачей, как и ожидалось --- ведь в области нет символов, которые могут входить в состав слова. Поскольку поиск завершился неудачей, то мы получаем сообщение об ошибке. Но этого нам тоже не нужно; мы хотим чтобы в этом случае Emacs сообщил нам "Область НЕ содержит слов." Эта проблема устраняется путем задания третьего аргумента t для функции re-search-forward, что заставляет функцию вместо отображения сообщения об ошибке просто вернуть значение nil, в случае если поиск завершился неудачей.

Однако, если вы внесете это изменение и заново протестируете его, в эхо-области появится сообщение "Подсчет слов в области ..." и ... оно будет отображаться до тех пор, пока вы не нажмете C-g (keyboard-quit).

Причина в следующем: поиск ограничен областью как и раньше, и если вы вызвали эту функция на области где слов нет, то поиск закончится неудачей --- ведь там нет символов составляющих слово, все как положено. Теперь функция re-search-forward возвратит nil, а не сообщит об ошибке. Больше она ничего не сделает, а точнее она не переместит курсор, что происходит в случае успешного поиска, в виде побочного эффекта функции. После того, как функция re-search-forward вернула nil вычислится следующее выражение цикла while. В этом выражении увеличивается значение count. Затем цикл повторяется. Проверка-истина-ложь возвращает истину, поскольку значение точки все еще меньше, чем значение конца области --- ведь функция re-search-forward не переместила точку. Цикл повторяется --- ... снова ... и снова.

Значит определение функции count-words-region надо еще немного подправить, для того чтобы проверка-истина-ложь цикла while возвращала ложь в случае неудачного поиска. Можно сказать по другому, в проверке-истина-ложь должны быть выполнены два условия перед тем, как переменная count будет увеличена --- точка должна находиться в пределах области и функция поиска должна успешно найти одно слово.

Поскольку оба эти условия должны быть выполнены вместе, то их можно обьединить в одном выражении с помощью функции and и использовать это выражение как проверку-истина-ложь для всего цикла while, следующим образом:

 
(and (< (point) end) (re-search-forward "\\w+\\W*" end t))

(See section 12.4 forward-paragraph: сокровищница функций, для получения информации о функции and).

Выражение re-search-forward возвращает t, если поиск успешен и в виде побочного эффекта перемещает курсор. Соответственно при нахождении новых слов курсор перемещается по области. Когда, наконец, функция поиска не смогла найти еще одно слово или когда курсор достигает конца области, то проверка-истина-ложь возвращает ложь, цикл while завершается, и функция count-words-region отображает в эхо-области результат поиска.

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

 
;;; Окончательная версия: while
(defun count-words-region (beginning end)  
  "Сообщает число слов в области.
Словом считается текст в котором за хотя мы одним символом входящим в
состав слова следует хотя бы один символ не входящий в состав слов.
Таблица синтаксиса текущего буфера определяет, какими должны быть эти
символы."
  (interactive "r")
  (message "Подсчет слов в области ... ")

;;; 1. Соответствующая инициализация.
  (save-excursion
    (goto-char beginning)
    (let ((count 0))

;;; 2. Запуск цикла while.
      (while (and (< (point) end))
                  (re-search-forward "\\w+\\W*" end t)
        (setq count (1+ count)))

;;; 3. Сообщаем о результатах пользователю.
      (cond ((zerop count)
             (message 
              "Область НЕ содержит слов."))
            ((= 1 count) 
             (message 
              "В области содержится 1 слово."))
       	    ((and (< 1 count) (> 5 count))
	     (message 
              "В области содержиться %d словa." count))
            (t 
             (message 
              "В области содержится %d слов." count))))))


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

13.2 Рекурсивный подсчет слов

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

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

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

Давайте начнем с функции, котороя будет отображать сообщение. Мы можем назвать ее также --- count-words-region.

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

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

 
;; Рекурсивная версия; использует функцию поиска regexp
(defun count-words-region (beginning end)  
  "документация..."
  (выражение-interactive...)

;;; 1. Соответствующая подготовка.
  (Сообщение о начале работы)
  (Инициализация переменных...

;;; 2. Подсчет слов.
    вызов рекурсивной функции

;;; 3. Отображает сообщение для пользователя.
    сообщаем о результатах работы))

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

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

С let, определение функции выглядит следующим образом:

 
(defun count-words-region (beginning end)  
  "Печатает число слов в области."
  (interactive "r")

;;; 1. Соответствующие подготовления.
  (message "Подсчет слов в области ... ")
  (save-excursion
    (goto-char beginning)

;;; 2. Подсчет слов.
    (let ((count (recursive-count-words end)))    

;;; 3. Отображение результатов работы.
      (cond ((zerop count)
             (message 
              "Область НЕ содержит слов."))
            ((= 1 count) 
             (message 
              "В области содержится 1 слово."))
       	    ((and (< 1 count) (> 5 count))
	     (message 
              "В области содержиться %d словa." count))
            (t 
             (message 
              "В области содержится %d слов." count))))))

Теперь нам надо написать рекурсивную функцию для подсчета слов.

Рекурсивная функция должна содержать поменьшей мере три части: `рекурсивную-проверку', `выражение-следующего-вызова' и рекурсивный вызов.

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

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

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

Третья часть рекурсивной функции --- это рекурсивный вызов.

Где-то, также нам понадобится часть, которая выполняет основную `работу' функции, часть, которая и производит счет слов. Необходимая часть!

Но сейчас мы уже можем схематично обрисовать рекурсивную функцию:

 
(defun recursive-count-words (region-end)
  "документация..."
   рекурсивная-проверка
   выражение-следующего-вызова
   рекурсивный вызов)

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

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

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

 
(and (< (point) region-end)
     (re-search-forward "\\w+\\W*" region-end t))

Обратите внимание, что функция поиска входит в состав рекурсивной-проверки --- функция поиска возвращает t в случае успеха и nil в случает неудачи. (See section Ошибка с пробелами in count-words-region, для объяснения того как работает re-search-forward).

Рекурсивная-проверка --- это проверка-истина-ложь в выражении if. Ясно, что в случае успеха рекурсивной-проверки, в then-часть выражения if должен происходить рекурсивный вызов функции; если рекурсивная-проверка закончится неудачей, то в else-части должен вернуться ноль, поскольку или точка вышла за пределы области или поиск завершился неудачей потому что не удалось найти слово.

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

Кроме того, что функция поиска возвращает t или nil в вызвавшую ее функцию, re-search-forward также перемещает точку вперед через слово. Именно это действие изменяет значение точки, так что рекурсивные вызовы прекратятся тогда, когда точка переместится в конец области. Соответственно выражение re-search-forward и есть выражение-следующего-вызова.

Схематично тело функции recursive-count-words выглядит следующим образом:

 
(if рекурсивная-проверка-выражение-следующего-вызова-два-в-одном
    ;; then
    рекурсивный-вызов
  ;; else
  вернуть-ноль)

Остается ответить на один вопрос, как производить подсчет слов?

Если вы не привыкли к созданию рекурсивных функций, то вам будет трудно ответить на такой вопрос. Но постепенно вы овладеете мастерством создания рекурсивных функций.

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

Рассмотрим следующие случаи:

В шаблоне, который мы уже набросали, else-часть выражения if возвращает ноль для случая, когда слов уже не осталось. Это значит, что then-часть выражения if, должна вернуть результат сложения единицы и значения, которое возвратит рекурсивный вызов функции на оставшейся части области.

Then-часть будет выглядеть следующим образом, где 1+ --- функция которая добавляет единицу к своему аргументу.

 
(1+ (recursive-count-words region-end))

Теперь вся функция recursive-count-words принимает следующий вид:

 
(defun recursive-count-words (region-end)
  "документация..."

;;; 1. рекурсивная-проверка
  (if (and (< (point) region-end)          
           (re-search-forward "\\w+\\W*" region-end t))

;;; 2. then-part: рекурсивный вызов
      (1+ (recursive-count-words region-end))  

;;; 3. else-part
    0))             

Давайте рассмотрим как работает эта функция:

Если в области нет слов, то вычисляется else-часть выражения if и функция возвращает ноль.

Если в области одно слово, то значение точки меньше чем значение region-end и поиск завершается успехом. В этом случае проверка-истина-ложь выражения if возвращает t и вычисляется then-часть выражения if. Запускается механизм счета. Это выражение возвращает значение (это будет значением всей функции), которое равно сумме единицы и значения возвращаемого рекурсивным вызовом.

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

Ясно, что если в области два слова, то первый вызов recursive-count-words вернет результат сложения единицы и значение, которое возвратит вызов recursive-count-words на области содержащей оставшееся слово. В результате один плюс один даст два, а это правильное значение.

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

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

Рекурсивная функция:

 
(defun recursive-count-words (region-end)
  "Возвращает количество слов между точкой и region-end."

;;; 1. рекурсивная-проверка
  (if (and (< (point) region-end)
           (re-search-forward "\\w+\\W*" region-end t))

;;; 2. then-часть: рекурсивный вызов
      (1+ (recursive-count-words region-end)) 

;;; 3. else-часть
    0))             

Верхняя функция:

 
(defun count-words-region (beginning end)  
  "Печатает число слов в области.
Словом считается текст в котором за хотя бы одним символом входящим в
состав слова следует хотя бы один символ не входящий в состав слов.
Таблица синтаксиса текущего буфера определяет, какими должны быть эти
символы."

  (interactive "r")
;;; 1. Соответствующие подготовления.
  (message "Подсчет слов в области ... ")
  (save-excursion
    (goto-char beginning)

;;; 2. Подсчет слов.
    (let ((count (recursive-count-words end)))    

;;; 3. Отображение результатов работы.
      (cond
       ((zerop count)
	(message 
	 "Область НЕ содержит слов."))
       ((= 1 count) 
	(message 
	 "В области содержится 1 слово."))
       ((and (< 1 count) (> 5 count))
	(message 
	 "В области содержиться %d словa." count))
       (t 
	(message 
	 "В области содержится %d слов." count))))))


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

13.3 Упражнения: Сколько знаков пунктуации

С помощью цикла while напишите функцию, которая подсчитывает знаки пунктуации (точки, запятые, знаки вопроса, воскличательные знаки и т.п) в области. Перепишите ее используя рекурсию.


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

This document was generated on March, 10 2004 using texi2html