lukas-juhas-6NddrjdsiNI-unsplash.jpg

fp-tsとoption

 
0
このエントリーをはてなブックマークに追加
Kazuki Moriyama
Kazuki Moriyama (森山 和樹)

fp-tsには値が存在するかどうかをデータ型として表現したOptionというものが存在する。
その紹介。

https://gcanti.github.io/fp-ts/modules/Option.ts.html

コード例について

コード例ではすべて以下のimportが挿入されているものとして書いている。

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

Optionとは

Optionは値がある場合にSome、ない場合にはNoneになるデータ型のことである。
fp-tsでは以下のように表現されている。

export interface None {
  readonly _tag: 'None'
}

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

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

Noneは値を何も取らずにただNoneであるだけ、Someは何かしらの値を一つ取り、それをvalueとして保持する。
そしてOptionはそれらのUnionである。
データ型と型によって実際の値のある無しを表現している。

TypeScriptではそもそもundefinedやnullの扱いがデフォで強力なので正直Optionの意味はあまりない。
ただし他の言語、例えばJavaなどではnullは型に現れず、すべての参照型がnullになる可能性を秘めているというヤバげな事情があったので有用である。
またTypeScriptやKotlinの様にnull的な値のいい感じの取り扱いをコンパイラがしてくれなくても、ある程度強めの静的型があればどの様な言語でもnull的な値のハンドリングをいい感じに実装できるのがOptionというデータ型である。

TypeScriptのようにnullなどのハンドリングをネイティブサポートしてくれる言語にとってOptionを導入するメリットしては、より関数型の構造にnullハンドリング文脈を組み込めるということがある。
例えばOptionというデータ型にnull文脈を翻訳しておけば、OptionはFunctorやMonadを始めとした様々な型クラスのインスタンスにできるので関数型的に扱いやすい。
あとは有用な関数群をライブラリが用意してくれている。

constructors

Optionをどうやって作成するか見ていこう。

none

ただNoneであるような定数。

O.none; // => { _tag: 'None' }

ただしこれがただのNoneより優れているのは型としてはOption<never>になっていることだ。
structual typingを採用しているtsにとっては正直あまり関係ないのだが、nominal typingを採用している言語にとってはこれが型推論の文脈で重要になってくる。

some

値を一つ渡してSomeを返す関数。

O.some(1); // => { _tag: 'Some', value: 'a' }

これも重要なのが型としてはOption<T>になっているということ。

fromEither

EitherからOptionを構成する。
LeftはNoneに、RightはSomeに対応する。

O.fromEither(E.right("a")); // => { _tag: 'Some', value: 'a' }
O.fromEither(E.left("a")); // => { _tag: 'None' }

fromPredict

boolを返す関数からOptionを構成する。
trueはSomeへ、falseはNoneに行く。

const even = (n: number) => n % 2 === 0;
O.fromPredicate(even)(2); // => { _tag: 'Some', value: 2 }
O.fromPredicate(even)(1); // => { _tag: 'None' }

getRight

EitherからRight値をOptionとして取り出そうとする。

O.getRight(E.right("a")); // => { _tag: 'Some', value: 'a' }
O.getRight(E.left("a")); // => { _tag: 'None' }

getLeft

EitherからLeft値をOptionとして取り出そうとする。

O.getLeft(E.right("a")); // => { _tag: 'None' }
O.getLeft(E.left("a")); // => { _tag: 'Some', value: 'a' }

destructors

fold

NoneとSomeの場合にそれぞれ変換処理を定義できる。
注意点としては変換処理は同じ型を返す必要がある。

const toStr = O.fold(
  () => "none",
  (a: string) => "some: " + a
);
toStr(O.none); // => none
toStr(O.some("a")); // => some: a

foldW

foldから変換処理の結果型が同じであるという制約をなくしたもの。

const toStrOrDouble = O.foldW(
  () => "none",
  (n: number) => n * 2
);
toStrOrDouble(O.none); // => none
toStrOrDouble(O.some(1)); // => 2

getOrElse

Option型からSome値の値を取り出そうとする関数。
Noneの場合には取り出す値がないのでその場合の値を指定できる。
Someから取り出す予定の値と、Noneの場合に設定した値は同じ型である必要がある。

O.getOrElse(() => 0)(O.none); // => 0
O.getOrElse(() => 0)(O.some(1)); // => 1

getOrElseW

getOrElseから取り出す値とNoneの場合の値の型が同じであるという制約を外したもの。

O.getOrElseW(() => 1)(O.none); // => 1
O.getOrElseW(() => 1)(O.some("a")); // => a

match

foldのalias。

matchW

foldWのalias。

guards

isNone

Noneを判定するtype guard。

O.isNone(O.none); // => true

isSome

Someを判定するtype guard。

const s = O.some("a");
O.isSome(s) && s.value; // => a

combinators

apFirst

2つのOption値をとってどちらもsomeならば最初の値を返す。
Apply型クラスから導出できる関数。
他の言語のライブラリだとproductLとか<*のような記号メソッドで名前がついている。

O.apFirst(O.some(1))(O.some("a")); // => { _tag: 'Some', value: 'a' }
O.apFirst(O.none)(O.some("a")); // => { _tag: 'None' }
O.apFirst(O.some(1))(O.none); // => { _tag: 'None' }

apSecond

apFirstの2つ目の値を返す版。
これもApply型クラスから導出できる。
他の言語のライブラリだとproductRとか*>みたいな名前がついている事がある。

O.apSecond(O.some(1))(O.some("a")); // => { _tag: 'Some', value: 1 }
O.apSecond(O.none)(O.some("a")); // => { _tag: 'None' }
O.apSecond(O.some(1))(O.none); // => { _tag: 'None' }

chainFirst

Option値からOption値を計算して、どちらもsomeだった場合に最初のOptionを返す。

O.chainFirst((a: number) => (a % 2 === 0 ? O.some(a) : O.none))(O.some(1)); // => { _tag: 'None' }
O.chainFirst((a: number) => (a % 2 === 0 ? O.some(a * 2) : O.none))(O.some(2)); // => { _tag: 'Some', value: 2 }
O.chainFirst((a: number) => (a % 2 === 0 ? O.some(a) : O.none))(O.none); // => { _tag: 'None' }

duplicate

Optionをネストさせる。

O.duplicate(O.none); // => { _tag: 'None' }
O.duplicate(O.some(1)); // => { _tag: 'Some', value: { _tag: 'Some', value: 1 } }

flap

Optionに包まれた関数を値に適用する。

O.flap(1)(O.some((a) => a * 2)); // => { _tag: 'Some', value: 2 }
O.flap(1)(O.none); // => { _tag: 'None' }

flatten

ネストしたOptionのレイヤーを一つなくす。

O.flatten(O.none); // => { _tag: 'None' }
O.flatten(O.some(O.some(1))); // => { _tag: 'Some', value: 1 }

utils

apS

名前付きでOptionを合成する。
ちゃんと型をつければ同じ名前に対しての合成はできない。
ここらへんはpipeと一緒に使うことがある程度想定されている臭い。

O.apS("b", O.some(1))(O.some({ a: 2 })); // => { _tag: 'Some', value: { a: 2, b: 1 } }
O.apS("b", O.none)(O.some({ a: 2 })); // => { _tag: 'None' }
O.apS("b", O.some(1))(O.none); // => { _tag: 'None' }

O.apS<"a", { a: number }, number>("a", O.some(1))(O.some({ a: 2 }));
// error TS2345: Argument of type '"a"' is not assignable to parameter of type 'never'.
O.apS<"a", { a: number }, number>("a", O.some(1))(O.some({ a: 2 })); // => { _tag: 'Some', value: { a: 2, b: 1 } }

bind

名前月でMonad.chainする感じ。

O.bind("a", (a: number) => O.some(a.toString()))(O.some(1)); // => { _tag: 'Some', value: { a: '1' } }
O.bind("a", (a: number) => O.some(a.toString()))(O.none); // => { _tag: 'None' }
O.bind("a", (a: number) => O.none)(O.some(1)); // => { _tag: 'None' }

bindTo

Option値に名前を割り当てる。

O.bindTo("a")(O.some(1)); // => { _tag: 'Some', value: { a: 1 } }
O.bindTo("a")(O.none); // => { _tag: 'None' }

elem

Optionの中身の等値性を見る。
比較にEqインスタンスが用いられる。

O.elem(N.Eq)(1, O.some(1)); // => true
O.elem(N.Eq)(1, O.some(2)); // => false
O.elem(N.Eq)(1, O.none); // => false

exists

Some値として値が存在した場合のPredictを行う。
Noneの場合は問答無用でfalse。

O.exists((a: number) => a % 2 === 0)(O.some(0)); // true
O.exists((a: number) => a % 2 === 0)(O.some(1)); // false
O.exists((a: number) => a % 2 === 0)(O.none); // false

getRefinement

type guardをOptionを介する事によって型安全に作成する。

const isA = (c: C): c is A => c.type === "B"; // 間違った判定だがコンパイルが通る

const isA = O.getRefinement<C, A>((c) => (c.type === "B" ? O.some(c) : O.none));
// Type '"B"' is not assignable to type '"A"'.

const isA = O.getRefinement<C, A>((c) => (c.type === "B" ? O.none : O.some(c)));
const a = { type: "A", a: 1 } as C;
isA(a) && a.a; // => 1

sequenceArray

Optionの配列(Array<Option<T>>)を配列のOption(Option<Array<T>>)に変換する。
Optionを集めるときにめちゃ便利。

O.sequenceArray([O.some(1), O.some(2)]); // => { _tag: 'Some', value: [ 1, 2 ] }
O.sequenceArray([O.some(1), O.none]); // => { _tag: 'None' }

traverseArray

配列からOptionの配列を作りつつ、結果がArray<Option<T>>になっているのをsequenceArrayするイメージ。

O.traverseArray((a: number) => (a % 2 === 0 ? O.some(a) : O.none))([2, 4, 6]); // => { _tag: 'Some', value: [ 2, 4, 6 ] }
O.traverseArray((a: number) => (a % 2 === 0 ? O.some(a) : O.none))([1, 2, 3]); // => { _tag: 'None' }

traverseArrayWithIndex

traverseArrayのOptionへの変換処理をindex付きでできるバージョン。

O.traverseArrayWithIndex((i, a: number) => (i < 3 ? O.some(a) : O.none))([
  1, 2, 3,
]); // => { _tag: 'Some', value: [ 1, 2, 3 ] }
O.traverseArrayWithIndex((i, a: number) => (i < 3 ? O.some(a) : O.none))([
  1, 2, 3, 4,
]); // => { _tag: 'None' }

interop

cainNullableK

chainやりつつOptionじゃなくnullableな結果を返す処理をOptionを返しているのと同じ様に扱える。

type Person = {
  name?: string;
};

O.chainNullableK((p: Person) => p.name)(O.some({ name: "john" })); // => { _tag: 'Some', value: 'john' }

fromNullable

nullableな値からOptionを作成する。

O.fromNullable(undefined); // => { _tag: 'None' }
O.fromNullable(1); // => { _tag: 'Some', value: 1 }

fromNullableK

nullableな値を返す関数をOptionを返すような関数に変換する。

O.fromNullableK((a: number) => (a % 2 === 0 ? a : undefined))(0); // => { _tag: 'Some', value: 0 }
O.fromNullableK((a: number) => (a % 2 === 0 ? a : undefined))(1); // => { _tag: 'None' }

toNullable

Option<T>をnull | Tに変換する。

O.toNullable(O.some(1)); // => 1
O.toNullable(O.none); // => null

toUndefined

Option<T>をundefined | Tに変換する。

O.toUndefined(O.some(1)); // => 1
O.toUndefined(O.none); // => undefined

tryCatch

例外を投げる関数からOptionを作成する。
正常に値を返す場合はSomeに、例外が投げられた場合にはNoneになる。

O.tryCatch(() => 1); // => { _tag: 'Some', value: 1 }
O.tryCatch(() => {
  throw new Error();
}); // => { _tag: 'None' }

tryCatchK

例外を投げる関数をOptionが返る関数に変換する。
正常終了した場合にはSomeに、例外が投げられた場合にはNoneになる。

const mayThrow = (a: number) => {
  if (a % 2 === 0) {
    return a;
  } else {
    throw new Error();
  }
};

O.tryCatchK(mayThrow)(0); // => { _tag: 'Some', value: 0 }
O.tryCatchK(mayThrow)(1); // => { _tag: 'None' }

型クラス

Optionに対しては以下の型クラスのインスタンスとそのOption用の関数が定義してある。

info-outline

お知らせ

K.DEVは株式会社KDOTにより運営されています。記事の内容や会社でのITに関わる一般的なご相談に専門の社員がお答えしております。ぜひお気軽にご連絡ください。