LoginSignup
5
2

More than 1 year has passed since last update.

Haskellがわかる人のためのfp-ts

Last updated at Posted at 2021-06-10

はじめに

fp-tsはTypescriptに関数型言語の一般的なパターンと抽象化を導入するためのライブラリです

バージョンは2.10.5です

公式のドキュメントは英語で、少し説明が少ないかな?と思ったので、日本語で最新バージョンの記事を書くことでfp-tsを触る人が増えることを期待します

注) 筆者はHaskell初心者です

2021/06/12 型制約付きの関数、モナド変換子について更新しました

基本

Typescriptにはアドホック多相がありません、なのでMaybeにfmapを使いたい、Eitherにfmapを使いたいとなったら、明示的に関数を選択する必要があります

後述しますがfp-tsではMaybeOptionfmapmapとなっています

import * as E from "fp-ts/Either";
import * as O from "fp-ts/Option";

const e = E.right(1);

console.log(E.map((a: number): number => a + 1)(e));

// => { _tag: "Right", right: 2}

const value: number | null = 1;
const o = O.fromNullable(value);

console.log(O.map((a: number): number => a + 1)(o));

// => { _tag: "Some", value: 2}

パターンマッチ?

matchという関数が提供されてる

import * as O from 'fp-ts/Option';
import * as f from 'fp-ts/function';

const some = O.some(1)
const none = O.none
const match = O.match(
  function onNone() {
    return 0;
  },
  function onSome(a: number) {
    return a;
  }
);

console.log(match(some))
// => 1
console.log(match(none))
// => 0

do構文

公式の説明

>>=に対応する関数はchaina <- hogeに対応するのはbind('a', ()=>hoge)returnに対応するのはofとなっています

a -> Monad aとなる関数はいくつかあります、Optionの場合fromNullableがあります

上記を用いてHaskellのdo構文をfp-tsで書き直すと以下のようになります

five :: Maybe Int
five =
  do
    a <- return 2
    b <- return 3
    return (a + b)
import * as O from "fp-ts/Option";
import {pipe} from "fp-ts/function";

five = pipe(
    O.Do,
    O.bind("a",()=>O.of(2)),
    O.bind("b",()=>O.of(3)),
    O.chain(({a, b})=>O.of(a + b))
    )

データ型の宣言

公式の説明

Typescriptは構造的部分型なので_tagをつかって何の値なのか判断できるようにします

interface Bar {
  readonly _tag: 'Bar'
  readonly value: string
}

interface Baz {
  readonly _tag: 'Baz'
  readonly value: boolean
}

// type
type Foo = Bar | Baz

// constructors
const Bar = (value: string): Foo => ({ _tag: 'Bar', value })

const Baz = (value: boolean): Foo => ({ _tag: 'Baz', value })

多相的データ型の宣言

公式の説明

引数を取るようにしてあげます

export interface None {
  readonly _tag: 'None';
}

export interface Some<A> {
  readonly _tag: 'Some';
  readonly value: A;
}

const some = <A>(a: A): Some<A> => ({ _tag: 'Some', value: a });
const none:None = ({ _tag: 'None' });

export type Option<A> = None | Some<A>;

型クラスのインスタンス

公式の説明

Eq型クラス

プリミティブな値

プリミティブな値のEq型クラスインスタンスは提供されてます

import * as s from "fp-ts/string"
import * as n from "fp-ts/number"

console.log(n.Eq.equals(1, 1))
// => true

console.log(s.Eq.equals("hello", "hello"))
// => true

オブジェクト

オブジェクトのEqはstruct関数で作成します

import * as s from "fp-ts/string"
import * as n from "fp-ts/number"
import {struct, Eq} from "fp-ts/Eq"

type Person = {
  name: string,
  age: number,
}

const PersonEq: Eq<Person> = struct({
  name: s.Eq,
  age: n.Eq,
})

const john: Person = {
  name: "John",
  age: 23
}

const mike: Person = {
  name: "Mike",
  age: 21
}

const john2: Person = {
  name: "John",
  age: 23
}

console.log(PersonEq.equals(john, mike))
// => false

console.log(PersonEq.equals(john, john2))
// => true

多相的データ型

多相的データ型のgetEq関数を使って動的に作成する必要があります

以下ではEitherのgetEqにLeftの値であるstringのEqインスタンスとRightの値であるnumberのEqインスタンスを渡してEitherのEqインスタンスを作っています

import * as E from 'fp-ts/Either';
import * as n from 'fp-ts/number';
import * as s from 'fp-ts/string';
import type { Eq } from 'fp-ts/Eq';

const EitherStrNumEq: Eq<E.Either<string, number>> = E.getEq(s.Eq, n.Eq);

console.log(EitherStrNumEq.equals(E.right(1), E.left('im left')));
// => false

console.log(EitherStrNumEq.equals(E.left('hello'), E.left('world')));
// => false

console.log(EitherStrNumEq.equals(E.right(1), E.right(1)));
// => true

Functor型クラス

型コンストラクタの引数の数に合わせてFunctor1Functor2Functor3Functor4を使います

#多相的データ型の宣言で作成したOptionをFunctor型クラスのインスタンスにします

import type { Functor1 } from 'fp-ts/Functor';

...Optionの宣言は省略

export const URI = 'Option'

export type URI = typeof URI

declare module 'fp-ts/lib/HKT' {
  interface URItoKind<A> {
    readonly [URI]: Option<A>
  }
}

export const option: Functor1<URI> = {
  URI,
  map: (ma, f) => ma._tag === "None" ? ma : some(f(ma.value))
}

変数URIやモジュールの宣言で高カインド型をOptionに対応できるようにしています。詳しくは「TypeScriptで高カインド型(Higher kinded types)」を参照してください

そして、Functorのインスタンスを作ります、今回は型引数が1つなのでFunctor1を使っています

1つ以上の引数を取る型コンストラクタをインスタンス化する場合は上記のように対応するURItoKind(URItoKind2、URItoKind3など)を拡張してあげる必要があります

型クラスの宣言

公式の説明

型クラスの宣言では対応するURISKindが必要になります

import type { URIS2, Kind2 } from 'fp-ts/HKT';

export interface Functor2<F extends URIS2> {
  readonly URI: F;
  readonly map: <E, A, B>(fa: Kind2<F, E, A>, f: (a: A) => B) => Kind2<F, E, B>;
}

URIS2

type URIS2 = keyof URItoKind2<any, any>

と定義されているのでURItoKind2を拡張してプロパティを増やすと自動でURIS2がとれる値が増えることになってます

Kind2も同様に

type Kind<URI extends URIS, A> = URI extends URIS ? URItoKind<A>[URI] : any

となっているのでURItoKind2の拡張に応じて、URIS2に対応する型を返すようになります

型クラス制約付きの関数

公式の説明

例えば関数をリフトする関数liftを作るとする、liftmapを利用するため、Functorのインスタンスが必要になる。

引数を1つ取る型に対しての実装は次のようになる

import type { Kind, URIS } from 'fp-ts/HKT';
import type { Functor1 } from 'fp-ts/lib/Functor';

export function lift<F extends URIS>(F: Functor1<F>): <A, B>(f: (a: A) => B) => (fa: Kind<F, A>) => Kind<F, B> {
  return (f) => (fa) => F.map(fa, f);
}

TDB: 公式の説明ではHKTを使っていますが、HKTの使い方については確認中です

アドホック多相がないため明示的にFunctorのインスタンスを渡している

これは次のように利用する

import * as O from "fp-ts/Option"
const optionLift = lift(O.Functor)

const f = (a: number): string => a.toString()

console.log(
  optionLift(f)(O.of(1))
)
// => { _tag: "Some", value: "1"}

これではEitherなどの引数を多く取る型には使えないので以下の拡張する

import type { Kind, Kind2, Kind3, Kind4, URIS, URIS2, URIS3, URIS4 } from 'fp-ts/HKT';
import type { Functor1, Functor2, Functor3, Functor4 } from 'fp-ts/lib/Functor';

function lift<F extends URIS>(F: Functor1<F>): <A, B>(f: (a: A) => B) => (fa: Kind<F, A>) => Kind<F, B>;
function lift<F extends URIS2>(F: Functor2<F>): <E, A, B>(f: (a: A) => B) => (fa: Kind2<F, E, A>) => Kind2<F, E, B>;
function lift<F extends URIS3>(F: Functor3<F>): <R, E, A, B>(f: (a: A) => B) => (fa: Kind3<F, R, E, A>) => Kind3<F, R, E, B>
function lift<F extends URIS4>(F: Functor4<F>): <S, R, E, A, B>(f: (a: A) => B) => (fa: Kind4<F, S, R, E, A>) => Kind4<F, S, R, E, B> {
  return (f) => (fa) => F.map(fa, f);
}

これでEitherに対してもliftがつかえるようになった

import * as O from "fp-ts/Option"
import * as E from "fp-ts/Either"
const optionLift = lift(O.Functor);
const eitherLift = lift(E.Functor);

console.log(optionLift((a: number): string => a.toString())(O.of(1)));
// => { _tag: "Some", value: "1"}
console.log(eitherLift((a: number): string => a.toString())(E.right(1)));
// => { _tag: "Right", right: "1"}

モナド変換子

StateT

例としてStateTを使ってStateT s Option aを作る

StateT s Option a専用にmapなどを作ってあげれば他のモナド同様に記述できる

import * as O from 'fp-ts/Option';
import * as ST from 'fp-ts/StateT';

const map = ST.map(O.Functor);
const of = ST.of(O.Pointed);
const evaluate = ST.evaluate(O.Functor);

console.log(
  evaluate({hello:'world'})(pipe(
    of<number,{hello: string}>(1),
    map((e)=>{
      return e.toString()
    }),
  ))
)
// => { _tag: 'Some', value: '1' }

mapofは生成用の関数が提供されているが、bindgetputはない

なので少し工夫する必要がある

bind

bindを作るためにはStateT s Option aChainインスタンスにする必要がある

Chainインスタンスにした後でfp-ts/Chainから提供されるChainの制約付きのbindを作る関数bindにChainインスタンスを渡すとbindが作れる


import * as C from 'fp-ts/Chain';
import * as O from 'fp-ts/Option';
import * as ST from 'fp-ts/StateT';
import { pipe } from 'fp-ts/function';
import type { Applicative2 } from 'fp-ts/lib/Applicative';
import type { Chain2 } from 'fp-ts/lib/Chain';
import type { Functor2 } from 'fp-ts/lib/Functor';

const URI2 = 'StateOption';
type URI2 = typeof URI2;

type StateOption<S, A> = ST.StateT1<'Option', S, A>;

// 型クラスインスタンスを作るためURItoKindに登録する
declare module 'fp-ts/lib/HKT' {
  interface URItoKind2<E, A> {
    readonly [URI2]: StateOption<E, A>;
  }
}

// 型クラスのメソッドを作る
// 'fp-ts/StateT'から提供されてるメソッドにより簡略化できる
const map = ST.map(O.Functor);
const ap = ST.ap(O.Chain);
const of = ST.of(O.Pointed);
const chain = ST.chain(O.Chain);

// 各インスタンスを作る
// ST.mapによって作られる`map`とは引数のもらい方が違うため調整する
const Functor: Functor2<URI2> = {
  URI: URI2,
  map: <E, A, B>(ma: StateOption<E, A>, f: (a: A) => B): StateOption<E, B> => {
    return map(f)(ma);
  },
};

// map同様引数を調整する
const Applicative: Applicative2<URI2> = {
  ...Functor,
  of,
  ap: <E, A, B>(fab: StateOption<E, (a: A) => B>, fa: StateOption<E, A>): StateOption<E, B> => {
    return ap(fa)(fab);
  },
};

// map同様...
const Chain: Chain2<URI2> = {
  ...Applicative,
  chain: <E, A, B>(fa: StateOption<E, A>, f: (a: A) => StateOption<E, B>): StateOption<E, B> => {
    return chain(f)(fa);
  },
};

// Chainの制約付きのbindを作る関数`C.bind`を使ってbind関数を作る
const bind = C.bind(Chain);

getput

fp-ts/Stategetputを使って一旦State s aにしてからState s Option aに戻す

import * as O from 'fp-ts/Option';
import * as S from 'fp-ts/State';
import * as ST from 'fp-ts/StateT';

const fromState = ST.fromState(O.Pointed);

const get = <S>()=>fromState<S,S>(S.get<S>())
const put = <S>(s:S)=>fromState<S,void>(S.put<S>(s))

使用例


// 与えられた関数の処理を後続する関数に繋がないための関数
const chainFirst = C.chainFirst(Chain);

type S = { hello: string }
console.log(
  evaluate({hello:'world'})(pipe(
    of<number,S>(1),
    bind("before",()=>{
      return get()
    }),
    // `put`は`State s Option void`を返すため`chain`で繋ぐとそれまでの処理が捨てられるので、`chainFirst`を使う
    chainFirst(()=>{
      return put({hello:"fp-ts"})
    }),
    bind("after",()=>{
      return get()
    }),
  ))
)
/* => {
  _tag: 'Some',
  value: { before: { hello: 'world' }, after: { hello: 'fp-ts' } }
}
*/

名前の違うものたち

型クラス Haskell fp-ts コメント
Functor fmap map
Applicative <*> ap Apply型クラスのメソッドとして定義されている
Monad >>= chain Chain型クラスのメソッドとして定義されている
Monad return of Pointed型クラスのメソッドとして定義されていて、PointedをApplicativeが継承し、さらにMonadが継承している
Monad a <- hoge bind("a", ()=> hoge)
Maybe Option Scalaに影響を受けていると思われる

TBD

参考

5
2
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
5
2