クラスの extends とトレイトの extends の違い

Scala の extends と with の意味合いがちょっと解りづらかったので整理してみました。

先に結論を書いておくと以下になります。

  • extends の直後は必ずクラスになる
  • トレイトは mixin するために必要な条件を持つ

これだけだとちょっと何言ってるんだって感じですので、順をおって説明します。まず単純な宣言から見て行きましょう。

class ClassA
trait TraitA

この単純な宣言はシンタックスシュガーで実現されており、実際に上記のコードは以下のように解釈されます。

class ClassA extends AnyRef
trait TraitA extends AnyRef

どちらも AnyRefクラスを extends する形となります。しかし ClassA と TraitA では extends の意味合いが大きく異なります。というのも、class ClassA extends AnyRef「ClassA が AnyRef を継承する」という意味になるのに対し、trait TraitA extends AnyRef「TraitA を mixin するクラスは AnyRef を継承している必要がある」という条件付けの意味になるのです。

従って全てのトレイトは mixin するために継承しなければならないクラスというものを持っています。これに矛盾する mixin は行えません。

class ClassA extends AnyRef
trait TraitA extends AnyRef  // TraitA を mixin するクラスは AnyRef を継承している必要がある

class ClassB extends ClassA
trait TraitB extends ClassA  // TraitB を mixin するクラスは ClassA を継承している必要がある

class ClassX extends AnyRef with TraitB // コンパイルエラー

ここまでが extends の直後にクラスを指定した場合の解釈です。では extends の直後にトレイトを指定した場合はどういう事になるのでしょうか。

class ClassA extends AnyRef
trait TraitA extends AnyRef

class ClassB extends ClassA
trait TraitB extends ClassA

class ClassC extends TraitA
trait TraitC extends TraitA

class ClassD extends TraitB
trait TraitD extends TraitB

これらはそれぞれ次のように解釈されます。

class ClassA extends AnyRef
trait TraitA extends AnyRef

class ClassB extends ClassA
trait TraitB extends ClassA

class ClassC extends AnyRef with TraitA
trait TraitC extends AnyRef with TraitA

class ClassD extends ClassA with TraitB
trait TraitD extends ClassA with TraitB

つまり extends の直後にトレイトを指定した場合、そのトレイトを mixin するために必要なクラスを extends したと解釈されます。この解釈により最終的に extends の直後には必ずクラス名が来ることになります。

extends に補われるクラスは指定したトレイトの条件であるクラスです。従って同じトレイトを mixin する場合でも順番によりコンパイルが通ったり通らなかったりします。

class ClassA extends AnyRef
trait TraitA extends AnyRef

class ClassB extends ClassA
trait TraitB extends ClassA

trait TraitC extends AnyRef with TraitA
trait TraitD extends ClassA with TraitB

class ClassX extends TraitD with TraitC // コンパイル通る
class ClassY extends TraitC with TraitD // コンパイルエラー

というわけでまとめると、

  • トレイトは mixin するために継承しなくてはならないクラスを持つ
  • extends トレイト はシンタックスシュガー

ということでした。

Loanパターンのアレに Conceptパターンを足してみたよ

Loanパターンをfor式で使えるようにしてみたよ に Conceptパターンを足してみました。

と言うのも、後始末のメソッドは close() だけとは限らず、リソースによって dispose() や release() destroy() など様々あります。それら全部に対応できるといいなと思ってやってみました。

trait Closer[-A] {
  def close(value: A)
}

class Loan[A] private (value: A, closer: Closer[A]) {

  def foreach[B](f: A => B): B = try {
    f(value)
  } finally {
    closer.close(value)
  }  

}
object Loan {
  
  def apply[A](value: A)(implicit closer: Closer[A]) = new Loan(value, closer)
  
  type Closeable = {def close()}
  implicit val closeable = new Closer[Closeable] {
    def close(value: Closeable) = value.close()
  }

  type Releasable = {def release()}
  implicit val destroyable = new Closer[Releasable] {
    def close(value: Releasable) = value.release()
  }
  
  type Disposable = {def dispose()}
  implicit val disposable = new Closer[Disposable] {
    def close(value: Disposable) = value.dispose()
  }

}

Closerというのがパターンで言う Concept役ですね。後片付けを担う Concept です。

こうすると何が嬉しいかというと、次のように FileLock なんかも忘れずに release() してくれます。便利。

object Main {
  
  def main(args: Array[String]) {
    import java.io._
    import Loan._
    
    for {
      in      <- Loan(new FileInputStream("source.txt"))
      reader  <- Loan(new InputStreamReader(in, "UTF-8"))
      buf     <- Loan(new BufferedReader(reader))
      out     <- Loan(new FileOutputStream("dest.txt"))
      writer  <- Loan(new OutputStreamWriter(out, "UTF-8"))
      lock    <- Loan(out.getChannel.lock())
    } {
      var line = buf.readLine()
      while (line != null) {
        writer.write(line)
        line = buf.readLine()
      }
    }
    
  }
  
}

Closer の実装クラスを増やせば、自前のクラスや他ライブラリの後片付けにも対応できます。リソースの開放メソッドに何かしら引数が必要とか言った場合にも対応できますね。

Loanパターンをモナドfor式で使えるようにしてみたよ

みんな大好きLoanパターンですが、複数のリソースを扱いたい時などネストが深くなってしまうのでちょっと困ってしまいます。

そこでLoanパターンをモナドfor式で使えるようにしてみました。

class Loan[T <: {def close()}] private (value: T) {

  def foreach[U](f: T => U): U = try {
    f(value)
  } finally {
    value.close()
  }  

}
object Loan {
  
  def apply[T <: {def close()}](value: T) = new Loan(value)
  
}

これによってfor式でシンプルな記述ができます。例えば次のような感じに。

object Main {
  
  def main(args: Array[String]) {
    import java.io._
    
    for {
      in     <- Loan(new FileInputStream("source.txt"))
      reader <- Loan(new InputStreamReader(in, "UTF-8"))
      buff   <- Loan(new BufferedReader(reader))
      out    <- Loan(new FileOutputStream("dest.txt"))
      writer <- Loan(new OutputStreamWriter(out, "UTF-8"))
    } {
      var line = buff.readLine()
      while (line != null) {
        writer.write(line)
        line = buff.readLine()
      }
    }
    
  }
  
}

ちょっと Java7 の try with resources みたいですね。

それにしても Scala の for式は便利です。

2011-07-01 00:07 追記

モナドではないよね、という至極まっとうな突っ込みを頂いたので表記を訂正。

たしかにモナドではないですね。

List内に最も多く出現するオブジェクトを取得するメソッド

Twitterでこんな処理はどう書けばいいんだろう?というツイートがあって、Scalaの勉強がてらちょっと書いてみました。

仕様は以下の通り

  • 引数で任意の要素型のListを受け取り、そのList内に最も多く出現するオブジェクトを取得する。
  • 最も多く出現するオブジェクトが複数ある場合もあるので、戻り値は Set[要素型] とする。
  • 引数で渡されるListは空の場合もありうる。

肝は最大の個数の要素が複数あった場合にその全てを返すところですね。最初に見つかったやつだけでよければ groupBy と maxBy を使えば簡単に書けるので。

最初に書いたのはこんな感じです。

def most[A](seq: Seq[A]): Set[A] = seq.groupBy(identity).foldLeft((0, Set[A]())) {
  case ((c, _), (x, xs)) if (c < xs.length)  => (xs.length, Set(x))
  case ((c, s), (x, xs)) if (c == xs.length) => (c, s + x)
  case (r, _) => r
}._2

パターンマッチがネストしたデータ構造を指定できるのは恐ろしく便利ですね。

汎用性をあげるために引数は List じゃなくて Seq にしています。

その後に、groupBy した後にソートすればもう少し簡単にできるかな?と思って書いたのが次のコード

def most[A](seq: Seq[A]): Set[A] = seq.groupBy(identity).toSeq.sortBy(_._2.length * -1) match {
  case l@((_, c) :: _) => l.takeWhile(_._2.length == c.length).unzip._1.toSet
  case _ => Set()
}

短くはなったけど複雑になったような……微妙な違和感が残ります。

ここまで書いたところでJavaにも浮気してみました。

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

...

public <E> Set<E> most(final Iterable<E> iterable) {
    final Map<E, Integer> counters = new HashMap<E, Integer>();
    for (final E e: iterable) {
        counters.put(e, counters.containsKey(e) ? counters.get(e) + 1 : 1);
    }
    int max = 0;
    final Set<E> result = new HashSet<E>();
    for (final Entry<E, Integer> entry : counters.entrySet()) {
        if (entry.getValue() > max) {
            result.clear();
            result.add(entry.getKey());
            max = entry.getValue();
        } else if (entry.getValue() == max) {
            result.add(entry.getKey());
        }
    }
    return result;
}

素直に書いたらこんな感じになると思います。見事に手続き的ですね。

せっかくなのでguava-librariesを使ってJavaでももう少しFunctionalな感じで書いてみました。

import static java.util.Collections.emptySet;
import static com.google.common.collect.Collections2.transform;
import static com.google.common.collect.HashMultimap.create;
import static com.google.common.collect.Multimaps.index;
import static com.google.common.collect.Sets.newHashSet;

import java.util.Collection;
import java.util.Collections;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import com.google.common.base.Functions;
import com.google.common.collect.Multimap;

...

public <E> Set<E> most(final Iterable<E> iterable) {
    if (!iterable.iterator().hasNext()) return emptySet();
    final Multimap<Integer, E> multi = create();
    for (final Entry<E, Collection<E>> e : index(iterable, Functions.<E>identity()).asMap().entrySet()) {
        multi.put(e.getValue().size(), e.getKey());
    }
    return newHashSet(new TreeMap<Integer, Collection<E>>(multi.asMap()).lastEntry().getValue());
}

ほんとはワンライナーでも書いたんですが、そちらはあまりにもキモかったのでお蔵入りにしました;-p

このコードを書くときに、NavigableMapを使用して要素数でグルーピングすれば lastEntry() で取得できる、というのを思いついたので、Scalaに転用すれば maxBy が使えるなと思い書いてみました。

ちなみに maxBy は scala2.9 からなので、2.8で利用できるようにこっそり追加しました。

// このimplicitメソッドで、2.8でも Traversable が maxBy を使用できるようになる
implicit def wrapI[A](t: Traversable[A]) = new AnyRef {
  def maxBy[B](f: (A) => B)(implicit c: Ordering[B]): A = 
    t.reduceLeft {(x, y) => if (c.gteq(f(x), f(y))) x else y}
}

def most[A](seq: Seq[A]): Set[A] =
  if (seq.isEmpty) Set() else seq.groupBy(identity).groupBy(_._2.length).maxBy(_._1)._2.unzip._1.toSet

見事ワンライナーになりました。…なりましたが……はたして見やすくなったのか……。

個人的には、もしプロダクトコードに書くとするなら一番最初に書いた奴がいいかな、と思ってます。

しかしこれ以外にもまだまだ色々な書き方ができそうですね。奥が深い。

Scala2.8 と Lift2.2 のアーキタイプを M2Eclipse 上に import する

Scalaの勉強を始めました。

Liftで何か作ろうとアーキタイプを作成したんですが、どはまりしたのでメモを残しておきます。

 mvn archetype:generate \
 -DarchetypeGroupId=net.liftweb \
 -DarchetypeArtifactId=lift-archetype-basic_2.8.1 \
 -DarchetypeVersion=2.2 \
 -DarchetypeRepository=http://scala-tools.org/repo-releases \
 -DremoteRepositories=http://scala-tools.org/repo-releases \
 -DgroupId={groupId} \
 -DartifactId={articfactId} \
 -Dversion=1.0

まずは maven で Lift2.2 Scala2.8.1 用のアーキタイプを生成

File > Import... > Maven Existing Maven Projects

できたフォルダをEclipseからimport

そうするとなぜかJavaプロジェクトとしてプロジェクトが作成されるため、buildが成功しません。

なので一度Eclipseを終了させ、

.projectファイルをエディタで開き

変更前

<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
    <name>${artifactId}</name>
    <comment></comment>
    <projects>
    </projects>
    <buildSpec>
        <buildCommand>
            <name>org.eclipse.jdt.core.javabuilder</name>
            <arguments>
            </arguments>
        </buildCommand>
        <buildCommand>
            <name>org.maven.ide.eclipse.maven2Builder</name>
            <arguments>
            </arguments>
        </buildCommand>
    </buildSpec>
    <natures>
        <nature>org.eclipse.jdt.core.javanature</nature>
        <nature>org.maven.ide.eclipse.maven2Nature</nature>
    </natures>
</projectDescription>

変更後

<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
    <name>${artifactId}</name>
    <comment></comment>
    <projects>
    </projects>
    <buildSpec>
        <buildCommand>
            <name>org.scala-ide.sdt.core.scalabuilder</name>
            <arguments>
            </arguments>
        </buildCommand>
        <buildCommand>
            <name>org.maven.ide.eclipse.maven2Builder</name>
            <arguments>
            </arguments>
        </buildCommand>
    </buildSpec>
    <natures>
        <nature>org.scala-ide.sdt.core.scalanature</nature>
        <nature>org.eclipse.jdt.core.javanature</nature>
        <nature>org.maven.ide.eclipse.maven2Nature</nature>
    </natures>
</projectDescription>

というように書き換える。

力技すぎますね……。

もっと上手い方法は無いものでしょうか。

Rails2.0 の Cookie Session を既存のJavaアプリに簡単に組み込める Servlet Filter

Rails2.0の Cookie Session Store 面白いですね。詳細については 2.0のcookie session storeを体感する といった記事が詳しいのでそちらに譲ります。

この Cookie Session が RESTfull かどうかといった議論は正直どうでもよくて(個人的にはSessionて概念が発生してる時点でRESTではないと思います)、単純に Session を Cookie に保存するという仕組みのメリットが非常に興味深いと思っています。特に惹かれるのが以下の2点。

アプリケーションサーバの分散に関しては既にあちこちでも言われていますね。Sessionリプリケーションは一切必要なく手軽にサーバを分散できます。それこそ違うデータセンターのサーバでも問題ありません。これから来るであろう大分散化時代にはうってつけではないでしょうか。

スレッド安全性について。これは結構今でも見落とされがちなんですが、Java の ServletAPI を使用したWebアプリケーションでは Session や Session に保存しているオブジェクトを操作する場合にスレッド安全性を確保することが必須となります。F5連打などで同時アクセスを行なった場合に複数のスレッドが単一のSessionオブジェクトにアクセスする可能性があるからです。

ところが、Cookie Session の場合、リクエスト毎に Sessionオブジェクトが独立するのでマルチスレッドに対して考慮する必要がなくなります。尤もその代償に常に後出優先になってしまうので、その点は注意する必要があります。

という訳で、こんな面白い機能は Java の Webアプリでもぜひ使ってみたいと思って車輪の再実装をしてみました。せっかくなので既存のアプリにも適用できるように Servlet Filter として実装しています。Filter の設定を追加するだけで、Session を操作しているコードは一切変更せずに Cookie Session を使用できるようにしています。

普通の Servlet Filter ですので GAE/J上でも動くと思います。GAE/J上で Session 情報が DataStore を占有するような場合にこれを使うことで課金から逃れることができるかもしれません。(CPUコストは上がるのでトレードオフでしょうが)

また、使用にあたり幾つか制限があります。

  • Session ID に関するメソッド郡は使用できません。
    • Session ID が存在しない為。関連するメソッドは全て UnsupportedOperationException を throw します。
  • HttpSessionListener#sessionDestroyed が必ず呼ばれるとは限りません。
    • Sessionオブジェクトはサーバには一切保存されないため、時間監視でSessionオブジェクトを削除するという事ができません。指定した生存期間後にSessionを破棄するのはクライアント側の責務になります。したがって悪意を持ったクライアントが指定した生存期間を超えたSessionオブジェクトを渡してくる可能性があります。ver2.0で生存期間を超えたSessionは受け入れないようにしました。 これが問題になる場合には、HttpSessionActivationListener#sessionDidActivate 等を利用して制御して下さい。HttpSession#invalidate が明示的に呼ばれた場合には必ず HttpSessionListener#sessionDestroyed は呼ばれます。
  • Session に保存するオブジェクトは全て Serializable である必要があります。
    • Cookie には Serialize した結果を書き込みます。直列化不可能なオブジェクトを Session に含める場合、HttpSessionActivationListener 等を用いて直列化可能な状態にしてください。

Rails2.0 の実装との違い

  • 暗号化のサポートをしています。設定で暗号化を有効にすると Cookie の値には暗号化された内容が書き込まれます。個人情報が含まれている場合でも、サーバの秘密鍵が漏洩しない限り安心です。
  • Cookie のサイズ制限が存在するため、シリアライズデータをZLIB圧縮ライブラリで圧縮しています。
  • Cookie を Session の中身と HMAC で2個使用します。同一ドメインで発行できる Cookie数には限りがありますが、データ容量を優先した結果 HMAC と別にしました。

システム条件および依存ライブラリ

  • JavaSE 5.0以上
  • ServletAPI 2.5以上
  • Apache Commons Codec 1.4
  • Apache Commons Logging 1.1

設定方法

web.xml に以下の Filter 設定を追加するだけです。

    <filter>
        <filter-name>CookieSessionFilter</filter-name>
        <filter-class>gakuzo.lab.cookiesession.CookieSessionFilter</filter-class>
        <init-param>
            <!-- Cookieに指定するドメイン。省略した場合、リクエストが送られたドメインになります。 -->
            <param-name>domain</param-name>
            <param-value>localhost</param-value>
        </init-param>
        <init-param>
            <!-- Cookieの secure属性を追加するかどうか。trueの場合、secure属性がつきます。省略時はfalse -->
            <param-name>secure</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <!-- Cookieを識別するアプリケーション名。省略した場合、ContextPath の / を _ に置換した値になります。 -->
            <param-name>applicationName</param-name>
            <param-value>example</param-value>
        </init-param>
        <init-param>
            <!-- Cookieのpath属性に指定する値。省略した場合、ContextPath の値になります。 -->
            <param-name>path</param-name>
            <param-value></param-value>
        </init-param>
        <init-param>
            <!-- HMACの計算に使用するアルゴリズム名。省略した場合、HmacSHA1 になります。 -->
            <param-name>hmacAlgorithmName</param-name>
            <param-value>HmacSHA1</param-value>
        </init-param>
        <init-param>
            <!-- 
                HMACの計算に使用する秘密鍵。省略できません。
                複数のアプリケーションを動かす場合、必ずこの秘密鍵はアプリケーション毎にユニークにしてください。
                さもないと、悪意を持ったクライアントがアプリケーション名を書き換えて別アプリのCookieを渡してきた場合に
                そのCookieを受け入れてしまう可能性があります。
            -->
            <param-name>hmacSecretKey</param-name>
            <param-value>3Z5U4br1lzs8gy/QcPMSPNiT3UOI6UYkE0JNhnT6Sg27VPvWpwrrGeFPI4jOI5A+LO6LXXOhFokt8kD1tWcNZg==</param-value>
        </init-param>
        <init-param>
            <!-- Cookieの値を暗号化するかどうか。省略した場合、false になります。 -->
            <param-name>cryption</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <!-- Cookieの値を暗号化する時に使用するアルゴリズム名。省略した場合、AES になります。 -->
            <param-name>cryptionAlgorithmName</param-name>
            <param-value>AES</param-value>
        </init-param>
        <init-param>
            <!-- Cookieの値を暗号化する時に使用する秘密鍵。cryption が true の場合は省略できません。 -->
            <param-name>cryptionSecretKey</param-name>
            <param-value>_mPkBsjkKtmax1fSGT0ziA</param-value>
        </init-param>
        <init-param>
            <!-- 
                Session の maxInactiveInterval の初期値です。省略した場合、0 (ブラウザ終了まで)になります。
                Session の maxInactiveInterval と Cookie の maxAge の仕様が異なるため、この Filter では以下のように扱います。
                maxInactiveInterval が負の値の場合、Session はタイムアウトせずに永続化させる必要がありますが、
                Cookie には永続化の指定が存在しないため、maxAge には Integer.MAX_VALUE を指定します。
                maxInactiveInterval が 0 の場合、
                maxAge には Cookie存在期間がブラウザが停止されるまでの間を意味する -1 を指定します。
                この挙動はこの Filter 独自の仕様です。
                maxInactiveInterval が正の値の場合、maxAge には maxInactiveInterval * 60 を指定します。
                Integerの範囲を超えてしまう場合、Integer.MAX_VALUE を指定します。
            -->
            <param-name>defaultMaxInactiveInterval</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <!-- 
                HttpSession に関する EventListener です。ServletAPI の仕様上、Filter からコンテナに登録してある
                EventListner を取得することができません。
                したがって、HttpSessionActivationListener、HttpSessionAttributeListener、HttpSessionListener の指定は
                listener要素ではなくここで指定する必要があります。
                カンマ区切りでクラス名を列挙してください。
            -->
            <param-name>listener</param-name>
            <param-value>
                com.example.FooHttpSessionActivationListener, 
                com.example.BarHttpSessionListener
            </param-value>
        </init-param>
    </filter>
    
    <filter-mapping>
        <filter-name>CookieSessionFilter</filter-name>
        <url-pattern>*</url-pattern>
    </filter-mapping>

Jarファイル

cookie-session-0.2.jar

2010-10-30 00:59 更新

基本的にSessionのタイムアウトCookieの有効期限に任せてたんですが、悪意を持ったクライアントが古いCookieを送信することもある、とのことなので最終アクセス時刻からサーバ側でもチェックするようにしました。

defaultMaxInactiveInterval が0以上に設定されている場合、最終アクセス時刻から現在時刻が defaultMaxInactiveInterval 以上経っていればそのCookieは無効とします。

2011-12-03 18:30 追記

プロジェクトを GitHub に上げました。 https://github.com/gakuzzzz/cookiesession

EnumのVisitorパターン

Enumに対してVisitorパターンを適用したいなーっていうケースはしばしばあったんですが、Visitor interface のメソッドオーバーロードのさせ方がわからずどうしたもんかな、と思ってました。

しかしよくよく考えればオーバーロードの解決って静的だからこれで十分だったんですね。

public enum AcceptableEnum {
    FOO {
        @Override
        public <R, E extends Throwable> R accept(final EnumVisitor<? extends R, E> visitor) throws E {
            return visitor.visitFoo();
        }
    },
    BAR {
        @Override
        public <R, E extends Throwable> R accept(final EnumVisitor<? extends R, E> visitor) throws E {
            return visitor.visitBar();
        }
    },
    HOGE {
        @Override
        public <R, E extends Throwable> R accept(final EnumVisitor<? extends R, E> visitor) throws E {
            return visitor.visitHoge();
        }
    };

    public abstract <R, E extends Throwable> R accept(EnumVisitor<? extends R, E> visitor) throws E;

    public static interface EnumVisitor<R, E extends Throwable> {
        R visitFoo() throws E;
        R visitBar() throws E;
        R visitHoge() throws E;
    }
}

難しく考えすぎてたようです。反省。