簡潔で不変なデータ型にいくつもメソッドを生やしていると、なんだか負けた気分になる。データ型の定義だけしておいて、後から必要なメソッドを必要なだけ増やしたい。しかも、共用体型で上手いこと動いて欲しい。
そんな欲望を叶えるために次のような型関数PipeSpaceと、関数pipeSpaceを作った。
欲望のかたまり
export type PipeSpace<T extends object> = Readonly<T & {
pipe: <U>(callback: (self: PipeSpace<T>) => U) => U
}>;
export function pipeSpace<T extends object>(object: T): PipeSpace<T> {
const self: PipeSpace<T> = {
...object,
pipe: callback => callback(self)
};
return self;
}
データ型の定義例
例えばOption型を定義する場合、次のように書く。まず、PipeSpaceで型の定義を囲み、pipeSpaceでオブジェクトの生成を囲めば完璧だ。
メソッドは一つも実装していない。だが焦る必要はない。コーヒでも飲んで、一息ついてから取り掛かろう。
import {PipeSpace, pipeSpace} from './PipeSpace'
export type Option<T> = PipeSpace<None | Some<T>>;
export type None = PipeSpace<{
isNone: true
}>;
export const none: None = pipeSpace({
isNone: true
});
export type Some<T> = PipeSpace<{
isNone: false,
value: T
}>;
export function some<T>(value: T): Some<T> {
return pipeSpace({
isNone: false,
value
});
}
拡張例
さて、Option型の値の中身を変換するmapと、中身を上手いこと取り出してくれるgetOrElseが欲しくなったとする。
欲しいなら所望のロジックを持った関数を素直に書けばいい。ただし、処理対象のOption型の値は単独で受け取る必要がある。そこだけは注意しよう。
import {Option, some} from './Option';
export function map<T, U>(transformer: (value: T) => U) {
return (option: Option<T>): Option<U> =>
option.isNone
? option
: some(transformer(option.value));
}
export function getOrElse<T>(defaultValue: T) {
return (option: Option<T>): T =>
option.isNone
? defaultValue
: option.value;
}
export function hint<T>(option: Option<T>): Option<T> {
return option;
}
使用例
この実装は次のように使える。
え?メソッドじゃないって???そんな細かいことは気にするな。pipe()に目を瞑れば、見た目はほとんどメソッドじゃないか。十分だろ?
import {some, none} from './Option';
import * as Option from './OptionUtil';
console.log(some(5)
.pipe(Option.getOrElse(-1)));
// => 5
console.log(some(5)
.pipe(Option.map(x => x * x))
.pipe(Option.getOrElse(-1)));
// => 25
console.log(none
.pipe(Option.getOrElse(-1)));
// => -1
console.log(Option.hint<number>(none)
.pipe(Option.map(x => x * x))
.pipe(Option.getOrElse(-1)));
// => -1
おわり
君の欲望は、どんな型(かたち)をしているんだい?