「コーディングを支える技術」を読む
読んだ感想、考えたこと、調べたことのメモ。 #JavaScript ではどうだっけ、みたいな話が多くなりそう。 ページ数は第5版のもの。
プログラムは楽をするために書くものです。楽をすることは、手抜きとは違います。手抜きをして後で苦しくなるのでは楽をしたとは言えません。 (p. 22)
わかる。とはいえ、直近で楽をすることと将来的に楽をすることがトレードオフになっていることも多く、現実的にはいつもその配分をどうするかを考えている気がする。
Python に標準添付されている ast ライブラリを使えば、特定のコードがどういう構文木になるかが分かります。
馬に手綱につけるように、 goto にも制限を加えたほうが、作られたコードを理解しやすいのです。 if......else や while や break は、そういう「制限付きの goto 」なのです。 (p.37)
if-else 文や while 文は goto があれば書けてしまうという話。
この認識はなかったのでなるほどなと思った。自分がプログラミング (C言語) を学んだ頃には「 goto は悪なので使うべきでない」が共通認識のようになっていて、そもそもコードを書くときの選択肢になかった。
しかし、ソースコードを再利用するためには goto 文だけでは力不足です。
goto 文ではできないこと、それは元の位置に戻ってくることです。 (p.45)
if-else 文などとは違って、 goto 文を使っても関数は作れないという話。これもその認識はなかった。
関数がなくても書けるけれども、関数を使ったほうが楽なことがあります。それは理解と再利用です。 (p.42)
普段コードを書いたり読んだりするとき、関数を使う時のメリットとしては「再利用できること」よりも「理解しやすくなること」のほうが実感することが多いし、だから命名にもかなり気を使っている。
こういう「おかしくなったら処理を停止して速やかに報告すべき」という設計思想は「フェイルファースト」と呼ばれています。 (p.76)
フェイルファーストは Fail-Fast のこと。 (First ではない。) ビジネス用語としても使われているみたいで、ググるとそっちのほうが多く出てきて困った。 逆に Fail-Safe (フェイルセーフ) という言葉もあり、これは「おかしくなったとしてもなるべく安全に動作するように」という設計思想。 js の Fail-Safe な挙動の例
配列の範囲外を取得しようとしても怒られず、 undefined が返る
関数呼び出し時の引数の数が足りなくても怒られず、 undefined が返る
引数は arguments オブジェクトという Array-like なオブジェクトに入るので、配列の範囲外アクセスに対して undefined が返るという挙動につながっていそう 逆に引数が多すぎても怒られない
js の Fail-Fast な挙動の例
Promise.all: 渡された promise のうち1つでも失敗するとその時点で reject される
1994年に作られた JavaScript では、 Perl と同様に何も宣言しない場合にグローバルスコープ、 var を付けて宣言した場合に静的スコープです。 (p.95)
スコープの話、ふわっとしか理解していなかったのでこれを機に調べた。
js のスコープには以下がある。
グローバル変数は内部的にはグローバルオブジェクト (web ページにおいては window) のプロパティである
なので、グローバル変数に window.hoge のようにアクセスすることができる
Module Scope: モジュールを import したときに囲まれるスコープ
モジュールの機能は単独のスクリプトのスコープにインポートされます。つまり、インポートされた機能はグローバルスコープから利用することはできません。
Function Scope: 関数によってつくられるスコープ
Block Scope: ブロック によってつくられるスコープ let, const で宣言された変数は、ブロックスコープの変数となる。 var で宣言した場合はならない。
code:javascript
var a = 1;
let b = 1;
{
var a = 2;
let b = 2;
}
console.log(a); // 2
console.log(b); // 1
また、変数の巻き上げという、以下のような挙動がある。これは js 特有のもの。 後のコードで宣言している変数を、例外を発生させることなく、宣言より前のコードで使用できる
その際、変数は undefined で初期化される
ただし、ECMAScript 2015 以降では、 const や let で宣言した場合巻き上げ時の初期化がされないため、宣言前に参照すると ReferenceError を吐く
code:javascript
console.log(x) // undefined
console.log(y) // ReferenceError
var x = 1;
const y = 2;
メモ: closure と lexical scope
たとえば Python の世界の中での値は、整数でも浮動小数点数でも文字列でも、すべて PyObject 型として扱えるよう、先頭部分が同じ形になっています。 (p.128)
js では全てのものが Object であり [[Prototype]] を持っている
また、 js には一般的に使われる整数型はなく、 Number は浮動小数点型である。そのため、整数同士の割り算が割り切れない場合は切り捨てではなく小数が返る。
code:javascript
const a = 12
console.log(a / 5) // 2.4
同じ型推論という言葉で表現されていても、どうやって型を推論しているかや、どれくらい推論できるかは言語によって異なります。 (p.130)
TypeScript の場合は以下のようになる
code:typescript
// identity の型は上手く推論できない
const identity = x => x // Parameter 'x' implicitly has an 'any' type.
// 型引数を書けば通る
const identity: <T>(x: T) => T = x => x
// identity(identity) は identity と同じ型になる
const recursive = identity(identity) // <T>(x: T) => T
console.log(recursive(1)) // 1
Java や Python、 Ruby など多くの言語では、配列が一番基本的なコンテナとして提供されています。たとえば Python で [1, 2, 3] と書いた場合、これらの要素はメモリ上では並んで格納されています。 (p.142)
なので、添字アクセスや挿入の計算量は O(1) である。
Shift_JIS と EUC-JP の設計の違いは、2バイト文字の2バイト目で1バイト文字の領域を避けるかどうかでした。 (略) Shift_JIS では、2バイト目が「プログラム上で特殊な意味を持った文字」になってしまうケースがあるのです。 (p.157)
Shift_JIS を使うとバグるとかエンコーディング方式をファイルの冒頭に書いたほうが良いとかは見かけたことがあったけれど、その理由をちゃんと知らなかった。
HTML5 の場合は <head> 内に <meta charset="utf-8"> のように記述することで指定する。 UTF-8 が唯一の有効な文字エンコーディングなので、それ以外の値は受け付けない。
スタイルシートで使う文字エンコーディングは @charset アットルールなどを使って指定することができる。 (さらに他の指定方法もある。)
HTTP 通信などにおいて MIME タイプを指定する際、 text/plain;charset=UTF-8 のように文字エンコーディングを指定できる
UTF-8 は、 Unicode という統一された文字セットを符号化する方法なのです。 (p.162)
Unicode は文字セット、 UTF-8 はエンコーディング方式。
文字列にもいろいろな違いがあります。まず「何が文字であるか」(文字集合)の違い、次に「どうやって文字をビット列で表現するか」(文字符号化方式)の違い、そして「どういう情報をどうメモリに格納するか」(文字列の実装)の違いです。 (p.168)
js の場合、文字集合は Unicode, 文字符号化 (エンコード) 方式は UTF-16, 実装は 16 ビットの符号なし整数値を要素とするの集合体。
プリエンプティブとは「他人の行動を阻止するための」という意味の単語で、 (p.172)
スレッドがプリエンプティブなことが割り込まれてしまう原因なのだから、協調的なスレッドを作ればいいじゃないか、というアプローチです。 Ruby の Fiber クラスや、 Python や JavaScript のジェネレータがこれです。 (p.178)
なるほど? ジェネレータをそういうものだととらえたことなかったけれど、 yield で割り込まれても良いタイミングを明示しているのは確かにそうだな。 (メモ)
この問題を解決しようとしているのがトランザクショナルメモリというアプローチです。 (略) コンセプトは「手元で試しにやってみて、失敗だったら最初からやりなおし、成功だったら変更を共有する」です。
「失敗」とは何を指すか?
トランザクションの中の処理が失敗する
トランザクション中に他の処理が書き込みを行い、変更を反映できない状態になる
「変更を反映できない状態」をどうやって判断するか?
書き込もうとしている値がトランザクション開始前と変わっている
(他にある?)
実装例など
このような制限を受けていない、「変数に代入する」「関数の引数として渡す」「関数の戻り値として返す」などが可能である値のことを「ファーストクラスの値」と呼びます。
初めて聞いたと思ったけれど、今は第一級オブジェクトという言い方のほうが使われているっぽい。 js では関数が First-class Object だけど、他の言語ではそうとは限らない。
さらに、プロトタイプを使った処理を楽に書けるように、 new という演算子が用意されています。関数fに new を付けて呼び出すと、次の4つの処理が行われます。 (略)
code:javascript
function Car(make, model, year) {
this.make = make
this.model = model
this.year = year
}
このとき、これは
code:javascript
const car1 = new Car('Eagle', 'Talon TSi', 1993)
これと等価 (多分)。
code:javascript
let car1 = {} // オブジェクトを作成
Object.setPrototypeOf(car1, Car.prototype) // car1 のプロトタイプを Car() のプロトタイプオブジェクトに変更
car1.constructor('Eagle', 'Talon TSi', 1993) // this を car1 として、 Car() の本体を実行
ちなみに、 Car.prototype ≠ Object.getPrototypeOf(Car) = Car.__proto__
Object.getPrototypeOf() は JavaScript 1.8.1 から導入された
(略) 子クラスを作るときに気を付けるべきポイントとしてよく言及されるのが「リスコフの置換原則」です。
親クラスのオブジェクト x が常に満たす条件 q が存在するとき、子クラスのオブジェクト y も条件 q を常に満たさなければならない。つまり、子クラスのオブジェクトは親クラスのオブジェクトとして扱っても問題ないようにしなければならない。
クラスには2つの相反する役割があります。1つ目は「インスタンスを作るためのもの」という役割で、このためには「完結した、必要なものを全部持った、大きなクラス」である必要があります。2つ目は「再利用の単位」という役割で、このためには「機能ごとの、余計なものを持っていない、小さなクラス」である必要があります。
クラスが「インスタンスを作るためのもの」として使われているときには、再利用の単位としては大きすぎるのです。それならば、再利用の単位という役割に特化した、もっと小さい構造 (トレイト = メソッドの束) を作ればよいのではないか?――これがトレイトの考え方です。 (p.231)
フロントエンドの、クソでかコンポーネント生まれがち問題もこれに近い気がする。
メモ