プログラミングを頑張る日記

プログラミングを勉強して、ハッカーになろう

Common Lispを頑張る(31)

今日も当然「Land of Lisp」です。
昨日までのLOOPに続き、今日はFORMATを見ていきます。

format関数

CL-USER> (format t "hey, ~a!~%today is ~d." "neko" 11)
hey, neko!
today is 11.
NIL
CL-USER>

前に「実践Common Lisp」をやったときの記憶を引っ張り出して書きました。
さて、じっくり見ていくことにしましょう。

formatの最初の引数、上の例ではtですが、これはformatが生成するテキストの出力先を指定するためのものだそう。
許される引数は下記のもの。
nil : 出力する代わりに、生成したテキストを文字列として返す。
・t : 結果をコンソールに出力する。このときの戻り値はnilとなる。
・stream :データを出力ストリームに書き出す。

ストリームは以前少し触りましたね。次の章で詳しい解説があるそうです。
nilは、文字列にしてくれると。ふむ。

CL-USER> (defparameter *hoge*
           (concatenate 'string (format nil "My name is ~a. " "neko") "I study Lisp."))
*HOGE*
CL-USER> *hoge*
"My name is neko. I study Lisp."
CL-USER>

CL-USER> (defparameter *hoge*
(concatenate 'string (format nil "My name is ~a. " "neko") "I study Lisp."))

CL-USER> *hoge*
"My name is neko. I study Lisp."
CL-USER> (format nil "My name is ~a. " "neko")
"My name is neko. "
CL-USER>
|

戻り値はただの文字列になるみたいですね。

2番目の引数は、制御文字列と呼ばれ、テキストの整形方法を指定できるようです。
原則、制御文字列中のテキストはそのまま出力されるのですが、ここに制御シーケンスとやらを書くこともでき、
それが出力形式に影響を与えるようです。
制御シーケンスは常にチルダ(~)で始まり、今日使ったもので言えば~aや~d、~%がそれに当たるようです。

制御文字列の後の引数は、実際の値、整形され表示されるべきデータだそうです。
制御文字列に従ってここの引数は解釈され整形されるということ。

制御シーケンス

~s、~a

ざっくり言ってしまえば、prin1のようにLispの値を表示するのが~s、
princのように人間に優しくしてくれるのが~aだそうです。

CL-USER> (format t "まるで~sのようだ" "prin1")
まるで"prin1"のようだ
NIL
CL-USER> (format t "まるで~aのようだ" "princ")
まるでprincのようだ
NIL
CL-USER>

これらの制御シーケンスにパラメータを与え、更に出力を制御することができる様子。

CL-USER> (format t "空白を使って10パディングする->~10a<-ここまで" "hoge")
空白を使って10パディングする->hoge      <-ここまで
NIL
CL-USER> (format t "右詰めで10パディングするには@を使う->~10@a<-ここまで" "hoge")
右詰めで10パディングするには@を使う->      hoge<-ここまで
NIL
CL-USER>

さらに、パラメータは一つ以上与えることもできるそう。2つ目以降のパラメータを与える際はコンマを使います。

CL-USER> (format t "コンマを使って9空白を出力->~10,9a<-ここまで" "hoge")
コンマを使って9空白を出力->hoge         <-ここまで
NIL
CL-USER>

2番目のパラメータで、何文字づつ表示幅を埋めるか指定できるようです。
上の例だと、hogeを出力した後に9文字分スペースを出力し、
合計が最初のパラメータの10文字を超えたので整形が終わっている模様。
ただ、何文字ずつパディングしたい!みたいなことってあんまりなさそうですよね。

たまにパディングというわけではなく、絶対に文字のあとにいくつかの空白が欲しい!ということはありそう。
それをさせてくれるのが3番目のパラメータだそうです。
このとき1番目と2番目のパラメータを指定する必要はなさそうですが、
そういう時はただコンマを置いて、これは何番目のパラメータだぞ、と示せばいいそう。

CL-USER> (format t "hogeの後に5文字分スペース:~,,5a:どう?" "hoge")
hogeの後に5文字分スペース:hoge     :どう?
NIL
CL-USER>

省略した場合にはデフォルトの値(つまりパディングしない)となるよう。

4番目のパラメータは、パディングに使われる文字を指定できます。

CL-USER> (format t "ボラボラボラボラ~20,,,'.@a" "ボラーレ・ヴィーア")
ボラボラボラボラ...........ボラーレ・ヴィーア
NIL
CL-USER>

数値を整形する

まずはさらっとまとめてみます。
~x : 16進数で表示する。
~b : 2進数で表示する。
~d : 10進数で表示する。
~f : 浮動小数点を表示する。
~$ : 価格を表示する。

CL-USER> (format t "~dを16進数で表示すると~x、2進数で表示すると~b" 1000 1000 1000)
100016進数で表示すると3E82進数で表示すると1111101000
NIL
CL-USER> (format t "円周率は~fです" pi)
円周率は3.141592653589793です
NIL
CL-USER> (format t "1ドルは大体~$円です" 112)
1ドルは大体112.00円です
NIL
CL-USER>

~dの第一パラメータはパディングを指定し、第二パラメータでパディング文字を指定します。

CL-USER> (format t "~10d円しか財布にない" 50)
        50円しか財布にない
NIL
CL-USER> (format t "~10,'xd円しか財布にない" 50)
xxxxxxxx50円しか財布にない
NIL
CL-USER>

~fについても最初のパラメータで表示幅を指定することができます。
浮動小数点数は指定された表示幅に小数点を含めて収まるように丸められるということです。
二番目のパラメータでは小数点以下に表示する桁数を指定でき、
三番目のパラメータは表示する数を10の指数倍するための、その指数。

CL-USER> (format t "円周率は~5fです" pi)
円周率は3.142です
NIL
CL-USER> (format t "円周率は~,20fです" pi)
円周率は3.14159265358979300000です
NIL
CL-USER> (format t "勝率は~,,2f%だ" 0.02)
勝率は2.0%だ
NIL
CL-USER>

新しい行へ

Common Lispで新しい行を始めるには、terpriとfresh-lineの2つの方法があるそうです。
terpri。はじめましてですね。
terpriは、必ず改行する。fresh-lineはその時のカーソルが行頭にない場合に限って改行するという違い。

CL-USER> (loop for i below 5 do (terpri))





NIL
CL-USER> (loop for i below 5 do (fresh-line))
NIL
CL-USER>

上の例ではflesh-lineは改行の必要はないと判断してくれました。スマートなやつです。

formatにもそれぞれ対応する制御シーケンスがいるようです。
~% : terpriに対応する。
~& : fresh-lineに対応する。

CL-USER> (loop for i below 5 do (format t "~%"))





NIL
CL-USER> (loop for i below 5 do (format t "~&"))
NIL
CL-USER>

これらの制御シーケンスがとるパラメータは一つ、改行数の指定です。

CL-USER> (format t "5行改行してくれ~5%5行改行したかな")
5行改行してくれ




5行改行したかな
NIL
CL-USER>

テキストを揃える

formatには、テキストを揃えるための機能もたくさん備わっているそうです。
テーブルを作ったり、センタリングしたりということができるそう。
例を示すためということで、ランダムな長さの文字列を返す関数を作ります。

CL-USER> (defun random-animal ()
           (nth (random 5) '("恐ろしい猫" "猫" "噛み付いてくる猫" "可愛い猫" "レア猫")))
RANDOM-ANIMAL
CL-USER> (random-animal)
"レア猫"
CL-USER> (random-animal)
"猫"
CL-USER>

次に上の関数で表を作ります。表を作るには~t制御シーケンスが便利だそう。
与えるパラメータは、整形後のテキストが現れるべきカラム位置だということですが…。

CL-USER> (loop repeat 10
              do (format t "~5t~a~20t~a~35t~a~%"
                         (random-animal)
                         (random-animal)
                         (random-animal)))
     噛み付いてくる猫       可愛い猫           可愛い猫
     レア猫            可愛い猫           レア猫
     レア猫            恐ろしい猫          噛み付いてくる猫
     噛み付いてくる猫       猫              可愛い猫
     猫              レア猫            可愛い猫
     レア猫            噛み付いてくる猫       可愛い猫
     レア猫            恐ろしい猫          恐ろしい猫
     噛み付いてくる猫       可愛い猫           可愛い猫
     噛み付いてくる猫       レア猫            レア猫
     猫              噛み付いてくる猫       可愛い猫
NIL
CL-USER> (defun random-animal ()
           (nth (random 5) '("恐ろしい猫" "猫" "噛猫" "可愛い猫" "レア猫")))
WARNING: redefining COMMON-LISP-USER::RANDOM-ANIMAL in DEFUN
RANDOM-ANIMAL
CL-USER> (loop repeat 10
              do (format t "~5t~a~20t~a~35t~a~%"
                         (random-animal)
                         (random-animal)
                         (random-animal)))
     噛猫             噛猫             噛猫
     猫              恐ろしい猫          可愛い猫
     噛猫             猫              猫
     猫              レア猫            噛猫
     レア猫            レア猫            猫
     猫              可愛い猫           レア猫
     恐ろしい猫          噛猫             噛猫
     噛猫             可愛い猫           可愛い猫
     猫              噛猫             噛猫
     噛猫             可愛い猫           噛猫
NIL
CL-USER> (loop repeat 10
              do (format t "~5t~a ~15t~a ~25t~a~%"
                         (random-animal)
                         (random-animal)
                         (random-animal)))
     可愛い猫      レア猫       レア猫
     噛猫        噛猫        恐ろしい猫
     噛猫        猫         猫
     レア猫       噛猫        レア猫
     レア猫       恐ろしい猫     噛猫
     恐ろしい猫     猫         レア猫
     レア猫       噛猫        可愛い猫
     可愛い猫      恐ろしい猫     噛猫
     噛猫        噛猫        恐ろしい猫
     噛猫        可愛い猫      猫
NIL
CL-USER>

色々試してみましたがどうにもうまくいきません。日本語だからでしょうか。

CL-USER> (defun random-animal ()
           (nth (random 5) '("dog" "tick" "tiger" "walrus" "kangaroo")))
WARNING: redefining COMMON-LISP-USER::RANDOM-ANIMAL in DEFUN
RANDOM-ANIMAL
CL-USER> (loop repeat 10
              do (format t "~5t~a ~15t~a ~25t~a~%"
                         (random-animal)
                         (random-animal)
                         (random-animal)))
     walrus    tiger     dog
     walrus    dog       walrus
     tiger     walrus    tiger
     walrus    tiger     tiger
     tiger     kangaroo  walrus
     tiger     kangaroo  tick
     walrus    walrus    tick
     walrus    walrus    dog
     dog       tick      kangaroo
     kangaroo  kangaroo  walrus
NIL
CL-USER>

諦めました。マルチバイト文字が悪いのでしょう、きっとそう。

次は、1行の中で動物名が等しい距離をとって表示されるようにしてみます。

CL-USER> (loop repeat 10
              do (format t "~30<~a~;~a~;~a~>~%"
                         (random-animal)
                         (random-animal)
                         (random-animal)))
dog         tiger          dog
dog        kangaroo        dog
tiger        tiger        tick
dog      kangaroo       walrus
walrus       tick       walrus
dog        dog        kangaroo
tick      walrus      kangaroo
dog        dog        kangaroo
dog          tick          dog
kangaroo      tiger      tiger
NIL
CL-USER>

制御文字列がヤバイことになってきました。
"~30<"が文字揃えの開始を示しています。ブロックが30文字の幅であることと"<"が開始の合図でしょうか。
~aが"~;"で区切られていますが、これが新たな字寄せの対象が来ることを示しているそうです。
最後は"~>"でブロックの終了を示しています。
…すんなり読めるようになれるのでしょうか。

綺麗なブロックにできたものの、列で見るとガタガタです。
センタリングするには、~<制御シーケンスに":@"フラグを与えてあげればいいそうです。

CL-USER> (loop repeat 10
              do (format t "~30:@<~a~;~a~;~a~>~%"
                         (random-animal)
                         (random-animal)
                         (random-animal)))
    tick    walrus    dog     
  tick  kangaroo   kangaroo   
  walrus  walrus   kangaroo   
    tiger    tick    tiger    
   tiger    walrus    tick    
  kangaroo   kangaroo   dog   
   walrus   kangaroo   tick   
   tiger    walrus    tick    
    kangaroo    dog    dog    
    tiger    tiger    tick    
NIL
CL-USER>

…ガタガタが極まってしまったような。行ごとにセンタリングしたらまあ、こうなりますよね。
綺麗に揃えるには、列ごとに独立させ10文字ずつセンタリングすればよいそうです。

CL-USER> (loop repeat 10
              do (format t "~10:@<~a~>~10:@<~a~>~10:@<~a~>~%"
                         (random-animal)
                         (random-animal)
                         (random-animal)))
 kangaroo  kangaroo    dog    
  tiger      dog     kangaroo 
  tiger      tick     tiger   
  tiger    kangaroo  kangaroo 
  tiger    kangaroo    dog    
  walrus    tiger      dog    
  tiger     tiger      tick   
  walrus    walrus    tiger   
   tick     tiger     tiger   
   tick     walrus     dog    
NIL
CL-USER>

綺麗にできました!落ち着いて読めば制御シーケンスも怖くない…多分。
ちなみに最初試していた日本語の動物たち、最後のformatでやってもガタガタでした。

formatの中で繰り返し

formatの中の制御シーケンスはそれだけで一つのプログラミング言語といってもいいぐらいだそう。
ドメイン特化言語とみなされることも多いそう。ワードだけは覚えておきます。
勿論ループだってできちゃうそう、"~{"と"~}"で。
~{と~}を含んだ制御文字列とリストを与えると、リスト中のデータをループで処理するそうです。

CL-USER> (format t "~{目の前を~aが横切った、~%~}ここは一体どこなんだ" *animals*)
目の前をdogが横切った、
目の前をdogが横切った、
目の前をtickが横切った、
目の前をkangarooが横切った、
目の前をdogが横切った、
目の前をdogが横切った、
目の前をdogが横切った、
目の前をwalrusが横切った、
目の前をwalrusが横切った、
目の前をwalrusが横切った、
ここは一体どこなんだ
NIL
CL-USER>

ループ一回に、複数の値を取ることもできるそうです。

CL-USER> (format t "~{あれは~aかな、いや、~aの見間違いだろう~%~}" *animals*)
あれはdogかな、いや、dogの見間違いだろう
あれはtickかな、いや、kangarooの見間違いだろう
あれはdogかな、いや、dogの見間違いだろう
あれはdogかな、いや、walrusの見間違いだろう
あれはwalrusかな、いや、walrusの見間違いだろう
NIL
CL-USER>

連続して同じ動物が続くリストだったせいで完全に変な人になってしまいました。
ちなみに10要素のリストなので…

CL-USER> (format t "~{あれは~aかな、いや、~aの見間違いだろう。いや~aか?~%~}" *animals*)
あれはdogかな、いや、dogの見間違いだろう。いやtickか?
あれはkangarooかな、いや、dogの見間違いだろう。いやdogか?
あれはdogかな、いや、walrusの見間違いだろう。いやwalrusか?
あれはwalrusかな、いや、; Evaluation aborted on #<SB-FORMAT:FORMAT-ERROR {1004389B13}>.
CL-USER>

3個ずつ要素を取り出すと11個めを取り出しに行ったところでエラーになりました。

クレイジーな整形トリック

CL-USER> (format t "|~{~<|~%|~,33:;~2d ~>~}|" (loop for x below 100 collect x))
| 0  1  2  3  4  5  6  7  8  9 |
|10 11 12 13 14 15 16 17 18 19 |
|20 21 22 23 24 25 26 27 28 29 |
|30 31 32 33 34 35 36 37 38 39 |
|40 41 42 43 44 45 46 47 48 49 |
|50 51 52 53 54 55 56 57 58 59 |
|60 61 62 63 64 65 66 67 68 69 |
|70 71 72 73 74 75 76 77 78 79 |
|80 81 82 83 84 85 86 87 88 89 |
|90 91 92 93 94 95 96 97 98 99 |
NIL
CL-USER>

制御シーケンス怖くないなんて一瞬でも思った自分が馬鹿でした。

えーと…ループが始まる前に|を出力していますね。そしてループが始まることを示しています。
次はブロックが始まることを示し…"|~%|"は縦棒、改行、縦棒を出力するんですよね…?
…縦棒が見当たらないしまだ改行していませんが。数字をまだ1つも出していないです。
これは置いておいて…"~:;"は、先立つ文字列を、現在のカーソル位置が2番目のパラメータで
指定されるカラム位置を超えていた場合に限り出力するようにするそう。
なるほど!33文字を超えた時に縦棒、改行、縦棒を出力するのですね。
そして、2文字分のスペースを取って数字を出力、出力し続けて1行が33文字を超えたらバーンと改行。
ループが終わったら縦棒をそっと置いて終わり。
ああ…難しかった。早く説明を読んでおけばよかった。というかこの後にコンマが置いてあるから
"|~%|"がパラメータなのかもしれないと気づけばよかったですね。

このあとにまたまたヤバイらしいゲームが、なんと1ページで作られているのですが、
それの解読はまた明日にしようと思います。

それではおやすみなさい。