困っているので、思考の整理を兼ねてメモしておく。各項目がまちがっていたらおしえてください。
- 動機
- Scala.js
- ScalablyTyped
- ScalablyTypedにおけるvoid
- ScalablyTypedにおけるasync
- TypeScriptにおけるasync void
- TypeScriptにおけるvoid methodのasynchronousな継承
- ScalaにおけるUnitのasynchronousな継承
- Obsidianのプラグインにおける継承
- Awaitが使えない
- 謎
- 謝辞
動機
自分はObsidianというテキストエディタを愛用しているのだが、愛が高じてプラグインを書きたくなってきた。プラグインはTypeScript(JS)で書くことができ、そのための型定義ライブラリも用意されている:
あとは、ドキュメントに従って特定のクラスを継承し、これをexport default
すると、Obsidianで読み込めるようになる。
Scala.js
Scalaのすごいライブラリ(というかビルド基盤)として、Scala.jsというのがある:
これを使うと、Scalaのコードを謎のテクノロジーでjavascriptにコンパイルしてくれる。しかも、普通にちゃんと動くので驚きだ。
そこで、今回はScalaでObsidianのプラグインを書こうとした。
ScalablyTyped
さて、当たり前だがObsidianのライブラリはTypeScript版しか提供されていない。そこで、第二のテクノロジーであるScalablyTypedを利用する。
このコンパイラプラグインは、謎のすごいテクノロジーによってTypeScript型定義ファイル(d.ts)をScalaのファサードに変換する。内部的にはScala.jsによってネイティブのJSのコードが呼び出されるように変換されるので、使う側からは何も考えずにScalaを呼び出しているような感覚でTSのライブラリを呼び出せる。
ScalablyTypedにおけるvoid
ScalablyTypedは、void
型をUnit
に変換する。
returnVoid(): void { ... }
def returnVoid(): Unit
ScalablyTypedにおけるasync
ScalablyTypedは、async functionをちゃんと変換してくれる。
async f(): string { ... }
上掲のようなTypeScriptのメソッドがある場合、以下のような型定義に変換される:
def f(): Promise[String]
Promise
はScala.jsが用意している、TypeScript上のPromise
を表現するラッパーで、これはそのままScalaのFuture
に相互変換できるようになっている*1。したがって、以下のものと等価に扱える:
def f(): Future[String]
TypeScriptにおけるasync void
TypeScriptでは、async
をつけた関数(メソッド)は、そのボディがPromise
を返さなくても、Promise
で包んでくれる:
async f() { return "foo" } // => Promise<String>になる
TypeScriptにおけるvoid methodのasynchronousな継承
TypeScriptでは、以下のような継承は可能なようだ:
class VoiComponent { load(): void {} } class VoiMyPl extends VoiComponent { override async load(): Promise<void> {} }
継承した子が、親のvoid
なメソッドをasync
キーワードによってPromise<void>
に変換している。これはコンパイルが通っている。
他方、このような継承は不可能なようだ:
class NumComponent { load(): number {return 1} } class NuMMyPl extends NumComponent { override async load(): Promise<number> {return 1} // does not compile }
Property 'load' in type 'NuMMyPl' is not assignable to the same property in base type 'NumComponent'. Type '() => Promise<number>' is not assignable to type '() => number'. Type 'Promise<number>' is not assignable to type 'number'.
ScalaにおけるUnitのasynchronousな継承
Scalaではこのようなことはできない:
import scala.concurrent.Future class A { def f(): Unit = ??? } class B extends A { override def f(): Future[Unit] = ??? // does not compile }
error overriding method f in class A of type (): Unit; method f of type (): concurrent.Future[Unit] has incompatible type
Obsidianのプラグインにおける継承
Obsidianでは、ちょうど先述した例に該当するようなコードが発生している。ライブラリにあるonload(): void
が、これを継承したプラグインではasync onload()
として使われているのだ。
ライブラリの記述をScalablyTypedはonload(): Unit
に変換してしまう一方、使う局面ではどうしてもFuture
でないとうまく動作しないようになっている。他方、勝手にシグネチャをFuture[Unit]
に書き換えると、オーバーライドに失敗して継承できない。
Awaitが使えない
Scala.jsでは、実装の都合でscala.concurrent.Await
を使うことができないので、なんとかしてシグネチャをFuture
にするほかない。
謎
いくつかの謎を洗い出すと以下のようになる:
- なぜTypeScriptは
void
を返すメソッドを継承してPromise<void>
にすることが許されているのか? - ScalablyTypedは
Unit
を返すメソッドをなんとかしてFuture[Unit]
として扱って良いようにできないだろうか?
謝辞
ちぇっちぇさんに色々テストコードを教えてもらいました。ありがとうございます。
これ半分嘘でした
— ちぇっちぇ (@Chen__TS) 2023年6月28日
今確認してみたらvoidだけ許されてるっぽい?https://t.co/VOCjVqTUC9
*1:特定のパッケージをimportすることが必要