狠狠撸

狠狠撸Share a Scribd company logo
? Chatwork
JSConf JP 2021
Chatwork 株式会社
CTO 室 / エンジニア採用広報
高瀬 和之 (@Guvalif)
関数型プログラミングの
デザインパターンひとめぐり with Ramda.js
 
? Chatwork
■ 概要 ?
Haskell ライクな関数型プログラミングを
JavaScript にて行えるようにするライブラリ "Ramda.js" を (一部) 例にして、
関数型プログラミングにおいて頻出のデザインパターンをご紹介します。
■ 関数型プログラミングに対する私の立ち位置 ?
- 良い設計を発見する ための、ベース知識として用いる
- 安全な開発を実現する ための、勘所として用いる
- これらは決してプログラミング言語に依存すること無く、
普遍的に応用可能 だと考える
- つまり、ライトユーザーです  (??????!
2
まえおき
 
? Chatwork 3
純粋関数
- 関数型プログラミングの大原則 → 主役は "純粋関数"
- 純粋関数とは?
- 引数に同じ値を与えたら、常に同じ戻り値を返す関数のこと
- なおかつ、副作用が存在しないもの
- "参照透過" というキーワードで、ひとえに説明しても良い
- なぜ純粋関数を用いるのか?
- 副作用が存在しない ≒ 挙動が予測しやすい
- 型システム を併用することで、予測可能性をさらに高める ことができる
const add: (_0: number, _1: number) => number =
(x, y) => x + y;
const log: (_: string) => void =
(s) => console.log(s);
純粋関数の例 (インプレイスな置き換えが可能) 純粋関数でない例 (外部から観測できない作用がある)
 
? 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);
 
? 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' });
 
? 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);
} ファクトリ関数とする
 
? 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>
 
? 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);
 
? 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
 
? 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);
}
};
 
? 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)));
 
? Chatwork 12
複合的なユースケース → "ブラックジャック" の例
- Qiita に参考記事を掲載しているので、ぜひ一読してみてください ?
- cf. 『JavaScript で (なるべく) 関数型にブラックジャックを実装する』 by Guvalif
×
 
? 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,
);
 
? Chatwork 14
まとめ
- 関数型プログラミングの考え方には、実用上も便利 なものが多い ?
- なおかつ、理論的背景 もしっかりしている ?
- Haskell を使うのは大変でも、Ramda.js だったらライト に始められる ?
- 時間があったら取り扱いたかったトピック:
- 合成可能な setter と getter の組,Lens
- Applicative による Validation の一般化
- map も、?lter も、reduce も自由自在,Transducer
- (思い切って Session 枠を取りにいっても良かった感 ?)
 
? Chatwork 15
働くを
もっと楽しく、
創造的に
We are Hiring !!!
Chatwork 株式会社では、
フロントエンド,バックエンドのどちらでも、
関数型の考え方で設計にチャレンジしたい
エンジニアを 募集?
しています ?

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 株式会社では、 フロントエンド,バックエンドのどちらでも、 関数型の考え方で設計にチャレンジしたい エンジニアを 募集? しています ?