2022-12-29

cl-defunのお勉強

cl-defunは通常のdefunに加えて便利な機能が付け加えられていますが、正直使いませんしやりたいことに対して過剰に複雑な気がしたのでこれまで学ぶのを避けてきました。

しかし必要になったので諦めて嫌々勉強することにしました。

cl-defunのドキュメント

まずはドキュメントを確認しましょう。

cl-defun is an autoloaded Lisp macro in ‘cl-macs.el’.
# cl-defun は ‘cl-macs.el’ に自動ロードされる Lisp マクロです。

(cl-defun NAME ARGLIST [DOCSTRING] BODY...)

Define NAME as a function.
# NAME を関数として定義します。
Like normal ‘defun’, except ARGLIST allows full Common Lisp conventions,
and BODY is implicitly surrounded by (cl-block NAME ...).
# ARGLIST が完全な Common Lisp 規則を許可し、BODY が (cl-block NAME ...)
# で暗黙的に囲まれていることを除いて、通常の「defun」と同様です。

The full form of a Common Lisp function argument list is
# Common Lisp 関数の引数リストの完全な形式は

   (VAR...
    [&optional (VAR [INITFORM [SVAR]])...]
    [&rest|&body VAR]
    [&key (([KEYWORD] VAR) [INITFORM [SVAR]])... [&allow-other-keys]]
    [&aux (VAR [INITFORM])...])

VAR may be replaced recursively with an argument list for
destructuring, ‘&whole’ is supported within these sublists.  If
SVAR, INITFORM, and KEYWORD are all omitted, then ‘(VAR)’ may be
written simply ‘VAR’.  See the Info node ‘(cl)Argument Lists’ for
more details.
# VAR は、再帰的に分解用の引数リストに置き換えることができます。これらの
# サブリスト内では「&whole」がサポートされています。 SVAR、INITFORM、
# KEYWORD をすべて省略した場合、「(VAR)」は単に「VAR」と記述できます。詳
# 細については、Info ノード「(cl)Argument Lists」を参照してください。

Web上だとArgument Lists (Common Lisp Extensions)にマニュアルがあります。(ちなみにCommon Lispの場合はCLHS: Section 3.4.1)

通常のdefunと違うのは次の点です:

  • 引数リストの形式を拡張
    • 分割代入(再帰的な引数リストと&whole指定)
    • &optionalの拡張(分割代入、初期値、指定有無変数)
    • &restの拡張(分割代入)
    • &bodyを追加(&restの別名)
    • &keyを追加(名前付き引数(Named parameter - Wikipedia)を実現)
    • &auxを追加(ローカル変数定義)
  • 関数の内部をcl-blockで囲む

cl-blockについては今回の興味の対象外なので、引数について見て行きます。

順番

(VAR...
 [&optional (VAR [INITFORM [SVAR]])...]
 [&rest|&body VAR]
 [&key (([KEYWORD] VAR) [INITFORM [SVAR]])... [&allow-other-keys]]
 [&aux (VAR [INITFORM])...])

&optional、&rest(または&body)、&key、&auxはこの順番でなければならないようです。

違う順番で書くと定義時にエラーになりました。

(cl-defun test-clfun (a b &rest args &optional e f)
  (list a b c d e f)) ;;Malformed argument list ends with: (&optional e f)

(cl-defun test-clfun (a b &key c d &optional e f)
  (list a b c d e f)) ;;Malformed argument list ends with: (&optional e f)

(cl-defun test-clfun (a b &key c d &rest args)
  (list a b c d args)) ;;Malformed argument list ends with: (&rest args)

(cl-defun test-clfun (a b &aux (z (+ a b)) &optional c d)
  (list a b c d z)) ;;Malformed argument list ends with: (&optional c d)

(cl-defun test-clfun (a b &aux (z (+ a b)) &rest args)
  (list a b c d z)) ;;Malformed argument list ends with: (&rest args)

(cl-defun test-clfun (a b &aux (z (+ a b)) &key c d)
  (list a b c d z)) ;;Malformed argument list ends with: (&key c d)

技術的にはどんな順番でも良さそうな物ですが、処理の順番としては自然な気もします。

同じ物(&~)を複数書いた場合は対応が分かれます。(Emacs 28.2時点)

(cl-defun test-clfun (a b &optional c d &optional e f)
  (list a b c d e f)) ;;OK!
(test-clfun 1 2) ;;Invalid function

(cl-defun test-clfun (a b &optional (c 100) d &optional e f)
  (list a b c d e f)) ;;OK!
(test-clfun 1 2) ;;OK!

(cl-defun test-clfun (a b &rest rest1 &rest rest2)
  (list a b rest1 rest2)) ;;Malformed argument list ends with: (&rest rest2)

(cl-defun test-clfun (a b &rest rest &body body)
  (list a b rest body)) ;;Malformed argument list ends with: (&rest body)

(cl-defun test-clfun (a b &key c d &key e f)
  (list a b c d e f)) ;;OK!
(test-clfun 1 2) ;;OK!
(test-clfun 1 2 :c 3 :d 4 :e 5 :f 6) ;;OK!

(cl-defun test-clfun (a b &aux (c (+ a b)) &aux (d (* a b)))
  (list a b c d)) ;;OK!
(test-clfun 2 3) ;;OK! (2 3 5 6)

&restで指定出来る変数は必ず一つだけということでしょう。複数の要素が指定出来る物(&~)は(連続する場合に限り)同じ物(&~)を許容する方針のようです(cl–do-arglist内でwhenではなくわざわざwhileが使われています)。ただし&optionalはcl-defun的には良くても実行時にエラーが出る場合がありました。&optionalは元々Emacs Lispで対応しているというあたりが関係しているのかもしれません。元々対応していない初期値指定を入れたら通るようになりました。

&optionalや&key、&auxの後に何も無いのは受け入れられるようです。

(cl-defun test-clfun (a b &optional)
  (list a b)) ;;OK
(test-clfun 1 2) ;;OK

(cl-defun test-clfun (a b &key)
  (list a b)) ;;OK
(test-clfun 1 2) ;;OK

(cl-defun test-clfun (a b &optional &key &aux)
  (list a b)) ;;OK
(test-clfun 1 2) ;;OK

&restに関しては次の要素が強制的に格納先になる他、末尾での挙動が意図した物なのかは不明です。

(cl-defun test-clfun (&optional &rest &key &aux)
  (list &key)) ;;OK (&keyという変数になります)
(test-clfun 1 2 3) ;;OK

(cl-defun test-clfun (&optional &rest)
  (list "Hello")) ;;OK! (&rest _と同様の使い方を想定している? たまたま?)
(test-clfun 1 2 3) ;;OK!

;;ちなみに↑は通常のdefunでは実行時エラーになります。

(defun test-fun (&optional &rest)
  (list "Hello"))
(test-fun 1 2 3) ;;Invalid function

VARに書けるもの(分割代入あるいは再帰的な引数リスト)

(VAR...
 [&optional (VAR [INITFORM [SVAR]])...]
 [&rest|&body VAR]
 [&key (([KEYWORD] VAR) [INITFORM [SVAR]])... [&allow-other-keys]]
 [&aux (VAR [INITFORM])...])

VARと書いてある部分には再帰的に引数リストが書けます。また、その引数リストの先頭には &whole 変数 という指定ができます。

引数リストを書いた場合はその引数に指定した値が分割代入されます。

(cl-defun test-clfun (a b (c &optional d e))
  (list a b c d e))
(test-clfun 1 2 '(3 4))
;;=> (1 2 3 4 nil)

(cl-defun test-clfun (a b (c1 (c21 c22 &optional c23 c24) &rest c3s) d)
  (list a b c1 c21 c22 c23 c24 c3s d))
(test-clfun 1 2 '(31 (321 322 323) 33 34) 4)
;;=> (1 2 31 321 322 323 nil (33 34) 4)

引数リストの先頭に &whole 変数 と書いてあると引数全体がその 変数 に格納されます。

(cl-defun test-clfun (a b (&whole all c d))
  (list a b c d all))
(test-clfun 1 2 '(3 4))
;;=> (1 2 3 4 (3 4))

ここで 変数 と書いているのはVARでは無いということです。ここでは分割代入はできません。

(cl-defun test-clfun (a b (&whole (all-c all-d) c d))
  (list a b c d all-c all-d))
(test-clfun 1 2 '(3 4));;Wrong type argument: symbolp, (all-c all-d)

&optional

[&optional (VAR [INITFORM [SVAR]])...]

&optionalは通常のdefunにもある機能ですが次の点が違います。

  • VARの分割代入
  • 初期値指定 (INITFORM)
  • 指定されたかを判別する変数 (SVAR)

INITFORM

INITFORMは&optionalや&keyword、&auxで変数の初期化に使う式です。

&optionalと&keyの所にあるINITFORMは指定されなかったときだけ評価されます。初期化されてから上書きされるわけではありません。

(let ((opt1-count 0)
      (kw1-count 0))
  (cl-defun test-clfun (&optional
                        (opt1 (cl-incf opt1-count))
                        &key
                        (kw1 (cl-incf kw1-count)))
    (list opt1 kw1))
  (test-clfun 100 :kw1 200)
  (message "%s %s" opt1-count kw1-count) ;;0 0
  (test-clfun)
  (message "%s %s" opt1-count kw1-count) ;;1 1
  (test-clfun 2)
  (message "%s %s" opt1-count kw1-count)) ;;1 2

INITFORMは関数内の最初の方で評価されます。呼び出す場所でマクロ展開・評価されるわけではありません。

(funcall
 (let ((a 2))
   (cl-defun test-clfun (b &optional (c (* a b)))
     (list a b c))
   #'test-clfun)
 3)
;;=>
;;レキシカルバインディング時: (2 3 6)
;;ダイナミックバインディング時: Symbol’s value as variable is void: a
(cl-defun test-clfun (b &optional (c (* a b)))
  (list a b c))

(let ((a 2))
  (test-clfun 3)))
;;=>
;;レキシカルバインディング時: Symbol’s value as variable is void: a
;;ダイナミックバインディング時: (2 3 6)

引数の左側は参照できて右側は参照できません。

(cl-defun test-clfun (a &optional (b (* a c)) &aux (c 100))
  (list a b c))
(test-clfun 2) ;;Symbol’s value as variable is void: c

SVAR

SVARには省略可能(&optionalまたは&key)な引数が指定されたかどうか(nilまたはt)を格納する変数を指定出来ます。

(cl-defun test-clfun (&optional
                      (opt1 100 opt1-supplied)
                      &key
                      (kw1 200 kw1-supplied))
  (list opt1 opt1-supplied kw1 kw1-supplied))
(test-clfun) ;;=> (100 nil 200 nil)
(test-clfun 1) => (1 t 200 nil)
(test-clfun nil :kw1 nil) ;;=> (nil t nil t)

引数の値がnilのときに省略されてnilになったのかnilを指定されたのかが区別できます。

ちなみにSVARは分割代入が可能ですが、nilかtしか渡されないのであまり意味は無いと思います。

(cl-defun test-clfun (&optional (opt1 100 (&whole opt1-sup-all &rest opt1-sup-args)))
  (list opt1 opt1-sup-all opt1-sup-args))
(test-clfun 1) ;;=> (1 t t)

&restまたは&body

[&rest|&body VAR]

&restまたは&bodyの後には一つのVARが続きます。

&restは通常のdefunにもある機能ですが、VARなので分割代入が出来ます。

(cl-defun test-clfun (a &rest (b c d &key e f))
  (list a b c d e f))
(test-clfun 1 2 3 4 :f 6) ;;=> (1 2 3 4 nil 6)

&bodyという表記には何か意味があるみたいですが詳しいことは知りません。

上でも書きましたが、末尾でVARを書かなくても受け入れられるケースがありますが意図的かは分かりません。

&key

[&key (([KEYWORD] VAR) [INITFORM [SVAR]])... [&allow-other-keys]]

&keyはいわゆる名前付き引数を実現するための機能です。

例えば次のような関数呼び出しを実現します。

(cl-defun test-clfun (&key a b c d)
  (list a b c d))
(test-clfun :d 345 :b 234 :a 123) ;;=> (123 234 nil 345)

;; より複雑な例 b:初期値, c:分割代入、初期値、指定の有無
(cl-defun test-clfun (&key a (b 222) ((:c (c1 c2)) '(301 302) c-supplied))
  (list a b c1 c2 c-supplied))
(test-clfun :c '(30001 30002) :a 1 :b 2) ;;=> (1 2 30001 30002 t)

キーワードの順番は自由です。

同じキーワードが指定された場合は最初のものが採用され後のものは無視(破棄)されます。

(cl-defun test-clfun (&key a b c d)
  (list a b c d))
(test-clfun :b 100 :b 101 :b 102) ;;=> (nil 100 nil nil)

INITFORMやSVARは&optionalの時と同じです。省略時はINITFORMの評価値か、INITFORMが無ければnilです。キーワードが指定されたかはSVARに指定した変数で判別可能です。

問題は肝心のキーワードと受け取る変数を指定する部分です。

(([KEYWORD] VAR) 略)

と書いてありますが、実際にはもう少し説明が必要でしょう。ここに書けるのは次の3パターンです。

シンボル
次の (シンボル) と等価です。
(シンボル 略)
キーワードと変数を同時に指定します。 シンボル の頭に:を付けたものがキーワードになります。もし シンボル の頭に_があるなら先に取り除いてからキーワードにします(未使用変数をマークできるようにするため)。引数の値は シンボル で指定した名前の変数に格納されます。
((シンボル VAR) 略)
シンボル がそのままキーワードになります。引数の値はVARに格納されます。VARなので分割代入が可能です。

末尾に&allow-other-keysが指定されていると定義されていないキーワードでも受け入れます。これは&restと組み合わせて取得したり単に無視することもできます。

(cl-defun test-clfun (&key a b c d)
  (list a b c d))
(test-clfun :d 345 :b 234 :a 123 :z 999) ;;Keyword argument :z not one of (:a :b :c :d)

(cl-defun test-clfun (&key a b c d &allow-other-keys)
  (list a b c d))
(test-clfun :d 345 :b 234 :a 123 :z 999) ;;=> (123 234 nil 345)

または呼び出し側で許可させることも出来ます。

(cl-defun test-clfun (&key a b c d)
  (list a b c d))
(test-clfun :d 345 :b 234 :a 123 :z 999 :allow-other-keys t) ;;=> (123 234 nil 345)
(test-clfun :allow-other-keys t :d 345 :b 234 :a 123 :z 999) ;;=> (123 234 nil 345)
(test-clfun :allow-other-keys nil :d 345 :b 234 :a 123 :z 999) ;;Keyword argument :z not one of (:a :b :c :d)

&optionalと&keyを同時に指定した場合

&optionalと&keyの両方が引数リストにある場合は注意が必要です。

例えば次のような書き方は問題ありませんが……

(cl-defun test-clfun (a b &optional c d &key e f)
  (list a b c d e f))
(test-clfun 1 2 3 4 :e 5 :f 6) ;;=> (1 2 3 4 5 6)
(test-clfun 1 2 3 4) ;;=> (1 2 3 4 nil nil)
(test-clfun 1 2 nil nil :e 5 :f 6) ;;=> (1 2 nil nil 5 6)

&optionalを省略して&keyを指定することはできません。

(test-clfun 1 2 :e 5 :f 6) ;;=> (1 2 :e 5 nil 6)

そもそも最初から次のようなミスもあり得ます。

(test-clfun :e 5 :f 6) ;;=> (:e 5 :f 6 nil nil)

位置引数(positional parameter)と名前付き引数(named parameter)の食い合わせが悪いという言い方も出来るかもしれません。&optionalまでが位置で指定する引数であり、キーワードはその後からになります。

&restと&keyを同時に指定した場合

&restと&keyは並列に処理されます。

(cl-defun test-clfun (a b &rest args &key c d e)
  (list a b c d e args))
(test-clfun 111 222 :c 3 :d 4 :e 5) ;;=> (111 222 3 4 5 (:c 3 :d 4 :e 5))

&optional引数の最後より後は全て&restで指定されたVARに入るとともに、それらは同時にキーワード引数として処理されます。

&restの方にはあくまで指定されたものがそのまま入ります。

(cl-defun test-clfun (a b &rest args &key c d e)
  (list a b c d e args))
(test-clfun 111 222 :c 3 :e 5 :c 33 :c 333 999 :allow-other-keys t) ;;=> (111 222 3 nil 5 (:c 3 :e 5 :c 33 :c 333 999 :allow-other-keys t))

&aux

[&aux (VAR [INITFORM])...])

&auxは関数内部で使える変数を定義するためのものらしいです。

次の二つの関数は等価です。

(cl-defun test-clfun (a b &aux (z (+ a b)))
  ""
  ...)
(cl-defun test-clfun (a b)
  ""
  (let ((z (+ a b)))
    ...))

&auxの部分はドキュメント文字列にも載りません。

なぜこんなものがあるのかは次のページの議論が参考になりそうです。

what is &aux used for?

あながち互換性のためだけのものとは言えないかもしれません。letの字下げが鬱陶しいと思ったことは度々あるので、それが抑えられるのは案外嬉しいかもしれませんね。

もし&auxが引数リストのどこにでも書けてINITFORMから参照できたらもっと有用だったかもしれません。……と思いましたが、VARには分割代入で再帰的な引数リストが書けるのですから次のような使い方は出来ますね。

(cl-defun test-clfun ((&rest lst &aux (lst-len (length lst))) ;;lengthを1回で済ます!
                      &optional (mid (/ lst-len 2)) (upper lst-len))
  (list lst mid upper lst-len))
(test-clfun '(1 2 3 4 5 6 7 8)) ;;=> ((1 2 3 4 5 6 7 8) 4 8 8)

(追記)&optionalな引数は指定もINITFORMも無い場合でもnilが分割代入されるのでしょうか。

(cl-defun test-clfun (&optional ((&rest lst &aux (lst-len (length lst)))))
  (list lst lst-len))
(test-clfun) ;;=> (nil 0)

うん、ちゃんと&auxの評価されて0になりますね。

実態に即した文法

以上を踏まえて実態に即した文法を書くとだいたい次のような感じでしょうか?

LAMBDA-LIST :
  ([VAR]...
   [&optional [SYMBOL|(VAR [INITFORM [SVAR]])]...]...
   [&rest|&body VAR]
   [&key [SYMBOL|(SYMBOL|(SYMBOL VAR) [INITFORM [SVAR]])]... [&allow-other-keys]]...
   [&aux [(VAR [INITFORM])]...]...)

VAR :
  SYMBOL|
  ([VAR]...
   [&whole SYMBOL]
   [&optional [SYMBOL|(VAR [INITFORM [SVAR]])]...]...
   [&rest|&body VAR]
   [&key [SYMBOL|(SYMBOL|(SYMBOL VAR) [INITFORM [SVAR]])]... [&allow-other-keys]]...
   [&aux [(VAR [INITFORM])]...]...)

SVAR :
  VAR

まぁ、ほとんどは明文化されていない未定義状態なのである日突然変わって鼻から悪魔が出ても文句は言えないかもしれません。

続く

何でこんな重箱の隅をつつくようなことをしているかというと引数リストを解析する必要があったからなのですが、それはまた次のお話しということで。あー、やっぱり面倒くさかった。嫌だ嫌だ。

Pingback / Trackback