Emacs Lispことはじめ ~OpenContest1を解く~

この記事は、SLP_KBIT Advent Calendar 2016 - Qiitaの1日目の記事です。

はじめに

ここ最近、SLPのVim勢がとっても本格的になっていてすごいな(小並感)となってます。
Emacs派の自分としては、もっとちゃんとEmacsの勉強をしないと失礼かなと思って絶賛勉強中。
そこで、Emacs派ももっと盛り上がって行きたいなと思ってEmacs Lispについて書こうかなっと思った次第です。
Emacs LispEmacsの設定書くための言語ではありますが、今回はわかりやすいと思い、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での条件文には、ifwhenunlesscondがあります。(本当は条件文ではなく全て関数らしいのですが)
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勉強しよう!