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

私家版 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 でも意図的にサポートされていない機能である。

コメント

このブログの人気の投稿

LIBLINEAR 2.41 で One-class SVM が使えるようになったので Perl から触ってみよう

改訂 (Sep 15, 2020): 必要のない手順を含んでいたのでサンプルコードと記述を修正しました。 CPAN に Algorithm::LibLinear 0.22 がリリースされました (しました。) 高速な線形 SVM およびロジスティック回帰による複数の機械学習アルゴリズムを実装したライブラリである LIBLINEAR への Perl バインディングです。 利用している LIBLINEAR のバージョンが LIBLINEAR 2.30 から LIBLINEAR 2.41 に上がったことで新しいソルバが追加され、One-class SVM (OC-SVM) による一値分類が利用可能になっています (しました。) OC-SVM って何 一値分類を SVM でやること。 一値分類って何 ある値が学習したクラスに含まれるか否かを決定する問題。 HBO の「シリコンバレー」に出てきた「ホットドッグ」と「ホットドッグ以外」を識別するアプリが典型。「ホットドッグ以外」の方は犬でも神でも一つの指輪でも何でも含まれるのがミソ。 二値分類の場合正反両者のデータを集める必要があるのに対して、一値分類の学習器は正例データのみしか要求しない (ものが多い。) 主な用途は外れ値検出で、もちろんホットドッグやホットドッグ様のものを検出したりもできる。 使い方 手順自体は他の二値ないし多値分類問題と同じです。つまり、 訓練パラメータを決めて 訓練データセットで訓練して テストデータセットで確度を検証して 十分良くなったらモデルを保存する といういつもの流れ。 訓練パラメータ use 5.032 ; use Algorithm::LibLinear ; my $learner = Algorithm::LibLinear ->new( epsilon => 0.01 , nu => 0.75 , solver => ' ONECLASS_SVM ' , ); solver => 'ONECLASS_SVM' が一値分類用のソルバです。LIBLINEAR の train コマンドで言うところの -s 21 。 OC-SVM の良いところは (ハイパー)...

Perl の新 class 構文を使ってみる

Perl 5 のオブジェクト指向機能は基本的には Python の影響を受けたものだが、データを名前空間 (package) に bless する機構だけで Perl 4 以来の名前空間とサブルーチンをそのままクラスとメソッドに転換し第一級のオブジェクト指向システムとした言語設計は驚嘆に価する。 実際この言語のオブジェクトシステムは動的型付言語のオブジェクト指向プログラミングに要求されるおよそあらゆる機能を暗にサポートしており、CPAN には Moose を筆頭とした屋下屋オブジェクトシステムが複数存在しているがその多くは Pure Perl ライブラリである。つまり「やろうと思えば全部手書きで実現できる」わけである。 そういうわけで Perl のオブジェクト指向プログラミングサポートは機能面では (静的型検査の不在という現代的には極めて重大な欠如を除けば) 申し分ないのだが、しかし Moose その他の存在が示しているように一つ明らかな欠点がある。記述の冗長さだ。 コンストラクタを含むあらゆるメソッドは第一引数としてレシーバを受ける単なるサブルーチンとして明示的に書く必要があるし、オブジェクトのインスタンス変数 (a.k.a. プロパティ / データメンバ) は bless されたデータに直接的ないし間接的に プログラマ定義の方法 で格納されるためアクセス手段は実装依存である。これはカプセル化の観点からは望ましい性質だが、他者の書いたクラスを継承するときに問題となる。ある日データ表現を変更した親クラスがリリースされると突然自分の書いた子クラスが実行時エラーを起こすようになるわけだ。 そうならないためにはインスタンス変数へのアクセスに (protected な) アクセサを使う必要があるのだが、そのためには親クラスが明示的にそれらを提供している必要があるし、そもそも Perl にはメソッドのアクセス修飾子というものがないので完全な制御を与えるならばオブジェクトの内部状態がすべて public になってしまう。 そのような事情もあり、特にパフォーマンスが問題にならないようなアプリケーションコードでは Moose のようなリッチな語彙を提供するオブジェクトシステムを使うことが 公式のチュートリアルでも推奨 されてきた。Perl コアのオブジェクトシステムの改良は...

Perl 5 to 6 - コンテキスト

2011-02-27: コメント欄で既に改訂された仕様の指摘がありました ので一部補足しました。 id:uasi に感謝します。 これはMoritz Lenz氏のWebサイト Perlgeek.de で公開されているブログ記事 "Perl 5 to 6" Lesson 06 - Contexts の日本語訳です。 原文は Creative Commons Attribution 3.0 Germany に基づいて公開されています。 本エントリには Creative Commons Attribution 3.0 Unported を適用します。 Original text: Copyright© 2008-2010 Moritz Lenz Japanese translation: Copyright© 2011 SATOH Koichi NAME "Perl 5 to 6" Lesson 06 - コンテキスト SYNOPSIS my @a = <a b c> my $x = @a; say $x[2]; # c say (~2).WHAT # Str() say +@a; # 3 if @a < 10 { say "short array"; } DESCRIPTION 次のように書いたとき、 $x = @a Perl5では $x は @a より少ない情報—— @a の要素数だけ——しか持ちません。 すべての情報を保存しておくためには明示的にリファレンスを取る必要があります: $x = \@a Perl6ではこれらは反対になります: デフォルトでは何も失うことなく、スカラ変数は配列を単に格納します。 これは一般要素コンテキスト(Perl5で scalar と呼ばれていたもの)及びより特化された数値、整数、文字列コンテキストの導入によって可能となりました。無効コンテキストとリストコンテキストは変更されていません。 特別な構文でコンテキストを強制できます。 構文 コンテキスト ~stuff 文字列 ?stuff 真理値 +stuff ...

去る6月に Perl 5.32.0 がリリースされたので差分を把握するために perldelta を読んだ件

要旨 Perl 5 メジャーバージョンアップの季節がやって来たのでまともな Perl プログラマの嗜みとして perldelta を読んだ。 今回は有り体に言えばルーティン的なリリースで、言語コアの拡張は他言語にも見られる構文が実験的に入ったくらいで大きな変化はない。新機能は RegExp の拡充が主である。 比較的重要と思われる変更点を抜粋する。 新機能 isa 演算子 実験的機能。Python とか Java における isinstance とか instanceof 。 これまでも UNIVERSAL::isa があったが、これはメソッドなのでレシーバにオブジェクトでもクラスでもない値 (i.e., 未定義値 / bless されていないリファレンス) を置くと実行時エラーが起きるのが問題だった: package Foo { use Moo; } package Bar { use Moo; extends ' Foo ' ; } package Baz { use Moo; } use feature qw/ say / ; sub do_something_with_foo_or_return_undef { my ( $foo ) = @_ ; # Returns safely if the argument isn't an expected instance, in mind. return unless $foo -> isa ( ' Foo ' ); ...; } # OK. do_something_with_foo(Bar->new); # |undef| is expected in mind, but actually error will be thrown. do_something_with_foo( undef ); これを避けるために今までは Scalar::Util::blessed を併用したりしていたわけだが、 isa 演算子は左辺が何であっても意味のある値を返すのでよりシンプルになる: # True +( bless +{} ...

Perl 5 to 6 - 遅延性

これはMoritz Lenz氏のWebサイト Perlgeek.de で公開されているブログ記事 "Perl 5 to 6" Lesson 12 - Laziness の日本語訳です。 原文は Creative Commons Attribution 3.0 Germany に基づいて公開されています。 本エントリには Creative Commons Attribution 3.0 Unported を適用します。 Original text: Copyright© 2008-2010 Moritz Lenz Japanese translation: Copyright© 2011 SATOH Koichi NAME "Perl 5 to 6" Lesson 12 - 遅延性 SYNOPSIS my @integers = 0..*; for @integers -> $i { say $i; last if $i % 17 == 0; } my @even := map { 2 * $_ }, 0..*; my @stuff := gather { for 0 .. Inf { take 2 ** $_; } } DESCRIPTION Perlプログラマは怠けがちです。彼らが使うリストも。 ここで怠惰という言葉が意味するのは、評価が可能な限り遅延されるということです。 @a := map BLOCK, @b のようなコードを書いたとき、ブロックは一切実行されません。 @a の要素にアクセスしようとしたときだけ map は実際にブロックを実行し、必要とされる分だけ @a を埋めます。 代入ではなくバインディングを使っていることに注意して下さい: 配列への代入は先行評価を強制することがあります(コンパイラがリストの無限性に気づかない限り; 無限リスト検出の詳細はまだ固まっていません)。 バインディングはそのようなことがありません。 遅延性は無限リストの取り扱いを可能にします: 引数すべてに操作を行うようなことさえしなければ、評価された要素に必要なだけのメモリしか必要としません。 しかし落とし穴があります: 長さの...