abcdefGets

ゲッツ!

mallocを再実装した話

C++ AdventCalendarの12日目

普段私はWEBのフロントエンドを仕事にしている。
つまり使う言語はjavascript/typescript等のScript言語だ。
ただ前職や趣味、OSS等でC++によく触っていたので昔実装したmallocの話をすることにした。

mallocとは

mallocとはC言語のstdlib.hに含まれるメモリ割り当て関数のことで、
C++やその他の多くの言語で内部的に利用されている。
ヒープを割り当てる方法はいくつかあるが、このmallocがもっともメジャーといえるだろう。

mallocを再実装した

今回はmallocを自分で再実装してちょっと早くした話を書く。
再実装した理由は色々あるが最も大きな理由はただの好奇心。
yatscというtypescriptコンパイラC++で書こうと思って実装を始めたときに作った。
ただしyatsc自体は未完で飽きて終了。
作ってみて思ったが、実際にプロダクトで使う場合はシンプルなメモリプールとか作ったほうがよっぽど利口だと思う。

Inside malloc

malloc関数は現在のモダンな環境ではmmapを利用してメモリを確保、それを各チャンクに分けて管理しているはず。
細かいことは以下のスライドに詳しい。

www.slideshare.net

Direction of implementations

今回はGoogleのTLMallocの実装と上記のスライドをベースにjust-fitアロケータを作成することにした。

基本的な構成は以下の通り。
メインスレッドにあるCentralArenaが各スレッドのLocalArenaを管理しており、
それぞれのArenaはTls(Thread Local Storage)に格納されている。
また、各LocalArenaはスレッドが終了すると、free-listに格納されて空き状態となり、
新たにスレッドが生成された場合に再利用される。
LocalArena自体もlinked-listになっており、nextポインタで各スレッドのLocalArenaはつながっている。
さらにLocalArenaは内部にChunkを持ち、それらはChunkHeaderと後続の複数のHeapHeaderによって管理されている。

Tlsにヒープを格納することでスレッド毎にヒープ領域を分割し、スレッド競合を防ぎつつjust-fitアロケータでメモリの無駄も防ぐ。
というのが、このmalloc実装のキーポイントになる。
ちなみにTlsを実装するのは面倒だったので、AndroidTls実装をコピーした。
Tlsのコールバックまで実装されており非常に便利。サンキューGoogle

以下はCentralArenaからLocalArenaまでの図

+----------------+  +---------------+
|  CentralArena  |--|  Main Thread  |
+----------------+  +---------------+
      |     |  +------------+  +--------------+
      |     +--|  Thread-1  |--|  LocalArena  |
      |     |  +------------+  +--------------+
      |     |  +------------+  +--------------+
      |     +--|  Thread-2  |--|  LocalArena  |
      |     |  +------------+  +--------------+
      |     |  +------------+  +--------------+
      |     +--|  Thread-3  |--|  LocalArena  |
      |        +------------+  +--------------+
      +------------+  +--------------+  +--------------+
      |  free list |--|  LocalArena  |--|  LocalArena  |-- ...
      +------------+  +--------------+  +--------------+

CentralArena

CentralArenaはメモリのアロケーションを要求されると、 GetLocalArena関数を呼び出して、TlsからLocalArenaを取り出すか空いているLocalArenaをfree-listから探し出して割り当てる。

以下のような感じでAtomicにlockを行ってスレッド競合を防いでいる。

LocalArena* CentralArena::FindUnlockedArena() YATSC_NOEXCEPT {
  // If released_arena_count_ is zero,
  // that mean free arena is not exists.
  // So we simply allocate new LocalArena.
  if (released_arena_count_.load() == 0) {
    return StoreNewLocalArena();
  }

  LocalArena* current = local_arena_.load(std::memory_order_relaxed);

  while (current != nullptr) {
    // Released arena is found.
    if (current->AcquireLock()) {
      return current;
    }
    current = current->next();
  }

  // If released arena is not found,
  // allocate new LocalArena.
  return StoreNewLocalArena();
}

ちなみにAcquireLockは以下のような感じ。Atomicにロックしている。

YATSC_INLINE bool AcquireLock() YATSC_NOEXCEPT {
  bool ret = !lock_.test_and_set();
  if (ret) {
    central_arena_->NotifyLockAcquired();
  }
  return ret;
}

さて上記FindUnlockedArenaを使って空いたLocalArenaを探し出すのに失敗した場合、 そこで初めてメモリを割り当てることになる。
このメモリ割り当ては以下のような感じで行う。

LocalArena* CentralArena::StoreNewLocalArena() YATSC_NOEXCEPT {
  Byte* block = reinterpret_cast<Byte*>(
      GetInternalHeap(sizeof(LocalArena) + sizeof(InternalHeap)));

  LocalArena* arena = new (block) LocalArena(
      this, new(block + sizeof(LocalArena)) InternalHeap(
          block + sizeof(LocalArena) + sizeof(InternalHeap)));
  arena->AcquireLock();

  LocalArena* arena_head = local_arena_.load(std::memory_order_acquire);

  // Run CAS operation.
  // This case ABA problem is not the matter,
  // because the local_arena_ allowed only one-way operation
  // that is append new LocalArena.
  // So head of local_arena_ is change only when new local arena is appended.
  do {
    arena->set_next(arena_head);
  } while (!local_arena_.compare_exchange_weak(arena_head, arena));

  return arena;
}

// Get heap space that used to allocate LocalArena and ChunkHeader.
YATSC_INLINE void* CentralArena::GetInternalHeap(size_t additional) {
  return VirtualHeapAllocator::Map(
      nullptr, sizeof(ChunkHeader) * kChunkHeaderAllocatableCount + additional,
      VirtualHeapAllocator::Prot::WRITE | VirtualHeapAllocator::Prot::READ,
      VirtualHeapAllocator::Flags::ANONYMOUS
      | VirtualHeapAllocator::Flags::PRIVATE);
}

解説すると、まず最初にVirtualHeapAllocatorを利用してメモリを割り当てる。
このVirtualHeapAllocatormmapのプラットフォーム毎の差分を吸収したクラス。
このVirtualHeapAllocatorからLocalArenaInternalHeapという内部向けの管理ヘッダ分のメモリを割り当てる。
それをinplacement newを利用してLocalArenaに割り当てる。
つまりLocalArenaは以下のようなメモリレイアウトになる。

  sizeof(InternalHeap)            + sizeof(LocalArena)
+-------------------------------+   +--------------+
|  InternalHeap(hidden header)  |---|  LocalArena  |
+-------------------------------+   +--------------+

あとは割り当てたLocalArenaのロックを獲得して、local-arenaのリストに格納する。
このリストに追加するのも当然atomicに行わなければならないので、CAS(Compare And Swap)を利用してリストに追加する。
CASを使うとABA問題を気にしなければならないが、コメントにある通りlocal_arena_のリストは追加しか行わないのでABA問題は気にしなくて良い。
これでめでたくCentralArenaからロックフリーでLocalArenaを確保することに成功した。
次はLocalArenaを実装する。

LocalArena

LocalArenaこそがメモリ割り当てのコアになっていて、この部分が一番面倒くさい。
LocalArenaはメモリ割り当て要求を受けるとsmall_bin_とよばれる小さなメモリ専用のツリーにメモリを割り当てていく。
今回はこのツリーはRedBlackTreeを利用した。
このsmall_bin_に要求メモリサイズ毎のリストを作りそこに割り当てていく。

+--------------+
|  LocalArena  |
+--------------+
       |
       |        +--------------+
       +--------|  small_bin_  |
                +--------------+
                        |
            +-----------------------+
            |                       |
        +-------+               +-------+
        | 1byte |               | 2byte |
        +-------+               +-------+
            |                       |          
      +-----------+           +-----------+    
      |           |           |           |    
  +-------+   +-------+   +-------+   +-------+
  | 3byte |   | 4byte |   | 5byte |   | 6byte |
  +-------+   +-------+   +-------+   +-------+

こんな感じでsmall_bin_には各サイズ毎のchunkのリストが格納される。
これでjust-fitなアロケータになる。
ただし面倒だったのは、このsmall_bin_をstd::mapとかで実装するのはちょっと難しく、
結局自分でRBTreeを実装する羽目になった。
何故かと言うと、今はメモリアロケーションの途中なのでヒープから値を確保することは不可能で、
各Chunkに追加のメモリ割り当てで格納できるコンテナが必要だったからである。
自分で実装したRBTreeはIntrusiveRBTreeというクラスで、ある条件を満たせばヒープからメモリのアロケーションをしなくてもツリーを構成することができる。 その条件とは格納される値自身がコンテナの実装をしていること。 つまり格納される値がRbTreeNodeというRedBlackTreeのNodeを継承していれば、
RBTree自身はそれをつなぎ合わせるアルゴリズムの実装のみでよく、
メモリ割り当てを必要としないで済む。

ただし、small_bin_は名の通り小さなメモリのみを格納する場所なので、64KBを制限とし、
それを超える場合には直接CentralArenaから巨大なメモリ領域をmmapで割り当てる。
そのときにはSpinLockでロックをかけるので当然遅くなる。

これでjust-fitなbinが実装できた。
さてsmall_bin_に格納されるメモリを実装していく。
small_bin_に格納されるメモリはChunkHeaderという管理ヘッダと実際に値を割り当てる後続のブロックで構成される。

ChunkHeader

ChunkHeaderクラスは実際に値を割り当てるヒープを管理している。
ChunkHeaderの後ろには各Heapを管理するHeapHeaderがつながっている。 確保されているヒープはheap_list_に格納され、
空いているヒープはfree_list_に格納される。

+---------------+
|  ChunkHeader  |
+---------------+
     |     |
     |     +--------------+  +------------+  +------------+  +------------+
     |     |  heap_list_  |--| HeapHeader |--| HeapHeader |--| HeapHeader |
     |     +--------------+  +------------+  +------------+  +------------+
     | 
     |
     +--------------+  +------------+  +------------+  +------------+
     |  free_list_  |--| HeapHeader |--| HeapHeader |--| HeapHeader |
     +--------------+  +------------+  +------------+  +------------+

そしてChunkHeaderはメモリ確保の要求が来ると、最初にfree_list_を探索する。
もしfree_list_にHeapHeaderがあればそれをheap_list_にAtomicに繋いで返す。
空きがなければ、最後につながれたHeapHeaderの使用量を確認して、まだ格納できるのであればHeapHeaderに値を追加する。
もしHeapHeaderに空きが無ければ、新たにHeapHeaderを割り当てる。

YATSC_INLINE void* ChunkHeader::Distribute() {
  auto free_list_head = free_list_.load(std::memory_order_acquire);
  if (free_list_head == nullptr) {
    // If heap is not filled.
    if (heap_list_->used() < max_allocatable_size_) {
      // Calc next positon.
      Byte* block = reinterpret_cast<Byte*>(heap_list_) + heap_list_->used();
      // Update heap used value.
      heap_list_->set_used(heap_list_->used() + size_class_);
      return block;
    } else {
      // If heap is exhausted allocate new memory.
      Byte* block = InitHeap(AllocateBlock());
      auto heap_header = reinterpret_cast<HeapHeader*>(
          block - sizeof(HeapHeader));
      heap_header->set_used(heap_header->used() + size_class_);
      return block;
    }
  }

  // Swap free list.
  while (!free_list_.compare_exchange_weak(
             free_list_head, free_list_head->next())) {}
  return reinterpret_cast<void*>(free_list_head);
}

HeapHeader

HeapHeaderクラスは実際にヒープの値を直接管理しているクラス。
このHeapHeaderChunkHeaderから割り当てられる、1 MBでアライメントされたメモリブロックとなっている。
なぜ1 MBでアライメントするかというと、メモリの節約のためにHeapHeaderが確保しているメモリブロックは管理ヘッダを持っていない。
本来はメモリを削除する場合にHeapHeaderを参照するための情報が必要なのだが、1 MBでアライメントすることによって、
メモリの下位16ビットをマスクするだけで、HeapHeaderの先頭アドレスを取得することができるようになる。
こうすることで1割当ごとにX64なら最低でも8 byte、x86なら4 byteの節約になる。

CentralArenaにメモリのfree要求が来た場合には以下のようにポインタのアドレス下位16ビットをマスクして、
HeapHeaderを取得する。

ChunkHeader::kAddrMask~0xFFFF

auto h = reinterpret_cast<HeapHeader*>(
    reinterpret_cast<uintptr_t>(ptr) & ChunkHeader::kAddrMask);
ASSERT(true, h != nullptr);
ChunkHeader* chunk_header = h->chunk_header();
ASSERT(true, chunk_header != nullptr);
chunk_header->Dealloc(ptr);

構造は以下の図の通り

+----------------------------+  +----------------------+  +-----------------------+
|  HeapHeader(0x103e600000)  |--|  value(0x103e600008) |--|  value(0x103e000010)  | 
+----------------------------+  +----------------------+  +-----------------------+

削除する場合

+----------------------+                                +----------------------------+
|  value(0x103e600008) | => 0x103e600008             => |  HeapHeader(0x103e600000)  |
+----------------------+           ^^^^^ Mask heare.    +----------------------------+

概ねこんな感じでjust-fitでロックを必要としないメモリアロケータを実装することができた。
パフォーマンスだが、シングルスレッドではmallocの約5倍ほど高速化することができたが、
マルチスレッドでは1.2 ~ 1.1倍ほどしか高速化せずちょっとなぁ〜という感じ。
また64 KB以上のビックな割当を行うとほとんどmallocと変わらないのでここは改善の余地がありそう。

という感じでmallocを再実装した話でした。

全体像は以下のようになる。

+----------------+  +---------------+
|  CentralArena  |--|  Main Thread  |
+----------------+  +---------------+
      |     |  +------------+  +--------------+
      |     +--|  Thread-1  |--|  LocalArena  |
      |     |  +------------+  +--------------+
      |     |                         |                                                  
      |     |                         |        +--------------+                
      |     |                         ---------|  small_bin_  |                
      |     |                                  +--------------+                
      |     |                                          |                       
      |     |                              ------------+------------           
      |     |                              |                       |           
      |     |                          +-------+               +-------+       
      |     |                          | 1byte |               | 2byte |       
      |     |                          +-------+               +-------+       
      |     |                              |                       |           
      |     |                        ------+------           ------+------     
      |     |                        |           |           |           |     
      |     |                    +-------+   +-------+   +-------+   +-------+
      |     |                    | 3byte |   | 4byte |   | 5byte |   | 6byte |
      |     |                    +-------+   +-------+   +-------+   +-------+
      |     |                    +---------------+                                                             
      |     |                    |  ChunkHeader  |                                                             
      |     |                    +---------------+                                                             
      |     |                         |     |                                                                  
      |     |                         |     +--------------+  +------------+  +------------+  +------------+   
      |     |                         |     |  heap_list_  |--| HeapHeader |--| HeapHeader |--| HeapHeader |   
      |     |                         |     +--------------+  +------------+  +------------+  +------------+   
      |     |                         |                           +----------------------------+  +----------------------+  +-----------------------+  
      |     |                         |                           |  HeapHeader(0x103e600000)  |--|  value(0x103e600008) |--|  value(0x103e000010)  |  
      |     |                         |                           +----------------------------+  +----------------------+  +-----------------------+  
      |     |                         |                                                                        
      |     |                         +--------------+  +------------+  +------------+  +------------+         
      |     |                         |  free_list_  |--| HeapHeader |--| HeapHeader |--| HeapHeader |         
      |     |                         +--------------+  +------------+  +------------+  +------------+         
      |     |  +------------+  +--------------+
      |     +--|  Thread-2  |--|  LocalArena  |
      |     |  +------------+  +--------------+
      |     |  +------------+  +--------------+
      |     +--|  Thread-3  |--|  LocalArena  |
      |        +------------+  +--------------+
      +------------+  +--------------+  +--------------+ 
      |  free list |--|  LocalArena  |--|  LocalArena  |-- ...
      +------------+  +--------------+  +--------------+ 

まとめ

結果得られたのはロックフリーな実装の難しさとメモリ割当関連のバグは発見が不可能に近いという教訓であった。
意味不明なSegvがきつい。

参考

https://www.slideshare.net/kosaki55tea/glibc-malloc

ソースは https://github.com/brn/yatsc/tree/master/src/memory どす〜

TypeScript 2.6 変更点と注意点

TypeScript2.6が出たので変更点を記載
RCからほぼ変更点がない。

Strict Function Typeフラグの導入

--strictFunctionTypesというフラグが導入される。
このフラグは--strictフラグに内包されており、--strictの場合は自動でONになるが、
--strictFunctionTypesfalseにすることで個別にOFFにすることもできる。

動作

関数の引数に対するVarianceの動作を変更する。
TypeScriptの関数のVarianceについては以前下のスライドで説明したので参照。

speakerdeck.com

今回の--strictFunctionTypesフラグがONになると、関数がBivariantではなくてContravariantになる。
つまり以下のような代入が許可されなくなる。

class Animal {
  ...
}

class Dog extends Animal {
  ...
}

declare let acceptAnimal: (animal: Animal) => void;
declare let acceptDog: (dog: Dog) => void;

acceptAnimal = acceptDog // Error
acceptDog = acceptAnimal // OK!

またこの変換が不可能になると、以下のように総称型の代入規則が変化する。

class Animal {
  ...
}

class Dog extends Animal {
  ...
}


class GenericAnimal<T> {
  someFn(animal: T) {}
}


delcare let animalAnimal: GenericAnimal<Animal>;
delcare let animalDog: GenericAnimal<Dog>;

animalAnimal = animalDog // Error.
animalDog = animalAnimal // OK.
// someFn(animal: Dog)
// someFn(animal: Animal)

これは関数がBivariantではなくなるために起こる。
Animalを受け取る関数にDogを受け取る関数を渡すと、DogAnimalの派生型である限り、
Dog固有の処理を行っていた場合にRuntimeエラーを起こす可能性が高く危険な代入になるが、
Animalを受け取る関数にDogを渡すのは安全であるため、このような変換規則になる。
そのため外から見るとDog <= Animalの変換をしているように見えるが、関数の引数という文脈で考えると、
この変換はAnimal <= Dogの変換となり自然に見える。

注意

--strictモードでこの機能が有効になるため、--strictオプションをONにしているコードに対しては、
この変換規則が強制的に適用される。
なので今まで逆の変換を行っているコードはすべてコンパイルエラーになる。
それが困る場合は--strictFunctionTypes falseでOFFにすることができる。

タグ付きテンプレートリテラルをキャッシュするようになった

タイトル通り。 一度作ったテンプレートリテラルオブジェクトをキャッシュするようになったので、以下のコードがtrueになるようになった。

export function id(x: TemplateStringsArray) {
  return x;
}

export function templateObjectFactory() {
  return id`hello world`;
}

let result = templateObjectFactory() === templateObjectFactory(); // true in TS 2.6

Emit Result in v2.5

"use strict";
exports.__esModule = true;
function id(x) {
    return x;
}
exports.id = id;
function templateObjectFactory() {
    return (_a = ["hello world"], _a.raw = ["hello world"], id(_a));
    var _a;
}
exports.templateObjectFactory = templateObjectFactory;
var result = templateObjectFactory() === templateObjectFactory(); // true in TS 2.6

Emit Result in v2.6

"use strict";
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
    if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
    return cooked;
};

function id(x) {
    return x;
}

var _a;
function templateObjectFactory() {
    return id(_a || (_a = __makeTemplateObject(["hello world"], ["hello world"])));
}

var result = templateObjectFactory() === templateObjectFactory();

ここに注目

// v2.5
function templateObjectFactory() {
    return (_a = ["hello world"], _a.raw = ["hello world"], id(_a));
    var _a;
}

// v2.6
var _a;
function templateObjectFactory() {
    return id(_a || (_a = __makeTemplateObject(["hello world"], ["hello world"])));
}

変数_aに生成済みテンプレートオブジェクトをバインドして再利用しているのがわかる。

コンパイルエラーメッセージ・コマンドラインヘルプが多言語化

localeに合わせて各種言語ファイルが使用されるようになった。

// @ts-ignoreコメントでjsファイルのエラーを抑制できるようになった。

--allowJSオプションを使ってJSファイルをロードした際に出るコンパイルエラーを、

if (false) {
  // @ts-ignore: Unreachable code error
  console.log("hello");
}

のような形で指定することで抑制できる。

--watchモードが高速化

--watch機能をすべて書き直して高速化した。 プロジェクト全体をコンパイルするのではなく、変化したファイルのみを処理することで処理速度が向上した。

WriteOnlyな値をunusedとして処理するように

TypeScript2.6では、--noUnusedLocals--noUnusedParametersオプションを刷新した。
値が宣言されているか書き込まれていても、その値が読み出されない限りunusedとしてマークされる。

function test(unused1: number) {
  unused1 = 0;
}

class Test {
  private unused2: number;
  constructor() {
    this.unused2 = 0;
  }
}

上記のサンプルコードではunused1unused2はともに書き込みはあるものの、
読み出しがないのでunusedとしてエラーになるので注意。

追記

LanguageService系の更新書くの忘れてた

implicit anyのQuick fixを追加

implicit anyが見つかった場合にnoImplicitAnyがtrueならQuickFixで推測した型を提案する。

JSDocをTypeScriptの型アノテーションに変換する

JSDocに書いた型注釈をTypeScriptの型に変換できるようになった。

呼び出しが必要なDecoratorにWarningを出すようになった

高階関数型の呼び出しが必要なDecoratorを呼び出し無しで使った場合に、
呼び出しを提案する。

@typesから自動でinstall

型定義がない場合に@typesから自動でインストールを提案する。

Ambient宣言内のdefault exportで式が使えなくなった

タイトルの通り

Intersection Typeの結果を変更

number & string, "foo" & 42,

のような不可能な型はnever型を返すように

lib.d.ts変更

Deprecation

getSymbolDisplayBuilderAPIが廃止になった。2.7で削除予定
基本的には代わりにTypeChecker#symbolToStringを使う。
もっと入り組んだユースケースの場合にはバージョン2.7でsymbolToStringを導入予定なのでそれを使う。

まとめ

今回はちょっと破壊的変更がいくつかあるので注意。
特に--strictをONにしている人は総称型のアップキャストをしていないかをチェックした方がいい。

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はもう触りたくないんじゃ!

おしまい。

HTML5 Conference 2017に登壇します

ひょんなことから

HTML5 Conference

に登壇する機会をいただけることに。

タイトルはDeep dive into TypeScriptということで、TypeScriptの紹介や、RoadMap・Issuesの話とかをさせて頂くつもり

同じ時間帯に任天堂の方のセッションとかあってアレなんですが、TypeScriptに興味のある方はぜひ。

Typescript 2.5 リリース

Typescript 2.5がでたので変更点をメモ

Optional-catch-bindingの導入

catch節のエラーオブジェクトが省略可能になった。
現在Stage3のOptional catch bindingが導入された形に。

サンプル

let input = "...";
try {
    JSON.parse(input);
}
catch {
    // ^ catch節の変数宣言を省略している。
    console.log("Invalid JSON given\n\n" + input)
}

jsにJSDocベースの型アサーションとキャストを導入

javascriptファイルもJSDocコメントで型チェック可能になった。
コメントで@ts-checkをつけるか、
コンパイラオプションでcheckJsオプションをtrueにすれば動作する。

型チェックの詳細は以下のWikiに書いてある。

Type Checking JavaScript Files · Microsoft/TypeScript Wiki · GitHub

重複したパッケージのリダイレクト

簡単に言うとnode_modulesのパッケージの同一性チェックをする。

つまり、moduleResolutionがnodeの場合node_modulesからモジュールをインポートするが、
その際にそのモジュールのpackage.jsonをチェックして、すでにインポートした事のあるモジュールかどうか確認する。
もし同一のモジュールであれば、すでにimport済みのモジュールにリダイレクトして同じモジュールをロードしない。
ちなみにpackage.jsonのversionとnameをチェックして同一性判定をする。

node_modules -
             |
             [A-package] -
             |          |
             |          node_modules -
             |                       |
             |                       [C-package]
             [B-package] -
                         |
                         node_modules -
                                      |
                                      [C-package]

この例だと、A-packageとB-packageが同じC-pacakgeを必要としているが、typescriptコンパイラは一度しかC-packageをインポートしない。
B-packageの必要とするC-pacakgeはA-packageのC-packageを参照する。

–preserveSymlinksコンパイラフラグの導入

–preserveSymlinksオプションが導入される。
このオプションはシンボリックリンクされたモジュールが他のモジュールをimportする場合に、
シンボリックリンク元のパスを起点とした相対パスではなく、
シンボリックリンクが置かれている場所からの相対パスとなる。

A -
  |
  B -
  |  |
  |  C'(Symblink of C)
  C

この例だと、C'モジュールのパス解決は–preserveSymlinksをセットしない場合は、
A/Cから始まることになる。
しかし–preserveSymlinksをセットすると、
A/B/C'からパス解決が行われる。

まとめ

意外と新機能は少なかったですね。

typescript 2.4 の新機能

typescript2.4がでたので新機能を確認。

Dynamic Import Expressionsのサポート

import('...')式がサポートされた。

import式を使うことで多くのバンドラーがコード分割をすることが可能になるので、
module: esnextで出力するのがおすすめだそう。

async function getZipFile(name: string, files: File[]): Promise<File> {
  const zipUtil = await import('./utils/create-zip-file');
  const zipContents = await zipUtil.getContentAsBlob(files);
  return new File(zipContents, name);
}

String Enumsのサポート

待望?の文字列enumがサポートされた。

enum Colors {
  Red = "RED",
  Green = "GREEN",
  Blue = "BLUE",
}

ただし制限として、数値enumの際に可能だった、メンバプロパティからプロパティ名の取得はできない。

Colors[Colors.Red] // これはできない。

interfaceのgeneric型のサポートを強化

戻り値の推論能力の強化

戻り値から型パラメータを導出できるようになった。

function arrayMap<T, U>(f: (x: T) => U): (a: T[]) => U[] {
  return a => a.map(f);
}

const lengths: (a: string[]) => number[] = arrayMap(s => s.length);

Promiseもこのようにエラーとすることができるように。

let x: Promise<string> = new Promise(resolve => {
  resolve(10);
  //      ~~ Error!
});

文脈からの型パラメータの導出

いかのような定義があった場合

let f: <T>(x: T) => T = y => y;

yanyになってしまっていた。
そのため、

let f: <T>(x: T) => T = y => y() + y.foo.bar;

のような式は型チェックをスルーしてしまっていたが、
2.4からyが正しく導出され、エラーとなるようになった。

generic関数の方チェックを厳格化

以下のような関数があった場合、

type A = <T, U>(x: T, y: U) => [T, U];
type B = <S>(x: S, y: S) => [S, S];

function f(a: A, b: B) {
  a = b;  // Error
  b = a;  // Ok
}

AがBと互換性があるかをチェックできるようになり、互換性があれば、代入も可能になった。

コールバック関数の方の反変性チェックを厳格化

2.4以前のバージョンでは以下のプログラムではエラーが起きなかった。

interface Mappable<T> {
  map<U>(f: (x: T) => U): Mappable<U>;
}

declare let a: Mappable<number>;
declare let b: Mappable<string | number>;

a = b;
b = a;

なぜなら、abがmap関数のパラメータfを通して変換可能であると判断されていたため。
しかし2.4以降は実際のabの型を比較を行うため、このプログラムはコンパイルエラーとなる。
この変更は破壊的変更となるので注意

Weak Typesの導入

以下のように、すべてがoptionalな型をWeakTypeとして区別することになった。

interface Options {
  data?: string,
  timeout?: number,
  maxRetries?: number,
}

2.4からはこのWeakTypeに対しても、存在しないプロパティを持つ型を代入するとエラーになる。

function sendMessage(options: Options) {
  // ...
}

const opts = {
  payload: "hello world!",
  retryOnFail: true,
}

// Error!
sendMessage(opts);
// optsとOptions型で一致するプロパティがないためエラー

この変更は破壊的変更となるので注意

現在以下のWorkaroundが提案されている。

  • 実際にプロパティが存在する場合のみ宣言する。
  • WeakTypeには{[propName: string]: {}}のようなインデックス型を定義する。
  • opts as Optionsのように型アサーションを使って変換する。

まとめ

型チェックの強化がメインの変更点になった。
破壊的変更が幾つかありますが、WeakTypeのとこはちょっと注意した方が良さそう。
あとはimport式をどう使うか。

babelのAsyncIterationバグ

問題

for-await-ofのボディで配列への分割代入を行うと、

Cannot read property 'file' of undefined

というエラーを投げてトランスパイルに失敗する。
どうやらscopeの解析に失敗しているらしい。

サンプルコード

async function g(t) {
  return new Promise(r => setTimeout(() => r([true]), t));
}

async function r() {
  for await (const t of [1000, 2000, 3000]) {
    const [result] = await g(t);
  }
}

r();

package.json

"dependencies": {
  "babel-cli": "^6.24.1",
  "babel-plugin-transform-async-generator-functions": "^6.24.1",
  "babel-plugin-transform-regenerator": "^6.24.1",
  "babel-plugin-transform-runtime": "^6.23.0",
  "babel-polyfill": "^6.23.0",
  "babel-preset-es2015": "^6.22.0",
  "babel-preset-stage-3": "^6.24.1",
  "babel-runtime": "^6.23.0"
},
"babel": {
  "plugins": [
    [
      "transform-runtime",
      {
        "helpers": false,
        "polyfill": false,
        "regenerator": true,
        "moduleName": "babel-runtime"
      }
    ]
  ],
  "presets": [
    "es2015",
    "stage-3"
  ]
}

解決

とりあえずbugとしてbabel側にはissueを立てておいた。

github.com

現状で取れる選択肢

配列ではなくオブジェクト形式で受け取る

データ構造の変更が必要だがまあ許容できるか?

async function g(t) {
  return new Promise(r => setTimeout(() => r({result: true}), t));
}

async function r() {
  for await (const t of [1000, 2000, 3000]) {
    const {result} = await g(t);
  }
}

r();

分割代入で受け取らない

その後にインデックスアクセスしなければならないのでちょっと面倒

async function g(t) {
  return new Promise(r => setTimeout(() => r([true]), t));
}

async function r() {
  for await (const t of [1000, 2000, 3000]) {
    const result = await g(t);
  }
}

r();

まとめ

はやく直ってくれると嬉しい