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)
がdictionary
3であるので次の式と等価になり、これは実際に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つの型にメソッドが設定される可能性はほぼなさそうである。↩