狠狠撸
Submit Search
関数型プログラミングのデザインパターンひとめぐり
?
2 likes
?
3,818 views
K
Kazuyuki TAKASE
Follow
このスライドは、2021/11/27 に開催された「JSConf JP 2021」で発表したものです。
Read less
Read more
1 of 15
Download now
Download to read offline
More Related Content
関数型プログラミングのデザインパターンひとめぐり
1.
? Chatwork JSConf JP
2021 Chatwork 株式会社 CTO 室 / エンジニア採用広報 高瀬 和之 (@Guvalif) 関数型プログラミングの デザインパターンひとめぐり with Ramda.js
2.
? Chatwork ■ 概要
? Haskell ライクな関数型プログラミングを JavaScript にて行えるようにするライブラリ "Ramda.js" を (一部) 例にして、 関数型プログラミングにおいて頻出のデザインパターンをご紹介します。 ■ 関数型プログラミングに対する私の立ち位置 ? - 良い設計を発見する ための、ベース知識として用いる - 安全な開発を実現する ための、勘所として用いる - これらは決してプログラミング言語に依存すること無く、 普遍的に応用可能 だと考える - つまり、ライトユーザーです (??????! 2 まえおき
3.
? Chatwork 3 純粋関数 -
関数型プログラミングの大原則 → 主役は "純粋関数" - 純粋関数とは? - 引数に同じ値を与えたら、常に同じ戻り値を返す関数のこと - なおかつ、副作用が存在しないもの - "参照透過" というキーワードで、ひとえに説明しても良い - なぜ純粋関数を用いるのか? - 副作用が存在しない ≒ 挙動が予測しやすい - 型システム を併用することで、予測可能性をさらに高める ことができる const add: (_0: number, _1: number) => number = (x, y) => x + y; const log: (_: string) => void = (s) => console.log(s); 純粋関数の例 (インプレイスな置き換えが可能) 純粋関数でない例 (外部から観測できない作用がある)
4.
? Chatwork 4 カリー化 -
"複数の引数を取る関数" と "単一の引数を取る関数" は、相互変換ができる - 理論的背景を知りたい方は ... → "積対象 指数対象 随伴" で Let's Google ? - cf. 『圏論と Swift への応用』 by inamiy さん const add : (_0: number, _1: number) => number = (x, y) => x + y; const add_: (_0: number) => (_1: number) => number = (x) => (y) => x + y; // 使い方が異なるだけで、効果は変わらない add(1, 2) === add_(1)(2);
5.
? Chatwork 5 カリー化による設計上のメリット -
例) カリー化による環境の固定 - 同様に、Dependency Injection にも応用できる - ちなみに、Ramda.js では全ての関数が標準でカリー化 されている ? const buildSendMessageRequest = ({ secret, roomId }: ChatworkEnv) => (body: string) => ({ method: 'POST', url: `https://api.chatwork.com/v2/rooms/${roomId}/messages`, headers: { 'X-ChatworkToken': secret, }, data: `body=${body}`, }); // builder: (body: string) => Request として、簡便に使いまわすことができる const builder = buildSendMessageRequest({ secret: 'XXXX', roomId: '123456789' });
6.
? Chatwork 6 副作用との付き合い方 -
"純粋関数が主役" という考え方より、副作用も観測可能にしたい ... - どうする? → 副作用をなるべくデータ型として表現する (≒ 作用を型で明示する) - 例) Tagged Union Type により、try - catch を用いずに異常系を表現する interface Left<T> { value: T; _tag: 'Left'; } interface Right<T> { value: T; _tag: 'Right'; } // 慣例的に、Left が異常系,Right が正常系を表す type Either<L, R> = Left<L> | Right<R>; function safeDiv( x: number, y: number, ): Either<Error, number> { if (y === 0) { return left(new Error('/ by 0 !')); } return right(x / y); } ファクトリ関数とする
7.
? Chatwork 7 作用を便利に扱うための演算群 -
慣例的に、ジェネリック関数の3つ組を考えることが多い - 理論的背景を知りたい方は ... → "計算効果 Monad" で Let's Google ? - cf. 『Notions of Computation and Monads』 by Eugenio Moggi // Haskell では 'pure' という名で知られる // cf. https://ramdajs.com/docs/#of of: <T>(value: T) => M<T> // Haskell では 'fmap' という名で知られる // cf. https://ramdajs.com/docs/#map map: <A, B>(f: (_: A) => B) => (_: M<A>) => M<B> // Haskell では 'bind' という名で知られる // cf. https://ramdajs.com/docs/#chain chain: <A, B>(kf: (_: A) => M<B>) => (_: M<A>) => M<B>
8.
? Chatwork 8 各演算群の直感的な意味 -
例) M<_> を Promise<_> で置き換えると ... - ※ 厳密には、Promise だと良い性質を持たないので注意! // "値" を "作用のある値" に変換する (※ もっとも自明な変換を用いる) const of: <T>(value: T) => Promise<T> = (value) => Promise.resolve(value); // "関数" を "作用のある関数" に変換する const map: <A, B>(f: (_: A) => B) => (_: Promise<A>) => Promise<B> = (f) => (promise) => promise.then(f); // "途中で作用をもつ関数" を "作用のある関数" に変換する const chain: <A, B>(kf: (_: A) => Promise<B>) => (_: Promise<A>) => Promise<B> = // .then メソッドの性質から、map と実装が変わらない (ややつまらない実装) (kf) => (promise) => promise.then(kf);
9.
? Chatwork of の役割: "値"
から ... "作用のある値" への変換 map の役割: "関数" から ... "作用のある関数" への変換 chain の役割: "途中で作用をもつ関数" から ... "作用のある関数" への変換 map(f): (_: M<A>) => M<B> f: (_: A) => B 9 各演算群の直感的な意味 (図解版) X A B M<A> M<B> map M<X> of: (_: X) => M<X> M<A> M<B> chain(kf): (_: M<A>) => M<B> kf: (_: A) => M<B> A chain
10.
? Chatwork 10 ADT
と Catamorphism - Union Type や Tuple Type を (複合的に) 用いたデータ型を、ADT と呼ぶ - ADT = Argebraic Data Type の意 → 代数的データ型 - ADT に対しては、自然な分解と変換 を定義することができる → 代表例が Catamorphism - 理論的背景を知りたい方は ... → "F 始代数" で Let's Google ? const either = <L, R, T>(lmap: (_: L) => T) => (rmap: (_: R) => T) => (m: Either<L, R>): T => { // 対称性を強調するために、それぞれの場合分けを明示的に記述 if (m._tag === 'Left') { return lmap(m.value); } if (m._tag === 'Right') { return rmap(m.value); } };
11.
? Chatwork 11 Catamorphism
の応用例 - 例) Either に対する、統一的なエラーハンドリング - ちなみに、配列に対する reduceRight も Catamorphism だったりする - いわゆる "畳み込み" と呼ばれる操作は、Catamorphism として一般化できる const lmap = (e: Error) => e.name; const rmap = (x: number) => `${x}`; const handler: (_: Either<Error, number>) => string = either(lmap)(rmap); // try - catch 方式と、大きく使い勝手が変わらない console.log(handler(safeDiv(N / M)));
12.
? Chatwork 12 複合的なユースケース
→ "ブラックジャック" の例 - Qiita に参考記事を掲載しているので、ぜひ一読してみてください ? - cf. 『JavaScript で (なるべく) 関数型にブラックジャックを実装する』 by Guvalif ×
13.
? Chatwork 13 現実的なユースケース
in Chatwork → リリース基盤のバックエンド実装 - Ramda.js をフル活用して、CLI や CRON を実装しています ? - 余談) 公式技術ブログに記事化したいと思い続けて、一年半が過ぎた ? const rejectEmptyEffect = R.ifElse( R.isEmpty, () => Promise.reject(new Error('スナップショットに含めるべきファイルが存在しません')), Promise.resolve.bind(Promise), ); const uploadFileEffect = createUploadFileEffect(s3Env, birdcageEnv); const uploadFileWithLoggingEffect = R.pipe( R.tap<File>((?le) => console.log(`Uploading: ${?le.name}`)), uploadFileEffect, );
14.
? Chatwork 14 まとめ -
関数型プログラミングの考え方には、実用上も便利 なものが多い ? - なおかつ、理論的背景 もしっかりしている ? - Haskell を使うのは大変でも、Ramda.js だったらライト に始められる ? - 時間があったら取り扱いたかったトピック: - 合成可能な setter と getter の組,Lens - Applicative による Validation の一般化 - map も、?lter も、reduce も自由自在,Transducer - (思い切って Session 枠を取りにいっても良かった感 ?)
15.
? Chatwork 15 働くを もっと楽しく、 創造的に We
are Hiring !!! Chatwork 株式会社では、 フロントエンド,バックエンドのどちらでも、 関数型の考え方で設計にチャレンジしたい エンジニアを 募集? しています ?
Download