前回、org-mode文書の中にCalcの数式を埋め込む話をしました。しかし課題として埋め込んだ数式からorg-modeの各値(テーブル、リスト、他のソースコードブロックの結果、等々)を参照できないという問題が残りました。インラインソースコードブロックであれば src_calc[:var tbl=table1]{...} のように書けば参照できますが、Calcの埋め込みモードにはそのような仕組みはありません。ただ、Calc用の関数を作ってしまえば実現可能なはずです。なので今回はそれを実装してみました。
作成したコードと使い方
作成したコードは次の通りです。
(defun calcFunc-orgref (refspec)
(save-current-buffer ;; 現在のバッファを保存・脱出時に復元
;; 式が埋め込まれているバッファを選択
;; (呼び出し時点では *Calculator* バッファが選択されているので)
(calc-embedded-original-buffer t calc-embedded-info)
(my-calc-value-from-elisp ;; Calc用の値へ変換
(org-babel-ref-resolve ;; 参照を解決
;; Calcの式REFSPECを文字列へ変換する
(my-calc-expr-to-string refspec)))))
(defun my-calc-expr-to-string (expr)
"Calcの式を文字列へ変換します。"
(pcase expr
;; ベクトル "table-1"
(`(vec . ,elements)
(apply #'string elements))
;; 変数 table1 or table#1
(`(var ,dname ,vsym)
(if (seq-contains-p (symbol-name dname) ?#)
;; 表示名に#が入っていたら変数名シンボルの方を使う
(symbol-name vsym)
(symbol-name dname)))
;; TODO: (calcFunc-subscr (var <name> var-<name>) ???)みたいなのも無理矢理 <name>_??? にする? (???の部分は数だったり変数だったりすると思う)
(_ (error "Cannot convert to string from %s" expr))))
(defun my-calc-value-from-elisp (value)
"Emacs Lispの値をCalcの値へ変換します。"
(cond
;; 整数はそのまま
((integerp value) value)
;; 浮動小数点数は……良い方法が分からない。
;; とりあえず文字列化してから読み込んでみる。
((floatp value) (math-read-number (number-to-string value)))
;; 文字列は整数のベクトルへ
((stringp value) (cons 'vec (string-to-list value)))
;; リストはベクトルへ(要素は再帰的に変換)
((listp value)
(cons 'vec (mapcar #'my-calc-value-from-elisp value)))
;; その他の型はとりあえずそのまま返してみる
(t value)))
これを使うと次のように書けます。(前回定義したマクロも使っています)
# ■テーブルを参照する例
| Name | Quantity |
|--------+----------|
| Apple | 15 |
| Orange | 24 |
| Banana | 3 |
|--------+----------|
| | 42 |
りんごは{{{calc(orgref(fruits)_3_2 => 15)}}}個、オレンジは{{{calc(orgref(fruits)_4_2 => 24)}}}個、バナナは{{{calc(orgref(fruits)_5_2 => 3)}}}、合計{{{calc(subvec(orgref(fruits), -1)_1_2 => 42)}}}個の果物があります。
# ■リストを参照する例
- 10
- 20
- 22 (←無視される)
- 24 (←無視される)
- 30
リストを参照すると $orgref(list#ex1) => [10, 20, 30]$ のようになります。(注意:マクロを使うと , がマクロの引数区切りになるので [10 までしかエクスポートされない!)
# ■コードブロック(と上のリスト)を参照する例
#+begin_src elisp :var lst=list-ex1
(apply #'+ lst)
#+end_src
リストの値の合計: {{{calc(orgref(list#sum) => 60)}}}
制作過程
こんなの簡単に実装できるだろうと思ったら色々と難しいところがありました。
- カレントバッファの調整
- 文字列を扱うのが難しい!(表示をコントロールするのが難しい!)
- どうやって参照先の値を取得するか
- 得られた値をCalc向けの形式へ変換
Calc用の関数を定義
Calc用の関数を作ること自体は簡単です。関数名を calcFunc- で始めるだけです。
(defun calcFunc-orgref (refspec)
...
)
これでCalcの数式の中から orgref(...) という形式で関数を呼び出せるようになります。
defmathというマクロを使う方法もありますが、どのような展開をするのか十分分かっている人でないと使うのが難しいですし、今回のような少し変わった用途の関数には向かないと思います。
問題は関数の中身。
簡単に思いつく仕様としては
- 参照の指定、つまり
:var NAME=ASSIGN(参照: Environment of a Code Block)のASSIGN部分の文字列を引数に取り - 参照先の値を取得し
- その値を返す
というだけのものです。
参照を解決
:var の処理をする関数はorg-babel-ref-resolveなので、単純に書けば次のようになるでしょう。
(defun calcFunc-orgref (refspec)
(org-babel-ref-resolve refspec))
試してみましょう。
#+begin_src elisp
123
#+end_src
$orgref("src1")=>$
$orgref("src1")=>$ の部分で C-x * u (update)すると……
$orgref([115, 114, 99, 49]) => orgref([115, 114, 99, 49])$
ナンデ!?
Calcにおける文字列の扱いと表示切り替え
何でかと言えば第一にCalcでは文字列は整数のベクトルだからです。第二にベクトルを文字列として表示するモードが有効になっていないからです。
文字列の表示モードを有効にするには、式の所で C-x * e を押してembedded modeに入り、 d " を押します(その後 q でembedded modeを抜けます)。すると次のようになります。
# [calc-mode: strings: t]
$orgref("src1") => orgref("src1")$
これで良さそうに見えますが、しかしこれでは通常の整数のベクトルも文字列として表示されてしまいます。つまり [1,2,3] が "\^A\^B\^C" と表示されてしまうのです。
一つの式の中で都合良く一部だけを文字列表示にして他の部分をベクトル表示にするという方法は見当たりませんでした。
まぁ、ひとまず orgref([115, 114, 99, 49]) になってしまうのは我慢しましょう。必要なら今操作したように表示モードを切り替えれば済む話です。
CalcのベクトルからEmacs Lispの文字列への変換
次の問題は評価結果が => orgref([115, 114, 99, 49]) になっている点です。これは期待した結果とは違います。期待した結果は参照先のsrc1が評価された結果、つまり 123 です。
原因はここまでの話を見れば察しの良い人はすぐに気がつくでしょう。関数 orgref の引数 refspec に引き渡されるのはCalcのベクトル [115, 114, 99, 49] であって、Emacs Lispの文字列では無いからです。つまり、org-babel-ref-resolve関数に引き渡す前にEmacs Lispの文字列に変換する必要があるわけです。
CalcのベクトルはEmacs Lispから見れば (vec 要素1 要素2 要素3 ...) という形式のリストです。carがシンボルvecでcdrが要素リストであるようなコンスセル (vec . 要素リスト) だと言っても良いでしょう。
となると変換するには次のようにします。
(defun calcFunc-orgref (refspec)
(org-babel-ref-resolve
;; (vec . 要素リスト)を要素リストの文字列へ変換
(apply #'string (cdr refspec))))
というわけでもう一度試してみましょう。すると次のようなエラーが出ました。
org-babel-ref-resolve: Reference ‘src1’ not found in this buffer
カレントバッファの切り替え
src1がバッファ内に無いと言っています。あれー、おかしいなぁ……。 calcFunc-orgref の定義部分で C-u C-M-x を押してEdebug可能にして再度実行。ステップ実行して調査するとすぐに原因は分かりました。
カレントバッファが *Calculator* になっています! そりゃ見つからないわ。埋め込みモードは背後でCalcのバッファと繋がっていて、式を評価するときはそっちがカレントバッファになっているんですね。
現在実行中の埋め込みモードの情報はcalc-embedded-info変数に入っていて、埋め込み元のバッファもそこからたどれます。それを利用すると、次のようにして元のorg-modeのバッファに切り替えられます。
(defun calcFunc-orgref (refspec)
(save-current-buffer ;; ←★現在のバッファを保存・脱出時に復元
(calc-embedded-original-buffer t calc-embedded-info) ;; ←★式が埋め込まれているバッファを選択
(org-babel-ref-resolve
;; (vec . 要素リスト)を要素リストの文字列へ変換
(apply #'string (cdr refspec)))))
再度実行すると望みの結果が得られました。
$orgref([115, 114, 99, 49]) => 123$
結果となるEmacs Lispの値をCalcの形式へ変換
次はテーブルを参照してみましょう。
| 1 | 2 |
| 3 | 4 |
$orgref("table1")=>$
$orgref("table1")=>$ の部分で C-x * u (update)。ん? 何かエラーが出ましたよ?
math-normalize-fancy: Can’t use multi-valued function in an expression
察しの良い人はもうお分かりだと思いますが、入力だけでなく出力も変換してやる必要があるのでした。さっきの例は結果が整数だったので良かったのですが、今回は結果はテーブルです。Emacs Lispレベルではorg-modeのテーブルはリストのリストになりますが、Calcレベルではベクトルのベクトルにしなければなりません。Calcのベクトルとは先ほども言及したように先頭がシンボルvecのリストです。
ひとまず変換する関数を書きましょう。
(defun my-calc-value-from-elisp (value)
(cond
;; 整数はそのまま
((integerp value) value)
;; 浮動小数点数は……良い方法が分からない。
;; とりあえず文字列化してから読み込んでみる。
((floatp value) (math-read-number (number-to-string value)))
;; 文字列は整数のベクトルへ
((stringp value) (cons 'vec (string-to-list value)))
;; リストはベクトルへ(要素は再帰的に変換)
((listp value)
(cons 'vec (mapcar #'my-calc-value-from-elisp value)))
;; その他の型はとりあえずそのまま返してみる
(t value)))
これを使えば次のように書けます。
(defun calcFunc-orgref (refspec)
(save-current-buffer ;; 現在のバッファを保存・脱出時に復元
(calc-embedded-original-buffer t calc-embedded-info) ;; 式が埋め込まれているバッファを選択
(my-calc-value-from-elisp ;; ←★Calc用の値へ変換
(org-babel-ref-resolve
;; (vec . 要素リスト)を要素リストの文字列へ変換
(apply #'string (cdr refspec))))))
そして再度評価すると……
| 1 | 2 |
| 3 | 4 |
$
orgref([116, 97, 98, 108, 101, 49]) => [ [ 1, 2 ]
[ 3, 4 ] ]
$
うまく行きました!
ちなみに部分的に取り出すなら次のようにするみたいです。
2行1列の値: $orgref([116, 97, 98, 108, 101, 49])_2_1 => 3$
2行目: $orgref([116, 97, 98, 108, 101, 49])_2 => [3, 4]$
2列目: $mcol(orgref([116, 97, 98, 108, 101, 49]), 2) => [2, 4]$
参照先の変数名での指定
それにしても参照先を指定する文字列がちゃんと表示されない(ベクトル表示になってしまう)のはやっぱり嫌ですね。何とかならないものでしょうか。解決方法の方向性はいくつかあると思うのですが、あまり強引なことはしたくありません。
orgref("table1") がダメなのであれば、シンプルに orgref(table1) と書かせるのはどうでしょうか。つまり変数名 table1 を指定させるわけです。本当に table1 という名前の変数がすでにあったらその値に置き換わってしまいますが、無ければ変数は変数のままで引数に渡されます。
例えば次のような関数を作ってargに何が渡されるか調べてみると良いです。
(defun calcFunc-testvar (arg)
(message "arg=%s" arg)
0)
Calcの中で testvar(table1) を評価すると(table1 という変数が既に無ければ) (var table1 var-table1) というリストが渡されてきます(各要素はシンボル)。ここで table1 はCalcの中で表示するときの変数名を表し、 var-table1 は実際に値を保持するEmacs Lispの変数名です。
ちなみにCalcの数式の中で table-1 とは書けません。これは table から1を引くという意味になってしまいます。この問題に対応するためなのかは知りませんが、Calcの数式の中では table#1 という書き方ができます。この変数はEmacs Lispレベルでは var-table#1 ではなく、 table-1 に対応づけられます。つまり (var table#1 table-1) になるわけです。
それらを踏まえて引数に渡されたCalcの値を文字列へ変換する関数を書いてみましょう。
(defun my-calc-expr-to-string (expr)
(pcase expr
;; ベクトル "table-1"
(`(vec . ,elements)
(apply #'string elements))
;; 変数 table1 or table#1
(`(var ,dname ,vsym)
(if (seq-contains-p (symbol-name dname) ?#)
;; 表示名に#が入っていたら変数名シンボルの方を使う
(symbol-name vsym)
(symbol-name dname)))
;; TODO: (calcFunc-subscr (var <name> var-<name) ???)みたいなのも無理矢理 <name>_??? にする? (???の部分は数だったり変数だったりすると思う)
(_ (error "Cannot convert to string from %s" expr))))
これを使うとorgref関数は次のように書けます。
(defun calcFunc-orgref (refspec)
(save-current-buffer ;; 現在のバッファを保存・脱出時に復元
(calc-embedded-original-buffer t calc-embedded-info) ;; 式が埋め込まれているバッファを選択
(my-calc-value-from-elisp ;; Calc用の値へ変換
(org-babel-ref-resolve
;; ★↓Calcの式REFSPECを文字列へ変換する
(my-calc-expr-to-string refspec)))))
これでめでたくテーブル名等がベクトルになるのを恐れずに書けるようになったわけです。
# "src1"の代わりに変数名src1で指定できる(本当にsrc1という名前の変数があったらダメ)
$orgref(src1) => 123$
# "table1"の代わりに変数名table1で指定できる
$
orgref(table1) => [ [ 1, 2 ]
[ 3, 4 ] ]
$
# src-2-1はsrc#2#1で指定できる
#+begin_src elisp
210
#+end_src
$orgref(src#2#1) => 210$
というわけで最初に書いたコードになったのでした。
おしまい
問題も残っているでしょうが、とりあえず最低限使えるものにはなりました。
とは言えそれほど使う機会があるでしょうか。元々、使うか分からないけれど簡単に作れるのだから作っておくか、という気持ちで作り始めたのですが、思った以上に手間がかかってしまいました。
素のEmacs Lispとの相互運用性を高めるような関数がもっとあれば良かったんですけどね。いや、あるのかもしれませんけど全部見ていないので分かりません。