Lispマル非入門

コンピュータは電子計算機とも訳されるように計算に用いるのが本流である、という信念*1の元に、私はちょっとした計算にもパソコンを用いている。

当然ながら*2計算にはLisp処理系のREPLを使用する。*3

ちょっとした計算にてLisp処理系のREPLを用いるメリットは、例えば何かを単純に足し引きする時に、足し引きする値ごとに演算子を書かなくて済むのである。

私はよく買い物の計画を練るのにREPLを用いる:

; ルーターのお守りを任されたけど使い方が分からん。
; 入門書でも読もうかな。
(+
 4730   ; 978-4839965402
 )
; => 4730

; そういえば新人向けのLinux入門書を見繕わないと。
; この辺りの本とかどうだろう?
(+
 4730   ; 978-4839965402
 1980   ; 978-4048913928
 2970   ; 978-4797380941
 )
; => 9680

; ついでに同僚のA氏が絶賛してた本も買おうかな……。
(+
 4730   ; 978-4839965402
 1980   ; 978-4048913928
 2970   ; 978-4797380941
 4488   ; 978-4839981723
 )
; => 14168

; ちょっと金額を減らしたいなあ。
; こんな感じでどうだろう?
(+
 4730   ; 978-4839965402
 2970   ; 978-4797380941
 4488   ; 978-4839981723
 )
; => 12188

; 端はポイントで支払おう。
(+
 4730   ; 978-4839965402
 2970   ; 978-4797380941
 4488   ; 978-4839981723
 -188
 )
; => 12000

買い物が1ヶ所のネットショップで済むならば、買い物かごの機能で合計金額を確認すれば済むだろう。だけど時には品揃えと単価の都合より複数のネットショップ(そして稀に実店舗も)に分散して購入を検討することもある。そんな時は、REPL上であれこれ項目をこねくり回して合計金額を計算することが多い。*4

中置記法を用いるプログラミング言語に慣れた身からすると、Lisp処理系での足し算は少し興味深い。

私の普段使いの言語では、加算は二項演算だ*5。そのため加算する際には値が2個必要だ。値が1個だけでは駄目だ。そして値が3個以上あってもいけない(演算子を都度追加しなくてはならない)。

# これはOK。
10 + 3
# => 13

# これはNG……というかREPLでは「右辺となる値の入力待ち」になりそう。
10 +

# これはNG。シンタックスエラー。
10 + 3 5

# 演算子を都度追加すればOK。
10 + 3 + 5
# => 18
10 + 3 + 5 + 2
# => 20

(後置記法/逆ポーランド記法を用いるプログラミング言語ではどうだろうか? Forthの解説文を読んだ感じでは、ワード+はスタックに積まれている値を2個使用するようだが……)

一方で、Lisp処理系では値が1個でも3個以上でも加算した結果を得ることができる。だから、REPLを使ってあれこれ考えながらこねくり回す時に、安心して「取り扱う値」にだけ集中していられる。演算子の書き忘れに起因するシンタックスエラーなどを気にしなくてもよい。

; これはOK。
(+ 10 3)
; => 13

; これもOK。
(+ 10)
; => 10

; これだってOK。
(+ 10 3 5)
; => 18
(+ 10 3 5 2)
; => 20

それと、中置記法の言語では例えば「10 + -3」のような書き方は合法だが、個人的に見た目に違和感があり、どうしても「10 - 3」と書きたくなる。しかし理由は不明だが、Lisp処理系で「(+ 10 -3)」と書いても違和感がない*6。だから安心して負の値のリテラルを加算するコードを書ける。

注意点としては、多くのLisp処理系には分数型があるので、整数値を除算した時に意図せず分数型の値が得られてしまうことがある。

; 72の法則を試してみよう。
; 年利4%だとどうだろう?
(/ 72 4)
; => 18

; 年利5%だと?
(/ 72 5)
; => 72/5

; おおっと、明示的に小数のリテラルで書かないと駄目だった。
(/ 72.0 5)
; => 14.4

変数については、letなどを用いて局所変数を使う分には、Lisp処理系ではワンライナーでも比較的書きやすい気がする。普段使いの言語でも、ブロックスコープを用いれば似たようなことを実現できるが、それをワンライナーで書くのはちょっとだけ面倒に感じる。この辺は、Lispの「式もデータ構造もリスト」という特徴がコードの見た目に及ぼす影響によるものだろう。

; 今はこんな感じ:
(+ 108)
; => 108

; 直近の想定値を加味すると:
(let ((n 21)) (+ 108 n -40 -21.2 -4.8))
; => 63.0

; さらに4回分の想定値を加味すると:
(let ((n 21)) (+ 108 n -40 -21.2 -4.8 (* n 4) 10 -40 -21.2))
; => 95.8

; ……うーん、条件を少し変更してみるか:
(let ((n 22)) (+ 108 n -40 -21.2 -4.8))
; => 64.0
(let ((n 22)) (+ 108 n -40 -21.2 -4.8 (* n 4) 10 -40 -21.2))
; => 100.8

ところで大域変数*7ではなく局所変数を用いるしょうもない理由の1つとして「letあたりならCommon LispSchemeのどちらのREPLでも大体同じ感じに書ける」というものがある。例えばここまでに書いたLispのコード片はCLISPGaucheのどちらでも動作する。*8

ここまで書いてきたように、私は単純な足し引きの計算にLisp処理系のREPLを多用しているのだが、それゆえに、ここまでに書いた内容よりも高度なコードはほぼ確実に書けない自信がある。

今、手癖の範囲で書けるLispのコードは、この程度が限界だ。

; 四捨五入を考慮しないなら、こんな感じ?
(let ((ir (* 0.31 0.66)) (lir 0.05)) (if (>= ir lir) ir lir))
; => 0.2046

if式は便利だ……KotlinのノリでSwiftでif式を書こうとしてシンタックスエラーしちゃう癖を何とかしたいなあ。CやC++ならちゃんと「if文」モードでコーディングできるんだけど、Swiftだとうまく切り替わらないの、何でだろう?

*1:そんなものはない。

*2:いや全くもって当然ではない。

*3:単純な計算を行うだけなのに、電子計算用のハードウェアでOSを動かして、OS上で数値計算用ではなくリスト処理用の言語処理系を動かして数値計算を行う――という大いなる計算資源の無駄遣いを楽しめるのも大人の醍醐味であろう。

*4:この手の計算に表計算ソフトを用いないのは、ひとえに私がへそ曲がりなだけである。

*5:もしかしたら「中置記法」かつ「加算が二項演算ではない」プログラミング言語も存在するかもしれないが、今のところ私はそのような言語に遭遇したことがない。

*6:個人の感想です。

*7:もしくはスペシャル変数。

*8:いい加減、この手の計算に用いるREPLをCommon LispSchemeのどちらか一方に統一したいところである――とか言いだしてから10年以上経過した気がする。