2015年10月30日金曜日

[SDN] 値クラスでケースクラスの型を強化

Scalaでは仕様をできるだけ型化するのがプログラミングのキモとなります。

積極的に型化するメリットはコンパイラでバグの検出をしてもらえる点にあります。Curry-Howard対応によってコンパイルがプログラムが仕様通りに実装されていることの証明となる点がScalaのような関数型言語を使う重要なメリットです。

しかし、ここでいう「仕様」とは型と(型で引数と復帰値を定義した)関数でのみ記述できるので、この「型」化できなかった「仕様」はコンパイラの証明対象から外れてしまいます。(テストプログラムで明示的にテストする必要があります。)

つまり、Scalaプログラミングでは「仕様」をいかに「型」として記述するのかが勝負どころといえます。

この型化を促進する機能の一つが値クラスです。

値クラスは、Longといった基本型の値やStringへの参照値といった値を、新しく定義した型として使用できるようにしたものです。(Value Classes and Universal Traits)

値クラスを使うことで、型を表現する具体的なオブジェクトを生成することなく値に対して適切な型チェックをおこなうことができるようになります。

ケースクラスの設計の際に値クラスを使って型を強化する方法について考えてみます。

基本形

アカウント管理の以下のケースクラスを素材に考えます。

case class Account(
  accountId: String,
  username: String,
  address: String
)
問題点

このケースクラスは実用上は十分ですが、誤った情報を設定されても検知できないという問題があります。

たとえば、以下のようにString型の変数addressは、Accountのaddress属性だけでなくaccountId属性、username属性にも設定することができます。

scala> val address = "Yokohama"
scala> val a = Account(address, address, address)
val a = Account(address, address, address)
a: Account = Account(Yokohama,Yokohama,Yokohama)

これは明らかなバグですがコンパイラではチェックすることができません。

typeで定義

Scalaではtypeキーワードを使って型の別名をつけることができます。

プログラムの可読性が向上する便利な機能です。

type AccountId = String
  type UserName = String
  type Address = String

case class Account(
  accountId: AccountId,
  username: UserName,
  address: Address
)
問題点

ただ別名は型そのものの定義をおこなうわけではないので、問題点は解消されません。

scala> val address = "Yokohama"
scala> val a = Account(address, address, address)
a: Account = Account(Yokohama,Yokohama,Yokohama)

case classを使う

型を明示的に使う場合、クラスを定義します。

今回の場合、以下のようにaccountId, username, addressに対応するクラスを定義します。用途的にはケースクラスが適切なのでケースクラスとして定義しています。

case class AccountId(v: String)
case class UserName(v: String)
case class Address(v: String)

新しく定義した3つのケースクラスAccountId, UserName, Addressを使ったAccountクラスは以下になります。

case class Account(
  accountId: AccountId,
  username: UserName,
  address: Address
)
問題点の解決

まずaddressとして文字列を指定した場合ですが、ケースクラスAccountIdとは型が違うので無事コンパイルエラーとなりました。

scala> val address = "Yokohama"
scala> val a = Account(address, address, address)
<console>:34: error: type mismatch;
 found   : String
 required: AccountId
       val a = Account(address, address, address)
                       ^

ケースクラスAddressを使用した場合も、コンパイルエラーとしてバグを検出できています。

scala> val address = Address("Yokohama")
scala> val a = Account(address, address, address)
<console>:34: error: type mismatch;
 found   : Address
 required: AccountId
       val a = Account(address, address, address)
                       ^

ケースクラスAccountId, UserName, Addressを使うと、正しくAccountオブジェクトの生成を行うことができました。

scala> val accountid = AccountId("A1234")
scala> val username = UserName("Taro")
scala> val address = Address("Yokohama")
scala> val a = Account(accountid, username, address)
a: Account = Account(AccountId(A1234),UserName(Taro),Address(Yokohama))
新たな問題点

基本的にはこの解で十分なのですが、(それぞれ属性を1つしか持たない)ケースクラスAccountId, UserName, Addressを導入したので、Accountオブジェクトを生成する際に生成されるオブジェクト数が増えてしまうという新たな問題がでてきました。

ケースクラスAccountId, UserName, Addressを導入しない前に生成されるオブジェクト数はAccountオブジェクト1つですが、ケースクラスAccountId, UserName, Address導入後はAccountオブジェクトに加えてAccountId, UserName, Addressの各オブジェクトも生成されるので4個のオブジェクトが生成されることになります。実に4倍の量のオブジェクトが生成されるわけです。属性をすべてこの方法で型化すると、1つのオブジェクトを生成毎にこの副作用も含めて1+属性数個のオブジェクトが生成されるようになります。

オブジェクトの生成数が増えると、オブジェクト生成のオーバーヘッドで処理の遅延が発生するだけでなく、ガベージコレクタの負担の増大、メモリ使用量の増大という問題が起きてきます。

小さなアプリケーションの場合はこれでも問題ありません。逆に型化することのメリットの方が大きいので、多少オーバーヘッドがあってもこの手法を採用するほうが得策です。

しかし大規模アプリケーションやフレームワーク的な基盤機能では見過ごすことができない重大な問題となります。

この問題への対応策として使用できるのが値クラスです。

値クラスを使う

属性を一つだけ持つクラスがAnyValをextendsすると値クラスとして定義されたことになります。

通常のクラスでもケースクラスでも使用できます。

ここでは3つのケースクラスを値クラスとして定義しました。

case class AccountId(v: String) extends AnyVal
case class UserName(v: String) extends AnyVal
case class Address(v: String) extends AnyVal

この3つのケースクラスを使ったケースクラスAccountは以下になります。特別な考慮は不要です。

case class Account(
  accountId: AccountId,
  username: UserName,
  address: Address
)

値クラスは通常のクラスに比べるとできることの制約もありますが、今回の用途では普通のクラスと同じように使用することができます。

問題解決の確認

それでは値クラスを使った場合でも、問題点が解決しているかを確認しましょう。

まずaddressとして文字列を指定した場合ですが、ケースクラスAccountIdとは型が違うので無事コンパイルエラーとなりました。

scala> val address = "Yokohama"
scala> val a = Account(address, address, address)
<console>:34: error: type mismatch;
 found   : String
 required: AccountId
       val a = Account(address, address, address)
                       ^

ケースクラスAddressを使用した場合も、コンパイルエラーとしてバグを検出できています。

scala> val address = Address("Yokohama")
scala> val a = Account(address, address, address)
<console>:34: error: type mismatch;
 found   : Address
 required: AccountId
       val a = Account(address, address, address)
                       ^

ケースクラスAccountId, UserName, Addressを使うと、正しくAccountオブジェクトの生成を行うことができました。

scala> val accountid = AccountId("A1234")
scala> val username = UserName("Taro")
scala> val address = Address("Yokohama")
scala> val a = Account(accountid, username, address)
a: Account = Account(AccountId(A1234),UserName(Taro),Address(Yokohama))

まとめ

ケースクラスの設計時に値クラスを使って型を強化する方法について考えてみました。

ケースクラスの属性を値クラス化したケースクラスを使うだけなので若干コーディングの手間はかかるものの簡単に実現できることが分かりました。

値クラスを使用することで、型によるエラーチェックを強化を行いつつ、オブジェクトの生成回数、メモリの使用量の増大を回避できることが期待できます。

Scala Design Note

0 件のコメント:

コメントを投稿