Продолжим рассмотрение неочевидных поучительных возможностей программирования на 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 позволяет по желанию программиста менять свой синтаксис и семантику, поэтому проблема вполне разрешима.
Для начала займёмся формированием представления массива байтов в шестнадцатеричном виде. Некоторые диалекты Scheme влючают встроенные функции для шестнадцатеричных преобразований, Gambit Scheme – нет, но их можно реализовать за пять минут:
;; Преобразование целого числа 0-15 в шестнадцатеричную цифру
(define (hex1 d)
(cdr (assq d '((0 . "0") (1 . "1") (2 . "2") (3 . "3")
(4 . "4") (5 . "5") (6 . "6") (7 . "7")
(8 . "8") (9 . "9") (10 . "A") (11 . "B")
(12 . "C") (13 . "D") (14 . "E") (15 . "F")))))
;; Преобразование байта в две шестнадцатеричные цифры
(define (hex2 b)
(string-append (hex1 (arithmetic-shift b -4)) (hex1 (bitwise-and b 15))))
;; Преобразование u8vector в шестнадцатеричную строку
(define (hex-encode v)
(string-concatenate (map hex2 (u8vector->list v))))Теперь мы можем при помощи нашей функции hex-encode преобразовать массив байтов к вразумительному виду:
> (hex-encode v)
"6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F"
Но как же сделать так, чтобы система сама печатала в таком виде?
К счастью, как это обычно принято в культуре 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
(define 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)Выполнив эти операторы, получаем требуемый результат!
> (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-vr) или на новый формат с помощью (##wr-set! new-vr).
