kmizuの日記

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

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


ネタ切れもいいところなのですが、前回の続きです。前回の引きで、

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


と書いたのですが、今回はそれの説明です。前回、

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


というコード例を示してみせたのですが、実は、これは本来の機能が縮退した形です。上記の例では、列挙型の値(相当)のものがパラメタを取ることはありませんでしたが、一般的には、パラメタを取ることができます。


とだけ言っても何がなんだか、なので簡単な例で説明します。


Scalaでは次のようなコードを書くことができます。

abstract sealed class XmlNode
case class Element(name: String, attributes: List[(String, String)], children: List[XmlNode]) extends XmlNode
case class Text(content: String) extends XmlNode


ここで、抽象クラスXmlNodeXMLのサブセットのノードのルートクラスです。ケースクラスElementは、XMLの要素ノードを表し、TextXMLのテキストノードを表しています。実際には、もっと詳細にする必要があるでしょうが、あくまで例なので勘弁してください。

このプログラムは、Javaで言うと、

public abstract class XmlNode {
}

public class Element extends XmlNode {
  private final String name;
  private final List<Attribute> attributes;
  private final List<XmlNode> children;
  public Element(String name, List<Attribute> attributes, List<XmlNode> children) {
    super();
    this.name = name;
    this.attributes = attributes;
    this.children = children;
  }
  public String getName() {
    return name;
  }
  public List<Attribute> getAttributes() {
    return attributes;
  }
  public List<XmlNode> getChildren() {
    return children;
  }
}

public class Attribute XmlNode {
  private final String name;
  private final String value;
  public Attribute(String name, String value) {
    super();
    this.name = name;
    this.value = value;
  }
  public String getName() {
    return name;
  }
  public String getValue() {
    return value;
  }
}

public class Text extends XmlNode {
  private final String content;

  public Text(String content) {
    super();
    this.content = content;
  }

  public String getContent() {
    return content;
  }   
}


のようなコードに相当します。GoF本でおなじみのCompositeパターンですね。ここで、Scalaの方が簡潔に書けてるぜ、ひゃっほー、というのが本題ではありません。*1


ここで、とりあえずテキストをパーズしてXmlNodeオブジェクトを作ることまではできたとします。問題は内容を取り出すときです。JavaならおそらくVisitorを作るか、XmlNodeをinstanceofでマッチするかのどちらかになるでしょう。しかし、Visitorはあらかじめデータ構造側にVisitorを組み込んでおかなければいけないのが嬉しくないですし、instanceofは型安全性に欠けます。


このような場合に、パターンマッチが本領を発揮します。次のコードを見てください。

val node: XmlNode = Element("foo", List("x" -> "1"), List(Text("hogehoge")))

node match {
  case Element(name, attributes, _) =>
    println(name)
    println(attributes)
  case Text(content) =>
}


nodeには都合上、あえて明示的に型をつけていますが、気にしないでください。
本題は、node match ...の部分です。これは、

  • nodeがElementなら、
    1. name変数にElementのnameを代入
    2. attributes変数にElementのattributesを代入
    3. _が付いた部分は無視
    した上で、=>以降の処理を行う
  • nodeがTextなら、=
    1. content変数にTextのcontentを代入
    した上で、>以降の処理を行う


という事を意味しています。これは、Javaっぽい擬似コードで書くと、

if(node instanceof Element) {
  Element e = (Element)node;
  String name = e.getName();
  List<Attribute> attributes = e.getAttributes(); 
  System.out.println(name);
  System.out.println(attributes);
}else if(node instanceof Text) {
  Text e = (Text)node;
}


のようになります。こうして比較してみるとわかりますが、Scalaのパターンマッチングは、構造を持ったオブジェクトに対して、

  • オブジェクトのクラスを判定して分岐する(if .. instanceof)
  • オブジェクトの要素を分解して、変数に代入する (String name = e.getName() ...)


という両方の機能を備えており、かつ、型安全なことがわかります。
パターンマッチが強力なのは、この二つの機能を同時に兼ね備えていることにあります。


また、sealedが付いているため、列挙型の場合と同様に、パターンに漏れがあった場合に警告が出ます。たとえば、以下のコードでは、nodeがTextの場合を考慮していませんが、そのような場合にちゃんと警告を出してくれます。

node match {
  case Element(name, attributes, _) =>
    println(name)
    println(attributes)
}
XmlNode.scala:7: warning: match is not exhaustive!


コンパイル時に警告を出すことを含めて、Javaenumなどでは、この機能(sealed + パターンマッチング)を簡単にエミュレートすることができません。


ここまで来るともはや「ふつーのオブジェクト指向言語」とは言えませんが、ともあれ、「関数型」というキーワードを出さなくても、パターンマッチなどの「関数型っぽい」機能を説明できることがわかってもらえた(ら嬉しい)のではないかと思います。


なんだかgdgdになってしまいましたが、今日はこの辺で。ではでは。

*1:確かにこのような構造を簡潔に書けて嬉しいという面はあるのですが