SlideShare a Scribd company logo
1 of 15
Download to read offline
© 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>((file) => console.log(`Uploading: ${file.name}`)),
uploadFileEffect,
);
 
© Chatwork 14
まとめ
- 関数型プログラミングの考え方には、実用上も便利 なものが多い 👌
- なおかつ、理論的背景 もしっかりしている 📖
- Haskell を使うのは大変でも、Ramda.js だったらライト に始められる 🚀
- 時間があったら取り扱いたかったトピック:
- 合成可能な setter と getter の組,Lens
- Applicative による Validation の一般化
- map も、filter も、reduce も自由自在,Transducer
- (思い切って Session 枠を取りにいっても良かった感 🤔)
 
© Chatwork 15
働くを
もっと楽しく、
創造的に
We are Hiring !!!
Chatwork 株式会社では、
フロントエンド,バックエンドのどちらでも、
関数型の考え方で設計にチャレンジしたい
エンジニアを 募集🔗
しています 🙌

More Related Content

What's hot

やはりお前らのMVCは間違っている
やはりお前らのMVCは間違っているやはりお前らのMVCは間違っている
やはりお前らのMVCは間違っているKoichi Tanaka
 
暗号技術の実装と数学
暗号技術の実装と数学暗号技術の実装と数学
暗号技術の実装と数学MITSUNARI Shigeo
 
モジュールの凝集度・結合度・インタフェース
モジュールの凝集度・結合度・インタフェースモジュールの凝集度・結合度・インタフェース
モジュールの凝集度・結合度・インタフェースHajime Yanagawa
 
オブジェクト指向プログラミングの現在・過去・未来
オブジェクト指向プログラミングの現在・過去・未来オブジェクト指向プログラミングの現在・過去・未来
オブジェクト指向プログラミングの現在・過去・未来増田 亨
 
Python 3.9からの新定番zoneinfoを使いこなそう
Python 3.9からの新定番zoneinfoを使いこなそうPython 3.9からの新定番zoneinfoを使いこなそう
Python 3.9からの新定番zoneinfoを使いこなそうRyuji Tsutsui
 
Pythonの理解を試みる 〜バイトコードインタプリタを作成する〜
Pythonの理解を試みる 〜バイトコードインタプリタを作成する〜Pythonの理解を試みる 〜バイトコードインタプリタを作成する〜
Pythonの理解を試みる 〜バイトコードインタプリタを作成する〜Preferred Networks
 
できる!並列・並行プログラミング
できる!並列・並行プログラミングできる!並列・並行プログラミング
できる!並列・並行プログラミングPreferred Networks
 
PlaySQLAlchemy: SQLAlchemy入門
PlaySQLAlchemy: SQLAlchemy入門PlaySQLAlchemy: SQLAlchemy入門
PlaySQLAlchemy: SQLAlchemy入門泰 増田
 
DDDのモデリングとは何なのか、 そしてどうコードに落とすのか
DDDのモデリングとは何なのか、 そしてどうコードに落とすのかDDDのモデリングとは何なのか、 そしてどうコードに落とすのか
DDDのモデリングとは何なのか、 そしてどうコードに落とすのかKoichiro Matsuoka
 
ドメイン駆動設計サンプルコードの徹底解説
ドメイン駆動設計サンプルコードの徹底解説ドメイン駆動設計サンプルコードの徹底解説
ドメイン駆動設計サンプルコードの徹底解説増田 亨
 
それはYAGNIか? それとも思考停止か?
それはYAGNIか? それとも思考停止か?それはYAGNIか? それとも思考停止か?
それはYAGNIか? それとも思考停止か?Yoshitaka Kawashima
 
イミュータブルデータモデルの極意
イミュータブルデータモデルの極意イミュータブルデータモデルの極意
イミュータブルデータモデルの極意Yoshitaka Kawashima
 
PHP の GC の話
PHP の GC の話PHP の GC の話
PHP の GC の話y-uti
 
目grep入門 +解説
目grep入門 +解説目grep入門 +解説
目grep入門 +解説murachue
 
イミュータブルデータモデル(入門編)
イミュータブルデータモデル(入門編)イミュータブルデータモデル(入門編)
イミュータブルデータモデル(入門編)Yoshitaka Kawashima
 
メタプログラミングって何だろう
メタプログラミングって何だろうメタプログラミングって何だろう
メタプログラミングって何だろうKota Mizushima
 
OpenAPI 3.0でmicroserviceのAPI定義を試みてハマった話
OpenAPI 3.0でmicroserviceのAPI定義を試みてハマった話OpenAPI 3.0でmicroserviceのAPI定義を試みてハマった話
OpenAPI 3.0でmicroserviceのAPI定義を試みてハマった話Daichi Koike
 
世界一わかりやすいClean Architecture
世界一わかりやすいClean Architecture世界一わかりやすいClean Architecture
世界一わかりやすいClean ArchitectureAtsushi Nakamura
 
オブジェクト指向の設計と実装の学び方のコツ
オブジェクト指向の設計と実装の学び方のコツオブジェクト指向の設計と実装の学び方のコツ
オブジェクト指向の設計と実装の学び方のコツ増田 亨
 

What's hot (20)

やはりお前らのMVCは間違っている
やはりお前らのMVCは間違っているやはりお前らのMVCは間違っている
やはりお前らのMVCは間違っている
 
暗号技術の実装と数学
暗号技術の実装と数学暗号技術の実装と数学
暗号技術の実装と数学
 
モジュールの凝集度・結合度・インタフェース
モジュールの凝集度・結合度・インタフェースモジュールの凝集度・結合度・インタフェース
モジュールの凝集度・結合度・インタフェース
 
オブジェクト指向プログラミングの現在・過去・未来
オブジェクト指向プログラミングの現在・過去・未来オブジェクト指向プログラミングの現在・過去・未来
オブジェクト指向プログラミングの現在・過去・未来
 
Python 3.9からの新定番zoneinfoを使いこなそう
Python 3.9からの新定番zoneinfoを使いこなそうPython 3.9からの新定番zoneinfoを使いこなそう
Python 3.9からの新定番zoneinfoを使いこなそう
 
Pythonの理解を試みる 〜バイトコードインタプリタを作成する〜
Pythonの理解を試みる 〜バイトコードインタプリタを作成する〜Pythonの理解を試みる 〜バイトコードインタプリタを作成する〜
Pythonの理解を試みる 〜バイトコードインタプリタを作成する〜
 
できる!並列・並行プログラミング
できる!並列・並行プログラミングできる!並列・並行プログラミング
できる!並列・並行プログラミング
 
PlaySQLAlchemy: SQLAlchemy入門
PlaySQLAlchemy: SQLAlchemy入門PlaySQLAlchemy: SQLAlchemy入門
PlaySQLAlchemy: SQLAlchemy入門
 
DDDのモデリングとは何なのか、 そしてどうコードに落とすのか
DDDのモデリングとは何なのか、 そしてどうコードに落とすのかDDDのモデリングとは何なのか、 そしてどうコードに落とすのか
DDDのモデリングとは何なのか、 そしてどうコードに落とすのか
 
ドメイン駆動設計サンプルコードの徹底解説
ドメイン駆動設計サンプルコードの徹底解説ドメイン駆動設計サンプルコードの徹底解説
ドメイン駆動設計サンプルコードの徹底解説
 
それはYAGNIか? それとも思考停止か?
それはYAGNIか? それとも思考停止か?それはYAGNIか? それとも思考停止か?
それはYAGNIか? それとも思考停止か?
 
イミュータブルデータモデルの極意
イミュータブルデータモデルの極意イミュータブルデータモデルの極意
イミュータブルデータモデルの極意
 
PHP の GC の話
PHP の GC の話PHP の GC の話
PHP の GC の話
 
目grep入門 +解説
目grep入門 +解説目grep入門 +解説
目grep入門 +解説
 
イミュータブルデータモデル(入門編)
イミュータブルデータモデル(入門編)イミュータブルデータモデル(入門編)
イミュータブルデータモデル(入門編)
 
メタプログラミングって何だろう
メタプログラミングって何だろうメタプログラミングって何だろう
メタプログラミングって何だろう
 
OpenAPI 3.0でmicroserviceのAPI定義を試みてハマった話
OpenAPI 3.0でmicroserviceのAPI定義を試みてハマった話OpenAPI 3.0でmicroserviceのAPI定義を試みてハマった話
OpenAPI 3.0でmicroserviceのAPI定義を試みてハマった話
 
世界一わかりやすいClean Architecture
世界一わかりやすいClean Architecture世界一わかりやすいClean Architecture
世界一わかりやすいClean Architecture
 
オブジェクト指向の設計と実装の学び方のコツ
オブジェクト指向の設計と実装の学び方のコツオブジェクト指向の設計と実装の学び方のコツ
オブジェクト指向の設計と実装の学び方のコツ
 
プログラムを高速化する話
プログラムを高速化する話プログラムを高速化する話
プログラムを高速化する話
 

関数型プログラミングのデザインパターンひとめぐり

  • 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>((file) => console.log(`Uploading: ${file.name}`)), uploadFileEffect, );
  • 14.   © Chatwork 14 まとめ - 関数型プログラミングの考え方には、実用上も便利 なものが多い 👌 - なおかつ、理論的背景 もしっかりしている 📖 - Haskell を使うのは大変でも、Ramda.js だったらライト に始められる 🚀 - 時間があったら取り扱いたかったトピック: - 合成可能な setter と getter の組,Lens - Applicative による Validation の一般化 - map も、filter も、reduce も自由自在,Transducer - (思い切って Session 枠を取りにいっても良かった感 🤔)
  • 15.   © Chatwork 15 働くを もっと楽しく、 創造的に We are Hiring !!! Chatwork 株式会社では、 フロントエンド,バックエンドのどちらでも、 関数型の考え方で設計にチャレンジしたい エンジニアを 募集🔗 しています 🙌