Typstのメソッド呼出を完全に理解する話
Typstの一部の型はメソッドをもつ。例えばarray型の値は自身の長さ(要素数)を取得するためのlen()メソッドをもっている。
#let ary = (1, 2, 3) #ary.len() //==> 3
ここで注意すべきなのは、これはary,lenという(function型の)フィールドに関数呼出の括弧を付けたものではない、ということである。実際、array型の値aryにはary.lenというフィールドは存在しない1。
#ary.len //--> error: cannot access fields on type array
Typstにおいてフィールドの参照とメソッドの参照が異なる概念であることはdictionary型をみればさらに明らかになる。以下の例をみてわかるように、フィールドとメソッドの空間は全く別になっている。
#let dict = (foo: calc.abs, len: 42)
#dict.len //==>42
#dict.len() //==>2
#dict.foo //==>abs (function値の表示)
#dict.foo(-8) //-->error: type dictionary has no method `foo`
#dict.keys //-->error: dictionary does not contain key "keys"
#dict.keys() //==>("foo", "len")
ということは、Typstではval.name(...)という形2の式は「メソッド呼出」を表すものであり、これとval.nameの形の「フィールド参照」とは全く別のものである、といえそうである。もし「フィールドのfunction値を呼び出す式」を書きたいのなら、val.name(...)という“形式”を回避する必要があり、簡単な方法としてはval.nameの部分に括弧を付ければよい。
#(dict.foo)(-8) //==>8 ('calc.abs(-8)'の値)
“メソッド呼出の意味論”についてはTypstの公式のドキュメントに説明がある。
すなわち、val.name(...)というメソッド呼出はtype(val).name(val, ...)と等価になる。先の例でdict.len()はtype(dict)がdictionary3であるので次の式と等価になり、これは実際に2を返す。
dictionary.len(dict)
Typstのメソッド呼出がなにもわからない話
ところで先のdictionaryの例でcalc.absという関数を使った。これは組込のcalcモジュール(module型の値4)に属している関数で、数値の絶対値を返すものである。通常はcalc.absに関数呼出の括弧を付けて使う。
#calc.abs(-8) //==>8
何の変哲もないコードであったはずだが、ここで先の考察を踏まえるとある疑問が湧いてくる。このcalc.abs(-8)というのは「メソッド呼出」なのであろうか?
この式はまさにval.name(...)という形なので形式の上ではメソッド呼出のはずである。ただし先のdictionaryやarrayの話と決定的に異なる点がある。calc.absは実際にcalcのフィールドとして存在するのである。これはcalc.absの部分に括弧を付けても呼び出せることからわかる。
#(calc.abs)(-8) //==>8
これを踏まえるとcalc.abs(-8)は「calc.absというフィールド値に関数呼出の括弧を付けた式」でありメソッド呼出でない気がしてくる🤔
やっぱりメソッド呼出でありそうな話
こういう関数を考える。
#let call-len(val) = val.len()
Typstは動的型の言語であるため、valの型は実行時にしか決まらない。もしここで、valにarrayの値とmoduleの値のどちらも受け付けるのであれば、val.len()という1つの式が成立する以上「calc.abs(-8)がary.len()とは異なる構文である」ということはありえないことになる。実際に確かめてみよう。
len()という関数をもつモジュール)#let len() = 42
#import "mod.typ" #let call-len(val) = val.len() #let ary = (1, 2, 3) #let dict = (foo: calc.abs, len: 42) #call-len(ary) //==>3 #call-len(dict) //==>2 #call-len(mod) //==>42
“期待通り”の結果になった。ということは、やっぱりcalc.abs(-8)はメソッド呼出である……?🤔🤔
やっぱりメソッド呼出でなさそうな話
calc.abs(-8)がメソッド呼出であるなら、先ほど紹介した“メソッド呼出の意味論”を満たすはずである。つまり、type(calc)はmoduleであるからcalc.abs(-8)は以下と同値になる。
module.abs(calc, -8)
つまり、module(type値)にはmodule.absというフィールド5がありその値は「引数のモジュールのabsフィールドの関数を呼び出す」という役割をもった関数、ということになる。もちろんモジュール内の関数名には任意の識別子が使えるので、この理屈に従うと「moduleにはありとあらゆる名前のフィールドが定義されている」というオソロシイことになる。まあ論理的にありえない話ではないので、実際に確かめてみよう。
#module.abs //-->error: type self does not contain field `abs` #module.abs(calc, -8) //-->error: type self does not contain field `abs`
どうやらそんなオソロシイ話はなかったようである😊 でもこれだとやっぱりcalc.abs(-8)はメソッド呼出ではない……?🤔🤔🤔
Typstのメソッド呼出がチョットデキル話
なにもわからなくなったので、処理系の実装をみてみよう。
詳細の説明は(メンドクサイので🙃)省くが、やはり、val.name(...)の形式の式の実行においてはvalの型によって解釈を変えているようである。
valの型がsymbol、function、type、moduleの何れかである場合6は、フィールドval.nameの値に対する関数呼出と解釈する。- それ以外の場合は先述の“メソッド呼出の意味論”に従う。
つまり結論としては:
val.name(...)はval.nameとは全く別の構文である。- しかし
valの型によって「メソッド呼出」になったり結局「val.nameの関数呼出」になったりする。 calc.abs(-8)は後者に該当するので「メソッド呼出」ではない。
まとめ
皆さん、そんな細かいことは一切気にせずに、どんどんTypstしましょう😃
- そもそも、array型の値はフィールドを一切持っていない。↩
-
nameは単一の識別子に限るが、valの部分は任意の式でよい。↩ -
つまり、トップレベルで
dictionaryとして定義されているtype型の値。↩ - 意外かもしれないがTypstではモジュールは第一級値(first-class value)である。↩
-
valがtype値である場合のval.name()は(module値であるときと同様に)フィールドのval.nameの関数呼出と同じ動作になる。例えばdictionary.lenというフィールドは実際に存在する。↩ -
本当はこの場合でも
type(val).nameのフィールドが存在する場合は“メソッド呼出の意味論”が優先されるようである。ただ型の性質を考える限り、この4つの型にメソッドが設定される可能性はほぼなさそうである。↩