Skip to content

part6 - スマートコンストラクタによるエラーハンドリング

いままでは、無効な入力があった場合はプログラムがクラッシュしていた。これを改善するために、Option型を使ってエラーハンドリングを行う。まずは、Create.fsを確認する。

Create.fs
module create
open System.IO
let counterDirectory = Path.Combine(".", "counter")
let counterFile = Path.Combine(counterDirectory, "counter.txt")
let readCounter =
if File.Exists(counterFile) then
let counter = File.ReadAllText(counterFile)
counter |> int
else
Directory.CreateDirectory(counterDirectory) |> ignore
File.WriteAllText(counterFile, "1") |> ignore
1
let incrementCounter =
let counter = readCounter + 1
File.WriteAllText(counterFile, counter.ToString()) |> ignore
counter
let create =
// まずはコンソールを全消去する。
System.Console.Clear()
Directory.CreateDirectory("./users") |> ignore
printfn "名前を入力してください(1文字以上、150字以内)。"
let name = stdin.ReadLine()
printfn "年齢を入力してください(0以上、150以下)。"
let age = stdin.ReadLine()
printfn "メールアドレスを入力してください(任意)。"
let email = stdin.ReadLine()
let id = incrementCounter
let csvData = sprintf "%d,%s,%s,%s" id name age email
let path = Path.Combine("./users", id.ToString() + ".csv")
File.WriteAllText(path, csvData) |> ignore

ここで考えるのは、名前の入力、年齢の入力、メールアドレスの入力だろう。名前の入力は、1文字以上、150字以内である必要がある。年齢の入力は、0以上、150以下である必要がある。メールアドレスの入力は任意である。これらの条件を満たさない場合は、エラーメッセージを表示して再度入力を求めるようにする。

まずは、名前の入力を考える。名前の入力は、1文字以上、150字以内である必要がある。これを満たさない場合は、再度入力を求めるようにする。実直なつくりにすると、以下のようになる。

let rec inputName () =
printfn "名前を入力してください(1文字以上、150字以内)。"
let name = stdin.ReadLine()
if name.Length = 0 || name.Length > 150 then
printfn "名前は1文字以上、150字以内で入力してください。"
inputName ()
else
name

これでも仕様を満たしている。悪くない。

でも、もう一歩先、スマートコンストラクタを使ってみよう。

スマートコンストラクタ

スマートコンストラクタを日本語にすると「賢い組み立て」みたいになるだろうか。なにがなんだかさっぱりわからないだろう。つまり、値を作成する際に検証を行い、不正な値が作成されることを防ぐことができる。

スマートコンストラクタは、createvalueの2つの関数で構成される。createは、値を作成する関数であり、valueは、値を取り出す関数である。createは、値を作成する際に検証を行い、不正な値が作成されることを防ぐ。createは、Result型を返す。Result型は、OkErrorのどちらかを返す。Okは、正常な値を表し、Errorは、エラーメッセージを表す。Result型はエラーとは異なり、プログラムを強制終了させない。もちろん、エラーの種類によっては強制終了させなければならないときもある(DBにつながらないとか)。それ以外の、プログラムを書いていて事前に想定されたおかしな入力などについては、Result型でスマートに処理することができる。

module Name
type Name = private Name of string
module Name =
let create (s: string) =
match s with
| s when s.Length = 0 -> Error "名前の入力は必須です。"
| s when s.Length > 150 -> Error "名前は150文字以内で入力してください。"
| s when s.Contains(",") -> Error "名前にカンマを含めることはできません。"
| _ -> Ok(Name s)
let value (Name s) = s

Nameのスマートコンストラクタ

それではスマートコンストラクタを用いた、名前の入力関数を作成してみよう。

let rec inputName message =
printfn "%s" message
let name = stdin.ReadLine()
match Name.create name with
| Ok name -> name
| Error message ->
printfn "%s" message
inputName "名前を入力してください(1文字以上、150字以内)。"

名前を入力してもらい、そのあとに名前をスマートコンストラクタで検証する。検証に成功した場合はName型の値を返し、検証に失敗した場合はエラーメッセージを表示して再度入力を求める。成功するまでループするわけだ。この関数をどこに置くのかは悩ましい。Name.fsに置くのが適切だろうか。それとも、Utils.fsなどにまとめるべきだろうか。今回は、ひとまずName.fsに置くことにしよう。

Name.fsの実装は以下の通りである。

Name.fs
module Name
type Name = private Name of string
module Name =
let create (s: string) =
match s with
| s when s.Length = 0 -> Error "名前の入力は必須です。"
| s when s.Length > 150 -> Error "名前は150文字以内で入力してください。"
| s when s.Contains(",") -> Error "名前にカンマを含めることはできません。"
| _ -> Ok(Name s)
let value (Name s) = s
let rec inputName message =
printfn "%s" message
let name = stdin.ReadLine()
match Name.create name with
| Ok name -> name
| Error message ->
printfn "%s" message
inputName "名前を入力してください(1文字以上、150字以内)。"

Ageのスマートコンストラクタ

次はAge型を作成する。年齢の入力は、0以上、150以下である必要がある。これを満たさない場合は、再度入力を求めるようにする。

type Age = private Age of int
let create (input: string) : Result<Age, string> =
match System.Int32.TryParse input with
| true, age when age >= 0 && age <= 120 -> Ok(Age age)
| true, _ -> Error "年齢は0歳から120歳の間である必要があります"
| false, _ -> Error "年齢は有効な数値である必要があります"

今回はSystem.Int32.TryParseを使用し、truefalseを返すようにした。trueの場合は、年齢が0以上、120以下であるかを検証し、falseの場合は、エラーメッセージを返すようにした。これで、年齢の入力をスマートコンストラクタで検証することができる。

Age.fsの実装は以下の通りである。

Age.fs
module Age
type Age = private Age of int
let create (input: string) : Result<Age, string> =
match System.Int32.TryParse input with
| true, age when age >= 0 && age <= 120 -> Ok(Age age)
| true, _ -> Error "年齢は0歳から120歳の間である必要があります"
| false, _ -> Error "年齢は有効な数値である必要があります"
let value (Age age) = age
let rec inputAge message =
printfn "%s" message
let age = stdin.ReadLine()
match create age with
| Ok age -> age
| Error message ->
printfn "%s" message
inputAge "年齢を入力してください(0歳から120歳まで)"

Emailのスマートコンストラクタ

最後に、メールアドレスの入力を考える。メールアドレスの入力は任意である。メールアドレスの入力がある場合は、メールアドレスの形式が正しいかを検証する。メールアドレスの形式が正しくない場合は、再度入力を求めるようにする。

open System.Net.Mail
type Email = private Email of string
module Email =
let create (s: string) =
match s with
| s when s.Length = 0 -> Ok(Email s) // 空文字を許可
| s when s.Length > 256 -> Error "メールアドレスは256文字以内で入力してください。"
| s ->
try
let _ = new MailAddress(s)
Ok(Email s)
with
| :? System.FormatException -> Error "有効なメールアドレスの形式ではありません。"
| _ -> Error "メールアドレスの検証中にエラーが発生しました。"
let value (Email s) = s

今回は、System.Net.Mail.MailAddressを使用してメールアドレスの形式を検証している。MailAddressのコンストラクタは、メールアドレスの形式がおかしい場合は例外をスローする。そのため、try withを使用して例外をキャッチする。メールアドレスの形式が正しい場合は、Okを返し、正しくない場合はエラーメッセージを返すようにした。

このようにvalidationを行うと煩雑になるが、実際に仕事でソフトウェアを開発していると、こういう些細な部分を作り込むか否かで大きく変わってくる。個人の趣味的なプログラムであれば、どこまでやるのかは難しいところだ。とはいえ、このスマートコンストラクタという機能は面白いので、使ってみると良いだろう。

Create.fsの修正

それではCreate.fsを修正していこう。

let create =
System.Console.Clear()
Directory.CreateDirectory("./users") |> ignore
let name = inputName "名前を入力してください(1文字以上、150字以内)。" |> Name.value
let age = inputAge "年齢を入力してください(0以上、150以下)。" |> string
let email = inputEmail "メールアドレスを入力してください(任意)。" |> Email.value
let id = incrementCounter
let csvData = sprintf "%d,%s,%s,%s" id name age email
let path = Path.Combine("./users", id.ToString() + ".csv")
File.WriteAllText(path, csvData) |> ignore

こんな感じでinputNameinputAgeinputEmailを呼び出すことで、名前、年齢、メールアドレスの入力を行うことができる。これで、無効な入力があった場合は、エラーメッセージを表示して再度入力を求めるようになった。

最後にCreate.fsの完成版を以下に示す。

Create.fs
module create
open Name
open Age
open Email
open System.IO
let counterDirectory = Path.Combine(".", "counter")
let counterFile = Path.Combine(counterDirectory, "counter.txt")
let readCounter =
if File.Exists(counterFile) then
let counter = File.ReadAllText(counterFile)
counter |> int
else
Directory.CreateDirectory(counterDirectory) |> ignore
File.WriteAllText(counterFile, "1") |> ignore
1
let incrementCounter =
let counter = readCounter + 1
File.WriteAllText(counterFile, counter.ToString()) |> ignore
counter
let create =
System.Console.Clear()
Directory.CreateDirectory("./users") |> ignore
let name = inputName "名前を入力してください(1文字以上、150字以内)。" |> Name.value
let age = inputAge "年齢を入力してください(0以上、150以下)。" |> string
let email = inputEmail "メールアドレスを入力してください(任意)。" |> Email.value
let id = incrementCounter
let csvData = sprintf "%d,%s,%s,%s" id name age email
let path = Path.Combine("./users", id.ToString() + ".csv")
File.WriteAllText(path, csvData) |> ignore

まとめ

今回は、スマートコンストラクタを用いたエラーハンドリングについて学んだ。これで例外が発生しなくなった。同じように、Update.fsでも作成した関数を使えるだろう。ただ、Update.fsの場合は、空の入力も認められたはずだ。そこは関数をもうひとつつくるのか、isRequireという引数を追加するのか、悩ましいところだ。あとはIncrementCounterなどもスマートコンストラクタにしても良いけれど、これはシステムが発行している数字なので大丈夫ということにしておこう。このあたりもしっかりと堅牢にしておいたほうが良い。