Emacs Lisp勉強会(基礎編)
可能な限り、Javaとの対比をしながらEmacs Lispを説明します。
Emacs Lisp勉強会(基礎編)
Emacsをまだインストールしていない人は、インストールしてから、この先を読んでください。 可能な限り、Javaとの対比をしながら説明します。
目次
- Emacs Lispとは
- 開発環境
- Emacs Lispの雰囲気(Javaとの比較)
- 関数型言語
- Emacs Lisp;言語の基礎
- なんでもオブジェクト
- シンボル
- コンスセル(リスト)
- 関数
- その他(連想リスト、ベクタ、ハッシュテーブル)
- Emacs特有の型(バッファ、マーカ、etc.)
- 実践
- ループ使ったら負け?
- 変数に代入したら負け?
Emacs Lispとは
マーケティング要素無しの、「write once, run anywhere」な言語です。
Javaの場合、次の欠点があります。
- マシンにインストールされている保証が無い
- 他人のマシンに勝手にインストールするのは迷惑
Emacs Lispの場合、上の問題がありません。
開発環境
my scratchファイルを作りましょう。
ファイルの先頭に次の記述をすると、自動でlisp-interaction-modeになります。
; -*- lisp-interaction -*-
- ctrl-jで式の評価ができます。
- meta-ctrl-iで、シンボル名の補完できます
- # 「式」と「評価」の言葉の意味は後述。
Emacs Lispの雰囲気(Javaとの比較)(1)
類推が当たっているとは限りません。注意してください。
(+ 1 3) ; [Java] 1 + 3 ? => 4 (+ 1 3 5) ; [Java] 1 + 3 + 5 ? => 9 (+ 1 (* 3 5)) ; [Java] 1 + (3 * 5) ? => 16
Emacs Lispの雰囲気(Javaとの比較)(2)
(setq foo "FOO") ; [Java] String foo = "FOO"; ? => "FOO" foo ; [Java] System.out.println(foo); ? => "FOO"
Emacs Lispの雰囲気(Javaとの比較)(3)
(if (= a b) (message "a equals to b") (message "a doesn't equal to b"))) ; [Java] ; if (a == b) { ; System.out.println("a equals to b"); ; } else { ; System.out.println("a doesn't equal to b"); ; } ?
# [tips] (= a b)は、数値が等しいかの判定。(/= a b)は、等しくない場合の判定です。
Emacs Lispの雰囲気(Javaとの比較)(4)
(defun my-hello-emacs () (message "hello emacs")) => my-hello-emacs ; [Java] ; String myHelloEmacs() { ; System.out.println("hello emacs"); ; } ? (my-hello-emacs) => "hello emacs" ; [Java] ; myHelloEmacs(); ?
Emacs Lispの雰囲気(Javaとの比較)(5)
(defun my-function-using-local-val (a b) (let ((sum (+ a b))) (if (> sum 100) "sum is over 100" "sum is under 100"))) ; [Java] ; String myFunctionUsingLocalVal(int a, int b) { ; int sum = a + b; ; if (sum > 100) { ; return "sum is over 100"; ; } else { ; return "sum is under 100"; ; } ; } ?
Emacs Lispの雰囲気(Javaとの比較)(6)
; list内の偶数の個数を数える (let ((cnt 0) (lst '(1 8 7 11 3 2))) (mapcar '(lambda (n) (if (= (% n 2) 0) (setq cnt (1+ cnt)))) lst) cnt) => 2 ; [Java] ; int cnt = 0; ; List<Integer> lst = Arrays.asList({1, 8, 7, 11, 3, 2}); ; for (Integer n : lst) { ; if (n % 2 == 0) { ; cnt++; ; } ; } ???
# あえてだいぶ違う例としてあげているので、あまり気にしないでください。
関数型言語
Lispは関数型言語に分類されます(*)。
(*) 後述する理由により、しない人もいます(宗教論争)。
「関数型」と対比されるのは「手続き型」です。
関数的プログラミングとは...
- 関数定義と関数呼び出しでプログラミングすることです(*)
(*) ラムダ計算の用語では、関数定義=>関数抽象(function abstraction)、関数呼び出し=>関数適用(function application)、と言います。
これだけ聞くと、Javaとあまり変わらない?
関数とは(1)
Javaでは、(Cの慣習から)「関数(function)」と「手続き(procedure)」を区別していませんが、関数的プログラミングの立場では、区別します。
関数とは
- 入力があって出力があります
- 副作用がありません
入力は関数の引数で、出力は関数の戻り値です。ここに関しては、Javaのメソッドを想像してもらって構いません。
関数とは(2)
「副作用が無い」は、関数呼び出しによって、「何も状態が変化しないこと」を意味します。
呼び出しで何かの変数の値を変化させたら(*)、それは関数ではありません。
(*) つまり、代入をしたら負けです。ローカル変数への代入は微妙で、それ自体は関数呼び出しの引数と同程度なのでセーフな気はします。しかし、そもそもローカル変数を使うこと自体が手続き的な気もするので、負けかもしれません。
何かの変数は、グローバル変数(Javaには無いですが)、オブジェクトやクラスのフィールド変数、引数などが考えられます。
関数とは(3)
Javaで、次のようなメソッドは関数的ではない、と言うことになります。
boolean method(List<String> list) { //略 list.add("foo"); return true; }
関数的にしたいなら、次のようになります。
List<String> method(List<String> list) { // unmodifiableList List<String> newlist = new ArrayList<String>(list) newlist.add("foo"); return newlist; }
このJavaのコードは非効率です。
Lispの場合、リストの先頭への要素追加なら、ここまで非効率にはなりません。
ただし、Mapで同様の例を考えると、関数的であることと効率は相反することがあります(たぶん)。
# 引数(関数の入力)を書き換えてしまうことを「破壊的」と言ったりします。
Lispは関数型言語なのか?
関数型言語の定義を
- 関数的プログラミングしかできないプログラミング言語(副作用のある手続きを言語仕様上、禁止している言語)
だとすると、Emacs Lispは当てはまりません。
# と言うより、この定義は厳しすぎて、当てはまる言語が少なそうです。
定義を緩くして
- (上で定義した意味での)関数を定義できるプログラミング言語
としてしまうと、ほとんどの現存する言語が当てはまってしまいます(JavaでもCでも当てはまる)。
- 関数をオブジェクトとして扱えるプログラミング言語
という定義は、それほど悪くも無い気がしますが、既存の手続き型言語がこの特徴を取り入れることは可能なので、定義に使うのはイマイチです。
結局、「オブジェクト指向型言語」というカテゴリにたいして意味が無いのと同程度に「関数型言語」というカテゴリにも意味は無いのかもしれません(あるいは、徐々に意味を失っているのかもしれません)。
# 意味を失っているのは、全ての言語がLispっぽく進化しているからだと言う人もいます。
実はEmacs Lispで書かれたコードは手続き満載...
副作用を期待した「関数」呼び出し満載です。
厳密な定義で「関数」という用語を使うなら、上の文は既に自己矛盾しています。
しかし、使い分けていると説明にならないので、以後、副作用があっても破壊的であっても、「関数」と呼びます。
副作用が無いことを強調したい場合は「関数的」という用語を使います。
Lispの文法
文法がほとんど無い、のがLispの特徴です。
いわゆる制御文(Javaのif文やwhile文など)がありません。
# ifやwhileという関数はあります。
他の多くの言語にある「式と文(statement)」の区別がありません。全てが「式(expression)」です。
「式」を「評価(evaluation)」すると、値を返します。
実は、演算子もありません。
式がどんなものかは、これから説明します。
Emacs Lisp;言語の基礎
- プログラム全体は、関数定義の集合でできています。
- それぞれの関数は、他の関数を呼び出しています。
- ここはあまり深く考えず、Javaのイメージ(クラス内のメソッド定義とメソッド呼び出し)で考えれば、そんなに遠いわけではありません。
- main()に相当するモノはありません。
- Emacs Lispの場合、たいていは、ユーザがインタラクティブに呼び出す関数がエントリポイントです。
Emacs Lisp;言語の基礎(2)
次の4つの概念を知れば、言語仕様自体は理解できます。 (言語仕様の理解と、読み書きできるかは別ですが...)
- オブジェクト
- シンボル
- コンスセル(リスト)
- 関数
以下、面倒なのでEmacs Lispをelispと略記します。
なんでもオブジェクト
scratchバッファで次のように書いて、ctrl-jを押してください。
"foo" ;ctrl-j (評価) "foo" ;評価結果
"foo"という文字列式を評価すると、"foo"という値を返したという意味です。
Javaとの比較で比喩的に言えば、一行目の"foo"で'new String("foo")'が起きているイメージです。
数値でも同様です。
42 ;ctrl-j (評価) 42 ;評価結果
ここもJavaとの比較で比喩的に言えば、'new Integer(42)'が起きているイメージです。
オブジェクトは本質的に名前がありません
"foo"も42もオブジェクトで、それらに名前はありません。
Javaで'new String("foo")'や'new Integer(42)'でできる「オブジェクトそのもの」には名前が無いのと等価です。
elispの場合、関数もオブジェクトです(後述)。関数も本質的に名前がありません。 ここはJavaと違う点です(Javaでは、メソッドには始めから名前があります)。
Javaの場合、オブジェクトを変数に代入して、名前の無かったオブジェクトに名前が付きます。
elispではシンボルを使って名前を付けます(これから説明します)。
オブジェクトは型を持つ(1)
オブジェクトは型を持ちます(オブジェクト指向風に言えば、オブジェクトは自分自身の型を知っています)。
ここはJavaと同じです。
- Emacs Lispの基本型
`symbol', `integer', `float', `string', `cons', `vector', `marker', `overlay', `window', `buffer', `subr', `compiled-function', `window-configuration', `process'
オブジェクトはかならずひとつの基本型に属します。
オブジェクトの基本型はtype-of関数で確認可能です(後述)。Javaとの比較で言えば、Object::getClass()のようなものです。
Javaのようにユーザ定義型が作れて、そのインスタンス化でオブジェクトを作れるのとは様子が違います(後述)。
オブジェクトは型を持つ(2)
Javaでの実装継承(extends)に相当するものは、言語仕様上はありません。
インターフェース継承(implements)に近いモノはありますが、言語仕様上あるのか、と言われると「無い」が回答です。
インターフェース継承もどきの例
- シークエンス型
- 文字列
- リスト
- ベクタ(配列)
これらは、全てlength関数の引数にオブジェクトを渡すことで、要素数を返します。 他にもいくつか共通する操作を提供します。
オブジェクトは型を持つ(3)
ただ、シークエンス型かどうかの判定は、Emacsのソースコードで次のように'or'で判定しているだけです。共通する操作も、内部で明示的に型判定をして処理を呼び分けているだけです。
//data.c DEFUN ("sequencep", Fsequencep, Ssequencep, 1, 1, 0, "Return t if OBJECT is a sequence (list or array).") (object) register Lisp_Object object; { if (CONSP (object) || NILP (object) || VECTORP (object) || STRINGP (object) || CHAR_TABLE_P (object) || BOOL_VECTOR_P (object)) return Qt; return Qnil; }
ユーザ定義型?
Javaでのクラス定義やインターフェース定義に相当する機能は、言語仕様上ありません。
「ユーザ定義型」に相当するコードは、次のように作ります。
- ある特定の性質を持ったオブジェクトを入力(引数)として受け付ける関数群を定義
上記の関数群により、オブジェクトの集合は、「共通した振るまい」を持つことになります。
結局、これが、オブジェクト集合が属する「型」になります。
一般には、オブジェクトの型を判定して真偽値を返す述語関数(後述)も提供します(foo-p)。
ユーザ定義型? (具体例)
; ring.el ;; This code defines a ring data structure. A ring is a ;; (hd-index length . vector) (defun ring-p (x) "Returns t if X is a ring; nil otherwise." (and (consp x) (integerp (car x)) (consp (cdr x)) (integerp (car (cdr x))) (vectorp (cdr (cdr x))))) (defun make-ring (size) "Make a ring that can contain SIZE elements." (cons 0 (cons 0 (make-vector size nil)))) (defun ring-length (ring) "Returns the number of elements in the RING." (car (cdr ring)))
シンボル
シンボルもオブジェクトです。
名前プロパティ(Java風に言えばString nameというフィールド)を持ちます(この値が「シンボル名」)。
シンボル名は、obarrayと呼ばれるハッシュテーブルのキーになります。
obarrayとはobject arrayの略ですが、「シンボルハッシュテーブル」という名前の方が実体を表していると思います。
Java風に言えば、obarrayはMap<String,Symbol>です。キーがシンボル名、値がシンボル(オブジェクト)です。
シンボルが作られると、obarrayに要素として追加されます(*)。
(*)正確には、シンボルの生成とobarrayへの要素の追加は独立していますが、通常は、同一視して問題ありません。ちなみに、obarrayへの要素追加をinternと呼びます。
シンボル(2)
シンボルの構成要素(elisp風にはセル。Java風にはフィールド)
- 名前(文字列)
- 値(ポインタ)
- 関数(ポインタ)
- プロパティリスト(Java風に言えば、Map<Object,Object>)。今日は説明を省略
シンボル; lisp.h
Emacsのソースから
// lisp.h struct Lisp_Symbol { struct Lisp_String *name; Lisp_Object value; Lisp_Object function; Lisp_Object plist; Lisp_Object obarray; struct Lisp_Symbol *next; /* -> next symbol in this obarray bucket */ };
Lisp_Objectは、Javaで言えばObjectだと考えてください(Cで言えば、voidポインタ + 型情報)。
シンボル(3)
ここまででシンボルに関して分かること
- 名前を持って、obarrayというハッシュテーブルの要素になるらしい
- 値を指せるらしい
- 関数を指せるらしい
実際に確かめてみます。
シンボルが分かると、elispの基本の半分は理解したことになります(たぶん)。
シンボルの実践
scratchバッファで次のように入力してctrl-jしてください。
foo ;ctrl-j
たぶん、次のようなエラーが起きます。
Debugger entered--Lisp error: (void-variable foo) eval(foo) 略
"foo"や42の評価とは異なる反応です。 何が起きたのでしょうか?
elispの型
少し視点を変えて、シンボルで無いものを見ます。 シンボルで無いものを先に見ておくことで、シンボルの位置付けが分かるからです。
Emacsのソースから引用します。
//lisp.h enum Lisp_Type { Lisp_Int, Lisp_Symbol, Lisp_Misc, Lisp_String, Lisp_Vectorlike, Lisp_Cons, Lisp_Float, /* This is not a type code. It is for range checking. */ Lisp_Type_Limit };
Lisp_Miscは今は無視してください。整数型、文字列型などがシンボル型と並んであるのが分かります。
詳細は後述しますが、Lisp_Vectorlikeは[ ]で囲みます。Lisp_Consは( )で囲みます。
type-ofで確認できるので確かめてみます。
type-of関数でオブジェクトの型を確認
以下、scratchバッファで確認してください。 出力行を => で示しています。
(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
謎の'(クォート)は何だ、と思うかもしれませんが、今はおまじないだと思ってください(後で説明します)。
シンボルに説明を戻すと
結局、数値ではなく、文字列でもなく("で囲まれていない)、コンスセルでもなく(丸カッコで囲まれていない)、ベクタでもなければ(角カッコで囲まれていない)、それはシンボルになります(*)。
(*) 正確には、これは入力構文の話で、かなり乱暴な断定ですが、今は無視してください。
シンボルに使える文字には制限があります。ちょうど、Javaで変数名やメソッド名に使える文字に制限があるのと同じです。Javaの変数名との大きな違いは記号文字('-','?','/'など)の多くが使えることです。演算子と言うものがないので、a-bが「aマイナスb」のように解釈されることはありません。
シンボルに説明を戻すと(cont.)
先ほどのfooの評価に戻ります。
foo ;ctrl-j Debugger entered--Lisp error: (void-variable foo) eval(foo) 略
実は foo を評価した時点で、fooという名前のシンボルが生成されて、(ご丁寧にも)obarrayにinternされています(この確認は後述します)。
上記エラーは何かと言えば、シンボルfooの値セルが空だというエラーです。
シンボルの値セル
シンボルの値セルが何かオブジェクトを指すようにするには、setq関数を使います。
(setq foo "FOO") ; ctrl-j => "FOO" foo ; ctrl-j => "FOO"
setqの呼び出しで"FOO"が返っているのは、setq関数の出力が"FOO"だからです(後述)。
fooを評価すると、"FOO"が返っています。シンボルfooの値セルが、"FOO"文字列オブジェクトを指しているからです。
シンボルの値セル(cont.)
内部的には次のシンボルができています。
// 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) ; まだ設定していないので、今はエラー
クォートの意味
前ページでfooには'(クォート)を付けていました。
クォートは「評価しない」ことを指示します。クォートしないと、基本的に評価されてしまいます。
(type-of 'foo) => symbol (type-of foo) => string ;もうエラーになりません。ただし、評価後、つまり値セルの指すオブジェクトの型が出力されます
'はquote関数の略記です。つまり次のように書いても同じです。
(type-of (quote foo))
setqのqはquoteのqです。setという関数もあります。
(set 'foo "foo") ;setqを使わない書き方 (set (quote foo) "foo") ;setqを使わない書き方
シンボルは任意のオブジェクトを指せます
シンボルはいわば変数のように使えます。
Javaの変数は型を持っていますが、シンボルは任意の型のオブジェクトを指せます。
# 専門用語では、シンボルがオブジェクトを指す関係を「束縛」と呼びます。
(setq foo 42) => 42 foo => 42 (type-of foo) => integer (setq foo 'bar) ;シンボルもオブジェクト => bar (type-of foo) => symbol
特別なシンボル(tとnil)と述語(predicate)
tとnilという特別なシンボルがあります。それぞれ真と偽を表します。
(symbolp t) => t (symbol-value t) => t t ; tを評価すると結果はt => t (symbolp nil) => t (symbol-value nil) => nil nil ; nilを評価すると結果はnil => nil
慣例として、真偽値を返す関数にはpもしくは-pを付けます(predicate)。
Javaであれば'boolean isSymbol(Object obj)'とするところで、symbolpです。 (他にもlistp, consp, stringp, vectorpなど。型を調べる以外でも-pを使います)
シンボルのまとめ
ここまでで分かったこと
- シンボルは名前を持つ (symbol-name関数で確認可能)
- シンボルの値セルは任意のオブジェクトを指す (symbol-value関数で確認可能)
- シンボルの指すオブジェクトの型はtype-ofで確認可能
コンスセル
コンスセルとはふたつのポインタ(*)を持ったオブジェクトです。
# elisp的にはスロット。carスロットとcdrスロット。
Emacsのソースから引用します。
// lisp.h struct Lisp_Cons { Lisp_Object car, cdr; };
後述するように、コンスセルのcdrが別のコンスセルを指すことで、リスト構造を作ります。コンスセルで作るリスト処理こそがLisp(LISt Processing)の名前の由来でもあります。
リストを見る前に、コンスセルをもう少し見ます。
コンスセルの表記
("foo" . "bar")
のように書きます(dotted pair notation)。
これは内部的には次のようなコンスセルです。
struct Lisp_Cons { Lisp_Object car; => "foo"文字列オブジェクトを指す Lisp_Object cdr; => "bar"文字列オブジェクトを指す };
コンスセルの生成
consという関数を使って、コンスセルを生成できます。
(cons "foo" "bar") => ("foo" . "bar")
# consはconstructの略です。
# 予想がついていると思いますが、コンスセルの名前はこのconsから来ています。
コンスセルの操作
carとcdrという関数で内部にアクセスします。
Java風に言えばgetterメソッドです。
car、cdr以外にコンスセルの中を参照する手段はありません。
(car '("foo" . "bar")) => "foo" (cdr '("foo" . "bar")) => "bar"
Java風に言えば、コンスセルはふたつのprivateフィールドとふたつのアクセサを持つだけの軽いオブジェクトです。
コンスセルのまとめ
- ふたつのスロットを持ちます。
- スロットは任意のオブジェクトを指せます。
- carとcdrという(アクセサ)関数でスロットの指すオブジェクトを取り出せます。
以下、carとcdrの用語は文脈に応じて、スロットの指すオブジェクトもしくはアクセサ関数を示します。
(脱線) 入力構文とオブジェクト
厳密に言えば、("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) ; ,のついたオブジェクトは評価。そうでないオブジェクトは未評価
リストへの道
コンスセルのcdrが別のコンスセルを指すと?
(cons "foo" '("bar" . "baz")) => ("foo" "bar" . "baz")
素直に表記を拡張すると、次のようになりそうですが、
("foo" . ("bar" . "baz"))
("foo" "bar" . "baz") は言わば省略表記です。
'("foo" . ("bar" . "baz")) => ("foo" "bar" . "baz")
リスト誕生
最後のcdrをnilにすると、
(cons "foo" '("bar" . nil)) => ("foo" "bar")
内部状態をより正確に示すなら、("foo" . ("bar" . nil)) ですが、面倒なので("foo" "bar")と書きます。
ふたつ目のコンスセルのcdrが、("baz" . nil)を指せば、("foo" "bar" "baz")の要素3つのリストになります。
これがelispのリストです。
リストの操作(1)
(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
# nilと()(空リスト)は内部で同じです
リストの操作(2)
イディオム
(setq foo (cons "value" foo)) ; リストfooに要素をprepend (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がなければ、評価してから引数に渡ります
リストの後続要素は、リストであるかもしれません。この場合、内側のリストを評価、つまり関数呼び出しをしてから、外側のリストの関数呼び出しをします(前ページで既にやっていますが)。
# ここまで、この説明無しに関数呼び出しを使ってきましたが、裏側で起きていることはこういうことです。
関数定義の方法
分かりやすい関数定義の方法と呼び出し。
(defun my-plus1 (n) (+ n 1)) => my-plus1 (my-plus1 10) => 11 (defun my-plus (m n) (+ m n)) => my-plus (my-plus 2 5) => 7
- mやnは仮引数です
- 関数の戻り値(=関数の評価結果)は、関数本体の最後の評価結果です
シンボルの関数セル(1)
defunを見て、関数に名前があると思うのは間違いです。
defunは、シンボルを作って、その関数セルが関数定義を指すようにしています。
(symbolp 'my-plus) => t (symbol-function 'my-plus) => (lambda (n m) (+ n m))
シンボルの関数セル(2)
carやdefunも、my-plusと同じことです(つまり、どちらもシンボルであり、関数定義を指しています)。
(symbol-function 'car) => #<subr car> (symbol-function 'defun) => #<subr defun> (symbol-function '+) => #<subr +>
subr(subroutineの略)は、Cで書かれた関数を意味しています。
構造(シンボルcarやシンボルdefunがあり、それらの関数セルが関数定義を指す)は同じです。
シンボルの関数セル(3)
値セルにsetqやsetがあったように、関数セルにはfsetがあります(fsetqはありません)。
(fset 'my-plus2 '(lambda (n) (+ n 2))) ; defunと同じ => (lambda (n) (+ n 2)) (my-plus2 10) => 12
シンボルの関数セル(4)
関数セルと値セルは別々にあります。
(setq foo "foo") => "foo" (fset 'foo '(lambda (s) (concat s "bar"))) => (lambda (s) (concat s "bar")) (foo foo) => "foobar"
lambdaとは
(lambda (引数 ...) (関数本体))
が関数定義です。
関数に名前なんてありません。が、そのまま引数を渡して関数を呼ぶことができます。
((lambda (m n) (+ m n)) 2 5) => 7
functionpから関数とは何かを知る
述語の一種にfunctionpがあります。
; 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で始まるリスト
- 関数セルが空ではないシンボル
関数呼び出し(1)
リストの先頭要素に「関数」があれば、関数呼び出しになります。
(my-plus 1 3) ;シンボルであれば関数セルの指す関数を呼び出す => 4 ((lambda (m n) (+ m n)) 1 3) ;シンボルlambdaで始まるリストも「関数」 => 4
関数呼び出し(2)
funcall関数は引数の1番目を関数として呼びます。
(funcall 'my-plus 1 3) => 4 (funcall '(lambda (m n) (+ m n)) 1 3) => 4
値セルにlambda
(setq foo '(lambda (m n) (+ m n))) => (lambda (m n) (+ m n)) (funcall foo 2 5) => 7
# これを見ると、値セルと関数セルを区別しない方がすっきりするのでは?、と思うかもしれません。
# 実際、Schemeでは区別していないようです。
シンボルのセルを明示的に空にする
(makunbound 'foo) ;値セルを空にする => foo (fmakunbound 'foo) ;関数セルを空にする => foo
連想リスト(association list)
(("foo" . "FOO") ("bar" . "BAR") ("baz" . "BAZ"))
リストの要素がコンスセル。
各要素のcarをキー、cdrをバリューとして扱うと、Java風に言えばMapとして使えます。
ただし、探索のオーダーはO(n)です。
探索O(1)の連想リストが欲しい場合、ハッシュテーブルを使います。 (Emacs21以降は基本型としてハッシュテーブルがあります。それ以前はベクタを使って、自前実装)
配列(cont.)
言語仕様として「配列」があると言うより、次の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; }
# 配列はunmodifiableです(要素の追加、削除はできない)。Javaと同じです。
# 文字列はmutableです(要素の書き換え自由)。Javaと違います。
ベクタ(1)
JavaでのObject[]相当です。
入力構文は角カッコで囲みます。
ベクタオブジェクトを評価するとベクタ自身を返します。
[1 3 5] => [1 3 5] (vectorp [1 3 5]) => t (setq foo [1 3 5]) ;quoteしてもしなくても同じ => [1 3 5] (vectorp foo) => t
ベクタ(2)
Object[]相当なので、要素の型は異なっていてもOKです(良いか悪いかは別として)。
(setq foo [1 "three" 5 "seven"]) => [1 "three" 5 "seven"] (aref foo 1) ; 1番目の要素の取得(indexは0ベース) => "three" (aset foo 1 "THREE") ; 1番目の要素の書き換え => "THREE" foo => [1 "THREE" 5 "seven"]
# arefもasetも、index指定が要素数以上だとエラーになります
ハッシュテーブル
JavaのHashMap<Object,Object>相当です。
(type-of (make-hash-table)) => hash-table ; 基本型 (setq ht (make-hash-table :test 'equal)) => #<hash-table 'equal nil 0/65 0x93ae408> (puthash "foo" "FOO" ht) => "FOO" (puthash "bar" "BAR" ht) => "BAR" (hash-table-count ht) => 2 (gethash "bar" ht) => "BAR"
obarray再び
内部的にはベクタを使ったハッシュテーブル(衝突時はチェインで連結)です。
(type-of obarray) => vector (length obarray) ;衝突したチェインをたどらない長さ (全要素ではない) => obarrayのベクタ長
obarrayの全要素をたどるにはmapatomsを使います。
(let ((cnt 0)) (mapatoms '(lambda (x) (setq cnt (1+ cnt)))) cnt) => obarrayの全要素数
新しいシンボルを生成するとobarrayの要素数が増えることを確認してください。
既にシンボル名がobarrayに(キーとして)存在する場合、obarrayから値(シンボルオブジェクト)を取得します。
letとは
明示的にローカルシンボルを作成できます。
(let (ローカルシンボル定義...) 式...)
ローカルシンボル定義は、シンボル単体、もしくは(シンボル 値)のリストのいずれかです。
(let (foo) t) ;ローカルシンボルfoo。値セルはnilに初期化 (let ((foo "FOO")) t) ;ローカルシンボルfoo。値セルは"FOO"に初期化 (let (foo (bar "BAR")) t) ;ローカルシンボルfooとbar (let ((foo "FOO") (bar "BAR")) t) ;ローカルシンボルfooとbar
ダイナミックスコープ(1)
次のような感覚を持ってコードを読めば、理解は難しくありません。
- Lispインタプリタがシンボルを見つけると、シンボル名をキーにobarrayを探索する
- ローカルシンボルは、obarrayを上書きする(スコープを抜けると、obarrayは元に戻る)
ローカルシンボルには、letで束縛したシンボルおよび関数の引数のシンボルがあります。
# ダイナミックスコープは過去の遺物?
# cf. perlのlocal
ダイナミックスコープ(2)
(setq foo "FOO") => "FOO" (defun my-show-foo () foo) => my-show-foo (let ((foo "BAR")) (my-show-foo)) => "BAR"
ダイナミックスコープ(3)
ダイナミックスコープが便利な時も、時々あります。
(let ((buffer-read-only t)) (...))
シンボルbuffer-read-onlyは、他の言語で言えば、グローバル変数のようなものです。
上記のletのボディ部で外部の関数呼び出しをしても、そこでbuffer-read-onlyはtになります。
letを抜けると、グローバルなbuffer-read-onlyシンボルには影響していません。
cf. レキシカルスコープ
ダイナミックスコープの対比概念は、レキシカルスコープです。
elispはダイナミックスコープですが、新しいlispはレキシカルスコープです。
Javaはレキシカルスコープです。
関数定義が入れ子にならない場合、レキシカルスコープは簡単です。なぜなら、ローカル変数以外の名前(変数名や関数名)は、必ずstaticな場所と結び付いているからです。
# 関数の引数やローカル変数は束縛があるので束縛変数、そうで無い変数を自由変数と呼びます。
関数定義が入れ子になった場合、自由変数(束縛がないシンボル)とオブジェクトの対応が静的ではなくなります。
(require 'cl) (lexical-let ((foo "FOO")) (defun my-show-foo () foo)) (let ((foo "BAR")) (my-show-foo)) => "FOO"
実践
- ループ使ったら負け?
- 変数に代入したら負け?
リストの要素の和を求める(1)
例えばJavaなら
int getSum(List<Integer> lst) { int sum = 0; for (Integer n : lst) { sum += n; } return sum; }
リストの要素の和を求める(2)
elispでループを使った解
(defun get-sum-loop (lst) (let ((sum 0)) (while lst (setq sum (+ sum (car lst))) (setq lst (cdr lst))) sum)) (get-sum-loop '(1 2 3 4 5 6 7 8 9 10)) => 55
リストの要素の和を求める(3)
elispでmapcarを使った解
(defun get-sum-mapcar (lst) (let ((sum 0)) (mapcar '(lambda (n) (setq sum (+ sum n))) lst) sum)) (get-sum-mapcar '(1 2 3 4 5 6 7 8 9 10)) => 55
リストの要素の和を求める(4)
elispでdolistを使った解
(defun get-sum-dolist (lst) (let ((sum 0)) (dolist (n lst) (setq sum (+ n sum))) sum)) (get-sum-dolist '(1 2 3 4 5 6 7 8 9 10)) => 55
リストの要素の和を求める(5)
elispで再帰を使った解
(defun get-sum-recur (lst) (cond ((null lst) 0) (t (+ (car lst) (get-sum-recur (cdr lst)))))) (get-sum-recur '(1 2 3 4 5 6 7 8 9 10)) => 55
# 末尾再帰では無い点は気にしないでください
リストの要素の和を求める(6)
本当は簡単な解がありますが...
(apply '+ '(1 2 3 4 5 6 7 8 9 10)) => 55 (+ 1 2 3 4 5 6 7 8 9 10) => 55
再帰(1)
リストlstの中で、指定した閾値(threshold)より小さい数の個数を返す関数
(defun count-less-than-threshold (lst threshold) (cond ((null lst) 0) (t (if (< (car lst) threshold) (1+ (count-less-than-threshold (cdr lst) threshold)) (count-less-than-threshold (cdr lst) threshold)))))
再帰(2)
リストlstの中で、指定した閾値(threshold)より小さい数だけのリストを返す関数
(defun list-less-than-threshold (lst threshold) (cond ((null lst) nil) (t (if (< (car lst) threshold) (cons (car lst) (list-less-than-threshold (cdr lst) threshold)) (list-less-than-threshold (cdr lst) threshold)))))
再帰(3)
less-thanがださいので、条件を関数で与えるように変更
(defun list-with-condition (lst condition-p) (cond ((null lst) nil) (t (if (funcall condition-p (car lst)) (cons (car lst) (list-with-condition (cdr lst) condition-p)) (list-with-condition (cdr lst) condition-p)))))
(list-with-condition '(1 2 3 4 5 7 8 9 10) '(lambda (n) (< n 4))) => (1 2 3)
Emacs Lispの嫌いな所(1)
- 名前空間が無い(緩い)のが恐い
Cと同程度と言ったら同程度ですが、Cの場合、staticな関数で名前空間を(ある程度)守れる点と、名前が重なれば、ほとんどの場合、コンパイル時もしくはリンク時に検出できます。
以下の例は極論ですが、実行時エラーでしか分からないのは恐すぎます。
(defun setq (n m) ; これで(setq foo "foo")は期待通りの動作をしなくなります (+ n m))
Emacs Lispの嫌いな所(2)
- 変数に型が無いのは、書くのは楽ですが、読むのは辛い
個人的に、読み手の負荷を軽減できるなら、書き手に多少の負荷を負わせるのは「正しい」と思っています。
ローカル変数の型はともかく、関数の引数の型と戻り値の型は、ぱっと見ただけで分かりたいです。
Emacs Lispの嫌いな所(3)
- 関数をカテゴリ化する何らかの仕組みが無いのが辛い
例えば、Javaで文字列の連接処理を行いたい場合、Stringクラスのリファレンスから探し始めます。
elispの場合、「文字列に対する操作」という観点での検索はかなり難しいです。せいぜい、concatenateという単語でaproposして、concatという関数を探せるぐらいです。
Cと同程度と言ってしまえば、同程度かもしれないので、これは慣れの問題かもしれません。
応用編に続く
実は今日の知識だけでは、まともにelispのプログラムは書けません。
elisp特有のバッファやポイントの知識が必要です。
Javaで言えば、ライブラリの知識が無いとまともにプログラムを書けないのと同じことです。