知識創造企業とアジャイル開発とスクラムと自分

気づかされる日々

最近は、海外出身の人たちと近い距離で仕事をしてて、文化の違いを感じて面白い。そういうところを気にするのか、そこは気にしないのか、とか学びながら、というか、日本人はあの部分は気にしないけど、そこは気にするってことか、みたいに、海外出身の人たちの文化を感じると同時に、日本の文化、それが当たり前すぎて気づくことが難しい部分、に気づかされるような日々を過ごしてた。

そんな中で、自分の中にこういう気持ちがあることにも目を向けさせられた。

スクラムとは違う何か

この10年間、スクラムを勉強して、そこから得た学びを元に実践の中に取り入れたりしつつ開発をしてきたのだけど、自分の中では、そろそろいいんじゃないかという気がしているんだなって気づかされた。これまで、スクラムの考え方を基準にして、組織の形や自分の役割やチームのあり方を疑って、改善してこれたと思う。そして、もう十分「良い活動をできる組織」になってきたと思う。(「良い組織」になったということよりも、そのベクトルのことを「良いな」と思っている)

なので、次の一歩として、こういうことを考えてみたいなと思った。

組織にあった開発スタイル

正直に言うと、この10年間、スクラムをちゃんとやったことはない。というか、できたことはない。そこで悩んでいる部分もあったけど、常に考えていたのは「今のみんなの頑張りをつなげたい」ということで、そのチームや組織の状況や現在のステージに合わせて「スクラムを学ぶ中で得たもの」を活用して、改善をしてきた。

なので、スクラムできてないなぁ、って目をそらすんじゃなくて、僕らの組織の形で目指せる開発スタイルってどういうものだろう?ということを考えても良い時期なのかなと思ったのだ。

日本からのスクラム・海外からのスクラム

さて、スクラムを学ぶ中で、自分の中にずっとふわっと置いてあった気持ちがある。それは「スクラムは海外の人が日本のやり方を参考にしたことが発端の開発手法」なんだよなということ。海外の人から見たスクラムと日本の人から見たスクラムは、違うものなのではないかということ。僕らはもっと暗黙知よりで動くことができそう。

最初に触れたように、僕は、海外出身の人たちと仕事をする中で、文化の違いを感じていた。良いとか悪いとかではなく、単に「違う」というだけなのだけど、海外の人の方が「個」が強くて、役割や境界の明文化を求める。(ここで僕が想像している「海外」は欧米だなと書いてて思った。アジア出身の人たちはまた違う)。それに対して日本の人は「場」が強くて、役割や境界はふわっとしていても気にせずに仕事をする。

この違いが面白いなぁと思うのと同時に、ここにヒントがありそうだなと感じ、もう少しスクラムと日本のことを知りたくなって、この本を手にとった。先に言うと、すばらしい本だった。

知識創造企業

books.rakuten.co.jp

この本は、日本企業が意識的・無意識的に開発し実践してきたその経営原理を「組織的知識創造」というコンセプトで捉え「知識創造企業」というコンセプトを提言している。25年前の本で、製造業の新製品開発を中心としたものなので、そのまま現在の自分の業界に当てはめることはできないのだけど、日本と欧米との文化の違いについての分析や考察など、はっとさせられることや考えさせられることがたくさんあった。

読んでいく中で「現在のスクラムアジャイル開発を野中さんが見たらどう感じるんだろう?」と考えずにはいられず、以前に買ったこの本を読み直してみた。

アジャイル開発とスクラム

books.rakuten.co.jp

これまた、良かった。特に野中さんがアジャイルなソフトウェア開発についてお話されているのが「それ、聞きたかったところです!」という気持ち。以前に読んだときには、形式的な部分だけを言葉で捉えていたのが、今回はもっと深いところで共感することができたと思う。この数年間の自分の経験や、知識創造企業を読んだことによる学びからの実感があったのだよなぁ。とても良かった。

そして、僕が感じているようなことは、平鍋さんが既に言葉にしていた。「本書を作るにあたって考えたのは、危機的な状況下で日本の経営のあり方が問われる中、日本ならではのイノベーションを今一度発信していけないかということでした。」僕は正直、イノベーションまでは考えてないけど、日本の文化ならではの開発手法・組織のあり方を見つけられたらいいなぁと思っている。

新しい一歩

日本の伝統的な部分には、あまり好きではない部分もあるので、その辺りを見直しつつ、欧米の考え方の良い部分から学びつつ、日本の文化の良い部分を土台にして、何か自分にとって新しい一歩を踏み出せたらいいなと思った。折角、日本の文化の上で、色んな国の出身の人たちと仕事をしているのだから、その強みをみんなと探していけたらいいなと思っている。

まぁ、まわりまわって「スクラムに戻ってきた」ってなったりするかもなぁとか、何もしないうちに年を重ねてしまうかもなぁとか思いつつ、とりあえずこの2冊の本を読んで、今感じていることを書いてみた。

読み終わった。JavaScript: The Definitive Guide, 7th Edition

とても良かった。2ヶ月くらいかけて読んだのかな。

www.oreilly.com

最新の情報だけじゃなくて、歴史的経緯から順を追って説明してくれてるので、ちょっと前のスタイルのコードも読めそうだし。どういう背景で新しい仕様が導入されたのかとかも納得だし。とても良かった。

↓ let, const, var の違いや、Destructuring Assignment が面白かったなー。

↓ ES2020 の Conditional Property Access や First Defined で、ほほーってなったし、 === は真面目に読んだ。

↓ for/of やオブジェクトリテラル。ES2018 の Spread Operator は普通にたくさん出てきそう。

↓ Sparse Array って普段は使わなさそうだなーとか、map() や includes() は使いそうだなーとか。Array-like オブジェクトかーとか。

↓ Function と this と Closure はなんとなくでしか捉えてなかったので学べて良かった。昔のやり方から始まる class の説明は分かりやすくて良かった。

↓ Module の歴史。Closure Base から Node を経て、ES6 の Module への流れも分かりやすかった。ES2020 の Dynamic Imports は Node の require() を置き換えるために必要そうだなとか。

↓ Set や Map と。Iterable と Generator Function。

↓ からの Promise と aync/await。面白い。

メタプログラミングと Symbol。

この後に Web Browser の章と Node.js の章をさらっと読んで、最後にツールの紹介があった。

### JavaScript に入門できたかな

これで、一旦 JavaScript に入門できたかな。

なんとなくコードは読めそうだし、それが古いのか新しいのかも分かりそう。それに、分からないコードがでてきても、調べることはできそう。

Node.js のコードは CommonJS と ES の間で難しそうだなぁと思いつつ。

### 良い流れだった

JSPrimer と Promise の本で基本をなぞったあとに、JavaScript: The Definitive Guide, 7th Edition を読んで知識を深める、という流れだったのだけど、これはとても良かった。もういちど JSPrimer と Promise の本 を読み直して、一旦 JS 入門は終わりにしようと思う。

jsprimer.net

azu.github.io

### 次は

Vue.js でも触ってみようかなぁ?

JavaScriptの勉強中:その9 Metaprogramming

週末だー!ということで本の続きを読む。

bufferings.hatenablog.com

## Ch. 14 Metaprogramming

今日はメタプログラミングメタプログラミングは普段使うことはあんまりないから書かなくてもいいかなぁと思いつつ、普段使わないから書かないと全く記憶にも残らないだろうなとも思ったのでメモくらい残しておく。

### Property Attributes

データプロパティとアクセサプロパティはそれぞれ4つの属性を持つ:

  • データプロパティ: value, writable, enumerable, configurable
  • アクセサプロパティ: get, set, enumerable, configurable

ちなみに

  • value: 値
  • get: getter 関数
  • set: setter 関数
  • writable: 値を変更可能かどうか
  • enumerable: for/in ループや Object.keys() で列挙可能かどうか
  • configurable: プロパティを削除したり、プロパティの属性を変更したりできるかどうか

こんな感じ:

> const sample = {
...     x: 1,
...     get octet() { return Math.floor(Math.random()*256); },
... };
undefined

> Object.getOwnPropertyDescriptor(sample, "x");
{ value: 1, writable: true, enumerable: true, configurable: true }

> Object.getOwnPropertyDescriptor(sample, "octet");
{
  get: [Function: get octet],
  set: undefined,
  enumerable: true,
  configurable: true
}
  • Object.getOwnPropertyDescriptor() は own property しか取得できないので、継承しているプロパティの属性を取得したい場合はプロトタイプチェーンから対象のオブジェクトを取得して使う。
  • 普通にプロパティを定義すると、書き込み可能、列挙可能、属性変更可能になるけど、Object.defineProperty()Object.defineProperties() を使うと属性を指定してプロパティを作成・変更できる。継承してるプロパティは変更できないので注意。
  • プロパティの追加やプロパティ属性の変更をできるかは、色々な条件の組み合わせになっててめんどくさそう。今は覚えずに、そういうことをするときがあったら確認しよっと。
  • Object.assign() は列挙可能なプロパティの値のみをコピーする。属性はコピーしない。つまり例えばアクセサプロパティの場合は、getter をコピーするんじゃなくて、getter が返してきた値をコピーするので注意

さっきのやつをそのまま使ってみるとデータプロパティになった。面白い:

> Object.assign({}, sample);
{ x: 1, octet: 107 }

### Object Extensibility

オブジェクトが拡張可能かどうかは extensible 属性で決まる。

  • 通常は拡張可能
  • Object.isExtensible() で確認できる
  • Object.preventExtensions() で拡張不可に変更できる
  • 一度拡張不可にするともう拡張可能には戻せない

オブジェクトの変更をできないようにするために、プロパティの configurable writable 属性と組み合わせて使うことが多い

  • Object.seal() はオブジェクトを拡張不可にすることに加えて、全ての own properties を nonconfigurable にする。
    • プロパティの追加や削除、属性の変更ができない
    • writable なプロパティの値の変更は可能
    • sealed オブジェクトを unsealed に戻すことはできない
    • チェックには Object.isSealed() を使う
  • Object.freeze() はもっと厳しい。オブジェクトを拡張不可にする・全ての own properties を nonconfigurable にすることに加えて、データプロパティをリードオンリーにする(setter を持ってるアクセサプロパティは影響を受けない)
    • Object.isFrozen() で確認可能

ただ、プロトタイプオブジェクトも含めて全部ロックダウンしたい場合は、チェーンをたどって適用する必要がある。

### Well-Known Symbols

Symbol.iterator と Symbol.asyncIterator

  • これはもう知ってるやつだ

Symbol.hasInstance

  • ES6 以降ではもし instanceof の右側のオブジェクトに [Symbol.hasInstance] メソッドが定義されてたら、そのメソッドが呼ばれる
> 10 instanceof {[Symbol.hasInstance](v){console.log("called");return v > 5;}}
called
true
> 10 instanceof {[Symbol.hasInstance](v){console.log("called");return v > 10;}}
called
false

へー

Symbol.toStringTag

  • toString() で表示される文字列表現の部分を指定することができる。"[object Range]" みたいに。指定してなかったら "[object Object]" になる。
> Object.prototype.toString.call({})
'[object Object]'
> Object.prototype.toString.call({[Symbol.toStringTag]:"Sample"})
'[object Sample]'
> Object.prototype.toString.call({get [Symbol.toStringTag](){return "Sample2";}})
'[object Sample2]'

ふーむ。まぁ、使わなさそう。

Symbol.species

これはまたなんか面白いなー。そうそう使うことなさそうだけど。

  • 例えば Array() コンストラクター関数は、このプロパティを読み取り専用で持っている
  • Arraymap() 関数などは、このプロパティからコンストラクターを取得している
  • それによって継承先のクラスを使って結果を返すことができる

へー。MyArray に対して map() を呼んだら MyArray で返されてる:

> class MyArray extends Array {}
undefined
> new MyArray(1,2,3).map(v => v * 2)
MyArray(3) [ 2, 4, 6 ]

コンストラクター関数にこのプロパティが定義されてるや:

> Object.getOwnPropertyDescriptor(Array, Symbol.species)
{
  get: [Function: get [Symbol.species]],
  set: undefined,
  enumerable: false,
  configurable: true
}

> Array[Symbol.species]
[Function: Array]
> MyArray[Symbol.species]
[Function: MyArray]

this.constructor[Symbol.species]コンストラクター関数を取ってるってことか。・・・ん? this.constructor にもコンストラクター関数が入ってると思うんだけど・・・、あぁ、用途が違うから分けてるってことかな。

書き換えるときは、直接値を設定することはできなくて、 getter だけだから。でも configurable だから、こうすれば書き換えられる:

> Object.defineProperty(MyArray, Symbol.species, {value: Array});
[Function: MyArray]
> MyArray[Symbol.species]
[Function: Array]

Array になった。じゃもういちど map() 呼び出してみよう:

> new MyArray(1,2,3).map(v => v * 2)
[ 2, 4, 6 ]

へー。今回は MyArray じゃなくて Array になった。

ふーん。この場合、 this.constructor には MyArray が入ったままだけど、this.constructor[Symbol.species] には Array が入ってるってことか。

> myArray.constructor
[Function: MyArray]
> myArray.constructor[Symbol.species]
[Function: Array]

他にも MapSet はこのプロパティを持ってるみたいね:

> Object.getOwnPropertyDescriptor(Map, Symbol.species)
{
  get: [Function: get [Symbol.species]],
  set: undefined,
  enumerable: false,
  configurable: true
}
> Object.getOwnPropertyDescriptor(Set, Symbol.species)
{
  get: [Function: get [Symbol.species]],
  set: undefined,
  enumerable: false,
  configurable: true
}

へー。そういうののサブクラスを作るときに覚えておいたら良いのかな。

Symbol.isConcatSpreadable

  • Arrayconcat をするときに spread するかどうかを判別するためのプロパティ。
  • ES6 より前は Array.isArray() で判別してたけど、ES6 以降は、まずこのプロパティをチェックして、なければ Array.isArray() で判別する。
# 普通に配列を渡すと展開される
> [].concat([1,2,3])
[ 1, 2, 3 ]

# ArrayLike オブジェクトを普通に渡しても、そのまま追加される
> let arrayLike = {length:2, 0:"a", 1:"b"}
undefined
> [].concat(arrayLike)
[ { '0': 'a', '1': 'b', length: 2 } ]


# Symbol.isConcatSpreadable プロパティをつけてあげると展開される
> arrayLike = {length:2, 0:"a", 1:"b", [Symbol.isConcatSpreadable]: true}
{
  '0': 'a',
  '1': 'b',
  length: 2,
  [Symbol(Symbol.isConcatSpreadable)]: true
}
> [].concat(arrayLike)
[ 'a', 'b' ]

面白いなー

Pattern-Matching Symbols

  • string.method(pattern, arg) は実際にはこう呼び出されてる→ pattern[symbol](string, arg)

RegExp のプロトタイプ見てみたらこうなってた:

> Object.getOwnPropertySymbols(RegExp.prototype);
[
  Symbol(Symbol.match),
  Symbol(Symbol.matchAll),
  Symbol(Symbol.replace),
  Symbol(Symbol.search),
  Symbol(Symbol.split)
]

へー。

例えば string.search(pattern, arg) の pattern のところに RegExp じゃなくて独自で Symbol.search メソッドを実装したクラスを渡せばそれが呼び出されるみたい。やらんけど。

Symbol.toPrimitive

  • オブジェクトをプリミティブに変換するのには toString()valueOf() が使われるけど、ES6以降では Symbol.toPrimitive でこの処理を上書きできる:

こういうことなのかな?:

> Number(1)
1
> Number({})
NaN
> Number({[Symbol.toPrimitive](){return 10;}})
10
> String({[Symbol.toPrimitive](){return 10;}})
'10'
> String({[Symbol.toPrimitive](){return "aaa";}})
'aaa'

### Template Tags

  • backtick で囲まれた文字列はテンプレートリテラルと呼ばれてるけど、関数の直後にテンプレートリテラルが続くものを “tagged template literal” と呼ぶ。
  • DSL を定義するのによく使われる。GraphQL用の gql や、Emotion の css など。

tagged template literal の関数定義には特別な文法はなくて、通常の関数定義と同じ。引数が以下の通りになっている:

  • 第一引数に文字列の配列
    • 変数の部分で区切られている。n個変数があったら、n+1個の文字列になっている。
  • 第二引数以降に変数の値

これは覚えておきたいなー。

### The Reflect API

  • これまでリフレクション系の処理は Object の static メソッドとかでやってきたけど、分かりやすいように Reflect というオブジェクトにもまとめ直したんかな。

### Proxy Objects

  • ES6 以降
  • ターゲットオブジェクトに対する処理をプロキシしてハンドラーに渡すことができる

まぁ、これもフレームワークを作るんじゃなくて、使う場合には、ほしい場面に出会うことはないだろうな。

### 今日の中で

普通に使うのは Symbol.iterator と Symbol.asyncIterator と tagged template literal くらいかな。

今日も面白かったー。残りはあと3章だけど、長そうな章たちだな。

VS Code Remote - Containers を Docker Compose で使うのだー!

VS Code の Remote - Containers プラグインを使うと Docker の中で開発ができて最高だよ、という記事を読んで面白そうなので触ってみた。

www.keisuke69.net

ちょっとぐぐったら、このプラグインに関する記事がいっぱい出てきた。結構前から人気なのね。知らなかったや。

## Remote - Containers って何なの?

は、だいたいこんな印象。Remote - Containers プラグインVS Code Server をコンテナの中にインストールして、ローカル側の VS Code がその VS Code Server とコミュニケーションする。

f:id:bufferings:20200608000048p:plain

それによって、実際はコンテナの中にある開発環境が、あたかもローカルにあるみたいな気持ちで VS Codeを使うことができる。だから、ローカル側に例えば PHP を入れてなくても、VS Code では PHPプラグインを動かして、コード補完やデバッグ実行などができる。便利。

  • VS Code の UI 拡張はローカル側で、Workspace 拡張はコンテナ側で動くらしい。雰囲気は分かる。
  • Docker のポートをローカル側にマッピングしておけば、Webアプリの動作確認なども可能。ですよね。

## Dockerfile or docker-compose.yml ?

最初に触ってみたときは少し戸惑った。

ローカルのソースコードは自動でコンテナの中にマウントされるっぽいのにされないし、ポートは特に devcontainer.json に書かなくてもマッピングされてそうだし、んー?ってなった。

けど、しばらく触ってみて、あぁ、これって Dockerfile を使ってる場合と docker-compose.yml を使ってる場合で設定の方法が違うっぽいな。と気づいた。僕は docker-compose.yml を使ってた。

もうしばらく触ってみて分かったのは、Dockerfile だけではできない部分を devcontainer.json で設定できたり、 Remote - Containers プラグインが自動的にサポートしたりしてるってこと。ポートのマッピングとか、ボリュームのマウントとか、環境変数の指定とか。ほほー。

docker-compose.yml を使う場合は、そういうサポートをほとんど必要としない。元々 Docker Compose にそういう機能がだいたい備わってるから。

じゃ、どっちを使おうかなと考えたんだけど、 docker-compose.yml を使えば Dockerfile でできることはカバーできるだろうから、 Dockerfile よりも docker-compose.yml を使う方向で進めてみることにした。

## やらないこと

  • メインで使う言語は、ローカルマシンで環境を整えたいなと思うので、このプラグインでは僕は使わないだろうなと思った。なので Java で使うつもりはない。今回は PHP で試してみてる。
  • 実行中のコンテナや k8s のコンテナにアタッチすることもできるみたいだけど、やらない。docker-compose.yml を用意してコンテナをビルドするー。
  • 複数プロジェクトで1つのコンテナ設定を共有することもできるみたいだけど、やらない。プロジェクトごとにコンテナ設定を用意する。
  • 設定は .devcontainer/devcontainer.json.devcontainer.json のどちらかが選べるけど、後者は使わない。ディレクトリを用意する。中に色々置きたいから。

さて、進もう。

## Docker Compose を使う場合の情報

を知りたいなという観点で、公式ドキュメントを読んだ。で、満足した。2020-06-07 時点の情報。

code.visualstudio.com

code.visualstudio.com

こういう観点でメモを残しておこうと思う。ほぼ忘れているであろう来月の自分のため。

  • (1) devcontainer.json の仕様 (for docker-compose.yml)
  • (2) Dockerfile を使った場合と docker-compose.yml を使った場合の違い
  • (3) その他、覚えておきたいことのメモ

## (1) devcontainer.json の仕様 (for docker-compose.yml)

https://code.visualstudio.com/docs/remote/containers#_devcontainerjson-reference

に仕様が書いてある。docker-compose.yml を使う場合は、以下の部分だけを知ってれば良さそう:

"Docker Compose" の部分

dockerComposeFile

  • 文字列または配列。必須。
  • devcontainer.json からの相対パスで docker-compose.yml を指定
  • 元々存在しているファイルなどを書き換えずに拡張したい場合には、複数ファイルを指定することが可能で、その場合は配列で指定。
  • プロジェクトのルートから .env を読み込むっぽい。docker-compose.yml の env_file で別のファイルを指定できるっぽい。

感想

  • たぶん僕は配列での複数ファイル指定は使わないし、基本的には .devcontainer の下に docker-compose.yml を置くと思うので docker-compose.yml と書くだけになりそう。
  • Laravel みたいに .env を使ってる場合は、変な感じにならないといいんだけどな。覚えとこ。

service

  • 文字列。必須。
  • docker-compose.yml の中のどのサービスに VS Code が接続するかを指定

感想

  • この設定自体は問題ないんだけど、ここで指定した以外のサービスは起動するのかな?どうなのかな?と思ったら次に書いてあった。

runServices

  • 配列
  • docker-compose.yml の中のどのサービスを実行するか
  • デフォルトは全てのサービス

感想

  • 基本的に docker-compose.yml には起動したいサービスを書くので、この設定は書かないだろう。デフォルトの動きで問題ない。

workspaceFolder

  • 文字列
  • VS Code がコンテナに接続するときに開くパス(コンテナの中のパス)
  • デフォルトは"/"
  • 普通はマウントしたソースコードのフォルダーを指定する

感想

  • そだね。マウントしたソースコードのフォルダーを指定すると思う

remoteEnv

  • name-value ペアのオブジェクト
  • リモート側の VS CodeVS Code が起動するプロセス(ターミナルなど)で利用できる環境変数を上書き。コンテナ全体の環境変数ではない。

感想

  • 今の所、必要なさそう。

remoteUser

  • 文字列
  • リモート側の VS Code を実行するユーザー
  • デフォルトはコンテナが使うユーザーと同じ

感想

  • Linux を使う場合は気にしておいたほうが良さそうだな。ファイルのオーナーが root にならないように。MacWindows だとその辺りは気にしなくて良さそう。

shutdownAction

  • none または stopComposestopCompose がデフォルト。
  • VS Code (Remote) が閉じられたときに、コンテナを停止するかどうか

感想

  • VS Code (Remote) を閉じたら docker-compose は停止してほしいので、デフォルトのままでいいな。

General の部分

General の部分にはこれ以外にも指定できる項目があるけど、 Docker Compose 使うならこれくらいで大丈夫だと思う:

name

  • 文字列
  • 表示名

感想

  • 適当で大丈夫。

extensions

感想

  • コンテナ側で開いた後にプラグインを右クリックすると devcontainer.json に追加できるので、そこから追加するのが楽
  • 例えば PHP だと PHP Extension Pack を追加したりとかそういうの

settings

  • VS Code の設定を書いておける

感想

  • PHP Extension Pack の設定とかあったら書いておくと便利なのかな

postCreateCommand

  • 文字列または配列
  • コンテナの作成が終わって起動するときに実行したいコマンドを書いておくことができる

感想

  • ソースコードもマウントされた状態で実行されるので composer install とか書くと良さそう
  • 毎回なのかな?1回だけなのかな?と思ってみてみたら1回だけっぽいな。 /root/.vscode-server/data/Machine/.postCreateCommandMarker というファイルがなければ実行してそのファイルを作成するっぽい。

結局

のところ、書くのはこれくらいだな:

{
  "name": "My PHP Application",

  "dockerComposeFile": "docker-compose.yml",
  "service": "php-fpm",
  "workspaceFolder": "/workspace",
  "extensions": [
    "felixfbecker.php-pack"
  ],
  "settings": [],
  "postCreateCommand": "composer install"
}

.devcontainer の中身

.devcontainer ディレクトリの中に docker-compose.yml を入れて、その隣にビルドに必要な Dockerfile などを置いておくのが好きだなと思った。こんな感じ:

❯ tree .devcontainer 
.devcontainer
├── devcontainer.json
├── docker
│   ├── app
│   │   └── Dockerfile
│   └── web
│       └── default.conf
└── docker-compose.yml

## (2) Dockerfile を使った場合と docker-compose.yml を使った場合の違い

以下の設定は docker-compose.yml を使う場合は devcontainer.json じゃなくて docker-compose.yml の設定で書く

  • ポートマッピング
  • 環境変数
  • ボリュームマウント
  • コンテナのユーザー
  • コンテナの起動コマンドや引数

でも、デフォルトの動作が Dockerfile を使う場合と異なり、 docker-compose.yml では明示的に指定しないといけないこともある。

起動コマンド

Dockerfile の場合はコンテナのデフォルトコマンドを上書きして /bin/sh -c "while sleep 1000; do :; done" にしてしまうらしい。コンテナが終了してしまわないように。

でも docker-compose.yml の場合は自分で command にこれを指定しないといけない。

# プロセスが終了してコンテナが終了してしまわないように上書きする
command: /bin/sh -c "while sleep 1000; do :; done"

ただ、今回僕が使ってみたのは php-fpm のコンテナでコマンドが終了しないので、これはやらなくても良さそう。

ボリュームマウントのタイプ

MacOS の場合は Remote - Containers プラグインはデフォルトでボリュームマウントの consistency に cached を使うらしいんだけど、docker-compose.yml を使う場合は明示的に指定しないといけない(んだと思う):

volumes:
  - ..:/workspace:cached

cached はホストOS側の変更がコンテナ側に伝わるのに遅延があるけど、パフォーマンスが良くなる。

Use bind mounts | Docker Documentation

コンテナユーザーの UID/GID

僕は Ubuntu を使ってるんだけど Linux の場合はボリュームマウントを使ったときに、コンテナのユーザーの UID/GID でファイルが書き込まれてしまうから例えば root ユーザーを使うと全部 root で書き込まれてしまう。Mac だとホスト側のユーザーになるんだけどね。Windows は知らないけど Mac と同じなんじゃないかなと予想。

なので Linux でボリュームマウントを使うときは UID/GID を指定しておかないといけない。Remote - Containers で Dockerfile を使ってる場合は自動でホスト側の UID/GID を使ってくれるみたい。いいなぁ。 docker-compose.yml の場合はそういう対応はないので、自分で指定する。僕のユーザーは 1000 だからユーザーさえ指定してれば基本的には大丈夫だけど。

## (3) その他、覚えておきたいことのメモ

Git

https://code.visualstudio.com/docs/remote/containers#_sharing-git-credentials-with-your-container

  • Git はコンテナの中に入れておくと良さそう。VS Code が変更点とかを表示してくれるから。
  • でも push とかの操作はローカル側からやろうかな。SSH キーのことを考えたり、コンテナの中のターミナルを自分に合うようにするの面倒だし。

とはいえ、コンテナの中から Git を使うのに便利なように考えてくれてる。

  • ~/.gitconfig は自動的にマウントされる
  • SSH Agent を使ってると自動的にフォワードされる

なので、特に気にせずに Git を VS Code (Remote) の中で利用できそうではある。

Named Volume

https://code.visualstudio.com/docs/remote/containers-advanced#_improving-container-disk-performance

MacWindows のボリュームマウントは遅いので、 vendor や node_modules のようにファイルがたくさん置かれるものを bind してしまうと読み込みがすごく遅くなってしまう。

かといってボリュームを使わなかったら毎回クリアされてしまって、毎回 composer install でライブラリーを引っ張ってくるのは、ない。

ということで、 named local volume を使う。そうすれば bind はされないけどコンテナを再起動しても情報は残る:

version: '3'
services:
    app:
        build: docker/app
        working_dir: /workspace
        volumes:
            - ..:/workspace:cached
            - app-vendor:/workspace/vendor

(...)

volumes:
    app-vendor:

root 以外でコンテナを実行している場合は postCreateCommand で初回にオーナーを更新しておく:

"postCreateCommand": "sudo chown user-name-goes-here vendor"

だいたいこれくらい知っておけば良さそうかなぁ。

JavaScriptの勉強中:その8 Promise / async await

ハイボール片手に。今日は Promise やるぞー。

bufferings.hatenablog.com

## 13.2 Promises

  • callback だとネストが深くなる→ Promise だとリニアに書ける
  • callback だとエラーハンドリングが難しい→ Promise だとエラー処理の方法が標準化されてる

わかる。

Promiseの用語

  • "fulfillled" - 最初のコールバックが呼び出されるとき
  • "rejected" - 2番目のコールバックが呼び出されるとき
  • "settled" - "fulfilled" か "rejected" になったとき
  • "pending" - まだ "fulfilled" でも "rejected" でもないとき
  • "resolved" - あとで説明する

"resolved" かー、なるほど?(わかってない

Promiseのチェーン

  • then() は新たな Promise オブジェクトを返す(p1 と呼ぶことにする)。この Promise は then() に渡した処理が終わると fulfilled になる。

もう少し詳しく言うと(resolvedの話)

  • then() に渡した処理が Promise 以外の値を返すと、then() の返した Promise (p1) は fulfilled になる
  • then() に渡した処理が Promise を返すと(p2)、then() の返した Promise (p1) は resolved にはなるが fulfilled にはならない
    • p2 が fulfilled になると p2 と同じ値で p1 も fulfilled になる
    • p2 が rejected になると p2 と同じ値で p1 も rejected になる

へー。 resolved と settled が違うんだねー。 Promise が Promise を処理するときに差が出てくる感じだな。

エラーハンドリング

  • .then() の2番目の引数に関数を渡してあげると、処理中に例外が発生したときにその関数が呼び出される
  • けど、実際はそうすることは少ない。 .catch() を使う。 .catch(handleError).then(null, handleError) のショートハンド
  • 例外が発生したら、そこから .catch() があるところまでチェーンを降りていく
  • .catch() の後に処理が続く場合、.catch() が普通に値を返すと、その次の処理にうつる

finally()

  • ES2018
  • コールバック関数は引数を受け取らない
  • .finally() のコールバック関数が返す値は基本的には無視される
  • .finally() で返された Promise は、その前の Promise の結果の resolved または rejected の値を返す
  • ただし .finally() で例外が投げられた場合はその値で rejected になる

へー。1個前の値が渡されるのかー。

Promise.all()

  • を使うと並列に実行することができる
  • Promise の配列を引数として受け取る
  • 戻り値の Promise は、入力値の Promise が1つでも rejected だったら rejected になる
  • 最初に rejected になった Promise が出た時点で、まだ実行中の Promise があっても、すぐに rejected になる
const urls = [ /* URLs */ ];
let promises = urls.map(url => fetch(url).then(r => r.text()));
Promise.all(promises)
    .then(bodies => { /* 処理 */ })
    .catch(e => console.error(e));
  • Promise.all() は実はもうちょっとフレキシブルで、入力値の配列の要素は Promise じゃなくても大丈夫。その場合は fulfilled された Promise のように扱われて、そのまま出力配列にコピーされる

あんまり使わなさそうかなぁとは思うけど、一応片隅に置いとくかな。

Promise.allSettled()

  • ES2020
  • all() のように Promise の配列を受け取る
  • rejected になることはない。全ての Promise の処理が終わるのを待って fulfilled になる
  • 結果はオブジェクトの配列になっていて、各要素に status プロパティがあり "fulfilled" または "rejected" がセットされている
  • "fulfilled" の場合は value プロパティに値が、"rejected" の場合は reason プロパティにエラーまたはリジェクトの値が入っている
> Promise.allSettled([Promise.resolve(1), Promise.reject(2), 3]).then(results => console.log(results));
Promise { <pending> }
> [
  { status: 'fulfilled', value: 1 },
  { status: 'rejected', reason: 2 },
  { status: 'fulfilled', value: 3 }
]

こっちのが all() より扱いやすいね。好きな感じ。

Promise.race()

  • 同時に実行して最初に fulfilled または rejected になったものだけを返す。

Promise を書く

  • 非同期に処理する必要がないものを Promise にして返したい場合は Promise.resolve()Promise.reject() を使えばいい
  • 新たに Promise を生成したい場合は Promise コンストラクターを使う
Promise p = new Promise((resolve, reject) => {
});
  • resolve 関数を使うと resolved または fulfilled になる
  • reject 関数を使うと rejected になる

## 13.3 acync and await

  • ES2017
  • この2つの新しいキーワードのおかげで Promise がすごく簡単に使えるようになって、Promise ベースの非同期のコードが同期のコードみたいに書けるようになった

await

  • await キーワードは Promise を受け取って、それを通常の値または例外に変換する。
let response = await fetch("/api/user/profile");
  • 同期処理のように書けるけど、実際は非同期処理になっている。なので await を使う関数それ自体も非同期な関数じゃないといけない。

try catch 書けるの幸せ。

async

  • await キーワードは async キーワードがついた関数の中でしか使えない、というルール。
async function getHighScore() {
    let response = await fetch("/api/user/profile");
    let profile = await response.json();
    return profile.highScore;
}
  • async な関数の戻り値は Promise になる。通常の値を返していたらそれが resolve された Promise を、例外を投げたらその例外で reject された Promise を返す。

お、あっさり async await が終わってしまった。シンプルでパワフルだなー。

## 13.4 Asynchronous Iteration

for/await ループ

  • ES2018

単に Promise の配列の場合、こんな風に書くことができるけど:

for(const promise of promises) {
    response = await promise;
    handle(response);
}

for/await ループを使うとシンプルにこう書ける:

for await (const response of promises) {
    handle(response);
}

これは普通の Iterator を使った場合で、ちょっとシンプルになるくらいだけど、Asynchronous Iterator の場合はもっと面白い。

Asynchronous Iterator

  • Symbol.iterator じゃなくて Symbol.asyncIterator でメソッドを定義する。
  • next() は Promise を返す。

ちなみに for/awaitSymbol.iterator に対しても動くけど、まず先に Symbol.asyncIterator が定義されていないかを探す。

Asynchronous Generators

  • async function * で Asynchronous Generator を定義できる
  • 内部では await を利用できる
  • yield で返される値は自動的に Promise でラッピングされる
async function* clock() {
    for(let count = 1; count <= 10; count++) {
        await new Promise(resolove => setTimeout(resolove, 100));
        yield count;
    }
}

async function test() {
    for await (let tick of clock()) {
        console.log(tick);
    }
}

test();

今まで勉強してきたのの組み合わせって感じね。

Asynchronous Iterators

Symbol.asyncIterator でメソッドを定義するだけだな。

class Clock {
    async *[Symbol.asyncIterator]() {
        for(let count = 1; count <= 10; count++) {
            await new Promise(resolove => setTimeout(resolove, 100));
            yield count;
        }
    }   
}

async function test() {
    for await (let tick of new Clock()) {
        console.log(tick);
    }
}

また別の例で、AsyncQueue を実装して、enqueue より先に dequeue を呼び出すことができるのとか面白かった。dequeue が unresolved な Promise を返すから。

Async なイテレーションは、実際にそういうのを使いたい場面に出くわしたら、もっとしっくりくるんだろうな。

はい、今日も面白かったー。

JavaScriptの勉強中:その7 Standard Library / Iterators and Generators

もう日課になってきた。

bufferings.hatenablog.com

## Ch. 11 The JavaScript Standard Library

  • JavaScript では Object を map や set のように使うこともできるし実際に使われてきた
  • けど、文字列に限定されるし "toString" みたいなプロパティもあってややこしい
  • なので ES6 では Set と Map クラスが導入された。

Set

  • 値の重複を持たないコレクション
  • Set は「順序を持たないコレクション」とよく言われるけど JavaScript の場合はちょっと違う。JavaScript の Set は要素が挿入された順番を覚えていて、iterate するのにその順番を使うから。

へー。日本語版の方には書いてないけど、英語版の方にはちゃんとそう書いてある:

Set - JavaScript | MDN

Set objects are collections of values. You can iterate through the elements of a set in insertion order. A value in the Set may only occur once; it is unique in the Set's collection.

  • 等価性のチェックには SameValueZero が使用される( https://tc39.es/ecma262/#sec-set-objects )。Array の includes() と同じだな。NaN もちゃんと判別されるってことね。
  • Set は iterable なので for/of ループや ... spread operator を使用することができる。
  • forEach() メソッドもある

Map

  • Map のキーの等価性チェックにも SameValueZero が使用される( https://tc39.es/ecma262/#sec-map-objects )
  • Map も iterable なので for/of ループや ... spread operator を使用することができる。
> let m = new Map([['x', 1], ['y', 2]]);
undefined

> m
Map(2) { 'x' => 1, 'y' => 2 }

> [...m]
[ [ 'x', 1 ], [ 'y', 2 ] ]

> [...m.keys()]
[ 'x', 'y' ]

> [...m.values()]
[ 1, 2 ]

> [...m.entries()]
[ [ 'x', 1 ], [ 'y', 2 ] ]

> for(let [key, value] of m) {
... console.log(`${ key }: ${ value }`);
... }
x: 1
y: 2
  • forEach() メソッドもある。ちょっと変だけど引数は (value, key) の順番になる:
matchMedia.forEach((value, key) => {
    // ...
});

配列を一般化したものとして考えてみると、インデックスが2番目の引数に来るのも分かる。

Error

  • JavaScript は何でも throw して catch することができる。プリミティブでも。(まじか・・・
  • でも普通は Error クラスかそのサブクラスを投げる
  • Error クラスは messagename の2つのプロパティと toString() メソッドを持っている
  • ES標準ではないけど、Nodeやブラウザでは stack プロパティがあって、スタックトレースを含む複数行文字列が格納される

URL

  • URL クラスはES標準ではないけど、IE以外のブラウザやNodeで利用できる
> new URL("https://example.com:8000/path/name?q=term#fragment");
URL {
  href: 'https://example.com:8000/path/name?q=term#fragment',
  origin: 'https://example.com:8000',
  protocol: 'https:',
  username: '',
  password: '',
  host: 'example.com:8000',
  hostname: 'example.com',
  port: '8000',
  pathname: '/path/name',
  search: '?q=term',
  searchParams: URLSearchParams { 'q' => 'term' },
  hash: '#fragment'
}

へー。便利そうだな。URL触りたいときには思い出そっと。

  • URL API が定義される前は escape() unescape()エスケープなどに使われていたけど非推奨になった。
  • その代わりに ES が導入したのは encodeURI() decodeURI()encoeURIComponent() decodeURIComponent()
  • これらのレガシー関数の問題は単一のエンコードを全体に適用するというところ。URLの部分によって異なるエンコードを用いている場合でも。
  • 解決策としては単純に URL クラスを使えばいい。

その他

WeakMap, WeakSet, Typed Arrays, Regular Expressions, Date, JSON, Intl, Console API とか色々あるけど、必要になったときに調べるんで良さそう。

## Ch. 12 Iterators and Generators

  • ES6 で Iterator が導入された
  • Iterable なオブジェクトは、 for/of ループで回したり、 spread operator で展開したり、 destructuring assignment で利用したりできる

Iterator がどうやって動くか

  1. Iterable なオブジェクトとは、特別な iterator メソッド( Symbol.iterator )を持っているオブジェクトのことで、そのメソッドは iterator オブジェクトを返す。
  2. Iterator とは next() メソッドを持っているオブジェクトのことで、そのメソッドは iteration result オブジェクトを返す。
  3. Iteration result オブジェクトは valuedone というプロパティを持ったオブジェクトのこと。

ループするときは iteration result オブジェクトの donetrue になるまで処理を続ける。頑張って書くとこういう感じ

let iterable = [1, 2, 3];
let iterator = iterable[Symbol.iterator]();
for (let result = iterator.next(); !result.done; result = iterator.next()) {
    console.log(result.value);
}
  • ビルトインの iterable なオブジェクトの Iterator オブジェクトは、それ自体も Iterable になっていて自分自身を返す。
  • つまり Iterator オブジェクト自身も Symbol.iterator メソッドを持っているということ
  • これは途中まで使った iterator を iterate したい場合に便利
let iterable = [1, 2, 3, 4, 5];
let iterator = iterable[Symbol.iterator]();
iterator.next();
iterator.next();

console.log([...iterator]); // => [3,4,5]

Iterable なオブジェクトを実装する

まずはジェネレーター関数を使わずにやってみる。

class MyIterable {
    [Symbol.iterator]() {
        let next = 0;
        let max = 10;
        return {
            next() {
                return (next <= max) ? { value: next++ } : { done: true };
            }
        }
    }
}


for (let x of new MyIterable()) console.log(x);
// 0から10までが出力された

動いたー。わーい。

便利なので iterator 自身も iterable にしておく:

class MyIterable {
    [Symbol.iterator]() {
        let next = 0;
        let max = 10;
        return {
            next() {
                return (next <= max) ? { value: next++ } : { done: true };
            },

            // 便利なので iterator 自身も iterable にしておく
            [Symbol.iterator]() { return this; }
        }
    }
}

ループの途中の breakreturn や例外などでイテレーションが中断される場合がある。その際に、ファイルやメモリーの開放のような後処理ができるように iterator には return() メソッドを実装できる。return() メソッドが存在する場合は、中断時に引数なしで呼び出される。return() はオブジェクトを返さなければならないが、そのオブジェクトは無視される。

class MyIterable {
    [Symbol.iterator]() {
        let next = 0;
        let max = 10;
        return {
            next() {
                return (next <= max) ? { value: next++ } : { done: true };
            },
            // iterator を iterable にしておくと便利
            [Symbol.iterator]() { return this; },
            // 後処理用(実際はこのクラスでは不要だけど)
            return() {
                console.log("return() が呼び出された");
                return {};
            }
        }
    }
}

let iterable = new MyIterable();

console.log("途中終了しない場合1");
for (let x of iterable){
    //
}

console.log("途中終了しない場合2");
[...iterable];

console.log("途中終了する場合1 break");
for (let x of iterable){
    break;
}

console.log("途中終了する場合2 return");
(function(){
    for (let x of iterable){
        return;
    }
}());

console.log("途中終了する場合3 例外");
try{
    for (let x of iterable){
        throw new Error();
    }
} catch {}

console.log("途中終了する場合3 destructuring assignment");
let [v1, v2] = iterable;

と、こんな感じで Iterable なオブジェクトを実装することができるけど、ちょっと面倒くさい。そこでジェネレーターの登場。

Generators

  1. ジェネレーター関数は function*アスタリスクをつけて定義する。
  2. ジェネレーター関数を呼び出すと、実際にはその関数のボディは実行されずに、ジェネレーターオブジェクトが返される。
  3. このジェネレーターオブジェクトは iterable になっている
  4. next() を呼び出すとジェネレーター関数のボディが呼び出されて yield ステートメントのところまで実行される。
  5. yield は ES6 で導入されたのだけど、この yield ステートメントで指定された値が next() の戻り値になる。

ほえー。

function* myGenerator(){
    yield 1;
    yield 2;
    yield 3;
    yield 4;
}

let generator = myGenerator();

// ジェネレーターオブジェクトは Iterator オブジェクトになってるので next() を呼ぶことができる
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
console.log(generator.next().value); // 3
console.log(generator.next().value); // 4
console.log(generator.next().done); // true

// さらにジェネレーターはそれ自身が Iterable で自分自身を返す
console.log(generator === generator[Symbol.iterator]()); // true

// なのでジェネレーター自体を Iterable なオブジェクトとして扱うことができる
console.log([...myGenerator()]); // [ 1, 2, 3, 4 ]

落ち着いて考えないとこんがらがってくるー。

ジェネレーター関数の定義は関数宣言以外にもこんな感じでかける:

// 関数式
const myGenerator = function*(){
    yield 1;
    yield 2;
    yield 3;
    yield 4;
}

// メソッド
let obj = {
    *myGenerator(){
        yield 1;
        yield 2;
        yield 3;
        yield 4;
    }
}

ということで、さっきジェネレーターを使わずに頑張って書いてたやつは、こういう風に書ける

class MyIterable {
    *[Symbol.iterator]() {
        for (let v = 0; v <= 10; v++) {
            yield v;
        }
    }
}

おー( return() はどうなるんだろう?と思ったら後で出てきた↓)

yield*

ジェネレーターを使って Iterable をこんな風に扱いたい場合に:

function* sequence(...iterables) {
    for(let iterable of iterables) {
        for(let item of iterable) {
            yield item;
        }
    }
}

console.log([...sequence("abc",[1,2,3])]);
// [ 'a', 'b', 'c', 1, 2, 3 ]

yield* を使うとこう書ける:

function* sequence(...iterables) {
    for(let iterable of iterables) {
        yield* iterable;
    }
}
  • yield* は Iterable オブジェクトを指定すると、それぞれの結果を対象に yield できる

Advanced Generator Features

ジェネレーター関数は、イテレーターを作るときに使われるのがほとんどだけど、処理を途中で止めてあとで再開できるというのがそもそもの機能。なので、その辺をもうちょっと詳しく見ていく。

return

  • return を書くと、そこで処理が終わるので、途中で処理を終わらせたい場合に使用する。
  • return に値を指定すると donetrue なのに value にも値が指定されている状態になる。この値は for/of や spread operator では無視される。
function* myGenerator(){
    yield 1;
    yield 2;
    return 3;
}

let g = myGenerator();
console.log(g.next());
// => { value: 1, done: false }
console.log(g.next());
// => { value: 2, done: false }
console.log(g.next());
// => { value: 3, done: true }
console.log(g.next());
// => { value: undefined, done: true }

// return の value は無視される
console.log([...myGenerator()]);
// => [ 1, 2 ]

yield

  • yieldステートメントのように扱ってきたけど、実は式。値を持つことができる。
  • next() の引数に値を指定すると、それが yield 式の値になる。yield から再開するときにその値が取得できる。
function* myGenerator(){
    console.log("myGenerator start");

    let y1 = yield 1;
    console.log(`myGenerator y1=${ y1 }`);

    let y2 = yield 2;
    console.log(`myGenerator y2=${ y2 }`);

    let y3 = yield 3;
    console.log(`myGenerator y3=${ y3 }`);
}

let g = myGenerator();
console.log("main start");

let n1 = g.next("a");
console.log(`main n1.value=${ n1.value }`);

let n2 = g.next("b");
console.log(`main n2.value=${ n2.value }`);

let n3 = g.next("c");
console.log(`main n3.value=${ n3.value }`);

let n4 = g.next("d");
console.log(`main n4.value=${ n4.value }`);

結果はこうなった

main start
myGenerator start
main n1.value=1
myGenerator y1=b
main n2.value=2
myGenerator y2=c
main n3.value=3
myGenerator y3=d
main n4.value=undefined

最初に next() で使った "a" は受け取ることができないね。

return()

あ、さっき気になったやつだ。

  • ジェネレーターには return() メソッドが定義されている
  • ジェネレーターに対して独自の return() を実装することはできない。クリーンアップをしたい場合は try/finally を使う。
  • return() を呼び出すと、ジェネレーターはクリーンアップのコードを実行する

そっか finally を使えばいいのか。

function* myGenerator() {
    try {
        console.log("1の前");
        yield 1;
        console.log("2の前");
        yield 2;
        console.log("3の前");
        yield 3;
        console.log("3のあと");
    } finally {
        console.log("finallyが呼ばれたー");
    }
}

console.log("return() を直接呼び出し =======");
let g = myGenerator();
g.next();
g.next();
g.return();

console.log("destructuring assignment =======");
let [v1] = myGenerator();

console.log("for/ofをbreak =======");
for (let v of myGenerator()){
    break;
}

結果はこう:

return() を直接呼び出し =======
1の前
2の前
finallyが呼ばれたー
destructuring assignment =======
1の前
finallyが呼ばれたー
for/ofをbreak =======
1の前
finallyが呼ばれたー

throw()

  • throw() を呼び出すと、今ポーズしている yield のところで throw される
  • throw() の引数に値を渡すとジェネレーター内の try/catch でキャッチ可能

今日も面白かった。

JavaScriptの勉強中:その6 Classes (後編) / Modules

ぼーっと今日も。半分くらいきたかな。

bufferings.hatenablog.com

ちょこちょこ勉強しながら「分かるようになってるのかなぁ?」とか思ってたけど、今日たまたま機会があって読んだら JavaScript のコードだいぶ理解できるようになってた。よかった。

### 9.4 既存のクラスへのメソッド追加

  • JavaScript のプロトタイプベースの継承の仕組みはダイナミック。なので、オブジェクトが生成された後にプロトタイプのプロパティを変更しても、その内容は反映される。
  • なので後からでもプロトタイプにメソッドを追加したりできる。ビルトインのクラス(Object とか String とか)の振る舞いも変更できてしまう。
  • けど、そういうことは基本的には、やらんほうが良い(わかる

### 9.5 サブクラス

サブクラスも、まずは ES6 より前のやり方から。

サブクラスとプロトタイプ

Range のサブクラスとして Span を作りたいときは、ES6 より前だとこんな感じになる:

// Range のプロトタイプから Span のプロトタイプを作る
Span.prototype = Object.create(Range.prototype);

// Range のコンストラクターじゃなくて Span を使いたいので書き換える
Span.prototype.constructor = Span;

でもスーパークラスコンストラクターやメソッドをサブクラスから呼び出すためのシンプルな方法はなかった。

extendssuper

ということで ES6。

extends

  • ES6 以降だと単に classextends を使ってスーパークラスを指定できる
  • extends を使った場合は static メソッドも継承される。これは extends の特別な機能として導入された。

new.target

super

## Ch. 10 Modules

モジュール化に関しては以前は JavaScript のビルトインサポートがなかったので

  • みんな自分で頑張ってやってて
  • Nodeが require() 関数を使ったモジュール化を使用してたんだけど
  • ES6 でそれとは違う exportimport が導入された

という流れ。

### 10.1 Module with Classes, Objects and Closures

  • クラスやオブジェクトを使えば、同じ名前のメソッドやプロパティがあってもぶつからないので、モジュールのように扱うことができる。
  • けど、内部実装の詳細をモジュールの中に隠す方法がない
  • なので、即時実行関数を使ったら内部実装の詳細をその関数に閉じ込めることができる
// aaa() や bbb() や CCC は外からは見えなくて
// Abcde だけが見える
const Abcde = (function () {
    function aaa(){
        // ...
    }
    function bbb(){
        // ...
    }
    const CCC = 1;

    return class Abcde {
        // ...
    }
}());
// 複数公開したいとき
const stats = (function () {
    function aaa(){
        // ...
    }
    function bbb(){
        // ...
    }
    const CCC = 1;

    function mean(data) {
        // ...
    }

    function stddev(data) {
        // ...
    }

    return { mean, stddev };
}());

stats.mean([1,2,3,4,5]);
stats.stddev([1,2,3,4,5]);

Closure-Based Modularity

  • そう考えると、単に JavaScript ファイルに書いてあるコードの前後に決まったテキストを入れて機械的に処理することで、モジュール化できることに気づく。
  • 例えば、ファイルの一覧を受け取ってそのコンテンツを即時実行関数で囲んで次のようにして一つの巨大なファイルを作ると:
const modules = {};
function require(moduleName) { return modules[moduleName]; }

modules["abcde.js"] = (function () {
    const exports = {};

    // abcde.js の中身
    function aaa() {
        // ...
    }

    exports.Abcde = class Abcde {
        // ...
    };

    return exports;
}());

modules["stats.js"] = (function () {
    const exports = {};

    // stats.js の中身
    exports.mean = function (data) {
        // ...
    }

    exports.stddev = function (data) {
        // ...
    }

    return exports;
}());
  • こんな風にして利用できる:
const Abcde = require("abcde.js");
const stats = require("stats.js");

let abcde = new Abcde();
let average = stats.mean([1,2,3,4,5]);
  • というのが、コードバンドリングツールのラフスケッチであり、Nodeなどで使われてる require() 関数の簡単な紹介でもある。

へー。

### 10.2 Modules in Node

Node では

  • require() 関数で import する
  • exports オブジェクトのプロパティに設定するか module.exports オブジェクト自体をリプレースするかで export する
  • 明示的に export したもの以外はそのファイル内でプライベートになる。

Node Exports

  • Node は exports というグローバル変数を定義している
  • export したかったら exports のプロパティとしてアサインすればいい:
const aaa = ...
const bbb = ...

exports.mean = data => ...
exports.stddev = function(d) {...}
  • でもだいたいの場合は export したいのは1個だけなのでそういうときは単に module.exports を使えば良い
module.exports = class Abcde { ... }
  • module.exports のデフォルト値は exports なので module.exports.mean = mean みたいに書くこともできるし、次のように書くこともできる:
module.exports = { mean, stddev }

ちょっと exportsmodule.exports の違いがよくわかんなくて公式サイトの説明を見てみた:

Modules | Node.js v14.3.0 Documentation

なるほど。

  • exportsmodule.exports のショートカットで、わざわざ module. をつけなくても良いように用意してくれてる
  • けど、 exports 自体を別のオブジェクトに書き換えてしまうと module.exportsexports が別のオブジェクトになってしまうから export されない
  • だからオブジェクト全体を書き換えなくていい場合には短く書ける exports を使えばいいし、全体を書き換えたい場合は module.exports を使えば良い

ってことか。へー。

Node Imports

  • import には require() 関数を使う
  • Node のシステムモジュールを import する場合はそのモジュールの名前を書くだけ:
const fs = require("fs");
const http = require("http");
  • 自分のモジュールを import したい場合はそのファイルからの相対パスで書く:
const stats = require('./stats.js');

(ダブルクォーテーションだったりシングルクォーテーションだったりするのは良くわからない。関係ないと思う。)

  • 絶対パスも書けるけど普通は使わない(そうだろうな
  • 拡張子( .js )を書かなくても大丈夫だけどつけるほうが一般的
  • 対象モジュールが複数のプロパティを export している場合は destructuring assignment で利用するものだけ受け取ることができる

んで ES6 で公式の JavaScript モジュールが出てきた。Node 13 では ES6 のモジュールがサポートされてるらしいけど、今のところは多くの Node のプログラムはまだ Node のモジュールを使ってる。(ふむー大変そう

### 10.3 Moudles in ES6

  • コンセプトとしては Node と同じ:export されていない限り各ファイルの中でプライベートになる
  • 違うのは構文。それと、Webブラウザーでモジュールを定義する方法も違う。

ES6 のモジュールは普通の JavaScript の "script" とはいくつかの点で異なる

  • 通常のスクリプトだとトップレベル宣言はグローバルになるけど、モジュールの場合はそれぞれのファイルがプライベートなコンテキストを持っている。
  • モジュールは自動的に strict mode になる。 'use strict' を書く必要はない。

ES6 Exports

  • 単に宣言の前に export をつければいい
export const PI = Math.PI;
export function abcde() {...}
export class Circle {...}
  • 一箇所にまとめたい場合は、それぞれの場所に export をつけるんじゃなくて、最後にひとまとめにして export すればいい
export { Circle, abcde, PI };
  • これオブジェクトのように見えるけどそうじゃなくて、 export の構文として波括弧の中でカンマ区切りのリストを受け取るようになってる
  • 1個だけ export したい場合、通常は export default を使う
export default class Abcde {
    // ...
}
  • export default を使うと import が少しだけ楽になる
  • 通常の export は名前がついてるものだけしかできないが eport default は匿名関数や匿名クラスを含むどんな式でも export できる
  • つまり export defeault はオブジェクトリテラルを export できるので、export default の次に波括弧で囲んであるものがあったら、さっきのと違ってそれは本当にオブジェクト。(へーww
  • exportexport default の両方を書くことは可能だけど普通はやらない。export default を書くならそれいっこだけ。
  • export はトップレベルにしかかけない。関数やクラスやループや分岐の中では使えない。つまり実行前に静的に決まるということ。

ES6 Imports

  • export default で export されてる場合はこんな風に書ける:
import Abcde from './abcde.js';
  • 識別子は定数になる
  • import は hoist されるけど普通は一番上に書く
  • export で export されてる場合は destructuring assignment のように書いて受け取る(「ように」ってことは実際はそうじゃないんだろうな)
import { mean, stddev } from "./stats.js";
  • スタイルガイドではちゃんと全部のシンボルを書くことが推奨されてるけど、全部まとめて import することもできる。 export が多い場合とか:
import * as stats from "./stats.js";
  • この場合 stats.mean()stats.stddev() のように stats オブジェクトを介して呼び出すことができる
  • あんまり一般的ではないけど、もし exportexport default が混ざってたらこんな風に書くことができる:
import Histogram, { mean, stddev } from "./histogram-stats.js";
  • もうひとつ別の使われ方としてこういう書き方もできる:
import "./analytics.js";
  • これはそのモジュールの関数を使う必要がない場合に使う。そのモジュールを呼び出すだけで処理をしてくれてそれでOKな場合など。
  • import するときに別名をつけることができる。名前がかぶる場合など:
import { render as renderImage } from "./imageutils.js";
import { render as renderUI } from "./ui.js";
  • export default の場合はそもそも名前を持ってないからいつでも名前をつけてるけど、さっきの例みたいに exportexport default が混ざっている場合にはこういうこともできる:
import { default as Histogram, mean, stddev } from "./histogram-stats.js";

Re-Exports

import したものを export したい場合には、こういう風にかけるが:

import { mean } from "./stats/mean.js";
import { stddev } from "./stats/stddev.js";
export { mean, stddev };

ES6 ではそれ用の特別な書き方を用意してくれてる:

export { mean } from "./stats/mean.js";
export { stddev } from "./stats/stddev.js";

ワイルドカードもいける:

export * from "./stats/mean.js";
export * from "./stats/stddev.js";

リネームもOK:

export { mean as average } from "./stats/mean.js";
export { stddev } from "./stats/stddev.js";

もし mean.js と stddev.js が export default を使っていた場合はこうなる:

export { default as mean } from "./stats/mean.js";
export { default as stddev } from "./stats/stddev.js";

逆に default として export したい場合には:

export { mean as default } from "./stats/mean.js";

Dynamic Imports with import()

  • ES2020 で導入。モジュールの動的インポート。Promise を返す。
  • クライアントWebアプリでは全部静的にインポートするより動的にインポートしたいからDOMを操作して実現してたけど、それが標準化された。
import("./stats.js").then(stats => {
    let average = stats.mean(data);
})

もしくは async 関数で使う

async analyzeData(data) {
    let stats = await import("./stats.js");
    return {
        average: stats.mean(data);
        stddev: stats.stddev(data);
    };
}
  • import の場合は文字列リテラルじゃないといけなかったけど、 import() の場合は文字列として評価される式ならなんでも大丈夫
  • import() は関数のように見えるけどそうじゃなくて operator 。
  • ブラウザだけじゃなくて、 webpack みたいなコードパッキングツールでも使える。static な import を使うと全部バンドルして大きなファイルになってしまうけど、 dynamic な import をうまく使えば、小さなバンドルに分けることができる。

なるほどねぇ。