fp-tsとio-tsを使ってWeb APIをいい感じに叩く

## はじめに
「fp-tsとかio-tsは使いこなせると便利だろうけどとっかかりがなぁ。。。」
「axios.getを叩いて返ってきたresponseの型をいい感じに推論したいなぁ。。。」
みたいな感情ないですか？僕はあります。

そういった方に向けて、fp-tsとio-tsを組み合わせたWebAPIの叩き方(with axios)の例を紹介します。
サンプルコードの実装は[こちらのサイト](https://dev.to/gnomff_65/fp-ts-and-beautiful-api-calls-1f55)を参考にしました。

### この記事では扱わないこと
この記事では上記ライブラリに興味を持ってもらうことを目的としているため以下の内容は取り扱わないです

- `fp-ts`や`io-ts`の導入方法
- `fp-ts`や`io-ts`で提供されている機能の詳しい説明
- 関数型言語について

### 環境
- OS: Ubuntu 18.04.6 LTS
- node: 12.22.5
- fp-ts: 2.11.5
- io-ts: 2.2.16

## 目次
- responseの型定義を行う
    - WebAPIを叩いた時の返り値の型を考える
    - io-tsを用いて上記の型を実装する
    - io-tsを用いた型チェックのやり方を考える
- response.dataの型チェックを行う
    - fp-tsを用いた関数合成について考える
    - fp-tsを用いて型チェック部分を実装する
- axiosを用いてWebAPIを叩く
    - fp-tsを用いた関数のパイプラインについて考える
    - fp-tsを用いてWebAPIを叩く部分を実装する

## responseの型定義を行う
というわけで早速やっていきます。
実際にaxiosを使ってWebAPIを叩く前に、返ってくるデータの型について考えます。

### WebAPIを叩くと返ってくるデータの型について
サンプルとして、`localhost:8080/ariticle`を叩くと以下の様なArticle型のデータが返ってくるとします。

```typescript
type Article = {
  title: string;
  discription: string;
  articleNumber: number;
};
```

### io-tsを用いて型定義を行う
この部分では、型チェックを行うためにio-tsを用いた型定義及び型チェックのやり方について紹介します。

io-tsでは`codec`というものを定義して型チェックをすることが出来ます。
先程のArticle型をio-tsのcodecを用いて実装すると以下の様になります。

```typescript
import * as t from "io-ts";

const ArticleCodec = t.type({
  title: t.string,
  discription: t.string,
  articleNumber: t.number,
})

// TypeScriptのtypeに変換可能
type Article = t.TypeOf<typeof ArticleCodec>;
```
上で定義したcodecは`codec.decode(型チェックしたい要素)`で型チェックが可能です。

decodeの返り値の型は`Either<Errors, T>`です。
Either型はfp-tsで追加された型です。([ドキュメント](https://gcanti.github.io/fp-ts/modules/Either.ts.html))

decodeに成功するとEither型の右側の要素(今回だとT型)が、失敗すると左側の要素(今回だとErrors型)が返ってきます
ただし実際に返ってくる値の型は`Errors | T`ではなく、あくまで`Either<Errors, T>`である点に注意してください。

#### decodeのサンプル

```typescript
const correctTypeData = {title: "タイトル", discription: "内容", articleNumber: 4};
console.log(ArticleCodec.decode(correctTypeData));
// { _tag: 'Right', right: {title: "タイトル", discription: "内容", articleNumber: 4} }

const wrongTypeData = {name: "なまえ"};
console.log(ArticleCodec.decode(wrongTypeData));
// { _tag: 'Left', left: ~~~ }
```

## responseの型チェック部分の実装
この部分では、APIからresponseが返ってきた際に先程紹介したcodecを用いて型チェックを行う部分について考えます。
目標として以下の内容を満たすコードを挙げます。

- 大体の型で型チェック出来る
- 型チェック後のデータはその型に推論されている
- 型チェックに失敗した時のエラーハンドリングが出来る

### fp-tsを用いた関数合成について

この部分ではfp-tsが提供している便利関数その1である`flow`と、TaskEither型を用いて実装します。
TaskEither型の説明は省略します(筆者自身の理解が浅く嘘を書きそうなので)
[TaskEitherのドキュメント](https://gcanti.github.io/fp-ts/modules/TaskEither.ts.html)

flowについてです。
flowは簡単に説明すると、<b>左から右への関数合成を行うもの</b>らしいです(by [公式](https://gcanti.github.io/fp-ts/modules/function.ts.html#flow))
なんのこっちゃ？なのでサンプルコードを用意しました。

#### flowのサンプル

```typescript
import { flow } from 'fp-ts/function'

const len = (s: string): number => s.length
const double = (n: number): number => n * 2

const input = "hello";

// いつもの書き方
var tmp = len(input);
console.log(double(tmp));
// 10

// flowを使う場合
const f = flow(len, double);
console.log(f(input));
// 10
```
公式のサンプルをそのまま持ってきました。これを見れば関数合成のイメージがなんとなく掴めたと思います。

### responseの型チェックを行う
では本題のaxiosからのreponseを受け取って型チェックを行う部分の実装を紹介します。

```typescript
function decodeWith<T>(codec: t.Type<T>): (i: unknown) => TE.TaskEither<Error, T> {
  return flow(
    codec.decode,
    E.mapLeft(
      () => new Error("cannot decode response"),
    ),
    TE.fromEither,
  )
}
```
この部分では、データを受け取ってdecodeして結果を返すというよりかはそういった処理を行う関数群を生成して返すという感じになっています。

flow部分の説明としては、入力データをcodec.decodeしてdecodeに失敗した(結果がleftの時)場合は`E.mapLeft( ~~~ )`でdecodeの返り値である`Either<Errors, T>`型のErrorsを返さずにError型を返すようにラップしてあげています。

またこの部分は関数群を生成して返すという性質上、呼び出し元が非同期で動いている場合はこの関数群も非同期で動作することを前提で実装する必要があります。なので返り値の型がEither型ではなくTaskEither型になっています(多分)

## axiosを使ってrequestを発行する
この部分では、axiosを使ってger requestを投げ結果を受け取る部分について考えます。

目標として以下の内容を設定します。

- requestを投げることが出来る
- responseを受け取ることが出来る
- 最低限のエラーハンドリングが出来る

### fp-tsを用いた関数のパイプラインについて

この部分では先程も登場したTaskEither型を沢山使います。
またfp-tsが提供する便利関数その2である`pipe`も使います。

pipeについてです。
先程紹介したflowと似ているやつだと思ってください。
pipeは式の値をパイプラインに繋いでいくというものです(by [公式](https://gcanti.github.io/fp-ts/modules/function.ts.html#pipe))
相変わらずなんのこっちゃなのでサンプルを用意しました。

#### pipeのサンプル
```typescript
import { pipe } from 'fp-ts/function'

const len = (s: string): number => s.length
const double = (n: number): number => n * 2

// without pipe
assert.strictEqual(double(len('aaa')), 6)

// with pipe
assert.strictEqual(pipe('aaa', len, double), 6)
```
公式のサンプルコードがとても分かりやすいのでそのまま持ってきました。
つまりそういうことです。
flowと違って渡したいデータも含める点が違います。(他にも色々違う点があると思いますが省略します)

### axiosを使ってrequestを発行する

ということでaxiosをあれこれする部分です

```typescript
import { pipe } from "fp-ts/function";
import axios from "axios";

function runHttpRequest<T>(codec: t.Type<T>, url: string): TE.TaskEither<Error, T> {
  return pipe(
    TE.tryCatch(
      () => axios.get(url),
      (error) => new Error(`${error}`),
    ),
    TE.map(response => response.data),
    TE.chain(decodeWith(codec)),
  )
}
```
runHttpRequest関数ではpipeを使うことによって以下の処理を順々に行います。

- axiosを用いてget requestを発行する
- responseからdataを取り出す
- 先程紹介した型チェックを行う関数群(decodeWith関数)にdataを渡して結果を受け取る

この関数の返り値の型はTaskEither<Error, T>となっていますが、実際にはTaskEither<axios部分で発生するError | decode部分で発生するError, T>となってます(多分)

## 実行する部分
最後にこれらの関数群を実行する部分を紹介します。

```typescript
import * as T from "fp-ts/Task";

function main() {
  pipe(
    runHttpRequest(ArticleCodec, "localhost:8080/aritcle"),
    TE.fold(
      (error) => T.of(console.log(error)), // error: Error型
      (data) => T.of(console.log(data)), // data: Article型
    ),
  )
}
```
ここでは、pipeを用いてrequestの結果をTE.foldに流しています。
TE.foldでは流れてきたEither型(今回はTaskEither型)がleftの場合は`(error) => ~~~`を、rightの場合は`(data) => ~~~`を実行するようになっています。

## 全体像

```typescript
import { pipe, flow } from "fp-ts/function";
import * as t from "io-ts";
import * as TE from "fp-ts/TaskEither";
import * as T from "fp-ts/Task";
import * as E from "fp-ts/Either";

/**
 * 受け取ったデータが指定された型に一致しているかどうか判断する
 * @param codec: 指定したい型
 * @returns 一致しているかどうかの結果
 */
function decodeWith<T>(codec: t.Type<T>): (i: unknown) => TE.TaskEither<Error, T> {
  return flow(
    codec.decode,
    E.mapLeft(
      () => new Error("cannot decode response"),
    ),
    TE.fromEither,
  )
}

/**
 * axiosを用いたget requestを発行しTaskEither型のオブジェクトを返す
 * @param codec: get responseのdataの型を指定
 * @param url: requestを投げる先
 * @returns: get requestの結果
 */
function runHttpRequest<T>(codec: t.Type<T>, url: string): TE.TaskEither<Error, T> {
  return pipe(
    TE.tryCatch(
      () => axios.get(url),
      (error) => new Error(`${error}`),
    ),
    TE.map(response => response.data),
    TE.chain(decodeWith(codec)),
  )
}


// 叩き方
const ArticleCodec = t.type({
  title: t.string,
  discription: t.string,
  articleNumber: t.number,
})

type Article = t.TypeOf<typeof ArticleCodec>

function main() {
  pipe(
    runHttpRequest(ArticleCodec, "localhost:8080/aritcle"),
    TE.fold(
      (error) => T.of(console.log(error)),
      (data) => T.of(console.log(data)),
    ),
  )
}
// リクエストを正常に投げられた場合
// 出力内容: { "記事のタイトル" "記事の内容" 記事の番号 }

// サーバー側の問題が発生した時
// 出力内容: 500 internal server error

// リクエストを正常に投げられたが、異なる型のデータが返ってきている場合
// 出力内容: cannnot decode response
```

## 最後に
こうやってfp-tsとio-tsを組み合わせることによってWebAPIをいい感じに叩くことが出来ます。
fp-tsとio-tsが組み合わさった時の強力なパワーを少しでも感じていただければ幸いです。

少しでも気になったそこのあなた
`npm i fp-ts io-ts`
で早速試してみましょう！！！