Option型は例外を表現できる型です。
この型はJavaScript環境にあらかじめ組み込まれたものではないので、型を使いたい場合は、自分で記述する必要があります。
Option型の特徴
Option型は、値の代 わりにコンテナ( container)を返します。
コンテナの中には値が入っていたり入っていなかったりします。 値を保持することさえできれば、ほぼすべてのデータ構造をコンテナにすることができます。
使い方はコンテナを返すメソッドを定義し、 そのメソッドを使うと、実際にはコンテナの中に値がない場合でも、操作を連鎖させることができます。
失敗する可能性のある複数の操作を連続して行う必要がある場合などに用います。
配列をコンテナとして使う
配列をコンテナとして使うことができます。
以下で定義したOption型は受け取った文字列を数値に変換し、変換できれば数値の配列を返します。
変換できない場合は空の配列を返します。
//コンテナを返すメソッド
function parse(str: string): number[] {
let num = parseFloat(str)
if (isNaN(num)) {
return []
}
return [num]
}
let num = parse("10.5")
num.map(_ => Math.round(_))
.forEach(_ => console.info(' number is', _)) //number is 11
map~forEachと操作を連鎖させています。
parseメソッドの引数に「”aaa”」などを渡しても、空の配列が返されるので操作は連鎖されます。
Option型の欠点は、エラーが発生した理由を利用者に伝えないことです。
何かがうまくいかなかったことを単に知らせるだけです。
失敗する可能性のある複数の操作を連鎖して行う
Option型が本当に輝きを放つのは、失敗する可能性のある複数の操作を連鎖(連続)して行う必要がある場合です。
先ほどはparseメソッドに渡す値を直接記述していましたが、今回は以下のようなメソッドで引数に渡す値を取得してみます。
function ask() {
let result = prompt('Please enter a number')
if (result === null) {
return []
}
return [result]
}
askメソッドはユーザーが入力した値を確認します。
ユーザーの入力があった場合は入力値の配列を、入力がなかった場合は空の配列を返します。
※)promptメソッドはコマンドプロンプトンよりユーザーに入力を求め、入力値を文字列で返す関数と仮定。
全体的に見ると以下のような感じです。
※)コマンドプロンプトンのユーザー入力は「10.5」と仮定。
function ask() {
let result = "10.5"
if (result === null) {
return []
}
return [result]
}
function parse(str: string): number[] {
let num = parseFloat(str)
if (isNaN(num)) {
return []
}
return [num]
}
ask()
.map(parse)
.map(_ => Math.round(_)) //エラー
.forEach(_ => console.info(' number is', _))
askメソッドを呼び出しユーザー入力値の配列を取得 → 配列の値を四捨五入 → 出力という連鎖の流れです。
しかし上記を呼び出すと、「.map(parse)」の返す値が number[][] になってしまい、 Math.round() でエラーになってしまいます。なので以下のように「.map(parse)」の返す値が number[] となるようにします。
// 配列の配列を、配列に平坦化
function flatten<T>(array: T[][]): T[] {
return Array.prototype.concat.apply([], array)
}
flatten(ask().map(parse))
.map(_ => Math.round(_))
.forEach(_ => console.info(' number is', _)) //number is 11
クラスをコンテナとして使う
今度はコンテナを配列ではなく、クラスにしてみます。
用意するクラスはSomeクラスとNoneクラスとそれぞれに実装するインターフェースです。
Someクラスは成功した場合に返すコンテナ、Noneクラスは失敗した時に返すコンテナです。
- 成功時はSomeクラスがコンテナとして返される
- 失敗時はNoneクラスがコンテナとして返される
今回も前回と同様、ユーザーの入力した文字列が数値に変換できた場合は成功、出来なかった場合もしくはユーザーの入力が無かった場合は失敗とします。
※)ユーザーが「”10.5″」と入力したと仮定して進めていくので、これ以下の結果は成功となるように進んで行きます。
ユーザー入力
まずユーザー入力の関数を設定します。
内容は配列の時と同じです。
異なる所は、配列を返すかクラスを返すかの違いです。
ユーザーの入力があれば成功のSomeクラス、なければ失敗のNoneクラスのインスタンスを返します。
あとで説明しますが、Someクラスはユーザーの入力値を保持しますので、初期化時にユーザー入力値を渡します。
function ask() {
let result = "10.5"
if (result === null) {
return new None
}
return new Some(result)
}
インターフェース
SomeクラスとNoneクラスそれぞれに実装するインターフェースです。
入力値によってSome(成功)かNone(失敗)が返されるかが決まりますので、共通のメソッドを実装しておきます。
flatMapは引数の関数の返す値によるのでオーバーロードで実装しています。
interface Option<T> {
// 引数の関数が失敗を表すNoneを返した時のメソッド
flatMap(f: (value: T) => None): None
// 引数の関数が成功を表すSomeを返した時及び実装のメソッド
flatMap<U>(f: (value: T) => Option<U>): Option<U>
// Someの場合は保持している値を返す
// Noneの場合は渡された引数の値をそのまま返す
getOrElse(value: T): T
}
flatMap<U>(f: (value: T) => Option<U>): Option<U>
Some(成功)やNone(失敗)が返されるメソッドの実装メソッドなのですが、Option<U>を返す記述があるのでSomeクラス(Optionを継承)への実装時にはオーバーロードで成功時のSomeを返すメソッドが記述できます。
引数の関数に渡される値は、Someを返すメソッドではSomeが保持している値(askにて初期化時に渡した値)が渡されます。
flatMap(f: (value: T) => None): None
上記のメソッドと同じように引数の関数にはSomeが保持している値(初期化時に渡した値)が渡されますが、Noneは失敗のメソッドなので呼び出された時は常にNone(自身)を返します。もちろん値の保持もありません。
※)SomeクラスのflatMapは保持している値を基に、成功または失敗を返すメソッドになります。
※)NoneクラスのflatMapは必ず自身を返すメソッドになります。
getOrElse(value: T): T
Someの場合は保持している値を返し、Noneの場合はgetOrElse呼び出し時に渡された引数の値をそのまま返します。
※)getOrElseは保持している値を取得するメソッドになります。
Someクラス(成功時コンテナ)
成功時に返されるSomeクラスです。
class Some<T> implements Option<T> {
constructor(private value: T) { }
//引数の関数が返す値によって戻り値が異なる
flatMap<U>(f: (value: T) => None): None
flatMap<U>(f: (value: T) => Some<U>): Some<U>
flatMap<U>(f: (value: T) => Option<U>): Option<U> {
return f(this.value)
}
getOrElse(): T {
return this.value
}
}
Optionインターフェースを継承しています。
constructorを記述して、初期化時に値を渡しその値を保持します。
flatMap
flatMapの引数の関数の戻り値がそのままメソッドの戻り値になっています。
引数の関数に渡される値は、Someが保持している値になります。
今さらですが、保持している値というのはユーザーの入力値になります。(ユーザーの入力があった場合)
この値によってflatMapが返す値が成功の場合はSome、失敗の場合はNoneになります。
厳密には引数の関数の実装でそのようになるように実装しなければならないので、現時点では保持しているT型の値を基にNoneもしくはSome<U>が返されるという事になります。
Some<U>のジェネリクスのUは、成功時はSomeのインスタンスが返されるので、その時の初期化時に渡される値がどの型にも対応できるようにしています。
今回は文字列を数値に変えるように引数の関数を実装するので、Uは必ずnumberになるようになります。
getOrElse
Someの場合のgetOrElseメソッドは保持している値を返すので、引数は省略しています。
Noneクラス(失敗時コンテナ)
失敗時に返されるNoneクラスです。
class None implements Option<never> {
//常にNoneを返す
flatMap(): None {
return this
}
//引数で渡した値が常に返される
getOrElse<U>(value: U): U {
return value
}
}
Optionインターフェースを継承していますが、Noneクラスは失敗の操作を表し、SomeクラスのようにT型の値を保存する必要はないのでジェネリクスは必要なく、継承しているOption型のジェネリクスにはnever(値を持たない型)を指定しています。
flatMap
NoneのflatMapは常にNone(自身)を返すので引数は省略しています。
getOrElse
保持している値はないので、引数を設定して呼び出し時に渡された引数がそのまま返される仕様になっています。
Noneは失敗を表すので引数にはエラー表示の文言を渡す想定です。
flatMapの引数に渡す関数
この関数の実装内容でflatMapの返すコンテナがSomeかNoneかに決まります。
インターフェースOptionと関連付けるため、関数名をOptionと同じにしてコンパニオンオブジェクトパターンとしています。
渡される引数によって返す値も変わってきますので、オーバーロードで対応しています。
function Option<T>(value: null | undefined): None
function Option<T>(value: T): Some<number>
function Option<T>(value: T): Option<number | None> {
if (value == null || typeof (value) != "string") {
return new None
}
let num = parseFloat(value)
if (isNaN(num)) {
return new None
}
return new Some(num)
}
● Noneを返す場合
- ユーザーの入力がなかった場合
- ユーザー入力が文字列でなかった場合
- ユーザーが入力した文字列が数値に変換できなかった場合
● Someを返す場合
- ユーザーの入力が文字列であり、その文字列が数値に変換できた場合
数値に変換できれば、その数値をSomeの保持する値として渡してインスタンスを返します。
Someに渡す値ですが、上記では「ユーザーの入力値」として説明していますが、厳密には呼び出しは連鎖をさせますので、前の操作でSomeが返され、そのSomeインスタンスが保持している値になります。
また、今回はユーザーが入力した文字列を数値に変換という事に限定しているので、ユーザー入力で値があった場合の戻り値のジェネリクス型はnumber(Some<number>)に指定しています。
実行
呼出しを連鎖順に1つづつ見ていきます。
まずはユーザー入力の関数askを呼び出します。
ask() // Option<string> Some{value:'10.5'}
前にも説明したように、ユーザー入力は文字列の「”10.5″」と仮定していますので、askの呼び出しでは文字列「”10.5″」を保持したSome(成功)のインスタンスが返されます。
次は連鎖させてflatMapメソッドを呼び出します。
ask() // Option<string> Some{value:'10.5'}
.flatMap(Option) // Option<number> Some{value:10.5}
askはSomeを返しているのでここで呼び出されているのは、SomeクラスのflatMapメソッドです。
そしてaskで返されたSomeが保持している値は文字列の「”10.5″」で、数値への変換は成功するのでflatMapは数値の「10.5」を保持したSomeを返します。
そして次は最終的な出力になります。
ask() // Option<string> Some{value:'10.5'}
.flatMap(Option) // Option<number> Some{value:10.5}
.getOrElse('Error') // 10.5
前の連鎖で、数値の「10.5」を保持したSomeが返されているので、SomeクラスのgetOrElseメソッドが呼び出されます。
SomeクラスのgetOrElseメソッドは保持している値を返すので、数値の「10.5」となります。
flatMapの返すコンテナをNoneにすると以下のように「Error」となります。
ask() // Option<string> Some{value:'10.5'}
.flatMap(Option) // Option<number> Some{value:10.5}
.flatMap(value => new None()) // None {}
.getOrElse('Error') // Error
まとめ
以下全てまとめたコードになります。
function ask() {
let result = "10.5"
if (result === null) {
return new None
}
return new Some(result)
}
interface Option<T> {
flatMap(f: (value: T) => None): None
flatMap<U>(f: (value: T) => Option<U>): Option<U>
getOrElse(value: T): T
}
class Some<T> implements Option<T> {
constructor(private value: T) { }
flatMap<U>(f: (value: T) => None): None
flatMap<U>(f: (value: T) => Some<U>): Some<U>
flatMap<U>(f: (value: T) => Option<U>): Option<U> {
return f(this.value)
}
getOrElse(): T {
return this.value
}
}
class None implements Option<never> {
flatMap(): None {
return this
}
getOrElse<U>(value: U): U {
return value
}
}
function Option<T>(value: null | undefined): None
function Option<T>(value: T): Some<number>
function Option<T>(value: T): Option<number | None> {
if (value == null || typeof (value) != "string") {
return new None
}
let num = parseFloat(value)
if (isNaN(num)) {
return new None
}
return new Some(num)
}
console.log(
ask() // Option<string> Some{value:'10.5'}
.flatMap(Option) // Option<number> Some{value:10.5}
.getOrElse('Error') // 10.5
)
console.log(
ask() // Option<string> Some{value:'10.5'}
.flatMap(Option) // Option<number> Some{value:10.5}
.flatMap(value => new None()) // None {}
.getOrElse('Error') // Error
)
まとめ
Option型は、失敗する可能性のある連鎖操作を扱うための強力な方法です。
指定された連鎖操作が失敗する可能性のあることを、型システムを通じてユーザーに知らせます。
オーバーロードされた呼び出しシグネチャを使って、可能なところではOptionをSomeまたはNoneだけに限定することで、コードをはるかに安全にする事もできます。
ただし、OptionはNoneを使って失敗を知らせるので、何が失敗したのか、なぜ失敗したのかについての詳細は得られません。また、Optionを使用しないコードと相互運用することはできません(Optionを返すように、それらのAPIを明示的にラップする必要があります)。