fp-tsとfree

1

fp-tsにはFree Monadを使っていきたいと思います。

Free Monadに関する説明は今回は省きます。

Free Monadと書いていますが、実はFreerMonadという感じがしていますがfp-tsがFreeという表現になっているのでFreeでいきます。

2

今回作るのは下のfp-tsのexampleや以下のgistを参考にしてPromiseを使った例を作っていきます。

検索した感じではPromiseを使った例がなかったので。

github.com

Free Monad example, Flow and fp-ts, npm install fp-ts · GitHub

3

fileのread, write周りを例に今回は作っていきます。

実はデータ型を定義するところまでは一緒です。それがFreeの強みなんですが

declare module 'fp-ts/lib/HKT' {
    interface URI2HKT<A> {
        Fs: FsF<A>;
    }
}

export const FsFURI = 'Fs';

export type FsFURI = typeof FsFURI;

export class Read<A> {
    public readonly _tag: 'Read' = 'Read';
    public readonly _A!: A;
    public readonly _URI!: FsFURI;
    constructor(readonly path: string, readonly more: (p: string) => A) {}
}

export class Write<A> {
    public readonly _tag: 'Write' = 'Write';
    public readonly _A!: A;
    public readonly _URI!: FsFURI;
    constructor(readonly path: string, readonly content: string, readonly more: A) {}
}

export class Show<A> {
    public readonly _tag: 'Show' = 'Show';
    public readonly _A!: A;
    public readonly _URI!: FsFURI;
    constructor(readonly message: string, readonly more: A) {}
}

export type FsF<A> = Read<A> | Write<A> | Show<A>;

function read(path: string) {
    return free.liftF(new Read(path, identity));
}

function write(path: string, content: string) {
    return free.liftF(new Write(path, content, undefined));
}

function show(message: string) {
    return free.liftF(new Show(message, undefined));
}

こういう感じになります。

あとはこれを解釈する部分を作っていきます。

Promiseをfp-tsで使う場合、選択肢としてTask or TaskEitherになるのですが一応真面目にError処理ができるTaskEItherを使っていきます。

FsF<A> => TaskEither<string, A>となるように型を合わせれば動くと思います。

function interpretTaskEither<A>(fa: FsF<A>): TaskEither<string, A> {
    switch (fa._tag) {
        case 'Read':
            return new TaskEither(
                new Task(async () => {
                    try {
                        const file = await fs.promises.readFile(fa.path, { encoding: 'utf8' });
                        return right(fa.more(file));
                    } catch (e) {
                        return left(`read error: ${e.message}`);
                    }
                }),
            );
        case 'Write':
            return new TaskEither(
                new Task(async () => {
                    try {
                        await fs.promises.writeFile(fa.path, fa.content);
                    } catch (e) {
                        return left(`write error: ${e.message}`);
                    }
                    return right(fa.more);
                }),
            );
        case 'Show':
            return fromIO(
                new IO(() => {
                    console.log(fa.message);
                    return fa.more;
                }),
            );
    }
}

あとはfoldFreeするだけです。 foldFreeに渡す値が型を見た時にわかりにくいのが難点で、ぱっと見理解するのがかなり難しめにできています。

export declare function foldFree<M extends URIS3>(M: Monad3<M>): FoldFree3<M>;
export declare function foldFree<M extends URIS3, U, L>(M: Monad3C<M, U, L>): FoldFree3C<M, U, L>;
export declare function foldFree<M extends URIS2>(M: Monad2<M>): FoldFree2<M>;
export declare function foldFree<M extends URIS2, L>(M: Monad2C<M, L>): FoldFree2C<M, L>;
export declare function foldFree<M extends URIS>(M: Monad1<M>): <F extends URIS, A>(nt: <X>(fa: Type<F, X>) => Type<M, X>, fa: Free<F, A>) => Type<M, A>;
export declare function foldFree<M>(M: Monad<M>): <F, A>(nt: <X>(fa: HKT<F, X>) => HKT<M, X>, fa: Free<F, A>) => HKT<M, A>;

ですが先ほどTaskEither型を使っているので、TaskEitherが定義されているところにある

export declare const taskEither: Monad2<URI> & Bifunctor2<URI> & Alt2<URI> & MonadIO2<URI> & MonadTask2<URI>;

を使えば大丈夫です。

つまり今回は型からわかるように以下の定義を使っていることがわかります。

export declare const taskEither: Monad2<URI> & Bifunctor2<URI> & Alt2<URI> & MonadIO2<URI> & MonadTask2<URI>;

最終的に実行する部分は以下のようになります。

const program = read('./package.json').chain(content => show(content).chain(() => write('./package2.json', content)));

async function main() {
    const result: Either<string, undefined> = await free
        .foldFree(taskEither)(interpretTaskEither, program)
        .run();

    if (result.isLeft()) {
        return console.error(result.value);
    }
}

今回TaskEitherを使っているのでawaitの部分でcatchする必要がありません。もしTaskを使っていた場合catchする必要があります。

今回のcodeはgistにあげてあります。

FreeFs.ts · GitHub

package.jsonがある環境ではfileの中身が標準出力に出されて、package2.jsonが作られていると思います

package.jsonがない場合、read error: ENOENT: no such file or directory, open './package.jon'以下のようなerrorになると思います。