11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TypeScriptの動かせるコードで型クラスを理解する(Semigroup, Monoid, Functor)

Last updated at Posted at 2022-01-30

やりたいこと

TypeScriptにおける型クラス(Type Class) について整理したい。

  • 断片的なコードでは理解が難しいので、TypeScript Playgroundで動かして試せるコードを用意する
    • リンクを開いて「実行」を押せばすぐ結果が表示される
    • 誰でもコードを改変していろいろ試すことができる
  • 長くないコードで比較的理解しやすいSemigroup, Monoid, Functorについて扱う
    • これらはTypeScriptに限らず、関数型プログラミングでは一般的な概念
  • Haskellや圏論等由来の用語はなるべく使わない
    • 厳密さや網羅性は犠牲になっている

TypeScriptにおける型クラスとは

任意の型 A に対して、「こんな操作ができるよ」と定義した「型」。

型クラスの実例1 Semigroup

サンプルコード SemigroupSample.ts (TypeScript Playground)

Semigroupの型クラス

任意の型 A に対して「結合(concat)ができるよ」と定義した「型」。

SemigroupSample.ts
interface Semigroup<A> {
  readonly concat: (x: A, y: A) => A
}

Semigroupのインスタンス(string編)

型クラスを「任意の型」で実現したものをインスタンスと呼ぶ。
以下はstringに対して「結合(文字列連結)ができるよ」と定義した「インスタンス」。

SemigroupSample.ts
const semigroupString: Semigroup<string> = {
  concat: (x, y) => x + y,
}

以下のように使うことができる。

SemigroupSample.ts
semigroupString.concat('hoge', 'fuga') // hogefuga

Semigroupの「結合」は、以下のように結合法則を満たす。
これはインスタンスを作る側が動作を保証する必要がある。

SemigroupSample.ts
semigroupString.concat('hoge', semigroupString.concat('fuga', 'piyo')) // hogefugapiyo
semigroupString.concat(semigroupString.concat('hoge', 'fuga'), 'piyo') // hogefugapiyo

Semigroupのインスタンス(number編)

以下はnumberに対して「結合(加算)ができるよ」と定義した「インスタンス」。

SemigroupSample.ts
const semigroupNumberAdd: Semigroup<number> = {
  concat: (x, y) => x + y,
}
semigroupNumberAdd.concat(10, 20) // 30

「結合」の処理は、結合法則を満たす処理なら何でもよい。
以下はnumberに対して「結合(乗算)ができるよ」と定義した「インスタンス」。

SemigroupSample.ts
const semigroupNumberMultiply: Semigroup<number> = {
  concat: (x, y) => x * y,
}
semigroupNumberMultiply.concat(10, 20) // 200

Semigroupのインスタンス(独自型編)

例えば以下のようなMenu型があったとする。

SemigroupSample.ts
type Menu = {
  name: string
  price: number // 円
  calorie: number // kcal
  solt: number // g
}

「結合」の操作が可能な型なら、Semigroupのインスタンスを作ることができる。
以下はMenuに対して「結合(メニューのまとめ?)ができるよ」と定義した「インスタンス」。

SemigroupSample.ts
const semigroupMenu: Semigroup<Menu> = {
  concat: (x, y) => {
    return {
      name: x.name + '' + y.name,
      price: x.price + y.price,
      calorie: x.calorie + y.calorie,
      solt: x.solt + y.solt,
    }
  },
}

これは以下のように使うことができる。

SemigroupSample.ts
const gohan: Menu = {
  name: 'ご飯',
  price: 120,
  calorie: 330,
  solt: 0,
}
const misoshiru: Menu = {
  name: '味噌汁',
  price: 80,
  calorie: 25,
  solt: 1.5,
}
const karaage: Menu = {
  name: '唐揚げ',
  price: 250,
  calorie: 350,
  solt: 3,
}
semigroupMenu.concat(gohan, misoshiru) // { name: 'ご飯と味噌汁', calorie: 355, solt: 1.5}

Semigroupの共通処理

ここまで見たとおりSemigroupであれば、「任意の型」を2つ「結合(concat)」に渡せば、「任意の型」が返ってくる。
このため「任意の型」が何なのかに関わらず、共通に使える処理を書くことができる。

SemigroupSample.ts
function printSemi<A>(semi: Semigroup<A>): (x: A, y: A) => void {
  return (x, y) => console.log(semi.concat(x, y))
}

printSemi(semigroupString)('hoge', 'fuga') // hogefuga
printSemi(semigroupNumberAdd)(10, 20) // 30
printSemi(semigroupNumberMultiply)(10, 20) // 200
printSemi(semigroupMenu)(semigroupMenu.concat(gohan, misoshiru), karaage)
    // { "name": "ご飯と味噌汁と唐揚げ", "price": 450, "calorie": 705, "solt": 4.5 } 

型クラスの実例2 Monoid

サンプルコード MonoidSample.ts (TypeScript Playground)

Monoidの型クラス

任意の型 A に対して「結合(concat)ができるよ」「基本の値(empty)があるよ」と定義した「型」。

MonoidSample.ts
interface Semigroup<A> {
  readonly concat: (x: A, y: A) => A
}

interface Monoid<A> extends Semigroup<A> {
  readonly empty: A
}

MonoidはSemigroupに「基本の値」というルールが増えたもの。
このためSemigroupをextendsして定義している。

Monoidのインスタンス(string編)

以下はstringに対して「結合(文字列連結)ができるよ」「基本の値(空文字)があるよ」と定義した「インスタンス」。

MonoidSample.ts
const monoidString: Monoid<string> = {
  concat: (x, y) => x + y,
  empty: '',
}

インスタンスを作るとき、「基本の値」は「どの順番で何度結合しても、全体の結果は変わらない」ような値にする。
これもインスタンスを作る側が動作を保証する必要がある。

MonoidSample.ts
monoidString.concat('hoge', monoidString.empty) // hoge
monoidString.concat(monoidString.empty, 'hoge') // hoge
monoidString.concat(
  monoidString.empty,
  monoidString.concat('hoge', monoidString.empty)
) // hoge

Monoidのインスタンス(number編)

以下はnumberに対して「結合(加算)ができるよ」「基本の値(0)があるよ」と定義した「インスタンス」。

MonoidSample.ts
const monoidNumberAdd: Monoid<number> = {
  concat: (x, y) => x + y,
  empty: 0,
}

以下はnumberに対して「結合(乗算)ができるよ」「基本の値(1)があるよ」と定義した「インスタンス」。

MonoidSample.ts
const monoidNumberMultiply: Monoid<number> = {
  concat: (x, y) => x * y,
  empty: 1,
}

このように、型と処理によって「基本の値」は変わってくる。

Monoidのインスタンス(独自型編)

以下はMenuに対して「結合(メニューのまとめ?)ができるよ」「基本の値(空のメニュー?)があるよ」と定義した「インスタンス」。

MonoidSample.ts
const monoidMenu: Monoid<Menu> = {
  concat: (x, y) => {
    return {
      name: x.name.length === 0 ? y.name : x.name + '' + y.name,
      price: x.price + y.price,
      calorie: x.calorie + y.calorie,
      solt: x.solt + y.solt,
    }
  },
  empty: {
    name: '',
    price: 0,
    calorie: 0,
    solt: 0,
  },
}

Monoidの共通処理

Monoidは「基本の値」があるため、「全部をくっつける」などの処理が作りやすくなる。

MonoidSample.ts
function concatAll<A>(monoid: Monoid<A>): (array: Array<A>) => A {
  return (array) => array.reduce(monoid.concat, monoid.empty)
}

console.log(concatAll(monoidString)(['hoge', 'fuga', 'piyo'])) // hogefugapiyo
console.log(concatAll(monoidNumberAdd)([10, 20, 30])) // 60
console.log(concatAll(monoidNumberMultiply)([10, 20, 30, 40])) // 240000
console.log(concatAll(monoidMenu)([gohan, misoshiru, karaage, salad]))
    // { "name": "ご飯と味噌汁と唐揚げとサラダ", "price": 600, "calorie": 765, "solt": 4.9 } 

型クラスの実例3 Functor

サンプルコード FunctorSample.ts (TypeScript Playground)

Functorの型クラス

任意の型 F に対して「何らかの処理(map)ができるよ」と定義した「型」。

FunctorSample.ts
export interface Functor<F> {
  readonly map: <A, B>(fa: F<A>, f: (a: A) => B) => F<B>
}

イメージは上の通りだが、上のコードはエラーになる。
TypeScriptでFunctorを実現するには一工夫が必要。
(詳細はサンプルコードを参照)

工夫した結果は以下の通り。

FunctorSample.ts
interface Functor<F extends URIS> {
  readonly URI: F
  readonly map: <A, B>(fa: Kind<F, A>, f: (a: A) => B) => Kind<F, B>
}

意味的には最初の通りなので、URIとかKindとかはひとまず無視していい。
「何らかの処理(map)」をもう少し噛み砕くと、以下のようなかんじ。

  • F<A> の値 fa を受け取って
  • 何かしらの関数 (a: A) => B を適用して
  • F<B> の結果を返す

Functorのインスタンス

今回の「任意の型」は、「値があるかも、ないかも?」を表すMyOption型を定義する。
(一般的なOptionを簡素化したもの)

FunctorSample.ts
type None = { _tag: 'none' }
type Some<A> = { _tag: 'some', value: A }
type MyOption<A> = None | Some<A>

以下はMyOptionに対して「何らかの処理(map)ができるよ」と定義した「インスタンス」。

FunctorSample.ts
const functorMyOption: Functor<URI> = {
  URI,
  map: <A, B>(fa: MyOption<A>, f: (a: A) => B): MyOption<B> => {
    return fa._tag === 'some'
      ? { _tag: 'some', value: f(fa.value) }
      : fa
  }
}

これも噛み砕くと、以下のようなかんじ。

  • MyOption<A> の値 fa を受け取って
  • 値があれば(some)
    • 何かしらの関数 (a: A) => B を適用して
    • MyOption<B> の結果を返す
  • 値がなければ(none)
    • そのまま返す

これを使い、以下のように「値のある場合だけ処理を実行する」コードを書くことができる。

FunctorSample.ts
const optA: MyOption<never> = { _tag: 'none' }
const optB: MyOption<string> = { _tag: 'some', value: 'hogehoge' }
console.log(functorMyOption.map(optA, (s: string) => s.length)) // { _tag: 'none' }
console.log(functorMyOption.map(optB, (s: string) => s.length)) // { _tag: 'some', value: 8 }

FunctorからApplicative, Monadができる

Functorの型クラスに
「値を何かしら処理して任意の型 F として返却できるよ(of, pure)」
の定義が加わればApplicativeの型クラスになり、
Applicativeの型クラスに
「任意の型 F の値と、AF<B> に変換する関数を渡して、F<B> を返却できるよ(chain, flatMap)」
の定義が加わればMonadの型クラスになる。

// Applicative で加わる定義
readonly of: <A>(a: A) => F<A>

// Monad で加わる定義
readonly chain: <A, B>(fa: F<A>, f: (a: A) => F<B>) => F<B>

(上記はイメージ、前述のFunctorの定義と同様にエラーになる)
長くなりそうなので別の機会に。

参考文献

TypeScriptの関数型ライブラリ fp-ts
Scalaライブラリ「cats」の解説 - Type classes
TypeScriptと型クラス
30分でわかるJavaScriptプログラマのためのモナド入門

11
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?