第12章「数」

概要

ANSI Common Lispの第12章「数」を説明します。

Common Lispと数

Common Lispは数値計算や数式処理に適したプログラミング言語です。なぜ適しているか、という点を5つ挙げてみます。
  1. 複素数や分数など、数学上の数の概念がネイティブにサポートされています。また、巨大数などもメモリの許す限りサポートされます。
  2. 変数(シンボル)を第1級のオブジェクトとして扱うことができるため、代数をそのまま表現することができます。(数値計算ではなく数式処理と呼ばれています。)
  3. 高速なコンパイラや最適化の方法が仕様に定められているので、素早くプロトタイプを作り、そのまま実用的な速度で動作することができます。
  4. REPLが組み込まれているため、試しながら作ることができます。
  5. ANSIで規格化された標準が存在するため、作成したプログラムは長期間使用することができます。
Common Lispで作られている最も有名な数式処理システムが「Maxima」です。これは2番目に挙げた特徴である代数計算を行うソフトウェアです。このソフトウェアは高校や大学で学ぶ多くの「公式」を知っており、実際の数ではなく代数(シンボル)でも解を得ることができます。Common Lispは直接使ったことがなくても、Maxima は大学の授業などで利用したことがある人も多いと思います。

実際のところ、Common Lispで数を扱う場合に気をつけるべきことはほとんどありませんが、以下の点は注意が必要です。
  • 複素数は数学上では全ての数を含む概念ですが、ANSI Common Lispのcomplex型(クラス)は狭義の複素数のみを意味しており、実数は含みません。そのため、数を表す最も広い型はnumberが用意されており、そのサブタイプであるcomplexrealは排他的な関係にあります。
  • ANSI Common Lispの等価判定述語関数としてデフォルトで採用されているeqlは型の判定と値の判定の両方を含むため、整数と小数点数を比較する場合などには注意が必要です。数学上の等価性を判定するには=述語関数を使用してください。
以下は、後者の注意点のサンプルです。
;; find関数の標準の述語関数は eql です。
;; そのため、10 と 10.0 は等価とは見なされません。
(find 10 '(1.0 5.0 10.0 20.0))
; => NIL

;; :test キーワードで述語関数を変更することができます。
(find 10 '(1.0 5.0 10.0 20.0) :test #'=)
; => 10.0

このページではANSI Common Lispの標準で定められた様々な計算用関数(オペレータ)をサンプルと共に紹介します。なお、数の型とクラスについては第4章「型とクラス」を参照してください。また、数に関連する例外処理については第9章「コンディション」を参照してください。

10進数以外の表記

オペレータではありませんが、ANSI Common Lispにおける数の表記として、10進数以外に以下のリーダーマクロを使うことができます。
Reader Macro: #b
2進数による数を表現します。
Reader Macro: #o
8進数による数を表現します。
Reader Macro: #x
16進数による数を表現します。
Reader Macro: #Nr
N進数による数を表現します。
#b11111111
; => 255

#o377
; => 255

#xFF
; => 255

#10r255
; => 255

ちなみに、数から特定の進数表現の文字列に変換するにはformat関数を使うことができます。
(mapcar #'(lambda (v) (format nil v 255)) 
        '("~b" "~o" "~x" "~10r"))
; => ("11111111" "377" "FF" "255")

比較

ANSI Common Lispでは数の比較のために様々な関数が用意されています。

等価性の判定: =, /=

等しいか等しくないかを調べるには=関数や/=関数を利用します。
(= 10 100)
; => NIL

(/= 10 100)
; => T

これらの関数は可変長引数に対応しているため、複数の数が全て等しいことや等しくないことを判定することができます。

大小の判定: <, >, <=, >=

数の大小を判定する関数としては標記の4種類が定められています。使い方は=などと同じです。

また、これらも可変長引数に対応しています。

正負の判定: plusp, minusp, zerop

正負の判定にはplusp関数とminusp関数を使うことができます。また、ゼロの判定にはzeropを用いることができます。
(plusp -0.0)
; => NIL

(minusp -0.0)
; => NIL

(zerop -0.0)
; => T

これらは引数を一つだけ取る関数なので、高階関数に渡すことができます。
(mapcar #'plusp '(1 -5 3 2 -6 7 9 -3))
; => (T NIL T T NIL T T NIL)

(count-if #'plusp '(1 -5 3 2 -6 7 9 -3))
; => 5

最大値と最小値: max, min

ANSI Common Lispでは最大値を求めるmax関数と最小値を求めるmix関数も標準で用意されています。
(max 1 -5 3 2 -6 7 9 -3)
; => 9

(min 1 -5 3 2 -6 7 9 -3)
; => -6

ちなみに、比較したい値が前節のようにリストになっている場合は、apply関数を使うとそのまま適用できます。
(apply #'max '(1 -5 3 2 -6 7 9 -3))
; => 9
冗長ですが、loopマクロのmaximize節やminimize節を利用することもできます。
(loop for i in '(1 -5 3 2 -6 7 9 -3) maximize i)
; => 9

偶数と奇数: evenp, oddp

ANSI Common Lispでは偶数であるかどうかを判定するevenp関数と奇数であるかどうかを判定するoddp関数も標準仕様で定められています。

これらの関数も引数を1つだけ取るので、高階関数で使うと便利かもしれません。
(mapcar #'evenp '(1 -5 3 2 -6 7 9 -3))
; => (NIL NIL NIL T T NIL NIL NIL)

(remove-if #'oddp '(1 -5 3 2 -6 7 9 -3))
; => (2 -6)

最大公約数と最小公倍数: gcd, lcm

比較という趣旨からは少し外れますが、最大公約数を求めるgcd関数と最小公倍数を求めるlcm関数もこの節で紹介します。
(gcd 18 24 36 54)
; => 6

(lcm 18 24 36 54)
; => 216

これらも可変長引数に対応しています。

計算

この節では基本的な四則演算から三角関数や指数・対数などまで、ANSI Common Lispに定められた計算用関数を紹介します。

四則演算: +, -, *, /

四則演算は+, -, *, *の各関数で行います。

特に注意すべきことはありませんが、除算は引数の型によって返り値が分数(ratio)になったり小数(float)になったりする点はCommon Lispらしい特徴かもしれません。
;; 整数と整数の除算は整数または分数
(/ 2 3)
; => 2/3

;; 小数点数が含まれる場合は結果も小数点数
(/ 2.0 3.0)
; => 0.6666667

;; 倍精度浮動小数点数による計算
(/ 2.0d0 3.0d0)
; => 0.6666666666666666d0

;; 単精度ではなく倍精度を標準にする場合は
;;    *READ-DEFAULT-FLOAT-*FORMAT*
;; スペシャル変数を変更する
(setq *read-default-float-format* 'double-float)
; => DOUBLE-FLOAT

;; 倍精度になっている
(/ 2.0 3.0)
; => 0.6666666666666666

また、これらのオペレータは他の言語で使われているような二項演算子ではなく、可変長引数を取る関数として準備されているため、高階関数で使うこともできます。
(mapcar #'+ '(1 2 3) '(4 5 6) '(7 8 9))
; => (12 15 18)

(reduce #'/ #(1 2 3 4 5))
; => 1/120

インクリメントとデクリメント: incf, decf

インクリメントとは、変数の値に一定の値を加算した新しい値を代入し直すという操作です。デクリメントは減算の場合に使われます。

これらの操作は単純に値を得るだけでなく、新しく計算された値を変数に束縛するため、「副作用」があると表現されます。「副作用」はANSI Common Lispで Side Effects の項に明記されています。

このように、変数の値を変更するため、インクリメントを行うdecfとデクリメントを行うdecfは関数ではなくマクロとして定義されています。いずれもsetfマクロの基盤の上に設計されたマクロです。
incfを用いてカウンターを作る例です。スペシャル変数を使わず、クロージャ(lambda)を使っています。
(defun make-counter ()
  (let ((n 0))
    #'(lambda () (incf n))))
; => MAKE-COUNTER

(defparameter count (make-counter))
; => COUNT

(funcall count)
; => 1
(funcall count)
; => 2
(funcall count)
; => 3
incfdecfsetfマクロで実装されたマクロなので、変更できるのは変数だけではありません。setfマクロの place になりうるものはなんでも変更可能です。また、変更する値も1に固定されている訳ではなく、指定することができます。以下の例ではベクトルの各要素を変更しています。
(let ((a #(10 10 10)))
  (print a)
  (incf (aref a 0) 10)
  (decf (aref a 1) 10)
  (incf (aref a 2) 20)
  (print a))
; #(10 10 10) 
; #(20 0 30)
; => #(20 0 30)
setfマクロについては第5章「データと制御フロー」を参照してください。

絶対値: abs

絶対値を得るにはabs関数を使います。もちろん高階関数でも使うことができます。
(abs -3)
; => 3

(mapcar #'abs '(1 -5 3 2 -6 7 9 -3))
; => (1 5 3 2 6 7 9 3)

商と剰余: floor, mod, truncate, rem

単なる除算を行うのではなく、余りを求めたい場合はいくつかの関数が用意されています。
Function: floor
引数として得た値からマイナスの無限方向に切り捨て、一番近い整数を返します。第2引数を指定した場合は除算をしてから切り捨てます。切り捨てた後の整数と、切り捨てられた端数部分を多値として返します。
Function: mod
第1引数と第2引数を取り、floor関数に適用した場合の「切り捨てられた端数部分」を返します。
Function: truncate
floor関数と同様の処理をしますが、マイナス無限方向ではなくゼロ方向に向かって切り捨てます。マイナスの値を除算する場合にfloorと結果が異なります。
Function: rem
mod関数と同様の処理をしますが、除算をする際にtruncate関数が使用されます。
以下がサンプルです。
;; 正の数の場合、結果は同じ
(floor 5 2)
; => 2 ;
;    1
(truncate 5 2)
; => 2 ;
;    1
(mod 5 2)
; => 1
(rem 5 2)
; => 1

;; 負の数の場合、結果が異なる
;; floor はマイナス無限方向に切り捨てるので、剰余はプラスになる
(floor -5 2)
; => -3 ;
;    1

;; truncate はゼロ方向に切り捨てるので、剰余がマイナスになる
(truncate -5 2)
; => -2 ;
;    -1

;; mod と rem は floor と truncate の2番目の多値になっている
(mod -5 2)
; => 1
(rem -5 2)
; => -1

なお、floortruncateは切り捨て関数としても使えます。
(floor 5.5)
; => 5 ;
;    0.5
(truncate 5.5)
; => 5 ;
;    0.5

(floor -5.5)
; => -6 ;
;    0.5
(truncate -5.5)
; => -5 ;
;    -0.5

切り上げと丸め込み: ceiling, round

前節のfloor関数とtruncate関数は切り捨てを行いますが、ceiling関数は切り上げを行います。こちらはプラスの無限に向かって切り上げます。
(ceiling 5.5)
; => 6 ;
;    -0.5

(ceiling -5.5)
; => -5 ;
;    -0.5

最も近い整数に丸め込みを行うにはround関数を使います。
(round 5.4)
; => 5 ;
;    0.4000001

(round 5.5)
; => 6 ;
;    -0.5

しかし、この関数は四捨五入ではありません。0.5(1/2)が付いている数は切り上げても切り捨ててもどちらの整数も同じだけ端数が発生しますが、round関数は「偶数に向かって丸め込む」という性質があるため、必ずしも四捨五入にはなりません。例えば、上の5.5の例では6に向かって「切り上げ」られていますが、以下の6.5の例では6に向かって「切り捨て」られています。
(round 6.5)
; => 6 ;
;    0.5

三角関数: pi, sin, cos, tan

ANSI Common Lispでは三角関数、逆三角関数、双曲線関数に関するオペレータも定められています。この小節では三角関数の基本である sin, cos, tan のみ紹介します。

まず、πはpiという定数として定められています。
pi
; => 3.1415926535897932385L0

どこまでの桁を確保するかは処理系依存ですが、'long-float型の定数であることは仕様で定められています。

そして、三角関数を扱うsin関数、cos関数, tan関数はそれぞれラジアンを引数に取ります。
(sin (/ pi 2))    ; 90度
; => 1.0L0

(cos (* 2 pi))    ; 360度
; => 1.0L0

(tan (/ pi 4))    ; 45度
; => 1.0L0

ただし、計算は浮動小数点数で行われるため、非常に小さいですが誤差が発生する点には注意が必要です。
;; 90度の cos は本来はゼロちょうどになるが、
;; 10^-20という小さい桁に値が残っている
(cos (/ pi 2))    ; 90度
; => -2.5082788068421680123L-20

;; * は変数として使うと直前の評価結果(返り値)を使うことができる
;; round で丸めると当然ゼロになる
(round *)
; => 0 ;
;    -2.5082788068421680123L-20

三角関数を使いこなすような場合は、このページ冒頭で紹介した Maxima の利用を検討してみてください。Maxima であれば数式処理ができるので、より正確に計算できます。
;;;; Maximaの使用例
(%i1) cos(%pi/2);
(%o1)    0

指数と対数: expt, exp, log, sqrt

ANSI Common Lispは指数計算や対数計算を行う関数も用意されています。

まず、基本となる指数関数はexptです。この関数は基数と指数を引数に取り、指数計算の結果を返します。
(expt 2 10)
; => 1024

次に、ネイピア数eを基数とする指数計算はexp関数で行うことができます。以下の例はネイピア数の1乗なので、ネイピア数自身を求めている場合です。
(exp 1.0L0)
; => 2.7182818284590452354L0

そして、対数を取る場合はlog関数を使います。対数を取りたい値と基数を引数に取りますが、基数はオプショナル引数で、省略するとネイピア数eが使われます。
(log 1024 2)
; => 10

なお、指数を取るexptで分数の指数を指定することもできます。
(expt 81 1/2)
; => 9

ただし、1/2の指数は「平方根」としてよく使われるため、sqrtという特別な関数が用意されています。
(sqrt 81)
; => 9

乱数

ANSI Common Lispでは乱数に関するオペレータも定められています。

乱数の発生: random

ある値未満の乱数を発生させるにはrandom関数を使います。

以下の例では、10以下の整数を乱数として10回発生させ、リストにしています。
(loop repeat 10 for i = (random 10) collect i)
; => (9 2 2 9 1 0 6 5 2 3)

引数に浮動小数点数を指定すると、その型に合う乱数が生成されます。先ほどの整数の例を'double-floatに変更してみます。
(loop repeat 10 for i = (random 10.0d0) collect i)
; => (0.775162505373802d0
;     4.855618071467114d0 
;     3.1145921615779293d0
;     8.108298146029572d0 
;     3.941721817179782d0 
;     1.92021317633025d0 
;     7.407186741644924d0
;     4.53863018090657d0 
;     5.2238183137323935d0 
;     8.481698618651356d0)
random関数はとてもシンプルですが便利な関数です。

乱数発生器の初期化: make-random-state

乱数を発生させるには、計算の素となる種(シード)が必要です。Common Lispではそのシードを「ランダムステート」と呼んでいます。

標準状態でのランダムステートは*random-state*というスペシャル変数に束縛されています。このランダムステートをコピーして取り出すには、make-random-state関数を使います。(以下のコマンドは処理系を再起動してから試してみてください。)
(defparameter *rs* (make-random-state nil))
; => *RS*

*rs*
; => #S(RANDOM-STATE
;    #*1110101001001001100000101010110011001011111111100001000111110000) 

*random-state*
; => #S(RANDOM-STATE
;    #*1110101001001001100000101010110011001011111111100001000111110000) 

ランダムステートはrandom関数に指定して使うことができます。以下の例のように、同一のランダムステートからは同一の乱数が生成されます。
(dotimes (v 10)
  (format t "~a: (~a, ~a)~%"
          v
          (random 10 *random-state*)
          (random 10 *rs*)))
; 0: (4, 4)
; 1: (2, 2)
; 2: (9, 9)
; 3: (2, 2)
; 4: (2, 2)
; 5: (9, 9)
; 6: (1, 1)
; 7: (0, 0)
; 8: (6, 6)
; 9: (5, 5)
; ; => NIL
make-random-state関数は引数によって動作が大きく変わります。
  • 引数がnilの時: *randome-state*に束縛されている現在のランダムステートをコピーします。
  • 引数がtの時: 新しいランダムステートを生成します。
  • 引数がランダムステートの時: 引数のランダムステートをコピーします。
同一のプログラム内で同じ一連の擬似乱数を発生させたい場合はmake-random-state関数を使用してください。

参考: メルセンヌ・ツイスター法

ANSI Common Lispでは前節のように擬似乱数に関する関数が定められていますが、実際に乱数をプログラムで使いたい場合、「メルセンヌ・ツイスター法」を使うことが多いと思います。メルセンヌ・ツイスター法についての説明は省略しますが、高速で品質の良い擬似乱数生成法として知られているアルゴリズムで、非常に一般的に利用されています。
Common Lispでメルセンヌ・ツイスター法を実装したパッケージとしてmt19937というものが公開されています。Quicklisp というサードパーティ製ライブラリインストールツールでも簡単にインストールできますし、ライセンスがパブリック・ドメインになっているため、自分のプログラムに組み込んで配布する場合も制限はありません。(手動で試したい場合は mt19937.lisp をダウンロードして、loadしてみてください。ここではASDFの使い方は説明しませんが、ASDFを使用する場合はmt19937.asdも合わせてダウンロードしてください。)
(load "mt19937")

(mt19937:random 10)
; => 2

mt19937:*random-state*
; => #S(MT19937:RANDOM-STATE
;       :STATE
;       #(0 2567483615 1 2601187879 3919438689 2270374771 3254473187 705526435
;         752899028 4259895275 1635503293 287311810 3348146311 587101971 1133963260
;         ...))
mt19937パッケージにもrandom関数や*random-state*変数など、ANSI Common Lispと同名のものが用意されています。もしANSI Common Lispのrandomではなくmt19937randomをパッケージ名なしで使いたい場合はshadowing-import関数で継承して使用してください。
;; MT19937 パッケージをロードした状態で random シンボルを継承する
(shadowing-import 'mt19937:random)
; => T

;; random シンボルは COMMON-LISP パッケージではなく MT19937 パッケージのものが使われる
(symbol-package 'random)
; => #<PACKAGE MT19937>

低水準操作

Common Lispは古くはLispマシンにも利用され、低水準な操作も得意としてきました。低水準な操作とは、例えばビット単位の演算やバイト単位の処理を指します。

この節では基本的なビット演算子やシフト演算子、バイト単位の処理に必要なオペレータを紹介します。

ビット演算: lognot, logior, logxor, logand

「ビット演算」とは、数を2進数で表記した場合の各ビットに対する演算を意味します。

Common Lispにおいて数は数であり、ビット('bit)もバイト('(unsigned-byte 8))も数の一種です。ビットは01の数を意味しますが、これは(integer 0 1)という型と同一ですし、バイトは(integer 0 255)と同一です。しかし、これらは異なる意味論を与えられているため、独立した型の名前がついています。

フラグを作る

例えば、3つのオプションのオン・オフを情報として保持する場合、2の3乗=8種類の情報があるため、0から7までの数で表現することができます。しかし、0から7までの数で表現するよりも、3つのビットの並びで表現する方がよりオプションのオン・オフを明確に表現できます。0から7までの数を2進数で表現すると以下のようになります。
(dotimes (i 7)
  (format t "~3,'0b~%" i))
; 000
; 001
; 010
; 011
; 100
; 101
; 110
; => NIL

これを「ビット」の意味論をベースに表現し直してみます。まず、オプションA, B, Cの3種類について、それぞれのビットの10を使ってオン・オフを表現することとします。
(defconstant +op-A+ #b100) ; 4
; => +OP-A+
(defconstant +op-B+ #b010) ; 2
; => +OP-B+
(defconstant +op-C+ #b001) ; 1
; => +OP-C+

各オプションがオンの状態の時にはそのビットが1に切り替わるので、2のn乗の値で定数を定義しておくのです。そして、オプションの操作にはビット演算子を利用します。
Function: lognot
論理否定:各ビットを反転させた数を返します。
Function: logior
論理和:複数のビットを比較して、どれか1つが1であれば1を、全て0なら0を返します。
Function: logxor
排他的論理和:複数のビットを比較して、全て0または全て1の場合に0を、それ以外の場合に1を返します。
Function: logand
論理積:複数のビットを比較して、全て1なら1を、それ以外の場合に0を返します。

フラグを立てる

例えば、オプションAとオプションBをオンにするには、論理和であるlogiorを用います。
(logior +op-A+ +op-B+)
; => 6

(format t "~3,'0b" *)
; 110
; => NIL

ビットを見ると#b110になっており、AとBのビットが1になっているのが分かります。

フラグを下げる

ビット演算はフラグを立てるだけでなく、下げることもできます。フラグを下げるには、論理否定lognotと論理積logandを用います。以下の例では、全てのオプションをオンにした*user*について、オプションBだけを取り消します。
(defparameter *user* nil)
; => *USER*
(setq *user* (logior +op-A+ +op-B+ +op-C+))
; => 7

(setq *user* (logand *user* (lognot +op-B+)))
; => 5

(format t "~3,'0b~%" *user*)
; 101
; => NIL

フラグを判別する

さらにビット演算は、フラグが立っているかどうかを判別することもできます。フラグの判別にも論理積logandを用います。

前節の続きで、*user*の各オプションがオンかどうかを判別します。
(if (= (logand *user* +op-A+) +op-A+) t nil)
; => T
(if (= (logand *user* +op-B+) +op-B+) t nil)
; => NIL
(if (= (logand *user* +op-C+) +op-C+) t nil)
; => T

(format t "User: ~b (= ~d)~%" *user* *user*)
; User: 101 (= 5)
; => NIL

フラグを変更する

さらにさらに、ビット演算を使うと現在の値を調べずにフラグを変更することもできます。常に変更したい場合、排他的論理和logxorを使うことができます。

例えば、以下の例では現在のオプションの状況を調べることなく、直接オプションのオン・オフを反転させます。オプションBとCを同時に反転させてみます。
(setq *user* (logxor *user* (logior +op-B+ +op-C+)))
; => 6

(format t "~b~%" *user*)
; 110
; => NIL

オフだったオプションBがオンに、オンだったオプションCがオフになっているのが分かります。

フラグ管理とビット演算

このように、ビット演算はオプションを管理する際にとても便利な性質があります。Common Lispではビットも普通の整数と同様にネイティブに扱うことができるため、必要に応じて利用を検討してみてください。

ちなみに、ここで紹介した3つのオプションはUnix系オペレーティングシステムにおけるパーミッションを参考に考えてみました。3bitは8進数に対応し、4bitは16進数に対応します。前者はパーミッションなどで使われており、後者はRGB(光の3原色)などで使われています。

なお、ANSI Common Lispにはbooleというビット演算のための汎用オペレータがありますが、個人的にはlogandなどの方が明瞭だと思いますので、boole関数は使用しません。booleは関数自体が汎用目的に設計されており、演算自体は別途定数として定められています。そのため、演算自体を引数として渡したりするような場合にはbooleの方が良いかもしれません。

シフト演算: ash

前節のビット演算は各ビットに対する演算でしたが、ビット全体に対する演算として「シフト演算」というものもあります。これはビット列を「ずらす」というものです。

ANSI Common Lispにおけるシフト演算はash関数で行います。例えば、先ほどの例でさらに3つのオプションを追加する場合、ビット列を左に3つシフトしなければなりません。
(format t "~3,'0b" *user*)
; 110
; => NIL

(format t "~3,'0b" (ash *user* 3))
; 110000
; => NIL

逆に右にシフトするには同じash関数で負の値を指定します。
(format t "~3,'0b" (ash *user* -2))
; 001
; => NIL

シフト演算は2のべき乗の乗算と除算と同じですが、これも掛け算や割り算とは異なる意味論があり、あくまでもビットを操作対象として考えた操作です。
*user*
; => 6

(ash *user* 1)
; => 12

(ash *user* 3)
; => 48

;; シフト演算は2のべき乗と等しい
(* *user* (expt 2 3))
; => 48

(ash *user* -2)
; => 1

;; 右シフトの場合は、端数が切り捨てられている
(* *user* (expt 2 -2))
; => 3/2

;; 2のマイナスのべき乗を floor したものに等しい
(floor (* *user* (expt 2 -2)))
; => 1 ;
;    1/2

バイト単位の操作: byte, ldb, dpb

前節までの「低水準操作」はビットを中心としたものですが、ビットの列は「バイト」とも呼ばれており、現代のコンピュータでは8bit(オクテット)をひとくくりにして扱うことがよくあります。1オクテットは0から255までの整数を表現することができ、2オクテットは0から65535までを表現することができます。

「バイト」という言葉は本来は「複数のビット」という意味でしたが、現代では「オクテット」の代わりに使われることが多いです。ただし、この節では分かりにくくなるため、8bitはオクテットと明示し、ビット列のことをバイトと呼ぶことにします。

1オクテットは8bitなので、4bitずつで見ると2つに分けることができます。4bitで分ける意味は4bitが16進数に対応するからです。16進数は0からFまでの数と文字を使って0から15までの数を表現します。つまり、1オクテットは16進数表現を2つ並べることで表現できます。
(format t "~x" 255)
; FF
; => NIL

(format t "~,,' ,4:b" 255)
; 1111 1111
; => NIL

バイト単位の操作はこのようにひとかたまりのビット列を処理対象とします。ある数を2進数で表現した時に、特定のビット列を抽出するにはbyte関数とldb関数を組み合わせます。

例えば、10進数の100は、2進数として表現すると#b01100100に、16進数として表現すると#x64になります。
(format t "~9,'0,' ,4:b" 100)
; 0110 0100
; => NIL

(format t "~x" 100)
; 64
; => NIL
100という10進数を2進数で表現した際の、#b01100100を4bitずつ取り出すと以下のようになります。
(ldb (byte 4 0) 100)
; => 4
(ldb (byte 4 4) 100)
; => 6
byte関数は取り出すbitの長さと開始位置を引数として取り、バイト指定子を返します。ここでいう「バイト」は必ずしも「オクテット」である必要はなく、ここでも4bitの長さで取り出しています。
ldb関数はバイト指定子と整数を引数に取り、整数を2進数表記した場合のビット列からバイト指定子で指定された部分のビット列を抜き出します。ldbload byte の意味です。
今度は上の例では100という1オクテット以内の数からビット列を抽出しましたが、このldbbyteの組み合わせは複数オクテットで構成される数でも使うことができます。

試しに、10進数の43981、16進数の#xABCDのビット列(バイト)を抜き取ってみましょう。
#xABCD
; => 43981

(format t "~19,'0,' ,4:b" #xABCD)
; 1010 1011 1100 1101
; => NIL
43981は2オクテットで表現されます。オクテット単位で表現すると、下位桁のオクテットが#xCDで、上位桁のオクテットが#xABです。
;; 下位の8bit(1オクテット)を取り出す
(ldb (byte 8 0) 43981)
; => 205

(format t "~9,'0,' ,4:b" 205)
; 1100 1101
; NIL

(format t "~x" 205)
; CD
; => NIL

;; 上位の8bit(1オクテット)を取り出す
(ldb (byte 8 8) 43981)
; => 171

(format t "~9,'0,' ,4:b" 171)
; 1010 1011
; => NIL

(format t "~x" 171)
; AB
; => NIL

さらにldb関数は(setf ldb)関数も用意されており、いわゆるアクセッサとして使うことができます。アクセッサとして使う、ということはビット列(バイト)の断片から整数を構築することができる、ということです。
(defparameter *n* 0)
; => *N*

(setf (ldb (byte 8 0) *n*) #xCD)
; => 205

(setf (ldb (byte 8 8) *n*) #xAB)
; => 171

*n*
; => 43981

これがどのように便利かということは、自分でバイトから数を構築すれば分かります。
(+ (* #xAB (expt 2 8)) #xCD)
; => 43981

上位の桁のバイトに2のべき乗を掛け、下位の桁のバイトを加算するとバイトから数を構築することができます。(setf ldb)の機能を使えば、計算を自分で考える必要はなく、バイトを適切な位置に配置するだけで数を構築できます。

バイト単位の操作はbyte関数とldb関数の組み合わせがメインですが、もう一つ古い歴史のある関数があります。それはdpb関数で、バイトの一部を変更するというものです。

例えば、#xABCDを2進数表記した時の下位4bit目と3bit目を0にしてみます。
(format t "~19,'0,' ,4:b" #xABCD)
; 1010 1011 1100 1101
; => NIL

(dpb #b00 (byte 2 2) #xABCD)
; 43969

(format t "~19,'0,' ,4:b" *)
; 1010 1011 1100 0001
; => NIL

2進数表記の桁を見比べて行けば、3bit目と4bit目だけ違うのがわかると思います。
ちなみに、違う桁を抜き出すには排他的論理和logxorが使えます。
(logxor #xABCD 43969) ; 43969がビットを置き換えた後の数
; => 12

(format t "~b" *)
; 1100
; => NIL

ANSI Common Lispでは他にも様々な低水準操作関数が用意されています。ただ、バイナリデータを扱うにはここで紹介したオペレータと第21章「ストリーム」で扱うread-byte, read-sequence, write-byteなどの入出力関数を知っておけば十分な低水準操作を行うこともできると思います。

Lispは抽象度の高い言語なので低水準操作を学ぶことは敬遠されがちかもしれませんが、バイナリ操作はプログラミングの幅も広がるので、ぜひ取り組んでみてください。

0 件のコメント :

コメントを投稿