Skip to content

Latest commit

 

History

History
1654 lines (1416 loc) · 53.9 KB

20210509122633-emacs_lisp.org

File metadata and controls

1654 lines (1416 loc) · 53.9 KB

Emacs Lisp

:header-args+: :wrap :results raw

概要

Emacsで使われているProgramming Language、Lispの方言。 Emacs自体のコードの多くがEmacs Lispで構成されているほか、拡張するために用いる。

Memo

M-xで引数を割り当てる

M-x ert-run-tests-interactively は実行できるが、 (ert-run-tests-interactively) は引数足りないエラーで実行できない。

interactive special formが関係している。interactiveの引数はarg-descriptor。arg-descriptorは、そのコマンドがインタラクティブに呼び出されたとき引数を計算する方法を宣言する。

https://github.com/kd-collective/emacs/blob/30cf1f34c583d6ed16bdc5b9578370f30c95fe1b/src/callint.c#L37

interactiveに渡したリストが、関数定義の引数に割り当てられる。

(defun interactive-test (select)
  (interactive '("test"))
  (insert select))

defunを読む

defunは関数定義でよく使うマクロ。どのような仕組みになっているのだろうか。

プロパティはなにか

任意のオブジェクトに情報を付加し、後で取り出せる仕組み。
(define-symbol-prop 'test-symbol 'test-group '(1 2 3))

(get 'test-symbol 'test-group)

関数とはlambdaで始まるリスト

Lispでは, 関数とは, lambdaで始まるリスト, そのようなリストをコンパイルしたバイトコード関数, あるいは, 基本関数のsubrオブジェクトです

関数はよくdefunで定義するが、これは単に便利関数というだけで、本質的にlambdaをつけているにすぎない。

https://github.com/kd-collective/emacs/blob/30cf1f34c583d6ed16bdc5b9578370f30c95fe1b/lisp/emacs-lisp/byte-run.el#L401-L442

interactiveモードか判定する変数

  • noninteractive 変数
    • Non-nil means Emacs is running without interactive terminal.

defmacroの短い例

macroを理解するには、それが必要な状況をイメージできるとわかりやすい。bodyを未評価の状態で渡すのが必要なので、defmacroを使う。

(defmacro cask--shut-up-unless-debug (&rest body)
  "The shut-up module is singularly designed to defeat *every*
 attempt at making your elisp package tractable."
  (declare (indent defun))
  `(if debug-on-error
       (cl-flet ((shut-up-current-output () (ignore)))
	 ,@body)
     (shut-up ,@body)))

letfを関数モックとして使う

モックとして使える。princをinsertに置き換えて、テスト可能にする例。

https://github.com/cask/cask/blob/bc168a11d7881a62657cdf19bab2e7966033ec2c/test/cask-cli-test.el#L48-L58

インデントマクロ

マクロ呼び出しをTABがどのようにインデントするべきか定義するために、declareフォームを使うことができる。

バッファの開き方を分岐させるときの書き方

動作の異なる関数を分岐で代入して、funcallで評価する。

https://github.com/kd-collective/org-roam/blob/abe63b436035049923ae96639b9b856697047779/org-roam-node.el#L454-L458

modus-themeを切り替える

modus-themes-toggleでダークテーマ、ライトテーマを切り替えられる。便利。

現在のメジャーモードの調べ方

判定や使いかたを調べるのに使える。

major-mode
-> "org-mode"

format-spec で設定の幅を広げる

(format-spec) で文字と関数をマッピングして、ユーザが設定しやすくできる。

(format-spec
     magit-buffer-name-format ;; "%x%M%v: %t%x"
     `((?m . ,m)
       (?M . ,(if (eq mode 'magit-status-mode) "magit" m))
       (?v . ,(or v ""))
       (?V . ,(if v (concat " " v) ""))
       (?t . ,n)
       (?x . ,(if magit-uniquify-buffer-names "" "*"))
       (?T . ,(if magit-uniquify-buffer-names n (concat n "*")))))

必須ではない依存パッケージの書き方

変数なら (defvar w3m-current-url)

関数なら (declare-function w3m-current-title “ext:w3m-util”)

と書くことで、読み込み先を指定されてることになるのでbyte-compileのエラーにならなくなる。

依存インストール

requireかと思いがちだが、一番上の ;; Package-Requires: ((emacs "25.1")) で依存インストールに利用されてる。requireはあくまでパッケージを読み込むだけ。

アクションと検索をわける

ace-link.elを見ていて、アクションとリンク検索を分けていた。リンク検索する → 決定を押したときのアクションを、それぞれのメジャーモードで判断するというわけだ。 単純で強力。 あとから追加するときも、その例にならえばいくらでもメジャーモードごとの追加ができるというわけだ。 ここから学べることは多いように思える。

  • (ace-link-info) - エントリーポイント
  • (ace-link-info-action) - アクション
  • (ace-link–info-current) - collectで使われるユーティリティ
  • (ace-link–info-collect) - リンクに番号をふる
(defun ace-link-info ()
  "Open a visible link in an `Info-mode' buffer."
  (interactive)
  (let ((pt (avy-with ace-link-info
              (avy-process
               (mapcar #'cdr
                       (ace-link--info-collect))
               (avy--style-fn avy-style)))))
    (ace-link--info-action pt)))

(defun ace-link--info-action (pt)
  (when (numberp pt)
    (push-mark)
    (goto-char pt)
    (let ((we (window-end)))
      (while (not (ignore-errors
                    (Info-follow-nearest-node)))
        (forward-char 1)
        (when (> (point) we)
          (error "Could not follow link"))))))

(declare-function Info-follow-nearest-node "info")
(declare-function Info-next-reference "info")
(declare-function Info-try-follow-nearest-node "info")
(declare-function Info-goto-node "info")

(defun ace-link--info-current ()
  "Return the node at point."
  (cons (cl-letf (((symbol-function #'Info-goto-node)
                   (lambda (node _) node))
                  (browse-url-browser-function
                   (lambda (url &rest _) url)))
          (Info-try-follow-nearest-node))
        (point)))

(defun ace-link--info-collect ()
  "Collect the positions of visible links in the current `Info-mode' buffer."
  (let ((end (window-end))
        points)
    (save-excursion
      (goto-char (window-start))
      (when (ignore-errors (Info-next-reference) t)
        (push (ace-link--info-current) points)
        (Info-next-reference)
        (while (and (< (point) end)
                    (> (point) (cdar points)))
          (push (ace-link--info-current) points)
          (Info-next-reference))
        (nreverse points)))))

マッピング

(ace-link-setup-default)でマッピングしているところを見つけた。参考になる。

ディレクトリ探索

(add-node-modules-path)はディレクトリ探索の参考になる。1つづつ上がって、node_modules/binを探索する。 コードが短いのも良い。

名前変換

引数の、関数のsymbolを取得する関数。いいな。

(defun all-the-icons--data-name (name)
  "Get the symbol for an icon family function for icon set NAME."
  (intern (concat "all-the-icons-" (downcase (symbol-name name)) "-data")))

↓みたいに使える。

(funcall (all-the-icons--data-name family))

mapcar

関数引数の関数はミソ。

Function: mapcar function sequence

この関数は、sequence の各要素に function を適用(訳注:apply)し、その結果のリストを返します。sequence が nil で終るリストでない場合、エラーになります。
(mapcar (function car) '((a b) (c d) (e f)))
=> (a c e)

debugger を起動しないようにする

何かの拍子に、debugger実行されるようになり、普通に実行できなくなった。 (debugger-list-functions) で関数を確認。adviceされてるとわかった。 (advice-remove ‘登録関数 ‘debug–implement-debug-on-entry) を削除して戻った。

すべてdebug対象に

(edebug-all-defs) でevalしただけで対象にするように設定する。→ 結局 C-uつけないとできない気が。 regionを選択して、 C-u M-x eval-region すると範囲内すべてが対象に。

オフにするときは(edebug-all-defs)をトグルしてオフにしてから、region指定して M-x eval-region

外部コマンド系パッケージ

  1. ユーザ入力やフラグを通してひたすら引数を収集して、compilation に渡す。
  2. コマンドを実行する。
  3. バッファを作成して外部コマンドの結果をいい感じに出力する。

avyの流れ

(let ((pt (avy-with ace-link-custom
            (avy-process
             (ace-link--custom-collect) ; 元バッファからリンクを収集して((名前1 . 位置1) (名前2 . 位置2)...)リストにする
             (avy--style-fn avy-style))))) ; リンク文字を表示して操作を待つ。
  (ace-link--custom-action pt)) ; アクション

リストのcustomの例

あとから追加、削除、編集が自由にできる。

(defcustom devdocs-alist
  '((c-mode           . "c")
    (c++-mode         . "c++")
    (clojure-mode     . "clojure")
    (coffee-mode      . "CoffeeScript")
    (common-lisp-mode . "lisp")
    (cperl-mode       . "perl")
    (css-mode         . "css")
    (elixir-mode      . "elixir")
    (enh-ruby-mode    . "ruby")
    (erlang-mode      . "erlang")
    (gfm-mode         . "markdown")
    (go-mode          . "go")
    (groovy-mode      . "groovy")
    (haskell-mode     . "haskell")
    (html-mode        . "html")
    (java-mode        . "java")
    (js2-mode         . "javascript")
    (js3-mode         . "javascript")
    (less-css-mode    . "less")
    (lua-mode         . "lua")
    (markdown-mode    . "markdown")
    (perl-mode        . "perl")
    (php-mode         . "php")
    (processing-mode  . "processing")
    (puppet-mode      . "puppet")
    (python-mode      . "python")
    (ruby-mode        . "ruby")
    (sass-mode        . "sass")
    (scala-mode       . "scala")
    (tcl-mode         . "tcl"))
  "Alist which maps major modes to names of DevDocs documentations."
  :type '(repeat (cons (symbol :tag "Major mode")
                       (string :tag "DevDocs documentation")))
  :group 'devdocs)

使う関数すらcustomにできる。

(defcustom devdocs-build-search-pattern-function
  'devdocs-build-search-pattern-function
  "A function to be called by `devdocs-search'.
It builds search pattern base on some context."
  :type 'function
  :group 'devdocs)

Gitリポジトリを調べる

(locate-dominating-file directory file)

(cl-defun eshell-git-prompt--git-root-dir
    (&optional (directory default-directory))
  "Return Git root directory name if exist, otherwise, return nil."
  (let ((root (locate-dominating-file directory ".git")))
    (and root (file-name-as-directory root))))

変数が束縛されていれば、という表現

(defun eshell-git-prompt-last-command-status ()
  "Return Eshell last command execution status.
When Eshell just launches, `eshell-last-command-status' is not defined yet,
return 0 (i.e., success)."
  (if (not (boundp 'eshell-last-command-status))
      0
    eshell-last-command-status))

arielコードメモ

http://dev.ariel-networks.com/articles/workshop/emacs-lisp-basic/ の元の内容に、たまにコメントをつけて読んだ。

  • 実装を見ている
  • わかりやすい例

ring

データ型定義のやり方。型述語の実装。

https://github.com/kijimaD/emacs/blob/master/lisp/emacs-lisp/ring.el#L48-L52

eww

https://github.com/kijimaD/emacs/blob/master/lisp/net/eww.el#L1

lisp.h

lispシンボルの実装。 https://github.com/kijimaD/emacs/blob/master/src/lisp.h#L798

(type-of 42)
=>integer
(type-of 3.14)
=> float
(type-of "foo")
=> string
(type-of '(1 2))
=> cons
(type-of '[1 2])
=> vector
(type-of 'foo)
=> symbol
(type-of ?a)  ; Cの'a'相当。内部的には数値
=> integer

中で起こってること

(setq foo "FOO") したとき、symbolオブジェクトは以下のようにセットされます。

// lisp.h
struct Lisp_Symbol
{
  struct Lisp_String *name;  => "foo"
    Lisp_Object value;  => "FOO"の値を持つ文字列オブジェクトを指す
    Lisp_Object function;  => 
    
    };

↑のように作られているオブジェクトを関数で調べてみます。

(symbolp 'foo)    ; シンボルか否かの判定。tが真。nilが偽。(後述)
=> t
(symbol-name 'foo)
=> "foo"
(symbol-value 'foo)
=> "FOO"
(boundp 'foo)   ; 値セルに値があればt、なければnil
=> t
(fboundp 'foo)  ; 関数セルに関数があればt、なければnil
=> nil
(symbol-function 'foo)
                                        ; まだ設定していないので、今はエラー

クォートの意味

クォートは「評価しない」ことを指示します。クォートしないと、基本的に評価されてしまいます。

(type-of 'foo)
=> symbol
(type-of foo)
=> string   ; 評価後、つまり値セルの指すオブジェクトの型が出力されます

'quote 関数の略記です。

シンボルのまとめ

ここまでで分かったこと

シンボルは名前を持つ (symbol-name関数で確認可能) シンボルの値セルは任意のオブジェクトを指す (symbol-value関数で確認可能) シンボルの指すオブジェクトの型はtype-ofで確認可能

コンスセルの表記

コンスセルとはふたつのポインタ(*)を持ったオブジェクトです。

実装 https://github.com/kijimaD/emacs/blob/master/src/lisp.h#L1350

struct Lisp_Cons
{
  union
  {
    struct
    {
      /* Car of this cons cell.  */
      Lisp_Object car;

      union
      {
        /* Cdr of this cons cell.  */
        Lisp_Object cdr;

        /* Used to chain conses on a free list.  */
        struct Lisp_Cons *chain;
      } u;
    } s;
    GCALIGNED_UNION_MEMBER
  } u;
};

最新のコードは若干変わっている。

  • union って何だろう。
  • 2つのポインタ…elispではcarとcdr

後述するように、コンスセルのcdrが別のコンスセルを指すことで、リスト構造を作ります。コンスセルで作るリスト処理こそがLisp(LISt Processing)の名前の由来でもあります。

コンスセルの表記

("foo" . "bar") これは内部的に。

struct Lisp_Cons
{
  Lisp_Object car;  => "foo"文字列オブジェクトを指す
    Lisp_Object cdr;  => "bar"文字列オブジェクトを指す
    };

オブジェクトが2つ組み合わされたもの。

コンスセルの生成

(cons "foo" "bar")
=> ("foo" . "bar")
  • consはconstructの略。

コンスセルの値

コンスセルの値にアクセスするには carcdr だけ使える。getterメソッドみたいなもの。

car、cdr以外にコンスセルの中を参照する手段はありません。

なるほど。

(car '("foo" . "bar"))
=> "foo"
(cdr '("foo" . "bar"))
=> "bar"

Java風に言えば、コンスセルはふたつのprivateフィールドとふたつのアクセサを持つだけの軽いオブジェクトです。

プログラム自体がオブジェクト

厳密に言えば、(“foo” . “bar”) という文字列は、コンスセルの(Java風に言えば)シリアライズ化した表現です。 後述するように、elispのプログラム自体はリスト表現で書きます。 これの意味することは、プログラム自体がオブジェクトであり、ソースコードはオブジェクトをシリアライズ化しただけの文字列と言えます。

  • シリアライズ化。
  • データとプログラムの区別がない…真髄的なところだということはわかる。

なんでもオブジェクト

(setq foo '("foo" . 42))  ; carに文字列、cdrに数値のコンスセルを指すシンボルfoo
=> ("foo" . 42)
(setq bar '(foo . foo))   ; quoteは全体に効いているので、carとcdrの両方がシンボルfoo
=> (foo . foo)
(symbol-value (car bar))
=> ("foo" . 42)
(symbol-value (cdr bar))
=> ("foo" . 42)
(setq bar `(,foo . foo))  ; backquoteの例
=> (("foo" . 42) . foo)   ; ,のついたオブジェクトは評価。そうでないオブジェクトは未評価

bar -> foo -> “foo” -> 42

シンボルを評価すると、値セルを返します。

cdrが別のコンスセル

(cons "foo" '("bar" . "baz"))
=> ("foo" "bar" . "baz")

("foo" . "bar" "baz") とはならない。

'("foo" . ("bar" . "baz"))
;; => ("foo" "bar" . "baz")

リスト化

最期のcdrをnilにするとリストに。

(cons "foo" '("bar" . nil))
;; => ("foo" "bar")

nilじゃないとコンスセルになる(前の節の通り)。

(cons "foo" '("bar" . "aaa"))
("foo" "bar" . "aaa")

リスト操作

(car '("foo" "bar" "baz"))
=> "foo"
(cdr '("foo" "bar" "baz"))
=> ("bar" "baz")
(cdr (cdr '("foo" "bar" "baz")))
=> ("baz")    ; dotted pair notationで書けば ("baz" . nil)
(cdr (cdr (cdr '("foo" "bar" "baz"))))
=> nil

面倒だけど、リストの操作が行えます。

(setq foo "a")
(setq foo (cons "value" foo))   ; リストfooに要素をprepend
;; => ("value" . "a")
(setq foo (cons "value" foo))   ; さらにprepend
;; => ("value" "value" . "a")
(setq load-path (cons (expand-file-name "~/elisp") load-path))

(list "foo" "bar" "baz")  ; 引数を要素に持つリストを生成
=> ("foo" "bar" "baz")

(append '("foo" "bar") '("baz"))  ; 連接したリストを生成
=> ("foo" "bar" "baz")
(setq load-path (append load-path (list (expand-file-name "~/elisp"))))

(car (nthcdr 1 '("foo" "bar" "baz")))   ; N番目の要素の取得
=> "bar"

評価

コンスセルの評価は次のように行います。

リストの先頭要素(先頭のコンスセルのcar)のシンボルの関数セルの指す関数呼び出し リストの後続要素(先頭以外のコンスセルのcar)を関数の引数として渡す。引数はquoteがなければ、評価してから引数に渡ります リストの後続要素は、リストであるかもしれません。この場合、内側のリストを評価、つまり関数呼び出しをしてから、外側のリストの関数呼び出しをします(前ページですでにやっていますが)。

  • コンスセルとリストの違い。コンスセルは ( . ) で、最後のcdrがnilでないもの。
  • リストは最後のcdrがnilのもの。
  • (append '("foo" "bar") '("baz")) の例.
    1. リストの先頭要素 append の関数セルの指す関数を呼び出す。
    2. リストの後続要素 '("foo" "bar") '("baz") が引数として呼び出される。quoteがあるので評価されない。リストのときは評価=関数呼び出しを1.と同様に内側→外側の順に行う。
(defun plus1 (n)
  (+ n 1))
=> plus1
(plus1 10)
=> 11
(defun my-plus (m n)
  (+ m n))
=> my-plus
(my-plus 2 5)
=> 7

関数の戻り値(=関数の評価結果)は、関数本体の最後の評価結果です

なるほど。

関数に名前はない

defunを見て、関数に名前があると思うのは間違いです。

defunは、シンボルを作って、その関数セルが関数定義を指すようにしています。

シンボルの定義を思い返してみます。

// lisp.h
struct Lisp_Symbol
{
  struct Lisp_String *name;  => "foo"
    Lisp_Object value;  => "FOO"の値を持つ文字列オブジェクトを指す
    Lisp_Object function;  => 
    
    };

確かに関数定義を指しています。

(defun foo () (message "a"))
(symbolp 'foo)
=> t
(symbol-name 'foo)
=> "foo"
(symbol-value 'foo)
=> error: (void-variable foo)
(symbol-function 'foo)
=> (lambda nil (message "a"))
(boundp 'foo)
=> nil
(fboundp 'foo) ; 関数定義
=> t

既存関数も同じ

どれもシンボルで、関数定義を指しています。

(symbol-function 'car)
=> #<subr car>
(symbol-function 'defun)
=> #<subr defun>
(symbol-function '+)
=> #<subr +>

subr(subroutineの略)は、Cで書かれた関数を意味しています。

構造(シンボルcarやシンボルdefunがあり、それらの関数セルが関数定義を指す)は同じです。

subrそうだったのか。 #<> はどういう意味なのだろう。

fset

値セルにsetqやsetがあったように、関数セルにはfsetがあります(fsetqはありません)。

(fset 'my-plus2
      '(lambda (n) (+ n 2)))   ; defunと同じ
=> (lambda (n) (+ n 2))
(my-plus2 10)
=> 12

関数セルと値セルを確認します。

(setq foo "foo")
=> "foo"
(fset 'foo '(lambda (s) (concat s "bar"))) ; 名前とリストの組み合わせ。どちらも未評価で渡す。
=> (lambda (s) (concat s "bar"))
(foo foo)
=> "foobar"

lambda

https://github.com/kijimaD/emacs/blob/master/lisp/subr.el#L106

(lambda (引数 ...) (関数本体))

処理の中身。名前と組み合わせると関数になります。

((lambda (m n) (+ m n)) 2 5)
=> 7

関数定義。declare(…関数やマクロに関する情報、infoで出てくる文章)の箇所を除くとこれだけです。 引数cdr(処理したい内容)でコンスセルを作って、関数セルと組み合わせてリストを作ります。なので名前はありません。

(defmacro lambda (&rest cdr)
  (list 'function (cons 'lambda cdr)))

関数とは何か

述語関数から見てます。この方法いいですね。

                                        ; subr.el
(defun functionp (object)
  "Non-nil if OBJECT is a type of object that can be called as a function."
  (or (subrp object) (byte-code-function-p object)
      (eq (car-safe object) 'lambda)
      (and (symbolp object) (fboundp object))))

elispにとって、「関数」とは次の4つのいずれかであることが分かります。

  • subroutine (Cで書かれた関数)
  • バイトコンパイルされた関数 (今はあまり気にしないように)
  • シンボルlambdaで始まるリスト
  • 関数セルが空ではないシンボル

関数呼び出し

リストの先頭要素に「関数」があれば、関数呼び出しになります。

リストがすべてに優先して存在します。

(my-plus 1 3)   ; シンボルであれば関数セルの指す関数を呼び出す
=> 4
((lambda (m n) (+ m n)) 1 3)   ; シンボルlambdaで始まるリストも「関数」
=> 4

funcall

funcall関数は引数の1番目を関数として呼びます。

(funcall 'my-plus 1 3)
=> 4
(funcall '(lambda (m n) (+ m n)) 1 3)
=> 4
  • '(lambda (m n) (+ m n)) は関数として呼ばれる。

- (lambda (m n) (+ m n)) でもいいみたい。

違いは何だっけ。… 評価して渡されるか。この場合は関数なので、評価されるのがいつでも結果は変わらない。

(funcall '(lambda () (+ 1 2)))
(if (eq 1 (+ 1)) 1) ; 1
(if (eq 1 '(+ 1)) 1) ; nil

(+ (+ 1) 1) ; 2
(+ '(+ 1) 1) ; (wrong-type-argument number-or-marker-p (+ 1))

値セルにlambda

つまり、値を関数でも呼び出せます。

(setq foo '(lambda (m n) (+ m n)))
=> (lambda (m n) (+ m n))
(funcall foo 2 5)
=> 7

なるほど…。

明示的に空にする

(makunbound ‘foo) ;値セルを空にする => foo (fmakunbound ‘foo) ;関数セルを空にする => foo

連想リスト(association list)

'(("foo" . "FOO") ("bar" . "BAR") ("baz" . "BAZ"))

リストの要素がコンスセル。

配列

配列は次の4つに分類できます。

  • ベクタ
  • 文字列
  • 文字テーブル
  • ブールベクタ

言語仕様として「配列」があると言うより、次のarrayp述語で「配列」型(基本型では無い)が定義されているようなものです。

// data.c
DEFUN ("arrayp", Farrayp, Sarrayp, 1, 1, 0, "Return t if OBJECT is an array (string or vector).")
  (object)
  Lisp_Object object;
{
  if (VECTORP (object) || STRINGP (object)
      || CHAR_TABLE_P (object) || BOOL_VECTOR_P (object))
    return Qt;
  return Qnil;
}

ベクタ/文字/文字テーブル/ブールベクタであればarray。なんだそりゃ。

(arrayp '(1 2)) ; nil
(arrayp "aaa")  ; t

ベクタ

[1 3 5]
=> [1 3 5]
(vectorp [1 3 5])
=> t
(setq foo [1 3 5])    ; quoteしてもしなくても同じ
=> [1 3 5]
(vectorp foo)
=> t

ベクタの操作

元ページ再掲

http://dev.ariel-networks.com/articles/workshop/emacs-lisp-basic/

リスト遊び

リスト

  • リストを構成するセルのCDRは m ,セルかnilを指している。

しかしポイントはなんでも指せるので、CDRはアトム(整数とか)も参照できる。 CARが1。CDRが2のセルは、表記方法では表現できない。 これを表現するために、ドット対表記が用意されている。

(cons 1 (cons 2 nil))
=> (1 2)

(cons 1 2)
=> (1 . 2)

nilで終端しないセルはドット対で表記する。 この方法で表現すると、 (1) => (1. nil) (1 2 3) => (1 . (2 . (3. nil))) みたいになる。ドット対は連想リストで用いられる。

Lispのデータ

  • セル(1対のポインタ。consで作られる。CARとCDRが指しているポインタが指すデータを見るには、carとcdrを用いる)
  • アトム(セル以外。整数とか)

Lispの評価

  • 式がセルなら関数を呼び出す。第1要素のシンボルの指す関数を実行する。引数は評価する。
  • 式がアトムならその値を返す。

quote

クオートをつけると評価せずそのまま返す。

(quote (1 2))
=> (1 2)

同じ意味:
'(1 2)

(setq dog 5)
dog
=> 5   ; 評価結果
'dog
=> dog ; シンボル自身を表現する

(setq dog “dog”) はシンボルdog(評価しない)に、”dog”を入れるということ。

ポインタ

変数から変数への代入は、ポインタを複製するということ。

(setq x 'dog)
(setq y 'dog)
(eq x y)
=> t
(setq z y)
(eq y z)
=> t
(setq x "dog")
(setq y "dog")
(eq x y)
=> nil ; 同じ中身の文字列だが、指しているポインタが異なるため。
(setq z y)
(eq y z)
=> t   ; 指しているポインタが同じため

変数はポインタを格納する箱。

On Lisp

On Lisp — 前書き

もう1つの関数定義

(defun double (x) (* x 2))
#'double ; 関数オブジェクトを得る

#'(lambda (x) (* x 2))

名前が関数呼び出しの先頭かシャープクォートの次に来ると関数への参照と見 なされ, それ以外では変数名と見なされる.

なので(double double)とかも可能。変数と関数の名前空間が異なっている。

  • 関数は普通のデータオブジェクト。なので変数が値として関数を持てる。
(setq x #'append)

2つの式は大体同じことをしている。

(defun double (x) (* x 2))

(setf (symbol-function 'double)
      #'(lambda (x) (* x 2)))

手続き定義…名前をコードと関連付ける。

関数を作るのにdefunは必要ではなく, 関数は何かのシンボルの値と して保存されなくてもいい. defunの背後には, もっと一般的な仕組みが隠れている: 関数を作ることと, それをある名前に関連づけることは別々の働きだ. Lispの関数の概念の一般性 全体までは必要ないとき, defunはもっと制限の強いプログラミング言語と同 じ位単純に関数定義を行う.

applyは、オブジェクトを関数として実行する。

(+ 1 2)
(apply #'+ '(1 2))
(apply (symbol-function '+) '(1 2))
(apply #'(lambda (x y) (+ x y)) '(1 2))

クロージャ

(defun make-adder (n)
  #'(lambda (x) (+ x n)))

は数を取り,「呼ばれると引数にその数を加えるクロージャ」を返す. その足 し算関数のインスタンスは幾らでも作ることができる。

> (setq add2 (make-adder 2)
        add10 (make-adder 10))
#<Interpreted-Function BF162E>
> (funcall add2 5)
7
> (funcall add10 3)
13

変数に引数をとった関数を入れる。すごいな。

(defun make-dbms (db)
  (list
   #'(lambda (key)
       (cdr (assoc key db)))
   #'(lambda (key val)
       (push (cons key val) db)
       key)
   #'(lambda (key)
       (setf db (delete key db :key #'car))
       key)))

末尾再帰

再帰関数とは自分自身を呼び出す関数だ. そして関数呼び出しの後に行うべき 作業が残っていなければ, その呼び出しは\emph{末尾再帰}だ. 次の関数は末 尾再帰でない。

(defun our-length (lst)
  (if (null lst)
      0
    (1+ (our-length (cdr lst)))))

再帰呼び出しから戻った後,結果を1+ に渡さなければいけないからだ. しか し次の関数は末尾再帰だ。

(defun our-find-if (fn lst)
  (if (funcall fn (car lst))
      (car lst)
    (our-find-if fn (cdr lst))))

抽象化

(defun 1st (exp) (car exp))
(defun 2nd (exp) (car (cdr exp)))
(defun 3rd (exp) (car (cdr (cdr exp))))

(setq order-func1 '((OP . 2nd) (ARG1 . 1st) (ARG2 . 3rd)))
(setq order-func2 '((OP . 1st) (ARG1 . 2nd) (ARG2 . 3rd)))

(defun order-func (sym odr-db)
  (cdr (assq sym odr-db)))

(defun op (exp order-db)
  (funcall (order-func 'OP order-db) exp))
(defun arg1 (exp order-db)
  (funcall (order-func 'ARG1 order-db) exp))
(defun arg2 (exp order-db)
  (funcall (order-func 'ARG2 order-db) exp))

(setq op-func1 '((+ . +) (- . -) (* . *)))
(setq op-func2 '((add . +) (sub . -) (mul . *)))

(defun op-func (sym op-db)
  (cdr (assq sym op-db)))

(defun calc (exp op-db odr-db)
  (cond
   ((atom exp) exp)
   (t (funcall
       (op-func (op exp odr-db) op-db)
       (calc (arg1 exp odr-db) op-db odr-db)
       (calc (arg2 exp odr-db) op-db odr-db)))))

(calc '(1 + (2 * 3)) op-func1 order-func1)
(calc '(add 1 (mul 2 3)) op-func2 order-func2)

関数

On Lisp — 関数

関数的プログラミングとは,副作用ではなく, 値を返すことで動作するプログ ラムを書くことだ.

副作用とはオブジェクトの破壊的な変更(rplacaの使用等) や変数への代入(setqの使用等)を含む.

reverse等のオペレータは,副作用でなく返 り値のために呼ばれるよう意図されている

setqを使うときはたいてい副作用だ。 lispのほとんどの関数は副作用のために呼ばれることを意図されていない。 だから副作用がほしいときはsetqを使う。

この習慣を育てるには時間がかかるかもしれない. 一つの方法は,以下のオペ レータは税金がかかっているつもりで扱うことだ:

set setq setf psetf psetq incf decf push pop pushnew rplaca rplacd rotatef shiftf remf remprop remhash

あとlet*もそうだ. この中に命令的プログラムが潜んでいることがしばしばあ る. これらのオペレータに税金がかかっているつもりになるのは, よいLisp のプログラミング・スタイルへ向かう手助けとして勧めただけで, それがよい スタイルの基準なのではない. しかし,それだけでもずいぶん進歩できるだろ う.

上は危険。

> (multiple-value-bind (int frac) (truncate 26.21875)
    (list int frac))
(26 0.21875)

多値。

関数的プログラムは,それが欲しがるものを求める。 命令的プログラムは,何をすべきかの指示を求める。

関数的プログラムの 「aと, x の第1要素の2乗から成るリスト を返せ.」

(defun fun (x)
  (list 'a (expt (car x) 2)))

命令的プログラミングではこうだ. 「xの第1要素を求め,それを2乗せよ. そ してaと,先程2乗した値から成るリストを返せ.」

(defun imp (x)
  (let (y sqr)
    (setq y (car x))
    (setq sqr (expt y 2))
    (list 'a sqr)))

その方法は,命令的プログラムは関数的プログラムを裏返しにしたものと思う ことだ. 関数的プログラムが命令的プログラムの中に隠れているのを見つける には, ただ裏返しにすればいい. この方法をimpで試してみよう.

だからルールはこうあるべきだ: 任意の関数呼び出しが, 自分だけが支配す るオブジェクトを安全に書き換えられるようにする.

何が引数と返り値を支配するのだろう?関数呼び出しは返り値として受け取る オブジェクトを支配するが, 引数として渡されるオブジェクトは支配しない, というのがLispの慣習のようだ. 引数に変更を加える関数は「破壊的」との呼 び名で区別されるが, 返ってくるオブジェクトに変更を加える関数には特に呼 び名がない.

ユーティリティ関数

On Lisp — ユーティリティ関数

Common Lispのオペレータは3種類に分かれる: 関数にマクロ(ユーザが作れる もの)と,特殊オペレータ(ユーザには作れない)だ. この章では,Lispを新 しい関数で拡張するテクニックを説明する. しかしここで言う「テクニック」 は普通の意味のものではない. そういった関数について知るべき重要な点は, それらをどうやって書くかということではなく,それらがどこから来たのかと いうことだ. Lispの拡張には,他の関数を書くときと大体同じテクニックが使 われることになる. そういった拡張を書くとき難しいのは, どうやって書く かを決めることではなく,何を書くかを決めることだ.

ユーティリティ関数について。 最初は本屋を検索する関数をこう書いた。

(defun find-books (towns)
  (if (null towns)
      nil
    (let ((shops (bookshops (car towns))))
      (if shops
          (values (car towns) shops)
        (find-books (cdr towns))))))

本当に欲しいのは(val ues (car towns) shops)だ。 これは一般化できる。

(defun find2 (fn lst)
  (if (null lst)
      nil
    (let ((val (funcall fn (car lst))))
      (if val
          (values (car lst) val)
        (find2 fn (cdr lst))))))

(find2 #’bookshops towns) だけで達成できるようになった。引数で関数を渡すようになった。

Lispプログラミング独特の特徴の一つは,引数としての関数の重要性だ. これ はLispがボトムアップ・プログラミングに適している理由の一部だ. 関数の骨 格を抽象化するのは,引数に関数を使うことで肉付けができるときには比較的 簡単だ.

なるほど。

Lispでは関数全体を引数として渡せるので,この考えをさらに深めることがで きる. 前述の例の両方で,特定の関数から始めて,関数を引数に取る一般的な 関数に進んだ. 1番目の例ではすでに定義されていたmapcanを使い, 2番目の例 では新しいユーティリティfind2を書いたが, 全体的な原則は同じだ: 一般部 分と個別部分を混ぜ合わせるのでなく, 一般部分を定義して個別部分を引数と して渡すこと.

filterは関数と1個のリストを取り, その関数がリスト適用されたときに非 nil値が返されるような要素全てをリストにして返す。

> (filter #'(lambda (x) (if (numberp x) (1+ x)))
          '(a 1 2 b 3 c d 4))
(2 3 4 5)
(defun longer (x y)
  (labels ((compare (x y)
                    (and (consp x)
                         (or (null y)
                             (compare (cdr x) (cdr y))))))
    (if (and (listp x) (listp y))
        (compare x y)
      (> (length x) (length y)))))

(defun filter (fn lst)
  (let ((acc nil))
    (dolist (x lst)
      (let ((val (funcall fn x)))
        (if val (push val acc))))
    (nreverse acc)))

(defun group (source n)
  (if (zerop n) (error "zero length"))
  (labels ((rec (source acc)
                (let ((rest (nthcdr n source)))
                  (if (consp rest)
                      (rec rest (cons (subseq source 0 n) acc))
                    (nreverse (cons source acc))))))
    (if source (rec source nil) nil)))

さまざまな検索ユーティリティ。

> (split-if #'(lambda (x) (> x 4))
            '(1 2 3 4 5 6 7 8 9 10))
(1 2 3 4)
(5 6 7 8 9 10)
(defun mapa-b (fn a b &optional (step 1)
                  (map-> fn
                         a
                         #'(lambda (x) (> x b))
                         #'(lambda (x) (+ x step)))

うーむ。急にむずかしくなってよくわからないぞ。 対応付け関数というのは役立ちそうだが。

(defun map-> (fn start test-fn succ-fn)
  (do ((i start (funcall succ-fn i))
       (result nil))
      ((funcall test-fn i) (nreverse result))
    (push (funcall fn i) result)))

シンボルとストリング

新しく作るユーティリティについて。

それらの新オペレータは,どれも(議論の余地はあるが)プログラムを読み辛 くしてしまう. プログラムを読み取れるようになる前に,それらのユーティリ ティを全て理解しなければいけない. こういった言明がなぜ誤解されるのかに ついては, popページで説明した例(一番近い書店を探した例)のことを考え てみて欲しい. そのプログラムをfind2を使って書けば, 「プログラムを読み 取れるようになる前に, この新ユーティリティの定義を理解しなければいけな いじゃないか.」 と不満を言う人が出てくる. それでは,find2を使わなかっ たとしてみよう. するとfind2の定義は理解しなくてもいいが, find-booksの 定義を理解しなければいけない. その中ではfind2の仕事が「書店を見つける」 という個別の課題と混ざっている. find2を理解するのはせいぜいfind-books と同じくらい難しいだけだ. また,ここでは新ユーティリティは1回しか使っ ていない. ユーティリティは繰り返し使うよう意図されたものだ. 実際のプ ログラムでは,find2を理解しなければいけないか, または3, 4個の特定目的 の検索ルーチンを理解しなければいけないかの,どちらかの選択だろう. 前者 の方が確実に簡単だ.

なるほど。十分に抽象的であればほかでも使えるし、理解として蓄積してほかのプログラムを読み書きするときに利用できる。

返り値としての関数

On Lisp — 返り値としての関数

前章では,関数を引数として渡せることが抽象化への可能性をどれ程大きくす るかを見た. 関数に対して行える操作が豊かな程,その可能性を深く利用でき る. 新しい関数を生成して返す関数を定義することで, 関数を引数に取るユー ティリティの効果を増幅できる.

(defun joiner (obj)
  (typecase obj
    (cons #'append)
    (number #'+)))

これはオブジェクトを引数に取り, その型に応じてそれらのオブジェクトを加 え合わせる関数を返す. これは数やリストに対して働く多態的な (polymorphic)連結関数の定義に使える:

なるほど。

(defun complement (fn)
  #'(lambda (&rest args) (not (apply fn args))))
> (remove-if (complement #'oddp) '(1 2 3 4 5 6))
(1 3 5)

関数を引数として渡せることは抽象化のための強力な道具だ. 関数を返す関数 が書けることで,それを最大限に利用できるようになる. 残りの節では関数を 返すユーティリティの例を幾つか挙げる.

文言のところどころを読んだ覚えがあるのだが、コードは全然覚えてない…。

関数メモ

–map

map(FORM LIST) はフォームとリストを引数にとり、リストにフォームを適用していく関数。

(--map (* 10 it) '(1 2 3 4 5))

seq-some

seq-some (pred sequence) は述語関数とリストを引数にとり、述語をリストに適用して1つでも条件を満たせば t を返す関数。

(seq-some 'oddp '(1 2 3 4))
(seq-some 'oddp '(2 4))

macroexp-progn

(macroexp-progn '(1
                  2
                  3
                  (* 2 2)))
(eval (macroexp-progn '(1
                        2
                        3
                        (* 2 2))))

–some(form list)

dashライブラリに含まれる関数。

LIST内に条件を1つでも満たすものがあればFORMを返す。 LIST要素はitにバインドされる。マクロすごいな。

(--some (evenp it) '(2 4))  ;; => t
(--some (evenp it) '(1 3)) ;; => nil
(--some (evenp it) '(1 3 2)) ;; => t

定義を見ても、どうやってitにバインドしてるのかわからない。

(defmacro --some (form list)
  "Return non-nil if FORM evals to non-nil for at least one item in LIST.
If so, return the first such result of FORM.
Each element of LIST in turn is bound to `it' and its index
within LIST to `it-index' before evaluating FORM.
This is the anaphoric counterpart to `-some'."
  (declare (debug (form form)))
  (let ((n (make-symbol "needle")))
    `(let (,n)
       (--each-while ,list (not (setq ,n ,form)))
       ,n)))

apply-partially

(apropos "apply-partially")

どうやって使うのかわからない。

(defun my-apply-partially (fun &rest args)
  (lambda (&rest args2)
    (apply fun (append args args2))))

/lisp/subr.el にはいろいろ見慣れた関数があるな。

buffer-read-only

バッファが読み込み専用なら t を返す。

called-interactively-p

直に実行してほしくないことがある。privateメソッドのように。 magitのコードから取ってきた。

(magit-blame-mode
 (when (called-interactively-p 'any)
   (setq magit-blame-mode nil)
   (user-error
    (concat "Don't call `magit-blame-mode' directly; "
            "instead use `magit-blame'"))))

cl-block

declare

(apropos "declare")

謎。

(declare (indent 2))

dolist

(let ((nums '(1 2 3 4 5))
      (sum 0))
      (dolist (num nums)
            (setq sum (+ sum num)))
      sum)

format-spec

フォーマット文字列を入れ込みたいときに使う。

(setq my-format "%h:%m")
(defun my-format-time (hour minute)
  (format-spec my-format
               `((?h . ,hour)
                 (?m . ,minute))))

(my-format-time 12 59)

line-number-at-pos

行数を求める関数。

(line-number-at-pos)

looking-at

(looking-at ".")

open-line

(apropos "open-line")

open-lineは改行するコマンド。カーソルは移動しない。

(open-line 1)

push

(push NEWELT PLACE) リストを先頭にくっつける関数。 だが、PLACEはsymbolである必要がある。直にリストを入れることはできない。

(let ((l '(a b c)))
  (push 'new l))

repeat

最後に実行したコマンドを繰り返す。

user-error

エラーを出力する。

(user-error "this is error!")

with-demoted-errors

(apropos "with-demoted-errors")
(with-demoted-errors "これはエラー %S" (/ 1 0))

Tasks

汎変数の解説。

末尾再帰を調べる

Schemeでは実装仕様で末尾再帰を要求してくるとのこと。