マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

Typstで“calc.abs(-8)”はメソッド呼出なのか

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()とは異なる構文である」ということはありえないことになる。実際に確かめてみよう。

[mod.typ](len()という関数をもつモジュール)
#let len() = 42
[main.typ](このファイルを実行する)
#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しましょう😃


  1. そもそも、array型の値はフィールドを一切持っていない。
  2. nameは単一の識別子に限るが、valの部分は任意の式でよい。
  3. つまり、トップレベルでdictionaryとして定義されているtype型の値。
  4. 意外かもしれないがTypstではモジュールは第一級値(first-class value)である。
  5. valがtype値である場合のval.name()は(module値であるときと同様に)フィールドのval.nameの関数呼出と同じ動作になる。例えばdictionary.lenというフィールドは実際に存在する。
  6. 本当はこの場合でもtype(val).nameのフィールドが存在する場合は“メソッド呼出の意味論”が優先されるようである。ただ型の性質を考える限り、この4つの型にメソッドが設定される可能性はほぼなさそうである。