2010年3月26日金曜日

By-name Parameters

先々週に、メインで使っていたThinkPadがとうとう壊れてしまった。ファンの故障なので修理できないこともなさそうだったけど、5年程使っているマシンでディスクも50GBと少なく、メモリ2GBは今となってはEclipse&Scalaの組み合わせには到底足りないので、観念して新しいものを買うことにした。

最近、周りの人のMac率が非常に高いこともあり、スペックが魅力的だったのでiMacにしてみた。到着したのが先週の金曜。ちょうど一週間経ったけど、やっと作業環境が整備できてきた。(そういえば、ブログもこの記事がiMacからの初投稿になる。)

CPUはCore i7。4コアで、Hyper Threading機能により擬似的に8コアになる。今後、マルチコアを前提としたプログラミングモデルに移行することが予想されるので、検証の基盤として期待している。

ということで、8コアの振舞いを見たいのと、ScalaのActorを試したいということもあり簡単な並列ソートプログラムを作って動かしてみた。このプログラムについてはいずれ取り上げるかもしれないけど、今回は並列ソートプログラムの性能測定用に作った以下の小さなプログラムがテーマ。

Tmr.scala
object Tmr {
  def run(proc: => Unit) {
    System.gc
    val start = System.currentTimeMillis
    proc
    val end = System.currentTimeMillis
    println("elapse(ms) = " + (end - start))
  }
}

このプログラムはScalaの特徴的な機能であるBy-name Parameterを使っている。By-name Parameterは関数リテラル/クロージャと字句的なトリックを使って、メタな機構を導入せずに擬似的な制御構文の拡張を可能にした、まさにスケーラブルを目指すプログラミング言語Scalaの影の主役ともいうべき重要な機能である。大事なのはrunメソッドのパラメタ「proc: => Unit」の所。通常の関数パラメタだとパラメタがない場合「proc: () => Unit」となるところを、パラメタリストを省略している。この省略記法をScalaではBy-name parameterと呼んでいる。

Tmr.scalaは小さなプログラムなので、ひと目で何をしているか分かると思う。

でもできることはすごい。

基本的な使い方は次のようになる。この場合は小さなソートが5msで終了している。

scala> Tmr.run(List(2, 1, 3).sort(_ < _))
elapse = 5

何となく、見逃してしまうかもしれないけど、関数リテラルや高階関数を持たないJavaなどプログラミング言語ではこういう書き方はできない。なぜなら、Tmr.runメソッドに制御が移る前に、引数の式が評価されてしまうから。地味だけど、関数型言語ならではの機能なのである。

試しに1000msスリープする式を引数にTmr.runメソッドを実行してみると経過時間が1003秒となった。妥当なところである。

scala> Tmr.run(Thread.sleep(1000))
elapse = 1003

ここまでの機能だけだと「影の主役」はちょっと大げさ。本当に感心するのは、以下のように引数として制御構造のボディのような複数の文の列を書くことができることである。このことによって擬似的な制御構文が追加可能になっている。

scala> Tmr.run {
     |   val list = List(2, 1, 3)
     |   list.sort(_ < _)
     | }
elapse = 2

この記法を支えているのは2つの特例。まず、パラメタが1つのパラメタリストは「(」「)」だけでなく、「{」「}」が使えるという特例がある。また、先ほど説明したようにBy-name Parameterはパラメタがない関数リテラルでパラメタリストを省略することができるという特例である。この2つの特例が字句上の工夫で、組み合わせて使用すると擬似的に制御構文を追加することができる。

こういった字句上の工夫というかトリックが色々と入っているのがScalaの特徴で、一筋縄ではいかないところである。プログラミング言語は、こういった字句上の工夫が使い勝手に大きく影響するということが、Scalaを使ってみるとよくわかる。

2010年3月16日火曜日

Android IO Utility

最近Androidを触る機会があり、いろいろと試してみている。

本当はAndroidもScalaで作りたいんだけど、色々と問題があるみたいなので今のところはあきらめてJavaで書いている。とはいえ、Eclipseのコード補完、リファクタリングはやはり便利で、フレームワークにプラグインするためのハンドラを書くことが中心のプログラムではScala+Emacsよりも便利である。Androidもこの範疇に入る。

Androidを触って感じたのは、全く組込みや制御系といった気がしないとうことである。体感的には普通のUNIX&Java&GUI&Webクライアントアプリを作っている手触り。

画面が小さいのとネットワークが遅いのとメモリが少ないのとキー入力がやりにくいのとGPSなどのデバイスがたくさんついているのが違うところ。でも、プログラミングモデルは普通のJavaでよい。昔の制御系みたいな職人芸は要らないから、アイデアしだいというところが、Androidがエンジニアに受けている理由のひとつだろう。もちろん、本格的なアプリケーションを作る段になれば、色々な技術が必要になるけれど、間口が広いというのはよいことである。

Androidは、通常ファイルの読み書きができるからAppEngineのプログラミングモデルと比べるとずいぶん楽である。ファイルをBLOB/CLOB的な使い方もできるし、JSONを使ってちょっとした情報を保存しておくこともできる。

モバイルアプリケーションなので、pause/resumeの状態遷移が発生した時の状態引継ぎが必須の作業になる。さらに、アプリケーションが強制終了されても、前回のセッションの状態に復元できることが望ましいので、その意味でもアプリケーションの状態保存のために手軽に使えるデータの保存場所が必要になる。この目的にJSONと通常ファイルの組み合わせが便利なのである。

また、ActivityやServiceといったエージェント間や同一エージェント内でも異なったスレッド間ではIPC的な通信になる。こういったエージェント間での情報受け渡しや情報文脈共有に不揮発性一時データを使う場合、RDBMSを使うのが本格的だけど、JSONをファイルに格納して受け渡すのが手軽で使いやすい。

もちろん、サーバーとの通信はJSONベースでよい。

以上の点から、AndroidではJSONによるデータの取り回しを前提にしたアーキテクチャにするのが筋がよさそうと感じたわけである。org.json.JSONObjectが基本で提供されているのも大きい。

JavaでXMLを使うのは少し煩雑だし、性能的にもいい所はないので、特別な要件がなければあまりXMLにこだわらないのがよいだろう。

そんなこともあり、AndroidIOUtilityというファイル入出力のユーティリティクラスを作った。

AndroidIOUtility.java
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;

import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;

public class AndroidIOUtility {
public static JSONObject loadJsonFromFile(String filename, Context context) \
    throws IOException, JSONException {
        return new JSONObject(loadStringFromFile(filename, context));
    }

public static void saveJsonFromFile(String filename, JSONObject json, \
    Context context) throws IOException, JSONException {
        OutputStream out = null;
        try {
            out = context.openFileOutput(filename, Context.MODE_PRIVATE);
OutputStreamWriter writer = new OutputStreamWriter(out, "utf-8");
            writer.append(json.toString(2));
            writer.flush();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {}
            }
        }
    }

public static String loadStringFromFile(String filename, Context context) \
    throws IOException {
        InputStream in = null;
        try {
            in = context.openFileInput(filename);
            InputStreamReader reader = new InputStreamReader(in, "utf-8");
            StringBuilder builder = new StringBuilder();
            char[] buf = new char[4096];
            int size;
            while ((size = reader.read(buf)) != -1) {
                builder.append(buf, 0, size);
            }
            return builder.toString();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {}
            }
        }
    }

public static void saveStringToFile(String string, String filename, Context \
    context) throws IOException {
        OutputStream out = null;
        Writer writer = null;
        try {
            out = context.openFileOutput(filename, Context.MODE_PRIVATE);
            writer = new OutputStreamWriter(out, "utf-8");
            writer.append(string);
            writer.flush();
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                } else if (out != null) {
                    out.close();
                }
            } catch (IOException ee) {}
        }
    }

public static void saveInputStreamToFile(InputStream in, String filename, \
    Context context) throws IOException {
        OutputStream out = null;
        try {
            out = context.openFileOutput(filename, Context.MODE_PRIVATE);
            byte[] buf = new byte[8192];
            int size;
            while ((size = in.read(buf)) != -1) {
                out.write(buf, 0, size);
            }
            out.flush();
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (IOException ee) {}
        }
    }

public static void saveResourceToFile(int resourceId, String filename, \
    Context context) throws IOException {
        InputStream in = null;
        try {
            in = context.getResources().openRawResource(resourceId);
            saveInputStreamToFile(in, filename, context);
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException ee) {}
        }
    }

public static void truncateFile(String filename, Context context) throws \
    IOException {
        OutputStream out = null;
        try {
            out = context.openFileOutput(filename, Context.MODE_PRIVATE);
            out.write(new byte[0]); // XXX needs check
            out.flush();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {}
            }
        }        
    }
}

内容は簡単なファイル入出力なので難しいところはない。それより、Android向けのアプリケーション・アーキテクチャを意識した機能セットという意味で見てもらえると参考になるかもしれない。

1つはJSONの入出力を基本に考えていること。理由は前述したとおり。

また、画像なども通常ファイルとしてキャッシュしたり受け渡ししたりすることになるので、生バイナリデータの入出力も必要になる。

saveResourceToFileメソッドは、リソースに格納した画像データをServiceへの受け渡し目的でファイルに書き出す必要があったので作った。リソースからのデータの取り出しも色々なパターンがありそうなので、目的に応じて用意するとよいだろう。

文字コードはUTF-8決め打ちにしている。もちろん、他の文字コードの読み書きが必要なケースも出てくるだろうけど、自アプリケーション内での通信や不揮発性一時データの保存に用途を絞ればUTF-8決め打ちが簡潔である。

アプリケーションデータはSDCardに置いておくのが作法のようなので、それむけのメソッドもいずれ作ることになると思う。ファイル入出力周りはGoogleのGuavaが便利そうなので、これを使うように改造してもよいかもしれない。

2010年3月10日水曜日

Scalaのロジック、Javaのロジック

OptionとListとforとflatMapではPersonのaddressメソッドの実装として、以下の2つを挙げた。Optionに対して、for文&パターンマッチングやflatMapメソッドを使うのがScala的なプログラミング。 いずれにしても一度ListにしてからList操作の関数を活用するのがポイントである。

Person.scala
class Person(val name: String) {
  var zip: Option[String] = None
  var prefecture: Option[String] = None
  var city: Option[String] = None

  def address: String = {
    (for (Some(s) <- List(zip, prefecture, city)) yield s).mkString(" ")
  }
}
PersonFlatMap.scala
class Person(val name: String) {
  var zip: Option[String] = None
  var prefecture: Option[String] = None
  var city: Option[String] = None

  def address: String = {
    List(zip, prefecture, city).flatMap(s => s).mkString(" ")
  }
}

Personのインスタンス変数zip, prefecture, cityを「List(zip, prefecture, city)」としてList化して、Listとして統一的に処理できるようにしている。そして、このListをfor文のジェネレータに指定したり、ListのflatMapメソッドを直接使ったりして、List操作を行うわけである。

for文では、「Some(s)」によるパターンマッチング、yieldによるListの生成もがすこぶる便利。

また「OptionとListとforとflatMap」で述べた通りOptionとflatMapの組合せは魔法的な便利さがある。

もう一つ、面白いのがmkStringメソッド。Listの各要素を引数で指定された区切り記号を挟んで連結したStringを生成するメソッドである。文字列処理では何かとこの処理が出てくるので、専用メソッドが基本クラスに定義されているのはとても便利である。分かってらっしゃる、という感じ。

さて、Scala的なプログラミングがいかに効率がよいかという点を確認するためにJava的なプログラミングでaddressメソッドを実装してみよう。ボクの場合は、以下のようにプログラムすることになる。

Person.java
public class Person {
    final String name;
    String zip = null;
    String prefecture = null;
    String city = null;

    public Person(String name) {
        this.name = name;
    }

    public String address() {
        StringBuilder buf = new StringBuilder();
        boolean first = true;
        if (zip != null) {
            first = false;
            buf.append(zip);
        }
        if (prefecture != null) {
            if (first) {
                first = false;
            } else {
                buf.append(" ");
            }
            buf.append(prefecture);
        }
        if (city != null) {
            if (first) {
                first = false;
            } else {
                buf.append(" ");
            }
            first = false;
            buf.append(city);
        }
        return buf.toString();
    }
}

    

Scalaでリライトするとこんな感じになる。

PersonNotList.scala
class Person(val name: String) {
  var zip: String = null
  var prefecture: String = null
  var city: String = null

  def address: String = {
    val buf = new StringBuilder
    var first = true
    if (zip != null) {
      first = false
      buf.append(zip)
    }
    if (prefecture != null) {
      if (first) {
        first = false
      } else {
        buf.append(" ")
      }
      buf.append(prefecture)
    }
    if (city != null) {
      if (first) {
        first = false
      } else {
        buf.append(" ")
      }
      first = false
      buf.append(city)
    }
    buf.toString
  }
}

なんとも長いプログラムになるけど、Javaで普通に書くとこうなってしまう。zip, prefecture, cityというインスタンス変数ごとにほとんど同じだけど少しずつ違うロジックを並べていくことになるのが美しくない。Stringを段階的に構築していくことになるためStringBuilderを導入することになり、その取り回しも煩雑である。

また「Listの各要素を引数で指定された区切り記号を挟んだStringを生成する」というmkStringメソッドの処理は、実は案外煩雑である。これを実現するためにfirstというフラグを使って区切り記号を挿入するタイミングを制御したり、StringBuilderの操作も必要になる。

以上の比較から分かる通り、Listというデータ構造を軸にして事前に用意されているアルゴリズムを適用していく関数型言語的なプログラミングモデルははまると非常に強力である。もちろん、ある特殊な場合のみ便利でそれ以外はそうでもないとか、逆に不便ということであれば価値は低いわけだけど、Scalaの場合は、はまる場所が頻出するので、結果としてプログラミング効率が大幅に向上する。

また、データ構造のアルゴリズム的な処理を簡潔に記述できたとしても、字句上の見た目が煩雑では効果半減である。この点もScalaは高いレベルでニーズを満たしている。

このあたりの違いが分かってくると、Javaでのプログラミングはなんとももどかしくなってくる。「Scalaはオブジェクト指向と関数型のハイブリッド」と聞いても「だから?」という感じだけれど、実際にコーディングしてみると関数型言語とのハイブリッドの効果がよく分かる。宣言的に書ける、副作用がないように書ける、といった理念的な部分も重要だけど、実務的には簡単に短く書けるということがより重要である。「簡単に短く書ける」理由をよくよく見てみると、Listというデータ構造と高階関数の組合せによるものであり、Lisp時代から不変の方程式である。この方程式を現代的なAlgol系の静的型付オブジェクト指向プログラミング言語の文法に違和感なく溶け込ますことができたのがScalaの美点といえる。

2010年3月6日土曜日

MavenでScala 2.8

Scalaプログラミングを始める際に、ちょっとした障壁になるかもしれないのがMavenのpom.xmlの定義である。もちろん、調べればわかることなんだけど、このちょっとしたことが大きな障壁になる。また、Scala 2.8はベータ版なので、少し余分に情報収集しないといけない。

そこで、参考に3月1日「XML, DOM, XSLT, NekoHtmlを使う」のsdoc2blogger用のpom.xmlを載せておきます。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 \
    http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.goldenport</groupId>
  <artifactId>sdoc2blogger</artifactId>
  <packaging>jar</packaging>
  <version>0.1</version>
  <name>sdoc2blogger</name>
  <build>
    <plugins>
      <plugin>
        <groupId>org.scala-tools</groupId>
        <artifactId>maven-scala-plugin</artifactId>
        <version>2.12.2</version>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
       <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
   <showDeprecation>true</showDeprecation> <!-- XXX not work -->
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>6</source>
          <target>6</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <mainClass>org.goldenport.sdoc2blogger.Main</mainClass>
              <packageName>org.goldenport.sdoc2blogger</packageName>
              <addClasspath>true</addClasspath>
              <addExtensions>true</addExtensions>
              <classpathPrefix></classpathPrefix>
            </manifest>
     <manifestEntries>
       <Class-Path>nekohtml-0.9.5.jar
       scala-library-2.8.0-20100228.024500-331.jar
       scalatest-1.0-for-scala-2.8.0-SNAPSHOT.jar
       sdoc2blogger-0.1.jar xercesImpl-2.4.0.jar</Class-Path>
     </manifestEntries>
<!--
     <manifestFile></manifestFile>
-->
          </archive>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <configuration>
          <descriptors>
            <descriptor>src/assemble/bin.xml</descriptor>
          </descriptors>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <properties>
    <scala.version>2.8.0-SNAPSHOT</scala.version>
  </properties>
  <repositories>
    <repository>
      <id>snapshots.scala-tools.org</id>
      <name>Scala-tools Maven2 Repository - Snapshots</name>
      <url>http://scala-tools.org/repo-snapshots</url>
    </repository>
  </repositories>
  <pluginRepositories>
    <pluginRepository>
      <id>scala-tools.org</id>
      <name>Scala-tools Maven2 Repository</name>
      <url>http://scala-tools.org/repo-releases</url>
    </pluginRepository>
  </pluginRepositories>
  <dependencies>
    <dependency>
      <groupId>org.scala-lang</groupId>
      <artifactId>scala-library</artifactId>
      <version>2.8.0-SNAPSHOT</version>
    </dependency>
    <dependency>
      <groupId>org.scalatest</groupId>
      <artifactId>scalatest</artifactId>
      <version>1.0-for-scala-2.8.0-SNAPSHOT</version>
    </dependency>
    <dependency>
      <groupId>xerces</groupId>
      <artifactId>xercesImpl</artifactId>
      <version>2.9.1</version>
    </dependency>
    <dependency>
      <groupId>nekohtml</groupId>
      <artifactId>nekohtml</artifactId>
      <version>0.9.5</version>
    </dependency>
  </dependencies>
</project>

プラグインは以下の通り。maven-scala-pluginがポイント。maven-jar-pluginとmaven-assembly-pluginは配備モジュールを生成するために入れている。

  • maven-scala-plugin
  • maven-compiler-plugin
  • maven-jar-plugin
  • maven-assembly-plugin

リポジトリは以下の通り。Scala 2.8はまだベータ版なのでhttp://scala-tools.org/repo-snapshotsをリポジトリに加えるのがポイント。

  • http://scala-tools.org/repo-snapshots
  • http://scala-tools.org/repo-releases

Scala処理系とScalaTestは、以下のように2.8.0-SNAPSHOT版を指定する。

<dependency>
      <groupId>org.scala-lang</groupId>
      <artifactId>scala-library</artifactId>
      <version>2.8.0-SNAPSHOT</version>
    </dependency>
    <dependency>
      <groupId>org.scalatest</groupId>
      <artifactId>scalatest</artifactId>
      <version>1.0-for-scala-2.8.0-SNAPSHOT</version>
    </dependency>

sdoc2bloggerに固有の依存性は以下の通り。nekohtmlのみの指定だと古いXercesを取ってきてしまうので、Xercesは最新版を明示的に設定している。

<dependency>
      <groupId>xerces</groupId>
      <artifactId>xercesImpl</artifactId>
      <version>2.9.1</version>
    </dependency>
    <dependency>
      <groupId>nekohtml</groupId>
      <artifactId>nekohtml</artifactId>
      <version>0.9.5</version>
    </dependency>

assemblyに使うbin.xmlは以下の通り。

bin.xml
<assembly>
  <id>bin</id>
  <formats>
    <format>dir</format>
  </formats>
  <includeBaseDirectory>false</includeBaseDirectory>
  <moduleSets>
    <moduleSet>
      <binaries>
        <outputDirectory>lib</outputDirectory>
        <unpack>false</unpack>
      </binaries>
    </moduleSet>
  </moduleSets>
  <dependencySets>
    <dependencySet>
      <unpack>false</unpack>
      <scope>runtime</scope>
      <outputDirectory>/</outputDirectory>
    </dependencySet>
  </dependencySets>  
</assembly>

普通はformatをzipにするところなんだけど、開発中のプログラムはディレクトリに展開しておいて、以下のようなスクリプトを使って実行可能にしているので、このようにしている。

#! /bin/sh

scala -cp \
    "z:/usr/local/src/sdoc2blogger/target/sdoc2blogger-0.1-bin.dir/sdoc2blogger-0.1.jar" \
    org.xmlsmartdoc.sdoc2blogger.Main $*

2010年3月5日金曜日

OptionとListとforとflatMap

ScalaのOptionのよいところは、仕様を正確に記述できて、仕様違反の検出をしてくれる上に、プログラミングが楽になるというおまけがついているところである。一般的には正確な仕様記述とプログラミング効率は相反することが多いので、この要因が両立するというのはかなりうれしいことだ。

クラスPersonのメソッドaddressの実装は、OptionとListとforを使って無理なく1行に収まっているけど、関数型言語の技を使わないとこの分量ではすまない。このあたりも面白いので少し細かく説明したいところだけど、今回はポイントだけ。

addressメソッドのポイントは、Option[String]のListに対してfor文で「Some(s)」という受け取り方をすることで、以下の処理を簡潔に記述できるところにある。

  • Noneを削除する
  • Some(s)からsを取り出す

「Some(s)」によるパターンマッチングとfor文を組み合わせることで、if文なしでこの処理が行えてしまうのがOptionを使った時のメリット。

Person.scala
class Person(val name: String) {
  var zip: Option[String] = None
  var prefecture: Option[String] = None
  var city: Option[String] = None

  def address: String = {
    (for (Some(s) <- List(zip, prefecture, city)) yield s).mkString(" ")
  }
}

もちろん、Optionを使わずnullを使った場合でも以下のように実装すればaddressメソッドは簡潔に記述することができるけど、for文中の「if s != null」ではなく、「Some(s)」という形のパターンマッチングを使って表示する情報の取捨選択を行っているOption方式の方が美しいし、コードも短くバグも起きにくい。小さな差だけれど、コーディング量が増えてくればボディーブローのように効いてくるところだ。

PersonNull.scala
class Person(val name: String) {
  var zip: String = null
  var prefecture: String = null
  var city: String = null

  def address: String = {
(for (s <- List(zip, prefecture, city); if s != null) yield s).mkString(" \
    ")
  }
}

Optionが出てきたらmatch文というのが一つのパターンなので、match文を使う実装を考えてみたんだけど、いいものが思いつかない。無理をして書くと以下のようになるけど、あまり便利ではなさそうだ。というよりOptionをnullに変換して処理を進めるロジックなのでダメプログラムの典型みたいなものである。

PersonMatch.scala
class Person(val name: String) {
  var zip: Option[String] = None
  var prefecture: Option[String] = None
  var city: Option[String] = None

  def address: String = {
    (for (s <- List(zip, prefecture, city)) yield {
      s match {
        case Some(s) => s
        case None => null
      }
    }).filter(_ != null).mkString(" ")
  }
}

また、以下のように:

(for (s <- List(zip, prefecture, city; if s.isDefined)) yield \
    s).map(_.get).mkString(" ")

や:

(for (s <- List(zip, prefecture, city)) yield \
    s).filter(_.isDefined).map(_.get).mkString(" ")

とも書けるけど、filterやmapを使い出すとfor文を使う意味があまりない。以下のように直接filterとmapを使った方が格段によい。

List(zip, prefecture, city).filter(_.isDefined).map(_.get).mkString(" ")

実はもっと良い方法がある。scala.Traversable(Scala 2.7系ではscala.Iterable)に定義されているflatMapが魔法のメソッド。Scalaのコレクションクラスは基本的にTraversableの子孫なのでflatMapはどのコレクションクラスでも使うことができる。

addressメソッドはflatMapメソッドを使って以下のように実装することができる。

PersonFlatMap.scala
class Person(val name: String) {
  var zip: Option[String] = None
  var prefecture: Option[String] = None
  var city: Option[String] = None

  def address: String = {
    List(zip, prefecture, city).flatMap(s => s).mkString(" ")
  }
}

OptionのListのflatMapメソッドが以下の処理を綺麗に行なってくれるので、ほとんどロジックらしいものを書かずにメソッドを実現することができた。

  • Noneを削除する
  • Some(s)からsを取り出す

さらに、flatMapメソッドの引数が「s => s」と無変換のクロージャとなっているので、ここに何らかの変換を行うクロージャを指定することで、それほど手間をかけずに、より複雑な処理を記述することができる余地がある。

このようにOptionは、「仕様を正確に記述できて、仕様違反の検出をしてくれる上に、プログラミングが楽になるというおまけがついている」のである。ここまでくるとOptionを積極的に使いたくなってくるでしょう。

もちろん、Listとfor文、パターンマッチング、さらにflatMapメソッドによるイディオムを知らなければ、Optionのこのような能力を引き出すことはできない。逆にmatch文の例のようにおかしなプログラムを書く事になってしまうかもしれない。

Scalaプログラミングは今まで以上にイディオムが鍵になりそうだ。

2010年3月3日水曜日

Optionとnull

2月28日にOptionについて取り上げた。

Optionを使用する本質的な意味は、オブジェクト間の関連における多重度を仕様として明確に記述すること。さらに、仕様違反を文法違反として検出することである。

多重度は0以上の整数値の集合として記述することができるけど、一般的には以下の4種類を基本とすればよい。

  • 1:必ず存在する。
  • 0または1:存在する場合と存在しない場合がある。
  • 1以上:1以上複数個存在する。存在しない場合はない。
  • 0以上:存在しないか複数存在する。

XML的には、それぞれ「記号なし」(必ず存在する)、「?」(0または1)、「+」(1以上)、「*」(0以上)の記号で表現したくなるところだ。

Javaのnullの問題とは、突き詰めていくと「必ず存在する(多重度1)」という仕様を記述することが不可能だったということである。

たとえば、Javaで以下のようなコードがあった場合、インスタンス変数zip, prefecture, cityやメソッドgetAddressの型を見ただけでは「必ずStringオブジェクトが格納されている/返ってくる(多重度1)」のか「nullが格納/返ってくることがある(多重度0または1)」のかということは分からない。安全サイドに立つと「nullが格納/返ってくることがある(多重度0または1)」を前提にプログラミングすることになるけど、これはプログラミングを著しく煩雑にする。多くの場合、インスタンス変数やメソッドを提供する側の意図は「必ずStringオブジェクトが格納されている/返ってくる(多重度1)」なので、利用者側で安全策を取らざるを得ないのは無駄な作業とも言える。

Person.java
public class Persion {
  String zip;
  String prefecture;
  String city;

  public String getAddress() {
    return (zip + " " + prefecture + " " + city)
  }
}

実用的な意味でのnullの問題はこの多重度の齟齬によってバグの温床になることである。

たとえば、上記のPerson.javaのgetAddressメソッドにはバグがある。

インスタンス変数zip, prefecture, cityのいずれかがnullの場合、出力される文字列に「null」という文字が含まれなんとも情けないことになるだろう。if文を使ってこの問題を回避するコードは当然書けるけど、大した処理もしていないのに悲しいほど煩雑なコードになる。

2月28日に取り上げたPerson.scala(以下に再掲)では、この多重度の問題をOptionを使ってきちんと記述できている。これに違反した場合にはコンパイラが文法エラーとして教えてくれる。

Person.scala
class Person(val name: String) {
  var zip: Option[String] = None
  var prefecture: Option[String] = None
  var city: Option[String] = None

  def address: String = {
    (for (Some(s) <- List(zip, prefecture, city)) yield s).mkString(" ")
  }
}

ScalaはJavaとの連携の問題もあってnullをサポートしている。このため、Person.scalaを以下のようにnullを使って実装することもできるけれど、ScalaにはOptionがあるのでわざわざnullを使うこともないだろう、ということである。

PersonNull.scala
class Person(val name: String) {
  var zip: String = null
  var prefecture: String = null
  var city: String = null

  def address: String = {
(for (s <- List(zip, prefecture, city); if s != null) yield s).mkString(" \
    ")
  }
}

とはいえ、ここで大事なのは、Optionを使うとプログラミングの手間が増えないのかということである。いくら理論的に美しい文法であってもプログラミングで大きな手間がかかるようであると、実用上は使えない。

ここで登場するのがScalaが関数型言語であるということ。関数型言語の特徴を活かしたコーディングをするとOptionを使うことで逆にプログラミングが楽になるのである。仕様を美しく正確に記述できて、仕様違反を文法エラーとして検出してくれる上に、プログラミングまで楽になるのであれば使わない手はないだろう。

もちろん、Optionを使って楽をするためには色々なイディオムを知っておく必要がある。追々ブログで取り上げていこうと思う。

2010年3月1日月曜日

XML, DOM, XSLT, NekoHtmlを使う

Bloggerを始めるにあたり、SmartDocが生成したHTMLをBlogger用に変換するScalaプログラムsdoc2bloggerを書いた。Scalaで、XML、DOM、XSLT、NekoHtmlを使う実例として参考になるかもしれない。

Scalaでは極めて簡単にXMLを扱うことができる。まず、XMLリテラルが使えるのが極めて大きい。プログラム内で生成したXMLデータ(scala.xml.Node, scala.xml.Elem)をtoStringで文字列に変換できるのも便利。さらに、XMLリテラル中に「{}」を使って式を埋め込むことができる。XMLを処理するプログラムを書くのにScalaは手離せない。

ただ、ScalaのXML操作系は独自体系(scala.xml.Node, scala.xml.Elem)になっているので、既存のDOMベースのコンポーネントを利用するにはscala.xml.NodeとDOMの相互変換が必要になる。sdoc2bloggerではSAXによる変換(DOMからscala.xml.Nodeへの変換)とテキストによる変換(XMLリテラルによるXSLTをDOMのTransformerに設定)を使っている。

sdoc2bloggerではDOMベースのコンポーネントとしてXSLTとNekoHtmlを使っている。XSLTによるXML変換はこれからニーズが高まりそうな技術の一つ。この例から分かるとおり、Scalaからも比較的簡単に行うことができるけど、もっと簡単に使えるようなラッパーがあるのが望ましい。ただし、XSLT的な変換はScalaのList処理でもっと簡単にできそうなので、XSLTプログラマが絶滅しかかっている現状では、あえてXSLTをサポートする誘引は少ないかもしれない。

NekoHtmlは、ブロークンなHTMLを読み込むことができるHTMLパーサー。今回はこれを使いたくてDOMを使うことになった。

XSLTで余分な空白を取り除く方法が分からなかったので、Scalaプログラムの方で処理を行っている。XSLTでやり方が見つからなかった場合、無理をしてXSLTの利用方法を追求するよりScalaプログラム側で対処した方が現実的である。

この他にXML要素をas-isで残す方法も見つけることができなかった。これはXSLTに地道にパターンを設定することで対処。

いずれにしてもXSLTは難しい。XSLTを触るのはかなり久々なのでカンも戻っていない。

それはともかく、XSLTは技を使わないと手続き的な処理を実現できないので、副作用のない変換はXSLTを使い、Scalaプログラムで補完するのが実用的な使い方かなと考えている。

Main.scala
package org.xmlsmartdoc.sdoc2blogger

import scala.xml._
import scala.xml.parsing._
import java.io.{FileReader, StringReader}
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.sax.SAXResult
import javax.xml.transform.stream.StreamSource
import org.xml.sax.InputSource
import org.cyberneko.html.parsers.DOMParser

/*
 * @since   Feb. 28, 2010
 * @version Mar.  1, 2010
 * @author ASAMI, Tomoharu
 */
object Main {
  val xslt = 
<xsl:stylesheet version="1.0" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
  <xsl:template match="/HTML/BODY">
    <div>
      <xsl:apply-templates />
    </div>
  </xsl:template>
  <xsl:template match="P">
    <p>
      <xsl:apply-templates />
    </p>
  </xsl:template>
  <xsl:template match="A">
    <a>
      <xsl:attribute name="href">
        <xsl:value-of select="@href"/>
      </xsl:attribute>
      <xsl:apply-templates />
    </a>
  </xsl:template>
  <xsl:template match="PRE[@class='program']">
<pre style="background-color: gainsboro;font-family: courier, \
    monospace;padding: 5pt;">
      <xsl:value-of select="."/>
    </pre>
  </xsl:template>
  <xsl:template match="PRE[@class='console']">
<pre style="background-color: black;color: white;font-family: courier, \
    monospace;padding: 5pt;">
      <xsl:value-of select="."/>
    </pre>
  </xsl:template>
  <xsl:template match="DIV[@class='caption']">
    <div style="background: lavender;font-weight: bold;">
      <xsl:value-of select="."/>
    </div>
  </xsl:template>
  <xsl:template match="DIV[@class='date']">
  </xsl:template>
  <xsl:template match="DIV[@class='author']">
  </xsl:template>
  <xsl:template match="DIV[@style='text-align:right']">
  </xsl:template>
  <xsl:template match="H1">
  </xsl:template>
</xsl:stylesheet>

  def main(args: Array[String]) {
    val reader = new FileReader(args(0))
    val is = new InputSource(reader)
    val parser = new DOMParser()
    parser.parse(is)
    val dom = parser.getDocument
    //
    val saxHandler = new NoBindingFactoryAdapter()
    saxHandler.scopeStack.push(TopScope)
    val transFactory = TransformerFactory.newInstance
    val ss = new StreamSource(new StringReader(xslt.toString))
    val trans = transFactory.newTransformer(ss)
    trans.transform(new DOMSource(dom), new SAXResult(saxHandler))
    saxHandler.scopeStack.pop
val xml = <div>{distill_child_elements(saxHandler.rootElem.child)}</div>
    println(xml)
  }

  private def distill_child_elements(nodes: Seq[Node]): Seq[Node] = {
    nodes.filter(_.isInstanceOf[Elem])
  }
}