kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

Scalaはオブジェクト指向言語です(3)


さて、引き続きふつーのオブジェクト指向言語としてのScalaを説明していきます。…と言いたいところなのですが、早くもネタ切れしてきました。ですが、行けるとこまで行ってみましょう。

  • sealed:列挙型の漏れを検出してくれるくらいの機能


    この解説は実際には間違いなのですが、sealedの99.999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999%がこの用途だと思って問題ないでしょう*1


    さて、sealedの本来の意味はどうでもいいので、何のために使えるかを説明することにします。


    まず、前提として、ScalaにはCとかC#Java 5以降にあるような列挙型がありません。というと、列挙型を普段使っておられる方においては、大変不便に感じられるかもしれません。


    しかし、Scalaでは列挙型とほぼ同等(というか実際にはもっと強力)な機能を実現できるイディオムがあるので、ちょっとめんどくさいですが、大体問題ありません。とりあえず、コードを見てみましょう。

    abstract class Language
    case object JAVA extends Language
    case object SCALA extends Language
    


    ここで、JAVAとSCALAという識別子が列挙型の要素に相当し、Languageが列挙型の型に相当します。case objectとかいうものは、単にcase classのちょっとした変種です。


    これを使うときには、次のようにします。

    val x: Language = JAVA
    x match { 
      case JAVA => println("Java!")
      case SCALA => println("Scala!") 
    } // Java!
    


    何故かJAVAとかSCALAがパターンマッチで使えていますが、置いておきましょう。ここで実際に何が行われているかというと、Java風味(というかほとんどJavaですが)の擬似言語で説明すると、次のようになります。

    Language x = new JAVA();
    if(x instanceof JAVA) {
      System.out.println("Java!");
    }else if(x instanceof SCALA) {
      System.out.println("Scala!");
    }else {
      throw new scala.MatchError("...");
    }
    


    オブジェクト指向プログラマの大敵たるinstanceofが出てきましたが、基本的に型安全なので安心してください。


    ちなみに、最初の記事では書き忘れていましたが、Scalaのパターンマッチは、switchと違って、マッチしないケースに引っかかると例外を投げます。defaultを書き忘れても検出してくれるので便利ですね。


    …横道にそれ過ぎたので、話を戻しましょう。とまあ、こんな感じでScalaでは列挙型をエミュレートできるのですが、一つ問題があります。たとえば、こんな
    クラス定義(列挙型のエミュレート)があったとしましょう。

    abstract class Language
    case object JAVA extends Language
    case object SCALA extends Language
    case object CSHARP extends Language //俺も仲間に入れてくれ
    

    ここで、

    val x: Language = CSHARP
    x match { 
      case JAVA => println("Java!")
      case SCALA => println("Scala!") 
    } 
    


    とかすると、CSHARPなんて言語は知らんぜよ、とばかりにscala.MatchErrorが飛んできます。これはこれで、default書き忘れて変な挙動になるよりはマシなのですが、Scalaはせっかく静的型付け言語なのですから、静的に列挙型の漏れが無いかチェックしたいところですよね。


    で、ここでようやく登場するのがsealedです。sealedを使ってクラス定義を書き直すと、次のようになります。

    abstract sealed class Language
    case object JAVA extends Language
    case object SCALA extends Language
    case object CSHARP extends Language //俺も仲間に入れてくれ
    


    最初の一行にsealedが追加されただけですね。でも、これが重要な意味を持つのです。


    このクラス定義をなんか別の.scalaファイルにおいてコンパイルしてから、次のコード(を含むクラス定義など)をコンパイルしてみましょう。

    val x: Language = CSHARP
    x match { 
      case JAVA => println("Java!")
      case SCALA => println("Scala!") 
    } 
    

    すると、コンパイル時に

    UsingLanguage.scala:3: warning: match is not exhaustive!
    missing combination         CSHARP
    
      x match {
      ^


    という警告が表示されるようになります。要は列挙型の要素をパターンマッチで網羅してないよ、という意味です。


    とまあ、このように、sealedを使うと、列挙型に対するパターンマッチの記述漏れを防ぐことができて便利なのです。


    とかいうと、Javaだったらそんなのコンパイルオプションとかで検出できるよ、という声が聞こえて来そうです。


    実際には、sealedは、単に列挙型のパターンマッチ記述漏れを検出するだけの機能ではないのですが、それを説明するのは面倒なので、また次の記事で紹介しましょう。

    ではでは。


*1:若干誇張しています