はじめに
fp-tsはTypescriptに関数型言語の一般的なパターンと抽象化を導入するためのライブラリです
バージョンは2.10.5です
公式のドキュメントは英語で、少し説明が少ないかな?と思ったので、日本語で最新バージョンの記事を書くことでfp-tsを触る人が増えることを期待します
注) 筆者はHaskell初心者です
2021/06/12 型制約付きの関数、モナド変換子について更新しました
基本
Typescriptにはアドホック多相がありません、なのでMaybeにfmapを使いたい、Eitherにfmapを使いたいとなったら、明示的に関数を選択する必要があります
後述しますがfp-tsではMaybe
はOption
、fmap
はmap
となっています
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構文
>>=
に対応する関数はchain
、a <- 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型クラス
型コンストラクタの引数の数に合わせてFunctor1
、Functor2
、Functor3
、Functor4
を使います
#多相的データ型の宣言で作成した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など)を拡張してあげる必要があります
型クラスの宣言
型クラスの宣言では対応するURIS
とKind
が必要になります
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
を作るとする、lift
はmap
を利用するため、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' }
map
やof
は生成用の関数が提供されているが、bind
やget
はput
はない
なので少し工夫する必要がある
bind
bind
を作るためにはStateT s Option a
をChain
インスタンスにする必要がある
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);
get
とput
fp-ts/State
のget
とput
を使って一旦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
参考