fp-tsとoption
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用の関数が定義してある。