abcdefGets

ゲッツ!

Ecmascriptのprotocolについて

Ecmascriptにprotocolを実装するという提案がある。

proposal-first-class-protocol

もともとGotanda.jsで発表した内容だけどいろいろ追記した。
資料はこれ

speakerdeck.com

内容

元々はinterfaceの提案だった。
それが名前と形を変えてprotocolの提案になった。

protocolとは?

classが実装すべき規約。
interfaceと違って型ではなく規約を提供する。
型クラスとかが近いのかな?

Ecmascript protocol

基本

今回の提案はちょっと特殊な形のprotocolの実装で、
実態はSymbolのコレクションになっている。

例.

protocol ProtocolName {
  // 実装が必要なシンボルを宣言
  thisMustBeImplemented;
}
  
class ClassName implements ProtocolName {
  [ProtocolName.thisMustBeImplemented]() {
    ...
  }
} 

上の実装を見るとわかるけど、protocolのメンバーは自動的にSymbolになる。
そしてprotocolを実装するクラスはそのSymbolを実装しなければいけない。
さらにprotocolは複数実装できる。

例.

protocol A { a; }
protocol B { b; }
  
class ClassName implements A, B {
  [A.a]() {}
  [B.b]() {}
}

またprotocolは実装を持つこともできる。

例.

protocol ProtocolName {
  // 実装が必要なシンボルを宣言
  thisMustBeImplemented;
  
  // メソッド実装
  youGetThisMethodForFree() {
    return this[ProtocolName.thisMustBeImplemented]();
  }
}
  
class ClassName implements ProtocolName {
  [ProtocolName.thisMustBeImplemented]() {
    ...
  }
}

const className = new ClassName();
className.youGetThisMethodForFree(); // ClassNameのインスタンスで呼べる

このprotocolは実装を持っていて、それを実装したクラスは同時に実装も引き継ぐ。
なのでtraitの様に振る舞うことができる。

拡張

さらに既存のクラスも拡張することができる。
たとえばArrayを拡張する場合。

protocol Functor {
  map;
}
  
Promise.prototype[Functor.map] = () => {
  ...
}

// このimplement関数でFunctorを実装したことを宣言
Protocol.implement(Promise, Functor);

このように既存クラスのprototypeを拡張し、implement関数を呼び出すことで、
既存のクラスにもprotocolを適用することができる。

チェック

あるクラスがprotocolを実装しているかどうかは、instanceofでは判定できない。
なので、implements演算子が提案されている。

Promise implements Functor // true

if (MyClass implements SomeProtocol) { }

上記の例のようにimplementsキーワードが演算子のように振る舞う。

extends

protocol自体は既存のprotocolを拡張することができる。

例.

protocol A { a; }
protocol B { b; }
protocol C extends A, B { c; }

こんな感じでprotocolは複数のprotocolを拡張することができる。

デフォルト実装

上記の拡張を利用するとデフォルト実装を持つprotocolを定義することができる。

例.

protocol A { a; }

protocol B extends A {
  [A.a]() { ... }
}

class Class implements B {}
const c = new Class();
c[A.a]();

protocol Bはprotocol Aを拡張し更にaを実装している。
なので、Classインスタンスに対してA.aを呼び出すことが可能になる。

static protocol

protocolはなんと静的プロパティ(!)に対しても作用させられる。

例.

protocol A {
  static b() {}
}
  
class C implements A { }
C[A.b]();

これはどうなんだ...

TypeScript

とりあえず既存のinterfaceは残すであろうとのこと。
protocolをどのような型として扱うかは未定。
Allow dynamic name in typesという型のプロパティ名にsymbolを使うことができるPRが進んでいるので、
これが実装されるとprotocolもスムーズにいけるかも。
ちなみにここで議論しています。

Question: TypeScript and proposal-first-class-protocols · Issue #18814 · Microsoft/TypeScript · GitHub

prototypeへの直接代入について

先程、既存のクラスにprotocolを実装するケースでprototypeへ直接代入するという例を出したが、
prototypeへの直接代入は何か気持ち悪い...(Object.definePropertyすらない時代からjsを書いていたせいなのか?)ので
それはやめてくれ!その代わり既存のクラスをopen-classにして拡張できるようにすれば良くない?
というアバウトな提案をしてみた。

protocol Functor {
  map;
}
  
// We can implements protocol after declaration.
Array implements Functor {
  [Functor.map]: Array.prototype.map
}

こんな感じ。

結果

とりあえず、classがprototypeを隠蔽したのにこれじゃES5時代じゃないか!
って思いを伝えたら。

classはprototypeを隠蔽したわけじゃないよ。prototypeへの代入もES5時代的ではない。
Ecmascriptのモンキーパッチ方法は常にprototypeだし、これからもjsは未来永劫変わらずこの方法なのさ。

とこのような調子で言われた。
まあEcmascriptがprototypeを隠蔽して、もっと 普通 のclassベース言語になりたがってるように見えていた私にとって、
これが目指す世界ならもう言うことは無かったので議論を打ち切った。

でもprototypeはもう触りたくないんじゃ!

おしまい。