スキップしてメイン コンテンツに移動

私家版 TypeScript 抽象データ型表現

TL, DR;

読んだ: TypeScriptの異常系表現のいい感じの落とし所 | Developers.IO

方向性はとても同意できるがデータがオブジェクトである積極的な理由がないのが分かる。今日び new Success(...) もあるまい。 構造的型付が原則なんだから Namespace Import する前提で型定義と関数を公開してしまった方が単純な FP スタイルで書けて勝手が良い。

そういうわけで僕ならこう書く。

使い方

import * as Result from './result';

function doSomethingFailable(): Result.T<number, Error> {
  const r = Math.random();
  return r < 0.5
    ? Result.success(r)
    : Result.failure(new Error('Something failed.'))
}

function orDefault<V>(result: Result.T<V, unknown>, defaultValue: V): V {
  return Result.match(result, {
    failure() { return defaultValue; },
    success(value) { return value; },
  });
}

const result = doSomethingFailable();
console.log(orDefault(result, NaN));  // Prints a number < 0.5, or NaN.

自明な flatMap / map がないのでより低水準な変換として match を提供しているが、もちろん型の利用者が合意できるなら Optional に類する定義を採っても良い:

function map<V, U, E>(f: V => U, result: Result.T<V, E>): Result.T<U, E> {
  return Result.match(result, {
    failure(value) { return Result.failure(value); },
    success(value) { return Result.success(f(value)); },
  });
}

function flatMap<V, U, E>(f: V => Result.T<U, E>, result: Result.T<V, E>): Result.T<U, E> {
  return Result.match(result, {
    failure(value) { return Result.failure(value); },
    success(value) { return f(value); },
  });
}

つまるところ何が違うか

値がユーザ定義クラスのインスタンスではなくて Object のインスタンスであることが違う。

JavaScript の用語だと両方オブジェクトに違いないが、後者は専ら構造体に類する複合データ型として使われるので、この記事では区別のために前者を特にオブジェクトとし、後者はレコードと呼ぶことにする。換言すると差異は値がオブジェクトではなくレコードであることである。

type T

このモジュールが提供する型は FailureSuccess そしてそれらの Union である T である。

何故最も主要な型が Result ではなく T なのかというと、Namespace Import が前提なので名前が冗長 (Result.Result) になるのを避けるためである。この流儀は OCaml から借用した。

その違いが何をもたらすか

脱カプセル化

端的に言うとカプセル化されなくなる。つまりデータ構造定義が公開インタフェースの一部になる。これを聞いて目尻が釣り上がる OO おじさん達はまわれ右。

静的型付された代数型の変更に憚るところはない。もし定義を変更する場合があったとして、それで互換性が破壊されるならビルドがちゃんと壊れるはずだ。コンパイラはエラー箇所を正しく列挙できるからそれを直せば良い。

だいたい Destructuring Assignment やら Rest/Spread Operator やらを使う時点でデータ表現はインタフェースの一部である。get / set で云々すればインタフェースと内部データ表現を分離することは不可能ではないが、そんなところに労力を費して見た目と意味論を乖離させるよりは直交的なデータ構造設計に腐心する方が生産的だろう。

new の除去

見た目の問題。任意の関数は値を返すのだ。コンストラクタが特別な地位にある必要はない。

副作用の明示

メソッドがレシーバの状態を変更するか否かシグネチャから分からないというのは C++ を除くほとんどのオブジェクト指向言語が抱えている欠陥である (「常識的にメソッドの名前で分かるだろ」という手合は素晴しい同僚に恵まれた運命に感謝した方が良い。)

データ表現にレコードを使うと値の操作は必然的に自由関数として定義することになるため、その操作が破壊的か非破壊的かはシグネチャで明示される:

interface T {
  x: string;
  y: number;
}

type S = Readonly<T>;

// Mutates the given object's |x| property.
function setX(obj: T, newValue: string): void {
  obj.x = newValue;
}

// Clone the given object with new |x| property.
function withX(obj: S, newValue: string): S {
  return { ...obj, x: newValue };
}

(ただし明示されるだけである。TypeScript は "strictFuntionTypes": true な環境でさえ不変オブジェクトを可変な同型オブジェクトにキャストできる variance 何ソレ言語なので呼出側が間違えると救われない。この辺は flow の方が整合性がある。)

また JavaScript でデータ変換パイプラインを書くときは lodash や ramda など関数指向の外部ライブラリを頼ることになるので、データ操作に自由関数を使うことは見た目の一貫性の点でも好ましい。

それにつけても with の惜しさよ

モジュールを Namespace Import すると当然だがその中の型や値は Namespace Object 経由で参照することになる。

これは設計の意図するところではあるが、const x: Optional.T<string> = Optional.orDefault(Optional.map(getOptional(...), f), 'default value'); などと書かれたら const O = Optional; とか const { map, orDefault } = Optional; したくなるのが人情であろう。

要は JavaScript の関数が多相性を持たないことが問題である (まあレコードをデータ表現に使う限り同じ型なので多相に仕様がないのだが。) map とか flatMap みたいな操作はほとんどあらゆるデータ型に定義されるので、複数のデータ型を同時に扱う場合に各実装を区別するには Namespace Object を含んだ完全修飾名で参照するしかない。

これは Haskell とか Scala とかだと勿論問題にならなくて、型クラスのインスタンスであると宣言しておけば勝手にアドホック多相になる。Common Lisp や Raku (former Perl 6) のようなジェネリック関数を動的にディスパッチするシステムでも、解決が実行時になることを別にすれば同様である。

一方 OCaml など型クラスを持たない ML 系の言語には実は JavaScript と同じ問題があるのだが、OCaml の場合は字句的スコープに限定して Namespace Object に相当するモジュールを開く let open という操作ができる:

module My_lazy = struct
  include Lazy

  let map f v =
    Lazy.from_fun (fun () -> f (Lazy.force v))

  let bind v f =
    Lazy.from_fun (fun () -> Lazy.force (Lazy.force v |> f))

  let ( >>= ) = bind
end

let () =
  let open My_lazy in
  from_val 42
  |> map string_of_int
  |> map print_endline
  |> force

一つの式中で複数の抽象データ型を使う場合には依然として一方に完全修飾名を使う必要があるなど注意は要るが、それでも大分簡潔になるのが分かると思う。

かつての JavaScript には同じ機能が存在した (厳密に言えば今でもある): with 文である。指定したオブジェクトをスコープ・チェーンの先頭に追加した字句的スコープを提供する構文で、つまりそのオブジェクトのプロパティに変数としてアクセスできようになる:

// CAUTION: JavaScript code, not TypeScript since the language does not support |with|.

import * as Result from './result';

with (Result) {
  const n = Math.random();
  const r = n < 0.5 ? success(n) : failure(new Error('higher than expected'));
  match(r, {
    failure(e) {
      console.log(e);
    },
    success(n) {
      ...
    },
  });
}

非常に便利なのだが実行時に動的に名前が導入されることが最適化の妨げになり性能へのペナルティが大きいことにより現在では使用が推奨されていない。TypeScript でも意図的にサポートされていない機能である。

コメント

このブログの人気の投稿

多分週刊チラシの裏 (Sep 28 - Oct 04, 2020)

Chrome Web Store が有料 Chrome 拡張の取扱を終了 Chrome Web Store で提供されている有料 Chrome 拡張及びアプリ内課金 API の両方が 2021 年 1 月いっぱいで廃止される。 開発者はそれまでに代替となるサードパーティの課金 API に移行し、購入済ライセンスの移行手段も用意する必要がある。 この決定の発表時点で新規の有料ないしアプリ内課金のある Chrome 拡張の新規登録は終了している。実際のところ 2020 年 3 月時点で既に「一時的に」停止されており、その措置が恒久化されただけとの由。 シェルスクリプティングには長いオプションを使え 「短いオプション (e.g., -x ) はコマンドライン上での略記である。スクリプトにおいては自分や将来の同僚のためにも長いオプション (e.g., ---do-something ) を与える方が理解が容易だろう」という主張。 異論の余地なく正論である。 CobWeb - COBOL to WebAssembly Compiler COBOL から WebAssembly へのコンパイラ。いやマジで。 Cloudflare が何を思ったか同社のサーバレス環境である Workers に COBOL 対応を追加した際 の成果物である。 COBOL から C へのトランスレータである GNU COBOL と C コードをコンパイルして WebAssembly を出力する Emscripten から成っており、他の言語に比べて軽量なバイナリを生成するとのこと。 「ウチではそんな風にはやらないんだ (“We don’t do that here”)」 昨今ソフトウェア開発のコミュニティでも Code of Conduct を用意するところが増えてきたが、コミュニティの文化を明文化するのは難しい。 長大な「べからず集」は息苦しいし、肯定的なガイドラインは時に抽象的で実効的に使えない。問題となるようなふるまいの動機が善意であった場合は特にそうだ。 仮に優れたガイドラインがあっても、それに基いて人を実際に咎めるのは骨が折れることである。初中やればコミュニティ内でも疎まれる。 話の分かる相手ならそれでもまだ説得する意義もあるが、Web 上の対話で当事者双方が納得し合っ...

OCaml で Web フロントエンドを書く

要旨 フロントエンド開発に Elm は堅くて速くてとても良いと思う。昨今の Flux 系アーキテクチャは代数的データ型と相性が良い。ところで工数を減らすためにはバックエンドも同じ言語で書いてあわよくば isomorphic にしてしまいたいところだが、Elm はバックエンドを書くには現状適していない。 OCaml なら js_of_ocaml でエコシステムを丸ごとブラウザに持って来れるのでフロントエンドもバックエンドも無理なく書けるはずである。まず The Elm Architecture を OCaml で実践できるようにするため Caelm というライブラリを書いている。俺の野望はまだまだこれからだ (未完) Elm と TEA について Elm というプログラミング言語がある。いわゆる AltJS の一つである。 ミニマリスティクな ML 系の関数言語で、型推論を持ち、型クラスを持たず、例外機構を持たず、変数の再代入を許さず、正格評価され、代数的データ型を持つ。 言語も小綺麗で良いのだが、何より付属のコアライブラリが体現する The Elm Architecture (TEA) が重要である。 TEA は端的に言えば Flux フロントエンド・アーキテクチャの変種である。同じく Flux の派生である Redux の README に TEA の影響を受けたと書いてあるので知っている人もいるだろう。 ビューなどから非同期に送信される Message (Redux だと Action) を受けて状態 (Model; Redux だと State) を更新すると、それに対応して Virtual DOM が再構築されビューがよしなに再描画され人生を書き換える者もいた——という一方向の流れはいずれにせよ同じである。 差異はオブジェクトではなく関数で構成されていることと、アプリケーション外部との入出力は非同期メッセージである Cmd / Sub を返す規約になっていることくらいだろうか。 後者は面白い特徴で、副作用のある処理はアプリケーションの外で起きて結果だけが Message として非同期に飛んでくるので、内部は純粋に保たれる。つまり Elm アプリケーションが相手にしないといけない入力は今現在のアプリケーションの完全な状態である Model と、時系列イベ...

Mac から iPhone のカメラを起動して写真を直接取り込める

Via: The Verge ID セルフィーや (物理) 書籍のページスキャンなど携帯電話のカメラを使って写真を取り込むことは日常的な所作になっているが、写真の使い途が何かの申し込み用 Web フォームなどで iPhone より Mac の方が操作し易いときなどは億劫だ。Mac 組込の FaceTime カメラは 720p とか 1080p しかなくて非力すぎ、かといって iPhone で一旦撮影したものを Photos から探して AirDrop するのも面倒である。 実は macOS Mojave / iOS 12 以降には Continuity Camera という機能がある。これを使うと Apple 製の Mac アプリケーションから iPhone / iPad のカメラを起動して、余計な中間コピーを残すことなく写真を Mac に転送できる。 使い方は簡単で、対応している Mac アプリケーションのコンテキストメニューに “Import (or Insert) from iPhone (or iPad)” という項目がある。“Take Photo” だと一枚、“Scan Documents” だと複数の写真を (歪み補正しつつ) 連続で撮影して転送できる。 対応 Mac アプリケーションは Finder のほか iWork (Keynote, Numbers, Pages), Mail, Messages, Notes, TextEdit となっている、のだが実は Preview でも使える。同様にコンテキストメニューあるいは “File” メニューから起動できる。

Project Euler - Problem 25

問題 原文 What is the first term in the Fibonacci sequence to contain 1000 digits? 日本語訳 1000桁になる最初の項の番号を答えよ. 解答 Gaucheのストリームライブラリを使ってみました。 (use util.stream) (define fibonacci-sequence (iterator->stream (lambda (yield end) (let loop ((a 1) (b 1)) (yield a) (loop b (+ a b)))))) (define (digits n) (define (digits-1 m acc) (if (< n m) acc (digits-1 (* m 10) (+ acc 1)))) (digits-1 1 0)) (define (solve) (+ 1 (stream-index (lambda (n) (= 1000 (digits n))) fibonacci-sequence))) (define (main argv) (display (solve)) (newline))

多分週刊チラシの裏 (Oct 19, 2020 - Feb 26, 2021)

週刊とは言ったが毎週刊とは言ってないという言い訳。 C++ のコンパイルを高速化する小技 ビルドシステムやツールを変更せずともコーディングだけで改善できるコンパイル時間短縮テクニック。 #include を減らす インライン化を明示的に避ける 関数オーバーロードの可視性を制限する 公開シンボルを減らす の 4 本。 歯医者で歯を治したら記憶能力を失った話 歯医者で簡単な治療を受けた日から後、記憶が 90 分しか保持できなくなった英国の軍人の話。まるで「博士の愛した数式」だが実話である。 DRPK で売られていた Sim City っぽいゲームのリバースエンジニアリング 平壌市内のアプリストア (物理) で売られていた Sim City 風ゲームがインストールに失敗してライセンス認証で止まってしまったのでなんとか動かせないものかとリバースエンジニアリングしてみた話。 日本にあっては DPRK のデジタル事情というと 3G セルラーが現役とか国内 Web サイトのリストがポスター一枚に収まるとか何故かコンピュータ将棋の古豪とかの断片的な情報が伝え聞かれる程度だが、近頃は Android タブレットでゲームなどもできるらしい。 国内のインフラ及びエコシステム事情に合わせて元々フリーミアム + アプリ内課金モデルだったものが買い切り 5,000 KPW (< 1 USD) になっているなど、我々が失った自由が我々よりも不自由な (はずだと我々が信じている) 国に残存しているのは皮肉だろうか。 typosquatting は単なる typo じゃ済まない typo を狙って人気のあるドメインやソフトウェアに類似した名前をつける手法 (typosquatting) は人を辟易させるのみならずセキュリティの脅威である。 IQT が 2017 年から 2020 年にかけて Python ライブラリの中央リポジトリである PyPI において行った調査で、メジャーなライブラリに名前を似せたマルウェアが 40 個確認されたとのこと。 その内 16 個が単純なスペルミス狙い (e.g., “urlib3” vs. “urllib3”) で、26 個は正当なパッケージと混同するような名前 (e.g., “nmap-python” vs. “pytho...