Продолжим рассмотрение неочевидных поучительных возможностей программирования на Gambit Scheme, начатое в предыдущих статьях.
На этот раз займёмся пристойной печатью значений типа u8vector, то есть массивов байтов.
Значения такого типа используются в Gambit Scheme везде, где нам нужно работать с представлением памяти компьютера на нижнем уровне. В частности, такими значениями представляются неформатированные пакеты UDP, отправляемые и получаемые по сети.
В текстах программ на Gambit Scheme константы типа u8vector представляются так: #u8(1 2 3 4 5)
или так: #(u8 #x01 #x02 #x03 #x04 #x05)
. К сожалению, их вывод примерно такой же.
Определим вектор v
из 100 байтов с десятичным значением 111:
(define v (make-u8vector 100 111))
и попробуем напечатать этот вектор.
Функции display
и write
отображают ещё куда ни шло как, хотя наблюдать дамп памяти в десятичном виде – так себе удовольствие:
> (display v)
#u8(111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111)
Функции print
и println
превращают наш вектор в нечитаемую кашу:
> (print v)
111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
Но самое невероятное, просто фантастическое убожество ожидает нас при наблюдении результата работы функции pp
(она же pretty-print
), по замыслу предназначенной печатать значения в легко пригодном для чтения виде, а также непосредственного ввода имени переменной v в REPL:
Скрытый текст
> (pp v) ; или просто v
#u8(111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111
111)
Немногим лучше системные сообщения об ошибках:
> (car v)
*** ERROR IN (stdin)@54.1 -- (Argument 1, pair) PAIR expected
(car
#u8(111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 ... #2
)
Нечего и говорить, что наблюдать сообщения об ошибках и диагностическую печать в таком виде – не то, чего хотелось бы в жизни.
Для начала займёмся формированием представления массива байтов в шестнадцатеричном виде. Некоторые диалекты Scheme влючают встроенные функции для шестнадцатеричных преобразований, Gambit Scheme – нет, но их можно реализовать за пять минут:
;; Преобразование целого числа 0-15 в шестнадцатеричную цифру
(define (hex1 d)
(vector-ref #("0" "1" "2" "3" "4" "5" "6" "7"
"8" "9" "A" "B" "C" "D" "E" "F")
d))
;; Преобразование байта в две шестнадцатеричные цифры
(define (hex2 b)
(string-append (hex1 (arithmetic-shift b -4)) (hex1 (bitwise-and b #xF))))
;; Преобразование u8vector в шестнадцатеричную строку
(define (hex-encode v)
(string-concatenate (map hex2 (u8vector->list v))))
С помощью введённой в одной из предыдущих статей скобочной нотации функцию hex1 можно представить ещё компактней:
(define (hex1 d)
[#("0" "1" "2" "3" "4" "5" "6" "7"
"8" "9" "A" "B" "C" "D" "E" "F") d])
Теперь мы можем при помощи нашей функции hex-encode
преобразовать массив байтов к вразумительному виду:
> (hex-encode v)
"6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F"
При желании можно последовательность цифр разбить пробелами на группы, но нам больше нравится компактность.
Конечно, мы бы могли на этом остановиться и использовать при печати явный вызов hex-encode
. Однако ведь запросто может получиться так, что u8vector
будет не самим по себе выдаваемым на печать значением, а, например, одним из элементов где-то на нижнем уровне сложного списка. Кроме того, такой подход никак не поможет в проблеме с системной диагностикой.
Как же сделать так, чтобы система сама печатала u8vector
в таком виде?
К счастью, как это обычно принято в культуре Lisp, системная библиотека Gambit Scheme сама написана на Gambit Scheme и доступна для расширения программистом.
Беглый анализ исходных текстов Gambit Scheme (конкретно файла lib/_io.scm
) показывает, что при печати все объекты преобразуются во внешнее представление при помощи функции ##wr
(последовательность ##
перед именем означает принудительное использование системного пространства имён). Значением имени ##wr
по умолчанию является полиморфная функция-форматировщик ##default-wr
, содержащая распознавание типа и вызов соответствующего типизированного форматировщика, например, ##wr-u8vector
. В определённые моменты (например, во время работы отладчика) вместо ##default-wr
может устанавливаться другой форматировщик. Для этого предназначена функция ##wr-set!
. Буквально там написано так:
(define ##wr ##default-wr)
(define-prim (##wr-set! x)
(set! ##wr x))
Давайте же установим поверх системного форматировщика свой собственный, который будет заменять значение u8vector
на его культурное представление в виде строки, и уже в таком виде передавать системному форматировщику:
;; красивый вывод u8vector
(redefine old-wr ##wr)
(define (new-wr we obj)
(cond ((##u8vector? obj)
(old-wr we (string-append "#u8:" (hex-encode obj))))
(else (old-wr we obj))))
(##wr-set! new-wr)
Наш собственный макрос redefine
мы рассматривали в одной из предыдущих статей. В принципе, тут можно написать просто
(define old-wr ##wr)
или
(define old-wr ##default-wr)
в зависимости от того, чего мы боимся меньше – повторного исполнения нашего кода инициализации или же запуска нашей программы в момент, когда системный форматировщик отличается от заданного по умолчанию. Вариант же с redefine
позволяет решить обе эти проблемы.
Выполнив эти операторы, получаем требуемый результат!
> (display v)
#u8:6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F
> (print v)
#u8:6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F
> (pp v)
"#u8:6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F"
> v
"#u8:6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F"
> (car v)
*** ERROR IN (stdin)@57.1 -- (Argument 1, pair) PAIR expected
(car
"#u8:6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6... #2
)
При необходимости можно в любой момент переключиться на формат по умолчанию с помощью (##wr-set! old-wr)
или на новый формат с помощью (##wr-set! new-wr)
.
Ограничением данного способа является то, что данные типа u8vector
, если они будут записаны куда-либо во внешнем представлении, при обратном чтении станут самыми натуральными строками, и с этим придётся отдельно разбираться, преобразовывая их обратно в u8vector
силами прикладной программы. Но это несложное дело.
Альтернативным подходом было бы написать свой собственный форматировщик ##wr-u8vector
, который представлял бы на печати u8vector
, не меняя его тип. Но, поскольку для типизированных форматировщиков в системной библиотеке не предусмотрена точка замены, нам пришлось бы при этом либо повторять у себя код функции ##default-wr
, либо модифицировать системное пространство имён, что было бы чревато возможной несовместимостью с будущими версиями транслятора. Кроме того, тогда вывод утратил бы свойство синтаксической совместимости со вводом, либо надо было бы модифицировать и лексический анализатор тоже.