LoginSignup
4
1

More than 3 years have passed since last update.

湯婆婆で学ぶScalaのエラーハンドリング

Last updated at Posted at 2020-12-17

初めに

本記事は2020年限界開発鯖アドベントカレンダーの18日目の投稿です。
また素人記事なため温かい目で見守ってください。
昨日の記事はこちら

只今の時刻は23:56。あと4分です。さようなら><

湯婆婆

湯婆婆ってなんだっけ?

はい。もうブームはさりました。。。。

おさらい

@NemesisさんのJavaで湯婆婆を実装してみるという記事からはしまった、ネットミームの一つ。
千と千尋の神隠しに出てくる湯婆婆が色々なプログラミング言語で実装してみるという至って簡単な内容。

僕もScalaで実装済みで今回はその際の記事のコードを再利用しています。
今回の記事で省略しているコードもあるので、不明な点があればこちらも合わせてお読みください

割といい内容なのでこれでエラーハンドリングについてお勉強してみる。

湯婆婆でのエラー

湯婆婆でエラーが出るときってどういうときでしょうか。
おそらく初めに思いつくのは名前が空だったときだと思います。
一度名前を空にして実行してみましょう

class errorContract() {
    val sc = new java.util.Scanner(System.in)

    def signature(): String = {
        io.StdIn.readLine()
    }

    var signatureName: String = signature()
}

class errorBaba(var User: Human) extends Human {
    name = "yubaba"

    def initMessage(): Unit = {
        println("契約書だよ。そこに名前を書きな。")
        val contract = new errorContract()
        User.name = contract.signatureName
        changeName(contract)
    }

    def changeName(contract: errorContract): Unit = {
        println("フン。「" + contract.signatureName + "」というのかい。贅沢な名だねぇ。")
        val newNameIndex: Int = Random.nextInt(User.name.length)
        User.name = User.name.substring(newNameIndex, newNameIndex + 1)
        println("今からお前の名前は「" + User.name + "」だ。いいかい、「" + User.name + "」だよ。分かったら返事をするんだ、「" + User.name + "」!!")
    }
}

なんのエラーハンドリングもされていません。
動かしてみましょう
image.png
入力の際にエンターをそのまま押すとこのようなエラーが出ました。
これは文字から文字の長さを取る際に文字数が0なのでランダムで0をmaxにして生成できないため起こっています。
該当コード

val newNameIndex: Int = Random.nextInt(User.name.length)

これではユーザーがミスしてそのままエンターを押してしまった際に、エラーが起き動かなくなってしまいます。

今までのエラー処理

まず紹介するのはtry-catchです。
殆どの言語の入門書にはこのエラー処理が紹介されていると思います。
とりあえず書いてみましょうか。

class trycatchContract() {
    val sc = new java.util.Scanner(System.in)

    def signature(): String = {
        io.StdIn.readLine()
    }

    var signatureName: String = signature()
}

class trycatchBaba(var User: Human) extends Human {
    name = "yubaba"

    def initMessage(): Unit = {
        println("契約書だよ。そこに名前を書きな。")
        val contract = new trycatchContract()
        User.name = contract.signatureName
        try {
            changeName(contract)
        } catch {
            case _: Exception => println("なんだいここで働きたくないのかい")
        }
    }

    def changeName(contract: trycatchContract): Unit = {
        println("フン。「" + contract.signatureName + "」というのかい。贅沢な名だねぇ。")
        val newNameIndex: Int = Random.nextInt(User.name.length)
        User.name = User.name.substring(newNameIndex, newNameIndex + 1)
        println("今からお前の名前は「" + User.name + "」だ。いいかい、「" + User.name + "」だよ。分かったら返事をするんだ、「" + User.name + "」!!")
    }
}

実装は簡単で、先程エラーが出ていた関数を使用する際に、tryで関数を囲み、catchで先程確認したエラーが出た場合の処理を書いています。

今回の場合、来るエラーが起こる箇所が把握しやすいし、スコープが極端に短いためいいですが、大規模開発の場合はそうは行きません。どこでなんのエラーが起こるのかを明確にし、それを未然に予知しておかなければなりません。

よって最近の実際の開発ではtry-catchを使った処理はだいぶ減ったと思います。(ないとは断言できない><)

万能型Either

try-catchの欠点であった曖昧さ回避を回避するためにエラーを型として保存しておくという思考のもと作られたのがEitherです

そもそも関数で使用する頃に名前が空であってはいけません。コレでは開発が進んできった際にどこかで予期せぬエラーが発生してしまうと思います。
なので名前が入力された時点で名前に異常がないかを確認しエラーを保存しておけば使う際にも便利ですし安全ですね。

EitherではRightとLeftが提供されています。
正しいとかかっているのでRightが成功の方で、Leftは大抵はエラーメッセージ等が入ります。

なので名前がしっかりあれば名前をRightになければLeftにエラーメッセージをいれます。

実際のコードを書いてみましょう

class eitherContract {
    val sc = new java.util.Scanner(System.in)

    def signature(): Either[String, String] = {
        val tmpName = sc.nextLine()
        if (tmpName != "") Right(tmpName) else Left("Name is empty")
    }

    var signatureName: Either[String, String] = signature()
}

class eitherBaba(var User: Human) extends Human {
    name = "yubaba"

    def initMessage(): Unit = {
        println("契約書だよ。そこに名前を書きな。")
        val contract = new eitherContract()
        contract.signatureName match {
            case Right(v) =>
                println("フン。「" + v + "」というのかい。贅沢な名だねぇ。")
                User.name = v
                changeName()
            case Left(_) =>
                println("なんだいここで働きたくないのかい")
        }
    }

    def changeName(): Unit = {
        val newNameIndex: Int = Random.nextInt(User.name.length)
        User.name = User.name.substring(newNameIndex, newNameIndex + 1)
        println("今からお前の名前は「" + User.name + "」だ。いいかい、「" + User.name + "」だよ。分かったら返事をするんだ、「" + User.name + "」!!")
    }
}

今回は名前が空の際にName is emptyというエラーメッセージを保存しています。

if (tmpName != "") Right(tmpName) else Left("Name is empty")

こちらの部分でエラーかを判別しています。
今回は考えられるエラー内容が一つですが、複数個の際もLeftに該当するエラーメッセージを保存することは変わりません。
さらに一度に2つのエラーが発生する場合もLeftに配列などでメッセージを保存すれば様々なエラーに対応できます。

最後に

image.png
同じ動きをするこのコード達。
多少の時間リソースを割いてでも安全に正確に作って保守性の高い安全なサービスを作っていきましょう。

それではまたどこかのScala界隈で。

良いクリスマスをお過ごしください!ただしリア充は爆発して

明日の記事はこちら

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1