Emacs Lispことはじめ ~OpenContest1を解く~
この記事は、SLP_KBIT Advent Calendar 2016 - Qiitaの1日目の記事です。
はじめに
ここ最近、SLPのVim勢がとっても本格的になっていてすごいな(小並感)となってます。
Emacs派の自分としては、もっとちゃんとEmacsの勉強をしないと失礼かなと思って絶賛勉強中。
そこで、Emacs派ももっと盛り上がって行きたいなと思ってEmacs Lispについて書こうかなっと思った次第です。
Emacs LispはEmacsの設定書くための言語ではありますが、今回はわかりやすいと思い、SLPおなじみのOpenContest1から幾つか抜粋して紹介していこうと思います。
四則演算 3変数の和と平均
コード
(setq x (string-to-number (read-string "x = "))) (setq y (string-to-number (read-string "y = "))) (setq z (string-to-number (read-string "z = "))) (princ-list "sum is " (+ x y z)) (princ-list "avg is " (/ (+ x y z) 3.0))
実行結果
$ emacs --script answer.el x = 1 y = 6 z = 10 sum is 17 avg is 5.666666666666667
解説
関数と演算子
EmacsLispでは、関数は以下のような形式で利用します
(関数名 値 ...)
C言語のprintfで例えると printf("Hello World!");
は (printf "Hello World!")
演算子なども同様で、前置記法で記述されます。
(+ 2 3) => 5
標準入力からの読込み
標準入力からの読込みは、read-string
関数で行います。
read-string
関数は、第一引数で与えられた文字列を出力した後、標準入力から読み込んだ文字列を返却します。この関数自体は、数値や文字列などの指定なく返却します。("Hello World"と入力すれば、返却値は"Hello World")
(read-string "x = ") => "3" (標準入力で入力された値)
文字列から数値への変換
read-lineで読み込んだ値は、文字列であるため、そのままでは演算出来ません。
そこで、string-to-number
関数を利用して、数値に変換します。
(string-to-number "3") => 3
変数への代入
Emacs Lispでは、変数宣言の必要はありません。
変数への代入は、setq
関数を利用します。(set
関数でも出来ます。違いはググってください)
(setq x 3) => x = 3
以上の処理により、1-3行目では、標準入力から読み込んだ値を数値として変数に格納しています。
標準出力への表示
princ-list
関数は、引数を結合して標準出力に表示します
(princ-list "Hello" "my" "name" "is" "Taro") => Hello my name is Taro
if構文による分岐
整数 n を入力し、2 でも 3 でも割り切れないときは、1、 2 でのみ割り切れるときは 2、3 でのみ割り切れるときは 3、 2 でも 3 でも割り切れるときは 6 を出力する。
解答
(setq n (string-to-number (read-string "n = "))) (cond ((eq (% n 6) 0) (princ "6\n")) ((= (% n 3) 0) (princ "3\n")) ((= (% n 2) 0) (princ "2\n")) (t (princ "0\n")))
実行結果
$ emacs --script answer.el n = 12 6
解説
cond文
EmacsLispでの条件文には、if
、when
、unless
、cond
があります。(本当は条件文ではなく全て関数らしいのですが)
if文は、else if
といったことが出来ません。 そこで、cond文を使います。cond文は、Cのswitch文みたいなものです。
以下の形式で実行します。
(cond (条件式 文) (条件式 文) (条件式 文))
上から順に条件式が実行され、条件がt(true)になった文を実行します。
どれにも当てはまらなかったときの条件は、条件式にt
と書くことで、必ず成立するようにして書きます。
for構文による低反復
初期値 c0, c1 と係数 r1, r2 を入力し、項数 n を指定する。 初期値 A(0)=c0, A(1)=c1 と漸化式 A(n)=r1×A(n-1)+r2×A(n-2) で定義される数列を、 第 0 項から第 n-1 項まで、各行に、項番 項値 を出力する。
コード
(setq c0 (string-to-number (read-string "c0 = "))) (setq c1 (string-to-number (read-string "c1 = "))) (setq r1 (string-to-number (read-string "r1 = "))) (setq r2 (string-to-number (read-string "r2 = "))) (setq n (string-to-number (read-string "n = "))) (princ-list "[0] " c0) (princ-list "[1] " c1) (setq count 2) (while (<= count (- n 1)) (setq next (+ (* c1 r1) (* c0 r2))) (princ-list "[" count "] " next) (setq c0 c1) (setq c1 next) (setq count (+ count 1)))
実行結果
$ emacs --script answer.el c0 = 2 c1 = 3 r1 = 1 r2 = 1 n = 5 [0] 2 [1] 3 [2] 5 [3] 8 [4] 13
解説
while文
EmacsLispには、for文に相当するものがないようなので、while文で行いました。
while文は、以下のような形式で記述します。
(while 条件式 文...)
インクリメントとclライブラリ
今回のコードでは、インクリメントを(setq count (+ count 1))
と書いています。
EmacsLispのデフォルトでは、インクリメントを行う関数は用意されていません。
しかし、clライブラリ(Common Lisp)をrequireすれば、incf
という便利なマクロを利用することが出来ます。
(require 'cl) ... (while (< count (- n 1)) ... (incf count))
2016年の指定月のカレンダー出力
最後に、みんなを悩ませた枠付きカレンダーの出力をやってみよう。
ほとんど、これまでに説明した内容で出来るはずです!
解答コードはこちら
(require 'cl) ;; 関数宣言========================================= ;; 該当月の日付を取得する関数 (defun get-day-count (month is-leap-year) (setq day-count 31) (cond ((or (eq month 4) (eq month 6) (eq month 9) (eq month 11)) (setq day-count 30)) ((eq month 2) (setq day-count 28) (when is-leap-year (setq day-count 29)))) day-count) ;; うるう年判定の関数 (defun is-leap (year) (or (eq (% year 400) 0) (and (eq (% year 4) 0) (/= (% year 100) 0)))) ;; 本体================================================ (setq year 2016) (setq month (string-to-number (read-string "month = "))) (princ "日 月 火 水 木 金 土\n") ;; うるう年の判定 (setq is-leap-year (is-leap year)) (setq days (get-day-count month is-leap-year)) (setq pasted-days 0) (setq pasted-month 1) ;; 月の初めまでの日数を格納 (while (< pasted-month month) (incf pasted-days (get-day-count pasted-month is-leap-year)) (incf pasted-month)) ;; 月の初めの曜日コードを取得(1月1日は金曜日) (setq first-day-code (% (+ pasted-days 5) 7)) (setq day-code-count 0) (while (< day-code-count first-day-code) (princ " ") (incf day-code-count)) (setq today 1) (while (<= today days) (princ (format "%02d " today)) (when (eq day-code-count 6) (princ "\n") (setq day-code-count -1)) (incf day-code-count) (incf today))
実行結果
$ emacs --script answer.el month = 12 日 月 火 水 木 金 土 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
さいごに
次回は、もっとEmacsLispっぽいコードを書こうかなとおもっています。
みんなも一緒にEmacsLisp勉強しよう!