Phantom property pattern

TypeScriptは、JavaScriptエンジンの動的セマンティクス上に、静的な型システムのセマンティクスが重なっているものです。ここで、JavaScriptとしては正しく実行できても、TypeScriptの型システム上ではちゃんと型が付かないという場合もあります。ときには、TypeScriptで型がつくように大幅に書き換えないといけない場合も出てきたりして、そうなると大変です。それでも、TypeScriptの型システムは高度な機能を持っているので、大体の場合は、うまく表現してやると、JavaScriptらしい書き方のままで型をつけることができてしまいます。今回は、JavaScriptでよくある、プリミティブ値をそのまま取り回すパターンに型を付けたいと思います。

プリミティブ型を扱うプログラミングは、素朴でわかりやすいですが、型の考えからするとかなり悪いものです。こういうプログラミングを行うと、あらゆるデータを文字列で表す様子から、ときに”stringly typed”と揶揄されます。文字列は、表したいものはとりあえず何でも表現できるのでとても便利ですが、いろいろな種類のデータが一つの型で表されてしまうのなら、型が役立たずになってしまいます。すべてがstring型のプログラムで、どうやってその文字列の中身がテキストなのか、URLなのか、メールアドレスなのかを判断するのでしょうか? 文脈を読み込んで察するか、ハンガリアン記法に回帰するか、さもなくばエスパーで当てるしかないでしょう。

この問題に対処するひとつの「まっとうな」方法は、プリミティブ値を直接使わず、適宜ラッパーオブジェクトを作り、その上でプログラミングを行うことでしょう。プリミティブ値ではなくオブジェクトを使えば、メソッドを追加することも容易です。しかし、実際にJavaScriptでそういうことが行われづらい原因として、ひとつはプリミティブ値をコンストラクタでラップするが手間だという点、ひとつは===による同一性比較がオブジェクト型に対しては工夫を加えなければ意味上の同値性比較にならない点、ひとつは性能上の問題点、ひとつはシリアライズ・デシリアライズの自明さが失われる点が挙げられます。要するに、プリミティブ値をオブジェクトで包むのはわりとデメリットもあるわけです。

今ここに、この問題へのもうひとつの対処方法、幽霊プロパティパターン phantom property pattern を提唱します。このパターンを使うと、プリミティブ値の型を好きなだけ定義できる。つまり、言語組み込みのプリミティブ値を取り回すプログラムでありながら、各プリミティブ値を別々の型として取り扱うことができるようになります。

幽霊プロパティパターンでは、まず、次のような型を定義します。

export type Tag<X, R extends keyof any, Y> = X & {
    [rel in R]: Y
}

x: Tag<X, R, Y>は「xはXで、xのRはYである」というような意味になります。

次に、幽霊シンボルを定義します。

// This is a phantom symbol, which does not exist at run time
declare const _unit: unique symbol
export type unit = typeof _unit

_unitは幽霊シンボルで、宣言しますが、実際には定義しません。_unitの型はユニークシンボル型ですが、このままでは型に名前がなく使いづらいので、unitというエイリアス名を付けます。

ここに定義したTag型コンストラクタと幽霊シンボルを組み合わせると、プリミティブ値の性質を幽霊プロパティとして表現できます。

例として、角度の単位を幽霊プロパティとして付加した型を定義してみましょう。

export type Scale<X, U> = Tag<X, unit, U>
export type Radian = Scale<number, "radian">
export type Degree = Scale<number, "degree">

ここで定義したRadian, Degreeは両方ともプリミティブ値の数値を元とする型ですが、実際には存在していない[_unit]プロパティの型が異なるため、相互に代入不可能となっています。

{
    const x: Degree = 30 as Degree
    const y: Radian = x // type error!
    console.log(x, y)
}

いいですね。これを使って、たとえば三角関数双曲線関数に単位をつけてあげるといい感じになります。

export const sin = Math.sin as (x: Radian) => number
export const tanh = Math.tanh as (x: Radian) => number
export const asin = Math.asin as (x: number) => Radian
export const atanh = Math.atanh as (x: number) => Radian

{
    const x = 30 as Degree
    const y = sin(x) // type error!
    console.log(y)
}

弧度法で渡すべきところをうっかり度数法で渡すミスを、これで防げますね。 相互に変換する関数も用意してあげましょう。

// Ref: https://tauday.com/tau-manifesto
const τ = 2 * Math.PI

/** converts radian to degree */
export function toDegree(x: Radian): Degree {
    return (x / τ * 360) as Degree
}

/** converts degree to radian */
export function toRadian(x: Degree): Radian {
    return (x / 360 * τ) as Radian
}

{
    const x = 30 as Degree
    const y = sin(toRadian(x)) // ok
    console.log(y)
}

例をもう一つ、今度はUnix時間とISO8601を扱ってみます。 今度は単位ではなくデータフォーマットの話なので、新しくdataformat型を定義しています。

declare const _dataformat: unique symbol
export type dataformat = typeof _dataformat

export type UnixTime = Tag<Unit<number, "millisecond">, dataformat, "unixtime">
export type ISODateTime = Tag<string, dataformat, "iso8601">
export const now = Date.now as () => UnixTime
export const toISODateTime = (t: UnixTime): ISODateTime => new Date(t).toISOString() as ISODateTime
export const toUnixTime = Date.parse as (d: ISODateTime) => UnixTime
export function isISODateTime(s: string): s is ISODateTime {
    return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(s)
}

{
    const t = 1514810096000 as UnixTime
    console.log(toISODateTime(t))
    const d = "2018-01-01T12:34:56Z"
    if (isISODateTime(d)) {
        console.log(toUnixTime(d))
    }
}

UnixTime型はデータフォーマットだけでなく、単位も幽霊プロパティで付加していますね。一口にUnix時間と言っても、実際にはミリ秒単位のものと秒単位のものが混ざっていることがよくあるのですが、こういう工夫が事故を防いでくれるはずです。

ISODateTime型の方は、型ガード関数を用意しました。こういう関数を用意して使うほうが、直接as ISODateTimeでダウンキャストするよりも安全です。

欠点

  • 本当ならinterface Tag<X, R, Y> extends X { [rel in R]: Y }とでも定義したいところですが、現状のTypeScriptではプリミティブ型を拡張できないため、やむなく交叉型 intersection type で表現しています。
  • 幽霊シンボルそれ自体はexportしないほうがいいでしょう。実在しない識別子なので、アクセスしても型エラーになりませんが実行時エラーになる可能性があります。型レベルで使うだけでシンボル自体はなくてもいいので、シンボルの型に名前を付けて、そちらをexportするといいです。
  • interfaceではないため、スニペットコンパイラのエラーメッセージ等で、型エイリアスが展開されて表示されてしまいます。unique symbolと表示されるだけなのは非常にまずいです。
    • f:id:mandel59:20180901034739p:plain
    • ワークアラウンドとして、幽霊シンボル自体にタグをつけるように書き換えると、なんとか分かるようになります。
      • f:id:mandel59:20180901035321p:plain
declare const _unit: unique symbol
export type unit = Tag<typeof _unit, "name", "unit">
export type Unit<X, U> = Tag<X, unit, U>

declare const _dataformat: unique symbol
export type dataformat = Tag<typeof _dataformat, "name", "dateformat">

まとめ

幽霊プロパティパターンを使うと、実行時コストに影響を与えることなく(ゼロコストで)、プリミティブ値にユーザー定義の型をつけることができるようになります。

高校の教科書に載っている「情報」の説明が変だという話

数研出版の教科書『高等学校 社会と情報』(平成24年2月27日検定済、平成27年1月10日発行)の序編第II章では、「情報の特徴」と題して、情報とは何かとその特徴についての説明が行われているのだが、そこで行われている「情報の有無」の説明にいまいち納得が行かない。

情報の有無

16本のマッチ棒をテーブルの上に投げたとき,たいていは,図1のような乱雑な並び方になる。偶然にマッチ棒が,図2のように「SOS(または505)」の文字の形になる可能性もあるが,きわめて確率が低い。私たちが,図2を見たら,人が手で並べたと思うだろう。

この2つの状態(図1の並び方と図2の並び方)のちがい(差)は,何だろうか。私たちが一目見てわかるように,この2つには大きな差がある。そのちがい(差)が,情報である。

f:id:mandel59:20170102025120j:plain

(『高等学校 社会と情報』14ページ)

並び方の乱雑な「テーブルに投げたマッチ棒」と、並び方に秩序があるように感じられる「人が並べた?マッチ棒」とを比較し、その差が情報であるとする「説明」は、理屈ではなく人間の直感に訴える説明であるため、理屈を気にしない人間ほど納得してしまうのではないかと思うけれども、しかしこの説明は変だ。

この説明は、乱雑な図1「テーブルに投げたマッチ棒」が〈情報がない方〉で、秩序だった図2「人が並べた?マッチ棒」が〈情報がある方〉だという想定で書かれているのではないかと思う。この説明をされて、何かが読み取れる方が〈情報がある方〉、何も読み取れない方が〈情報がない方〉だと受け取るのは、普通の感覚だと思う。

しかし、考え方によっては「左の方が情報が多い」と結論づけることもできる。どういうことか? こんな遊びを考えてみよう。

この遊びは2人でやる。片方の人間が、机の上のマッチ棒の状態をできるだけ少ない言葉で説明する。もう片方の人間は説明を聞いて、元の状態を見ずに、マッチ棒の状態を言葉だけから再現する。

図1と図2、それぞれでこの遊びをやるとどうなるだろうか。図1のマッチ棒の状態は乱雑で、素直に説明する言葉が見つからない。「マッチ棒が散らばっている」と言うだけでは図1の状態を精度よく再現することはできないから、マッチ棒1本1本について、その位置と向きを細かく説明する必要がある。一方、図2のマッチ棒の状態は秩序だっている。「マッチ棒が505の形に並んでいる」という言葉だけでも、ある程度再現可能だろう。マッチ棒の向きが問題だとしても、それぞれについて上下左右で指定すればいい。

結局、図1は図2よりも説明に必要な言葉が多いので、より情報が多いと考えられる。

こういう説明もできることを考えると、図2の方が〈情報がある方〉なのは一目瞭然だとするわけにはいかないはずだ。

乱雑に並んだマッチ棒は、意味のわかるパターンを含んではいない。しかし、意味のわかるパターンを含まないというのは、情報を持たないということではない。例えば、ジャングルの木々の並びにも、人間にとって直接意味のわかるパターンは含まれていないが、しかし、ジャングルの木々の並びを覚えることで、自分が今ジャングルのどこにいるのかを知ることができるようになり、ジャングルでの生活に役立てられるだろう。件の教科書は「情報」とは「意思決定の材料になるもの」(15ページ)だという考えを載せているが、その考えで情報を捉えるとしても、ジャングルの例で分かるように、人間が意図しない自然発生の差異もまた人間の意思決定の材料となる「情報」であるはずだ。

人間の作成したコンテンツを主に取り扱っていると「情報は人間が生む」という錯覚に陥りがちだが、情報を生むのは人間だけではない。宇宙のあらゆる存在が情報源となりうるのであり、それは人間がその意味を読解できるかとは関係がない。