-
Notifications
You must be signed in to change notification settings - Fork 0
scala column : static type 1
##静的型でできること(1)
いきなりですが、このコラムを読んでおられる方々は、静的型付けに対してどのようなイメージをもっておられるでしょうか。今まで、静的型付け VS. 動的型付けの議論をさんざん見てきましたが、静的型の能力に関して正しく理解されている議論をあまり見かけたことがありません。静的型付け VS. 動的型付けの議論で、静的型付け(および明示的な型付け)のメリットとして主に挙げられるのは次の2点でしょう。 コンパイル時に単純なエラーを発見することができる 静的型を明示的に書くことにより、コードを読む人間により多くの情報が伝わる これらのメリットは間違いではありませんが、あくまでJava的な型システム上での話であり、静的型システムによって検出できることは、もっとたくさんあります。このシリーズでは、Scalaの静的型システムを駆使することで、どのような事ができるのかを紹介していきたいと思います。
ここで第1回の題材を何にしようか迷ったのですが、わかりやすい例としてファイルの読み込み/書き込みAPIを挙げてみることにします。
まず、java.io.RandomAccessFileのAPIリファレンスを見てみましょう。RandomAceessFileは、コンストラクタでパス名を表す文字列またはFileオブジェクトを受け取り、読み書き両方が可能な入出力ストリームを生成します。また、引数modeによって、読み込みのみ("r")、読み書き両方("rw")などの指定を行うことができます。しかし、このRandomAccessFileは、実行時にread/writeする際に初めてmodeがチェックされます。そのため、modeに"r"を指定してwriteしようとすると例外が発生してしまうといったイケてない点があります。静的型付け言語なのに実行時になって初めて例外がthrowされるとか、非常に勿体無いので、なんとかしたいところです。
ここで、読み込みのみとか制限したいなら(InputStream/OutputStream)使え、とかいう声が聞こえて来そうですが、クラス数が無駄に増えるのはイケてないので却下します。
というわけで、Scalaの型システムを駆使して、この問題を解決してみることにします。といっても、何か黒魔術的な事を行うわけではありませんので、ご安心ください。
この問題の根源はどこにあるのでしょうか?端的に言うと、RandomAccessFileがジェネリックな型でない事が一因です。というわけで、まずはRandomAccessFileをジェネリックにしてみます。
class IOStream[M <: AccessType](path: String) extends Closeable
名前がIOStreamに変わっていますが、これは混乱を避けるためであり、特に意味はありません。
ここで、AccessTypeというなにやらよくわからない型が出てきますが、これは特に難しいものではありません。定義としては、
sealed trait AccessType extends NotNull
trait Read extends AccessType
trait Write extends AccessType
trait RW extends AnyRef with Read with Write
という非常に単純なものです。定義の名前を見れば想像が付くかと思いますが、これは、IOStreamが 読み込み専用である(Read) 書き込み専用である(Write) 読み書き両方可能である(RW) のいずれかであることを表しています。さて、再びIOStreamの定義に戻ってみましょう。
class IOStream[M <: AccessType](path: String) extends Closeable
IOStreamは型パラメータMを取り、MはAccessTypeのサブタイプのいずれかになるため、静的に
読み込み専用である 書き込み専用である 読み書き両方可能である かがわかります。さて、クラス定義についてはこれでいいのですが、問題はメソッド定義です。一体どうやって、 MにReadが指定された場合は、書き込みできない MにWriteが指定された場合は、読み込みできない MにRWが指定された場合は、読み書きどちらもできる といった事を静的にチェックできるのでしょうか。抽象的な説明はおいといて、コードをさらしてしまいましょう。
object Resources {
import java.io.{RandomAccessFile, Closeable}
sealed trait AccessType extends NotNull
trait Read extends AccessType
trait Write extends AccessType
trait RW extends AnyRef with Read with Write
class IOStream[M <: AccessType](path: String) extends Closeable {
private val raf = new RandomAccessFile(path, "rw")
def read()(implicit readP: M <:< Read): Int = raf.read()
def readLine()(implicit readP: M <:< Read ): String = raf.readLine()
def write(bytes: Array[Byte])(implicit writeP: (M <:< Write)): Unit = raf.write(bytes)
def write(byte: Int)(implicit writeP: (M <:< Write)): Unit = raf.write(byte)
def close(): Unit = raf.close()
}
def open[T <: AccessType](path: String): IOStream[T] = new IOStream[T](path)
}
少しごちゃごちゃしていますが、注目する必要があるのは、
def read()(implicit readP: M <:< Read): Int = raf.read()
と
def write(bytes: Array[Byte])(implicit writeP: (M <:< Write)): Unit = raf.write(bytes)
の2箇所だけです。
前者は、IOStreamが読み込み用に開かれていない状態で(MがReadのサブタイプでない)、read()しようとするとコンパイル時にエラーになる、ということを表しています。
後者は、前者と似たようなもので、IOStreamが書き込み用に開かれていない状態で(MがWriteのサブタイプでない)、write()しようとするとコンパイル時にエラーになる、ということを表しています。
readLine()とかclose()はとりあえず付けたおまけのようなもので、特に注目する必要はありません。
さて、このIOStreamを使ってみることにしましょう。利用側のコードは次のようになります。
object Usage {
import Resources._
val inStream = open[Read]("foo.txt")
inStream.read()
//inStream.write(10) // compile error
val outStream = open[Write]("foo.txt")
outStream.write(10)
// outStream.read() // compile errror
}
コメントアウトをはずすと、
Resources.scala:28: error: Cannot prove that Resources.Read <:< Resources.Write.
inStream.write(10)
^
Resources.scala:33: error: Cannot prove that Resources.Write <:< Resources.Read.
outStream.read()
^
two errors found
というエラーがコンパイル時に出力されます。コンパイルエラーの意味がわかりにくいですが、@scala.annotation.implicitNotFoundというアノテーションを付けることで、わかりやすいエラーメッセージを出すようにカスタマイズできますので、ご安心ください。
ここまで何度も「コンパイル時」にということを強調したことからもわかるように、Scalaでは、型システムをうまく活用することで、APIの利便性を失うことなく、Javaよりも強力な静的チェックを行うことができます(Haskell等と比較するとどうか、という話はあるのですが、結構ややこしい話になるので、またの機会に、ということでご勘弁いただければと思います)。
第1回は以上ですが、Scalaの型システムで静的にチェックできることはこの程度ではありません。第2回以降では、より発展的な話題を扱ってみたいと思います。それでは、また。