LINUX.ORG.RU

Макрос do-stream

 ,


1

3

Ребята, практикуюсь в проектировании и реализации CL макросов. В стиле do-макросов (dolist, dotimes, ...) сделал вот такую штуку:

(defmacro do-stream ((var stream &key result (read-function '#'read-line) (eof nil eof-supplied-p)) &body body)
  (once-only ((stream stream)
              (read-function read-function))
    (with-gensyms (start)
      `(block nil
         (tagbody
            ,start
            (let ((,var
                   ,(if eof-supplied-p
                        (with-gensyms (%)
                          `(let ((,% (funcall ,read-function ,stream nil ,eof)))
                             (if (equal ,% ,eof)
                                 (return ,result)
                                 ,%)))
                        `(handler-case
                             (funcall ,read-function ,stream t)
                           (end-of-file () (return ,result))))))
              ,@body
              (go ,start)))))))

Раскрывается следующим образом. С eof так:

(with-open-file (s "input")
  (let (result)
    (do-stream (line s :result result :eof "Hello")
      (push line result))))

(WITH-OPEN-STREAM (S (OPEN "input"))
  (LET (RESULT)
    (LET ((#:STREAM953 S) (#:READ-FUNCTION954 #'READ-LINE))
      (BLOCK NIL
        (TAGBODY
         #:START955
          (LET ((LINE
                 (LET ((#:%956
                        (FUNCALL #:READ-FUNCTION954 #:STREAM953 NIL "Hello")))
                   (IF (EQUAL #:%956 "Hello")
                       (RETURN RESULT)
                       #:%956))))
            (PUSH LINE RESULT)
            (GO #:START955)))))))
Без eof, вот так:
(with-open-file (s "input")
  (let (result)
    (do-stream (item s :result result :read-function #'read)
      (declare (type (or symbol fixnum) item))
      (push item result))))

(WITH-OPEN-STREAM (S (OPEN "input"))
  (LET (RESULT)
    (LET ((#:STREAM944 S) (#:READ-FUNCTION945 #'READ))
      (BLOCK NIL
        (TAGBODY
         #:START946
          (LET ((ITEM
                 (HANDLER-CASE (FUNCALL #:READ-FUNCTION945 #:STREAM944 T)
                               (END-OF-FILE NIL (RETURN RESULT)))))
            (DECLARE (TYPE (OR SYMBOL FIXNUM) ITEM))
            (PUSH ITEM RESULT)
            (GO #:START946)))))))

Интересует мнение экспертов общелелиспа. С ходу вопросы такие:

  • Полезен ли такой макрос или в CL принято подобную логику обхода файла делать по другому?
  • Нормален ли макрос в плане дизайна? (вместе &optional аргумента result, сделал &key ввиду необходимости задания read-function и eof)
  • Нормален ли макрос в плане реализации? Какие есть косяки?
  • Есть ли подводные камни в обработке условия end-of-file при отсутствии eof?
  • Временную переменную (при наличии eof) задал как %. Это норм или в сообществе принято как-то по другому именовать?

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

1. Вполне полезен 2. Годится 4. Т.к. функция read-function кастомная, то можно задать любую, но не всякая read-* принимает аргументом eof, например какая-нибудь read-sequence этого не делает. 5. Вообще я не встречал такого именования, назови лучше обычно.

grouzen ★★ ()
(block nil
  (tagbody
    #:start
    ... (return result)
    ... (go #:start)))

Можно сделать сильно проще:

(loop
  ... (return result))

Сейчас оттестю и напишу целиком.

naryl ★★★★★ ()
Последнее исправление: naryl (всего исправлений: 1 )
Ответ на: комментарий от naryl
(defmacro do-stream ((var stream &key result (read-function '#'read-line) (eof nil eof-supplied-p)) &body body)
  (once-only (stream read-function)
    `(loop
        (let ((,var
               ,(if eof-supplied-p
                    (with-gensyms (%)
                      `(let ((,% (funcall ,read-function ,stream nil ,eof)))
                         (if (equal ,% ,eof)
                             (return ,result)
                             ,%)))
                    `(handler-case
                         (funcall ,read-function ,stream t)
                       (end-of-file () (return ,result))))))
          ,@body))))
naryl ★★★★★ ()

loop/iterate уже есть :) И без них сложно избежать присутствующего у тебя лапшекода накопителя. С другой стороны do гораздо «расширябельнее» и по сравнению с ним ты экономишь лишь на умолчальном #'read-line. Который к тому же сам по себе «без ничего» редко нужен.

antares0 ★★★★ ()

И если серьезно так смотреть на потоки то много где хорошо бы использовать read-sequence/buffer по соображениям производительности и не забывать про кодировки и ос-специфичные переводы строк. Короче боюсь что под этим грузом идея минималистичного do-stream либо рухнет либо необратимо мутирует :)

antares0 ★★★★ ()
Последнее исправление: antares0 (всего исправлений: 1 )
Ответ на: комментарий от grouzen

Благодарю за ответ!

4. Т.к. функция read-function кастомная, то можно задать любую, но не всякая read-* принимает аргументом eof, например какая-нибудь read-sequence этого не делает.

Угу. Но тут, вероятно, по аналогии с in-stream (in-file) в iterate (http://common-lisp.net/project/iterate/doc/Sequence-Iteration.html#Sequence-I...), т.е. указать в документации к макросу, что аргументы функции и поведение (в смысле сигнализирования end-of-file на конце) для корректной работы должны быть как у read. А можно как-то задать ограничение, например ftype декларацией на read-function? С ходу не понимаю как и можно ли это сделать.

5. Вообще я не встречал такого именования, назови лучше обычно.

Спасибо, ясно. Поправлю.

unwind-protect ()
Ответ на: комментарий от anonymous

С этим лучше в http://lisper.ru

Спасибо. Возможно продублирую чуть попозже, если будет необходимость. Я просто заметил, что и тут крайне много лисперов :)

unwind-protect ()
Ответ на: комментарий от naryl

Можно сделать сильно проще

Угу, я тоже подумал loop simple form использовать, но меня смутил dolist в sbcl. Она на первом же шаге раскрывается в (block nil ...) и tagbody:

(let (result)
  (dolist (x (list 1 2 3) result)
    (push (1+ x) result)))

(LET (RESULT)
  (BLOCK NIL
    (LET ((#:N-LIST827 '(1 2 3)))
      (TAGBODY
       #:START828
        (UNLESS (ENDP #:N-LIST827)
          (LET ((X (SB-EXT:TRULY-THE (MEMBER 1 2 3) (CAR #:N-LIST827))))
            (SETQ #:N-LIST827 (CDR #:N-LIST827))
            (TAGBODY (PUSH (1+ X) RESULT)))
          (GO #:START828))))
    (LET ((X NIL))
      X
      RESULT)))
Но с другой стороны, simple loop раскрывается подобным же образом (правда тут ещё и progn зачем-то):
(loop
   (do-something))

(BLOCK NIL (TAGBODY #:G826 (PROGN (DO-SOMETHING)) (GO #:G826)))
Спасибо! loop читается лучше конечно. Поправлю.

unwind-protect ()
Ответ на: комментарий от antares0

loop/iterate уже есть :)

Да. В iterate есть in-stream, in-file (а в loop, вроде нет).

без них сложно избежать присутствующего у тебя лапшекода накопителя.

Не очень понял, извини. О чём идёт речь? Что под накопителем подразумевается? result?

С другой стороны do гораздо «расширябельнее» и по сравнению с ним ты экономишь лишь на умолчальном #'read-line.

do — это эти do, do*?

unwind-protect ()
Ответ на: комментарий от unwind-protect

в loop, вроде нет

Не о том подумал :(

Что под накопителем подразумевается? result?

(let ((result))
   (do-stream (... :result result ...))
      (push ... result)))

Все вместе. iter c collect - проще и короче. И c усложнением задачи это будет нарастать.

do — это эти do, do*?

Да

antares0 ★★★★ ()
Последнее исправление: antares0 (всего исправлений: 1 )
Ответ на: комментарий от antares0

И если серьезно так смотреть на потоки то много где хорошо бы использовать read-sequence/buffer по соображениям производительности и не забывать про кодировки и ос-специфичные переводы строк.

Это интересно :) Но всё же задачу изначально немного по другому видел: весьма частенько надобно пробежаться определённым способом по потоку (в частности, файлу) и сделать какие-то действия. И очень хотелось делать это весьма простым способом с ходу, без boilerplate, который неизбежен при simple loop или даже do. Кстати, к вопросу о do. Если хочется добежать до маркера конца потока, то:

(with-open-file (s "input")
  (let (result)
    (do ((line (read-line s nil :eof) (read-line s nil :eof)))
        ((eq line :eof)
         result)
      (push line result))))
Тут видно, что повторяется read-line. Не очень приятно. Если же хотим гарантированно добежать до конца, то ещё хуже получается:
(with-open-file (s "input")
  (let (result)
    (do ((line (handler-case
                   (read-line s)
                 (end-of-file () (return result)))
               (handler-case
                   (read-line s)
                 (end-of-file () (return result)))))
        (nil result)
      (push line result))))
Понятно, что мы можем boilerplate в символический макролит вынести, но от этого ещё не понятнее всё станет... А с do-stream вполне себе приятно, в простейших, конечно, случаях. Как dolist для простейших действий. Ну и конечно понятно, что для каких-то более или менее сложных преобразований лучше взять iterate.

Короче боюсь что под этим грузом идея минималистичного do-stream либо рухнет либо необратимо мутирует :)

Безусловно мутирует! Но это задача совсем другого уровня. Благодарю!

unwind-protect ()
Ответ на: комментарий от unwind-protect

Тут видно, что повторяется read-line. Не очень приятно.

Иногда можно так (для примера):

(with-open-file (s "input")
  (let (result)
    (do ((line #1=(read-line s nil :eof) #1#))
        ((eq line :eof)
         result)
      (push line result))))
anonymous ()

Нормален ли макрос в плане реализации? Какие есть косяки?

Замени #'equal на #'eq. Твоя реализация сейчас сглючит если в файле будет строка «Hello». #'read-line возвращает тот же объект по завершении потока. Кстати, я обычно использую неинтернированные символы с этой целью: (eq #1='#:eof (readline t nil #1#)) или как-то так.

Временную переменную (при наличии eof) задал как %. Это норм или в сообществе принято как-то по другому именовать?

Как правило, лучше не выпендриваться и знаки пунктуации без особых причин не использовать. Просто код читать легче.

anonymous ()
Ответ на: комментарий от anonymous

Замени #'equal на #'eq. Твоя реализация сейчас сглючит если в файле будет строка «Hello».

Но ведь это же не глюк, это фича :) eof-value, на сколько я понимаю, и нужен для того, что либо до значения eof-value доходим, либо до конца. Я думаю, что #'read, #'read-line и подобные проектировались именно с этим расчётом (я прав или гоню?). Если всегда нужно было бы до конца потока доходить, то реализовывать eof в do-stream не имело бы никакого смысла — можно ограничиться обработкой end-of-file. Также если заменить eq на equal, то всё равно, например при #'read можно поймать символ эквивалентный по eq. Но в случае:

(eq #1='#:eof (readline t nil #1#))

Не поймаем :) И это круто. Надо попробовать переписать гарантированный проход по всему потоку до конца без обработки end-of-file.

unwind-protect ()

Полезен ли такой макрос или в CL принято подобную логику обхода файла делать по другому?

Нет, твой велосипед бесполезен абсолютно. Кому надо напишет что-то вроде

(loop for line = (read-line stream nil) while line do ...)

Запарили со своим лиспом ;(

hvatitbanit ()

Временную переменную (при наличии eof) задал как %. Это норм или в сообществе принято как-то по другому именовать?

Нет, это не норм. «Норм» - это когда из названия переменной понятно, что она делает. Ваш КО. % можно пихнуть в конец символа. Как, ЕМНИП, в кацкеле пихают '

hvatitbanit ()
Ответ на: комментарий от hvatitbanit

Нет, твой велосипед бесполезен абсолютно.

Возможно :) Но удобно всё равно.

Кому надо напишет что-то вроде (loop for line = (read-line stream nil) while line do ...)

Но далеко не всем нравиться loop. Мне тоже не нравится, как минимум из-за того, что редактор не может нормально отступы проставить из-за такого странного синтаксиса.

Запарили со своим лиспом ;(

Ну так ведь я тэги специально проставил, что бы тем, кому не интересен lisp просто проигнорировали тему. Зачем же читать тогда?

unwind-protect ()
Ответ на: комментарий от antares0

Угу. Только тут есть некоторая проблема: если в теле определённом в progn сигнализируется end-of-file и не обработается, то мы выпрыгнем из do.

unwind-protect ()
Ответ на: комментарий от unwind-protect

Но далеко не всем нравиться loop. Мне тоже не нравится, как минимум из-за того, что редактор не может нормально отступы проставить из-за такого странного синтаксиса.

У меня в emacs всё работало, но slime всё поломал :)

К тому же, есть iter (зоопарк сплошной)

hvatitbanit ()
Ответ на: комментарий от hvatitbanit

У меня в emacs всё работало, но slime всё поломал :)

Уж лучше slime, чем loop :)

К тому же, есть iter (зоопарк сплошной)

Угу. iter мне тоже нравится. Но слышал, что, дескать, реализован он криво (code-walker кривой). Но я не знаю правда-ли это и какие последствия из-за этого. Сорцы iter-а я не осилил :-(

unwind-protect ()
Ответ на: комментарий от unwind-protect

Но слышал, что, дескать, реализован он криво (code-walker кривой)

Не знаю, все пользуются. Но я нет, ибо не стандарт.

hvatitbanit ()
Ответ на: комментарий от hvatitbanit

Чисто для справки - в let можно писать не (var nil) а просто var

Я знаю, но в том случае так было нагляднее.

antares0 ★★★★ ()
Последнее исправление: antares0 (всего исправлений: 1 )
Ответ на: комментарий от unwind-protect

реализован он криво (code-walker кривой) ... какие последствия из-за этого.

Он не умеет раскрывать внешние macrolet-ы. На этом вроде бы и все.

antares0 ★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.