Share
Embed
Transcript
40 slides
-
Welcome to the "Fantasy Land" 🧚 − 代数的構造をめぐる冒険 − #TSKaigi
Hokuriku 2025 --- @Guvalif (TAKASE Kazuyuki) -
余談) 運営スタッフの @berlysia さんとは、同じ会社 & 本部の所属です 自己紹介 - 高瀬
和之 (TAKASE Kazuyuki) >=> 𝕏:@Guvalif Profile { works = [ "IoT エンジニア", "フロントエンド・エンジニア", "開発人事", "TechPM (← 現職)", "エンジニア育成 (← 複業)" ], memo = "関数型 XXX を名乗りがち 😗" } -
冒険の地図 (本編) 🧭 >=> `Promise` の型定義 `Result` の型定義 `Promise` の振る舞い
`Result` の振る舞い 代数的構造の仕様 抽象化した型定義 抽象化した振る舞い 代数的構造 "Fantasy Land" 🧚 -
`Promise` の型定義 es5.d.ts を参照することで `PromiseLike<T>` という型定義が得られる (冗長さを省くと以下): ``` ``` >=>
interface PromiseLike<T> { then<A, B>( onfulfilled: (value: T ) => A | PromiseLike<A>, onrejected: (reason: any) => B | PromiseLike<B>, ): PromiseLike<A | B>; } - `then` メソッドを持つ - `onfulfilled` および `onrejected` として、Callback を設定できる - いずれの Callback も ある型の値 もしくは `PromiseLike` で包まれたある型の値 を返す -
`Result` の型定義 (1) result.ts を参照することで `Result<T, E>` という型定義が得られる (冗長さを省くと以下): ```
``` >=> type Result<T, E> = Ok<T, E> | Err<T, E>; class Ok<T, E> { map<A> (f: (t: T) => A ): Result<A, E> { ... } andThen<A, F>(f: (t: T) => Result<A, F>): Result<A, E | F> { ... } } class Err<T, E> { map<B> (f: (t: T) => B ): Result<B, E> { ... } andThen<B, F>(f: (t: T) => Result<B, F>): Result<B, E | F> { ... } } -
`Result` の型定義 (2) 型定義が対称なので、簡単のために `Ok<T, E>` のみに着目すると ... ``` ```
>=> type Result<T, E> = Ok<T, E> | Err<T, E>; class Ok<T, E> { map<A> (f: (t: T) => A ): Result<A, E> { ... } andThen<A, F>(f: (t: T) => Result<A, F>): Result<A, E | F> { ... } } - `map` および `andThen` メソッドを持つ - それぞれ `f` として Callback を設定できる: - `map` の Callback は ある型の値 を返す - `andThen` の Callback は `Result<?, F>` で包まれたある型の値 を返す -
e.g. `onfulfilled` に着目してみる (1) `onfulfilled` に関係ない部分を削除し Pretify すると ... ```
``` >=> interface PromiseLike<T> { then<A, B>( onfulfilled: (value: T ) => A | PromiseLike<A>, onrejected : (reason: any) => B | PromiseLike<B>, ): PromiseLike<A | B>; } interface PromiseLike<T> { then<A>(onfulfilled: (value: T) => A | PromiseLike<A>): PromiseLike<A>; } -
ここで、シンボル名を無視して 型に着目 すると、定義の類似 が見て取れる ``` ``` e.g. `onfulfilled` に着目してみる (2)
`then` の定義を Overload すると ... (※ 厳密には完全分離できない可能性があることに注意) ``` ``` >=> interface PromiseLike<T> { then<A>(onfulfilled: (value: T) => A ): PromiseLike<A>; then<A>(onfulfilled: (value: T) => PromiseLike<A>): PromiseLike<A>; } class Ok<T, E> { map<A> (f: (t: T) => A ): Result<A, E> { ... } andThen<A, F>(f: (t: T) => Result<A, F>): Result<A, E | F> { ... } } -
現在地点の確認 >=> `Promise` の型定義 `Result` の型定義 `Promise` の振る舞い `Result` の振る舞い
代数的構造の仕様 抽象化した型定義 抽象化した振る舞い 代数的構造 "Fantasy Land" 🧚 ✅ ✅ ✅ -
暗黙的な `this` にも型を明示することで、`then` メソッドの振る舞いを整理してみる: with Promise _then(onfulfilled)_ `Promise` の振る舞い >=>
Promise<T> _onfulfilled_ Promise<A> this T A _then(onfulfilled)_ Promise<T> Promise<A> this T _onfulfilled_ ※ 簡単のために `onfulfilled` のみに着目 then<A>(this: PromiseLike<T>, onfulfilled: (value: T) => A ): PromiseLike<A> then<A>(this: PromiseLike<T>, onfulfilled: (value: T) => PromiseLike<A>): PromiseLike<A> -
暗黙的な `this` にも型を明示することで、`map` および `andThen` メソッドの振る舞いを整理してみる: with Result _map(f)_ `Result`
の振る舞い >=> Result<T, E> _f_ Result<A, E> this T A _andThen(f)_ Result<T, E> Result<A, E> this T _f_ ※ 簡単のために型パラメータ `E = F` として図示 map<A> (this: Result<T, E>, f: (t: T) => A ): Result<A, E> andThen<A>(this: Result<T, E>, f: (t: T) => Result<A, E>): Result<A, E> -
`PromiseLike<?>` と `Result<?, E>` を、ざっくり `M<?>` として抽象化してみる: _foo(f)_ 似ている Interface
に着目する >=> M<T> _f_ M<A> this T A _bar(kf)_ M<T> M<A> this T _kf_ ※ いったんそれぞれのメソッド名は仮置き with M foo<A>(this: M<T>, f: (t: T) => A ): M<A>; bar<A>(this: M<T>, kf: (t: T) => M<A>): M<A>; -
現在地点の確認 >=> `Promise` の型定義 `Result` の型定義 `Promise` の振る舞い `Result` の振る舞い
代数的構造の仕様 抽象化した型定義 抽象化した振る舞い 代数的構造 "Fantasy Land" 🧚 🤔 ✅ ✅ -
- 型 の表現は似ている ✅ - Interface の表現は似ている ✅ - しかし、振る舞いの対応
までは確信が持てない 🤔 現状の整理 >=> _foo(f)_ M<T> _f_ M<A> this T A _bar(kf)_ M<T> M<A> this T _kf_ with M -
"代数的構造" を考える 【 "代数的構造" とは?】 - おもに抽象代数学で議論される概念 - 型 (≒
集合) と Interface (≒ 演算) に関して、どのようなルールが満たされるか を定めるもの - プログラミングで言うところの、デザインパターンに相当するもの 【 なぜ "代数的構造" を考えたいか?】 - 数学に由来する概念なので、論証の正確性 を担保しやすい - 19 世紀から研究されているので、巨人の肩に乗りやすい 🥳 >=> -
事前知識:TypeScript ⇔ Fantasy Land における型シグネチャの比較 >=> 関数型 多相型 (いわゆるジェネリクス) カインド
(型関数) 型制約 // `,` 区切りで引数を増やす (_0: A, _1: B) => C -- `->` で連結して引数を増やす A -> B -> C // 項に紐づく `<T>` 部分で多相性を明示 length: <T>(_0: T[]) => number -- 小文字で記述することで多相性を明示 length :: [t] -> Int // 型に紐づく `<T>` 部分で型引数を明示 type Thenable<T> = ... -- スペース区切りで型引数を明示 Thenable t // `extends` に続けて制約を記述 sort: <T extends Ord>(_0: T[]): => T[] -- `=>` の手前に制約を記述 sort :: Ord t => [t] -> [t] -
次のような型 (と Interface) を考える: ``` ``` e.g. Setoid の仕様を読み解いてみる >=>
interface S { ['fantasy-land/equals'](s: S): boolean; } `S` が Setoid という代数的構造を満たすとしたら 、任意のインスタンス `a`, `b`, `c` に対して: 1. `a['fantasy-land/equals'](a) === true` 2. `a['fantasy-land/equals'](b) === b['fantasy-land/equals'](a)` 3. If `a['fantasy-land/equals'](b)` and `b['fantasy-land/equals'](c)` , then `a['fantasy-land/equals'](c)` ... これら 3 つのルールが成立 する -
試しに以下の Interface に関して、Fantasy Land の仕様を探してみる 🔍: というわけで ... >=> _bar(kf)_
M<T> M<A> this T _kf_ with M ⇔ ※ もう片方の Interface に関しては、ぜひ皆さまのお手元で探索まで 💪 -
次のような型 (と Interface) を考える: ``` ``` 候補:Chain の仕様 >=> interface
M<T> { ['fantasy-land/chain'](kf: (t: T) => M<A>): M<A>; } `M<T>` が Chain という代数的構造を満たすとしたら、任意のインスタンス `m` に対して: - `m['fantasy-land/chain'](f)['fantasy-land/chain'](g)` is equivalent to `m['fantasy-land/chain'](x => f(x)['fantasy-land/chain'](g))` ... このルールが成立する ⇒ `PromiseLike<T>` と `Result<T, E>` の 双方でルールが成立する なら、Chain として抽象化 できる! -
現実案:Property-based Testing を用いる 宣言的な記述 にもとづいて、ランダムな入力パターンを自動生成しテストを行う 仕組みのこと TypeScript においては fast-check などのライブラリで実現できる
📝 【 代数的構造の現実的な確認手順 】 1. 型や Interface を観察し、共通化できそうかあたりを付ける 2. Fantasy Land を参照し、仕様の候補を洗い出す 3. Propety-based Testing を活用し、仕様を (大まかに) 満たせるか確認する >=> -
Property-based Testing の実装例 (1) `Promise` そのままだと Interface を満たせないので、ラップした class を準備:
>=> class M<T> { private constructor(public readonly value: PromiseLike<T>) {} ['fantasy-land/chain']<A>(kf: (t: T) => M<A>): M<A> { return new M(this.value.then(t => kf(t).value)); } static of<T>(t: T): M<T> { return new M(Promise.resolve(t)); } } -
Property-based Testing の実装例 (2) >=> Chain の仕様をもとにテストを作成 ( `Result` 版も同様な流れで作成できる
): const chainAssociativity = fc.asyncProperty( fc.integer(), async (x) => { const m = M.of(x); // f と g を動的に用意するとより精緻 const lhs = await m['fantasy-land/chain'](f)['fantasy-land/chain'](g).value; const rhs = await m['fantasy-land/chain'](x => f(x)['fantasy-land/chain'](g)).value; return lhs === rhs; }, ); await fc.assert(chainAssociativity, { numRuns: 100 }); -
実際どうなるか? 以下の条件のもと、`Promise` と `Result` は Chain として抽象化しても問題ない ✅: - `onrejected`
が実行されることのない `Promise` のみを用いる - 型定義が `(reason: any) => B | PromiseLike<B>` な時点で怪しさはあったが、実際ダメ 😇 - `Promise<Promise<T>>` については考えない - なぜならネストした `Promise` のインスタンスは そもそも作ることができない から 代数的構造に着目したおかげで、破綻の兆候を事前に検知 できたとも言える → より良い Interface の探索 にもつながる ( e.g. `() => Promise<Result<T, E>>` ) >=> -
現在地点の確認 >=> `Promise` の型定義 `Result` の型定義 `Promise` の振る舞い `Result` の振る舞い
代数的構造の仕様 抽象化した型定義 抽象化した振る舞い 代数的構造 "Fantasy Land" 🧚 ✅ ✅ 𝑭𝒊𝒏. -
Fantasy Land の気になるところ class の利用を前提にしている こと 🤔 (※ class を絶対悪とは思っていません)
【 class の良いところ 】 - 🥳 戻り値を単一の class に閉じることで、メソッドチェーンによる見通しの良さが得られる - 🥳 class 自体が 1 つの名前空間として機能する 【 class の微妙なところ 】 - 🤔 Serialize と Deserialize に関する考慮が必要 - 🤔 不変クラスを徹底しないと、内部に持つ状態が不用意な複雑性を招きかねない >=> -
e.g. 関数による実装 Currying Data-last Function を用いることで、関数合成がメソッドチェーン とみなせるが ... >=> const
data = [ { name: 'john', age: 20, gender: 'm' }, { name: 'marry', age: 22, gender: 'f' }, { name: 'samara', age: 24, gender: 'f' }, ]; R.pipe( data, R.filter((x) => x.gender === 'f'), R.groupBy((x) => x.age), ); data .filter((x) => x.gender === 'f') .groupBy((x) => x.age); - そのためだけにライブラリを導入するか?🤔 - 初見だと少し ギョッ としないか?🤔 -
TC39:Call-this Operator ECMAScript Stage-1 Proposal として議論されているもの TypeScript 界隈だと @yukukotani さんが積極的にアウトプットされているので、ぜひチェックまで!🔍
>=> origin :https://speakerdeck.com/yukukotani/my-own-typescript-future -
- `Promise` と `Result` を例に、型や Interface を通じて相互の類似を考えた - 一方で、似ている からといって
必ずしも共通化できるとは限らない ことも確かめた - Fantasy Land (代数的構造 の仕様) は、そのような 共通化を紐解くヒント 【 さらに学ぶために 】 代数的構造は "データ構造" とも密接な関連を持つので、以下の記事も参考まで 📝 まとめ >=>