Регистрация | Войти
Lisp — программируемый язык программирования
Предыдущая Оглавление Следующая

15. Практика: переносимая библиотека файловых путей

Как было сказано в предыдущей главе, Common Lisp предоставляет абстракцию файловых путей, и предполагается, что она изолирует вас от деталей того, как различные операционные и файловые системы именуют файлы. Файловые пути предоставляют удобное API для управления именами самими по себе, но когда дело доходит до функций, которые на самом деле взаимодействуют с файловой системой, всё не так гладко.

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

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

API

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

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

Переменная *FEATURES* и обработка условий при считывании.

Перед тем, как реализовать API в библиотеке, которая будет корректно работать на нескольких реализациях Common Lisp, мне нужно показать вам механизм для написания кода, предназначенного для определённой реализации.

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

Этот механизм состоит из переменной *FEATURES* и двух дополнительных частей синтаксиса, понимаемых считывателем Lisp. *FEATURES* является списком символов; каждый символ представляет собой «свойство», которое присутствует в реализации или используемой ей платформе. Эти символы затем используются в выражениях на свойства, которые вычисляются как истина или ложь, в зависимости о того, присутствуют ли символы из этих выражений в переменной *FEATURES*. Простейшее выражение на свойство — одиночный символ; это выражение истинно, если символ входит в *FEATURES*, и ложно в противном случае. Другие выражения на свойства — логические выражения, построенные из операторов NOT, AND или OR. Например, если бы вы захотели установить условие, чтобы некоторый код был включен только если присутствуют свойства foo и bar, вы могли бы записать выражение на свойства (and foo bar).

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

Начальное значение *FEATURES* зависит от реализации, и функциональность, подразумеваемая любым присутствующим в ней символом, тоже определяется реализацией. Однако, все реализации включают по крайней мере один символ, указывающий на неё саму. Например, Allegro Common Lisp включает символ :allegro, CLISP включает :clisp, SBCL включает :sbcl и CMUCL включает :cmu. Чтобы избежать зависимостей от пакетов, которые могут или не могут существовать в различных реализациях, символы в *FEATURES* - обычно ключевые слова, и считыватель связывает *PACKAGE* c пакетом KEYWORD во время считывания выражений. Таким образом, имя без указания пакета будем прочитано как ключевой символ. Итак, вы могли бы написать функцию, которая ведёт себя немного по-разному в каждой из только что упомянутых реализаций так:

(defun foo ()
  #+allegro (do-one-thing)
  #+sbcl (do-another-thing)
  #+clisp (something-else)
  #+cmu (yet-another-version)
  #-(or allegro sbcl clisp cmu) (error "Not implemented")
)

В Allegro этот код будет считан, как если бы он был написан так:

(defun foo ()
  (do-one-thing)
)

тогда как в SBCL считыватель прочитает это:

(defun foo ()
  (do-another-thing)
)

а в реализации, отличной от тех, на которые специально установлены условия, будет считано следующее:

(defun foo ()
  (error "Not implemented")
)

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

Создание пакета библиотеки

Кстати о пакетах, если вы загрузите полный код этой библиотеки, то увидите, что она определена в новом пакете, com.gigamonkeys.pathnames. Я расскажу о деталях определения и использования пакетов в главе 21. Сейчас вы должны отметить, что некоторые реализации предоставляют свои пакеты, которые содержат функции с некоторыми такими же именами, как вы определите в этой главе, и делают эти имена доступными из пакета CL-USER. Таким образом, если вы попытаетесь определить функции этой библиотеки, находясь в пакете CL-USER, вы можете получить сообщения об ошибках о конфликтах с существующими определениями. Чтобы избежать этой возможности, вы можете создать файл с названием packages.lisp и следующим содержанием:

(in-package :cl-user)

(defpackage :com.gigamonkeys.pathnames
  (:use :common-lisp)
  (:export
   :list-directory
   :file-exists-p
   :directory-pathname-p
   :file-pathname-p
   :pathname-as-directory
   :pathname-as-file
   :walk-directory
   :directory-p
   :file-p
)
)

и сделать LOAD на него. Тогда в REPL или в начале файла, в который вы печатаете определения из этой главы, напечатайте следующее выражение:

(in-package :com.gigamonkeys.pathnames)

В дополнение к избежанию конфликтов имён с символами, уже доступными в CL-USER, создание пакета для библиотеки таким образом также сделает проще использовать её в другом коде, как вы увидите из нескольких будущих глав.

Получение списка файлов в директории

Вы можете реализовать функцию для получения списка файлов одной директории, list-directory, как тонкую обёртку вокруг стандартной функции DIRECTORY. DIRECTORY принимает особый тип файлового пути, называемого шаблоном файлового пути, который имеет одну или более компоненту, содержащую специальное значение :wild, и возвращает список файловых путей, представляющих файлы в файловой системе, которые соответствуют шаблону2). Алгоритм сопоставления — как большинство вещей, которым приходится иметь дело с взаимодействием между Lisp и конкретной файловой системой — не определяется стандартом языка, но большинство реализаций на Unix и Windows следуют одной и той же базовой схеме.

Функция DIRECTORY имеет две проблемы, с которыми придётся иметь дело list-directory. Главная проблема состоит в том, что определённые аспекты поведения этой функции различаются достаточно сильно для различных реализаций Common Lisp, даже для одной и той же операционной системы. Другая проблема в том, что, хотя DIRECTORY и предоставляет мощный интерфейс для получения списка файлов, её правильное использование требует понимания некоторых достаточно тонких моментов в абстракции файловых путей. С этими тонкостями и стилевыми особенностями различных реализаций, само написание переносимого кода, использующего DIRECTORY для таких простых вещей, как получение списка всех файлов и поддиректорий для единственной директории, могло бы стать разочаровывающим опытом. Вы можете разобраться со всеми тонкостями и характерными особенностями раз и навсегда, написав list-directory и забыв о них после этого.

Одна тонкость, которая обсуждалась в главе 14 — это два способа представлять имя директории в виде файлового пути: в форме директории и в форме файла.

Чтобы DIRECTORY возвратила вам список файлов в /home/peter/, вам надо передать ей шаблон файлового пути, чья компонента директории — это директория, которую вы хотите прочитать, и чьи компоненты имени и типа являются :wild. Таким образом, может показаться, что для получения списка файлов в /home/peter/ вы можете написать это:

(directory (make-pathname :name :wild :type :wild :defaults home-dir))

где home-dir является файловым путём, представляющим /home/peter/. Это бы сработало, если бы home-dir была бы в форме директории. Но если бы она была бы в файловой форме — например, если бы она была создана разбором строки "/home/peter" - тогда бы это выражение вернуло список всех файлов в /home, так как компонента имени "peter" была бы заменена на :wild.

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

Чтобы облегчить это, вам следует определить несколько вспомогательных функций. Одна, component-present-p, будет проверять, «существует» ли данная компонента в файловом пути, имея в виду не NIL и не специальное значение :unspecific.3). Другая, directory-pathname-p, проверяет, задан ли файловый путь уже в форме директории, и третья, pathname-as-directory, преобразует любой файловый путь в файловый путь в форме директории.

(defun component-present-p (value)
  (and value (not (eql value :unspecific)))
)


(defun directory-pathname-p  (p)
  (and
   (not (component-present-p (pathname-name p)))
   (not (component-present-p (pathname-type p)))
   p
)
)


(defun pathname-as-directory (name)
  (let ((pathname (pathname name)))
    (when (wild-pathname-p pathname)
      (error "Can't reliably convert wild pathnames.")
)

    (if (not (directory-pathname-p name))
      (make-pathname
       :directory (append (or (pathname-directory pathname) (list :relative))
                          (list (file-namestring pathname))
)

       :name      nil
       :type      nil
       :defaults pathname
)

      pathname
)
)
)

Теперь кажется, что можно создать шаблон файлового путь для передачи DIRECTORY, вызвав MAKE-PATHNAME с формой директории, возвращённой pathname-as-directory. К несчастью, благодаря одной причуде в реализации DIRECTORY в CLISP, всё не так просто. В CLISP, DIRECTORY вернёт файлы без расширений, только если компонента типа шаблона является NIL, но не :wild. Так что вы можете определить функцию, directory-wildcard, которая принимает файловый путь в форме директории или файла, и возвращает шаблон, подходящий для данной реализации, используя проверку условий при считывании для того, чтобы делать файловый путь с компонентой типа :wild во всех реализациях, за исключением CLISP, и NIL в CLISP.

(defun directory-wildcard (dirname)
  (make-pathname
   :name :wild
   :type #-clisp :wild #+clisp nil
   :defaults (pathname-as-directory dirname)
)
)

Заметьте, что каждое условие при считывании работает на уровне единственного выражения после #-clisp, выражение :wild будет или считано, или пропущено; ровно как и после #+clisp, NIL будет прочитано или пропущено.

Теперь вы можете первый раз вгрызться в функцию list-directory.

(defun list-directory (dirname)
  (when (wild-pathname-p dirname)
    (error "Can only list concrete directory names.")
)

  (directory (directory-wildcard dirname))
)

Утверждается, что эта функция будет работать в SBCL, CMUCL и LispWorks. К несчастью, остаётся парочка различий, которые надо сгладить. Одно отличие состоит в том, что не все реализации вернут поддиректории данной директории. Allegro, SBCL, CMUCL и LispWorks сделают это. OpenMCL не делает это по умолчанию, но сделает, если вы передадите DIRECTORY истинное значение по специфичному для этой реализации ключевому аргументу :directories. DIRECTORY в CLISP возвращает поддиректории только когда ей передаётся шаблон файлового пути с :wild в последнем элементе компоненты директории и NIL в компонентах имени и типа. В этом случае, он вернёт только поддиректории, так что вам придётся вызвать DIRECTORY дважды с разными шаблонами и скомбинировать результаты.

Как только вы заставите все реализации возвращать директории, вы узнаете, что они также различаются в том, возвращают ли они имена директорий в форме директорий или файлов. Вы хотите, чтобы list-directory всегда возвращала имена директорий в форме директорий, так, чтобы вы могли отличать поддиректории от обычных файлов, основываясь просто на имени. За исключением Allegro, все реализации этой библиотеки поддерживают это. Allegro, c другой стороны, требует передачи DIRECTORY характерного для этой реализации аргумента :directories-are-files со значением NIL, чтобы заставить её возвратить директории в форме файлов.

Как только вы узнали о том, как сделать так, чтобы каждая реализация делала то, что вы хотите, само написание list-directory становится просто делом сочетания различных версий при помощи проверки условий при чтении.

(defun list-directory (dirname)
  (when (wild-pathname-p dirname)
    (error "Can only list concrete directory names.")
)

  (let ((wildcard (directory-wildcard dirname)))

    #+(or sbcl cmu lispworks)
    (directory wildcard)

    #+openmcl
    (directory wildcard :directories t)

    #+allegro
    (directory wildcard :directories-are-files nil)

    #+clisp
    (nconc
     (directory wildcard)
     (directory (clisp-subdirectories-wildcard wildcard))
)


    #-(or sbcl cmu lispworks openmcl allegro clisp)
    (error "list-directory not implemented")
)
)

Функция clisp-subdirectories-wildcard на самом деле не является присущей CLISP, но так как она не нужна никакой другой реализации, вы можете ограничить её условием при чтении. В этом случае, так как выражение, следующее за #+ является целым DEFUN, будет или не будет включено всё определение функции, в зависимости от того, присутствует ли clisp в *FEATURES*.

#+clisp
(defun clisp-subdirectories-wildcard (wildcard)

  (make-pathname
   :directory (append (pathname-directory wildcard) (list :wild))
   :name nil
   :type nil
   :defaults wildcard
)
)

Проверка существования файла

Чтобы заменить PROBE-FILE, вы можете определить функцию с именем file-exists-p. Она должна принимать имя файла и, если файл существует, возвращать то же самое имя, и NIL, если не существует. Она должна быть способна принимать имя директории и в виде директории, и в виде файла, но должна всегда возвращать файловый путь в форме директории, если файл существует и является директорией. Это позволит вам использовать file-exists-p вместе с directory-pathname-p, чтобы проверить, является ли данное имя именем файла или директории.

Теоретически, file-exists-p достаточно похожа на стандартную функцию PROBE-FILE, и на самом деле, в нескольких реализациях — SBCL, LispWorks, OpenMCL – PROBE-FILE уже даёт вам то поведение, которого вы хотите от file-exists-p. Но не все реализации PROBE-FILE ведут себя так.

Функции PROBE-FILE в Allegro и CMUCL близки к тому, чего вы хотите — они принимают имя директории в обоих формах, но, вместо возвращения имени в форме директории, просто возвращают его в той же самой форме, в которой им был передан аргумент. К счастью, если им передаётся имя недиректории в форме директории, они возвращают NIL. Так что, в этих реализациях вы можете получить желаемое поведение, сначала передав PROBE-FILE имя в форме директории — если файл существует и является директорией, она возвратит имя в форме директории. Если этот вызов вернёт NIL, вы попытаетесь снова с именем в форме файла.

CLISP, с другой стороны, снова делает это по-своему. Его PROBE-FILE немедленно сигнализирует ошибку, если передано имя в форме директории, вне зависимости от того, существует ли файл или директория с таким именем. Она также сигнализирует ошибку, если в файловой форме передано имя, которое на самом деле является именем директории. Для определения, существует ли директория, CLISP предоставляет собственную функцию: probe-directory (в пакете ext). Она практически является зеркальным отражением PROBE-FILE: выдаёт ошибку, если ей передаётся имя в файловой форме или если передано имя в форме директории, которое оказалось именем файла. Единственное различие в том, что она возвращает T, а не файловый путь, когда существует названная директория.

Но даже в CLISP вы можете реализовать желаемую семантику, обернув вызовы PROBE-FILE и probe-directory в IGNORE-ERRORS4).

(defun file-exists-p (pathname)
  #+(or sbcl lispworks openmcl)
  (probe-file pathname)

  #+(or allegro cmu)
  (or (probe-file (pathname-as-directory pathname))
      (probe-file pathname)
)


  #+clisp
  (or (ignore-errors
        (probe-file (pathname-as-file pathname))
)

      (ignore-errors
        (let ((directory-form (pathname-as-directory pathname)))
          (when (ext:probe-directory directory-form)
            directory-form
)
)
)
)


  #-(or sbcl cmu lispworks openmcl allegro clisp)
  (error "file-exists-p not implemented")
)

Функция pathname-as-file, которая нужна вам для реализации file-exists-p в CLISP является обратной для определённой ранее pathname-as-directory, возвращающей файловый путь, являющийся эквивалентом аргумента в файловой форме. Несмотря на то, что эта функция нужна здесь только для CLISP, она полезна в общем случае, так что определим её для всех реализаций и сделаем частью библиотеки.

(defun pathname-as-file (name)
  (let ((pathname (pathname name)))
    (when (wild-pathname-p pathname)
      (error "Can't reliably convert wild pathnames.")
)

    (if (directory-pathname-p name)
      (let* ((directory (pathname-directory pathname))
             (name-and-type (pathname (first (last directory))))
)

        (make-pathname
         :directory (butlast directory)
         :name (pathname-name name-and-type)
         :type (pathname-type name-and-type)
         :defaults pathname
)
)

      pathname
)
)
)

Проход по дереву директорий

Наконец, чтобы завершить библиотеку, вы можете реализовать функцию, называемую walk-directory. В отличие от ранее определённых функций, эта функция не нужна для сглаживания различий между реализациями; она просто использует функции, которые вы уже определили. Однако, она довольно удобна, и вы будете её несколько раз использовать в последующих частях. Она будет принимать имя директории и функцию, и вызывать функцию на всех файлах входящих в директорию рекурсивно. Она также принимает два ключевых аргумента: :directories и :test. Когда :directories истинно, она будет вызывать функцию на именах директорий, как на обычных файлах. Аргумент :test, если предоставлен, определяет другую функцию, которая вызывается на каждом файловом пути до того, как будет вызвана главная функция, которая будет вызвана только если тестовая функция возвратит истинное значение.

(defun walk-directory (dirname fn &key directories (test (constantly t)))
  (labels
      ((walk (name)
         (cond
           ((directory-pathname-p name)
            (when (and directories (funcall test name))
              (funcall fn name)
)

            (dolist (x (list-directory name)) (walk x))
)

           ((funcall test name) (funcall fn name))
)
)
)

    (walk (pathname-as-directory dirname))
)
)

Теперь у вас есть полезная библиотека функций для работы с файловыми путями. Как я упомянул, эти функции окажутся полезны в следующих частях, особенно в частях 23 и 27, где вы будете использовать walk-directory, чтобы продраться через дерево директорий, содержащих спамерские сообщения и MP3 файлы. Но до того как мы доберёмся до этого, мне, тем не менее, нужно поговорить о объектной ориентации, теме следующих двух глав.

1)Одним слегка назойливым последствием того, как работает обработка условий при чтении, является то, что непросто написать проваливающийся case. Например, если вы добавите в foo поддержку новой реализации, добавив ещё одно выражение, охраняемое #+, вам придётся помнить о том, что нужно добавить то же самое свойство или выражение со свойством после #-, или будет вычислена форма с ERROR, когда будет запущен ваш новый код.
2)Другое специальное значение, :wild-inferiors, может появляться как часть компоненты директории шаблона файлового пути, но в данной главе это не понадобится.
3)Реализации могут возвращать :unspecific вместо NIL как значение компоненты файлового пути в определённых ситуациях, например когда эта компонента не используется в этой реализации.
4)Это немного неправильно в том смысле, что если PROBE-FILE сигнализирует ошибку по другой причине, этот код будет работать неправильно. К несчастью, документация CLISP не указывает, какие ошибки можут сигнализировать PROBE-FILE и probe-directory, и эксперимент показывает, что они сигнализируют simple-file-errors в большинстве ошибочных ситуаций.
Предыдущая Оглавление Следующая
@2009-2013 lisper.ru