19. Type Parameterization
type parameterizationについて説明する。
scalaではtypeは何かに特定しなくてはならない。
javaではraw typeを許しているのと異なる。
information hiding
type parameterizationを使って行う。
purely functional queues
information hidingの実例として取り上げる。
19.1 Functional queues
Listの様にimmutableなpurely functional queuesを考える。
head, tail, appendのoperationが必要。
mutableと同様にO(1)でのoperationを行いたい。
head, tailはListと同様にしてもappendはO(n)になってしまう。
要素を前半leadingと後半trailingの二つのListに分ける。
leading ::: trailing.reverse がqueueの全体像。
leadingがemptyの時のみhead, tail時にtrailing.reverseでmirrorする。
reverseはQ(n)だがtailすればhead, tailのO(1)の実施をn回分charge出来る。
head, tail, appendが同回数ならasymptotically(漸近的)にO(1)となる。
class Queue[T](private val leading: List[T], private val trailing: List[T]) {
private def mirror =
if (leading.isEmpty) new Queue(trailing.reverse, Nil)
else this
def head = mirror.leading.head
def tail = { val q = mirror; new Queue(q.leading.tail, q.trailing) }
def append(x: T) = new Queue(leading, x :: trailing)
}
19.2 Information hiding
先のclass Queueのコンストラクタは前半と後半のリバースと言う不自然な使用。
効率のよって使いやすが犠牲にされている。
内部の実装がAPIにみえてしまっている。
どう隠蔽するか?
Private constructors and factory methods
privateを付けることでprimary constructorも非公開に出来る。
class Queue[T] private (...)
typeとしては公開されたまま。コンストラクタのみが非公開。
コンストラクタはクラス内とcompanion objectから呼べる。
auxiliary constructorは使える。
def this(elems: T*) = this(elems.toList, Nil)
T*は引数の任意回の繰り返し。Section 8.8
Array[T]として渡される。
factory methodをcompanion objectに定義する。
object Queue {
// constructs a queue with initial elements `xs'
def apply[T](xs: T*) = new Queue[T](xs.toList, Nil)
}
classとobjectを同じファイルで置くことでcompanion objectになる。
Queue(1, 2, 3)の様に自然な形で初期化出来る。
global objectのapply methodはobject名で呼べるのでglobal関数に見える。
実際はscalaではmethodは必ずclassかobjectに含まれている。
An alternative: private classes
trait Queue[T] { def head: T; ... }
object Queue {
def apply[T](xs: T*): Queue[T] = { ... }
private class QueueImpl[T](...) { ... }
}
traitでpublic interfaceのみを公開する。
class全体をobject Queueのprivateのclassとして隠ぺいする。
19.3 Variance annotations
trait Queue[T]自体はtypeではない。type parameterを取るので。
typeではないので、値を定義できない。
def doesNotCompile(q: Queue) {} はコンパイルエラーになる。
type parameterを特定すれば(parameterized)型になる。
Queue[String], Queue[Int]は型。
def doesCompile(q: Queue[Int]) {} はコンパイル出来る。
type parameterを取るtrait, classなどをtype constructorと呼ぶ。
type constructorはfamily of typesをgenerateする。
type parameterを取るtrait, classなどをgenericとも呼ぶ。
個々の型を一般化generalizeしたものだから
type parameterのsubtypingと生成された型の関係をvariantと言う。
covariant (flexible)
型に+を付けて表現する。
trait Queue[+T] { ... }
SがTのsubtypeならQueue[S]もQueue[T]のsubtypeになる。
Queue[T]をQueue[S]の型の変数に代入出来るということを。
Tを戻り値の型として使う時。
Queue[T]に実施出来る演算は、Queue[S]にも出来ると言う事。
immutableなら問題ない場合が多い。(全てではない)
mutableの場合は矛盾した操作が出来てしまう可能性あり注意必要。
contravariant
型に-を付けて表現する。
trait Queue[-T] { ... }
SがTのsubtypeならQueue[T]はQueue[S]のsubtypeになる。
covariantの反対。上の例は不自然。。。Section 19.6
Tを引数の型として使うとき。
Tが一般化するとそれを引数とする関数は特殊化する。
nonvariant (rigid)
型に-に何もつけない場合。
trait Queue[T] { ... }
SがTのsubtypeでもQueue[S]とQueue[T]は無関係。
mutableの場合はこの方が安全??
Variance and arrays
scalaのarrayはnonvariant(rigid)
だだし、javaとの互換性のためにArray[Object]型には何でもup cast出来る。
javaのarrayはcovariant(flexible)
type parameterが無かった時からの互換性のため。
19.4 Checking variance annotations
covariant, contravariantに出来るかは、type parameterの使われ方による。
mutableのfieldでNGなのも、setterのdef x_=(x: T)があるから。
imutableでもメソッドの引数にtype parameterの使われたらcovariantに出来ない。
compilerでtype parameterの使われ方がチェックされる。
全てのtype parameterの使用場所を3つに分類する。
covariant(+)のtype parameterが使えるのpositiveな場所のみ。
contravariant(-)のtype parameterが使えるのはnegative場所のみ。
nonvariantはneutralな場所を含め全ての場所で使える。
top levelでのクラス宣言はpositive
原則、ネストされた場所は外側の場所と同じ分類になる。
ネストと言うのは型パラメタ[]のネストと言う意味。
Array[List[T]]のようなネスト。
例外も幾つかある。
メソッドの値引数の型は外側の分類と逆(flipped)になる。
positive <-> negative, neutralはそのまま。
メソッドのtype parameterは外側の分類と逆(flipped)になる。
途中で使われるクラスのtype parameterはそのtype parameterの宣言による。
type parameterが+で宣言されていると変わらない。
type parameterが-宣言されていると逆(flipped)になる。
type parameterがnonvariantなら分類もneutralになる。
一回neutralになるとその内側の場所は全部neutralになる。
追うのが大変なので整合性のチェックはcomilerに任せた方が良い。
abstract class Cat[-T, +U] {
def meow[W^-](volume: T^-, listener: Cat[U^+, T^-]^-)
: Cat[Cat[U^+, T^-]^-, U^+]^+
}
Wはメソッドのtype parameterなので逆(flipped)になって-
volume, listenerはメソッドのvalue parameterなので逆(flipped)になって-
listenerのCat[U, T]
Uはclass Cat[-T, +U]の第一型パラメタが-なので-から逆(flipped)になって+
Tはclass Cat[-T, +U]の第二型パラメタが+なので-のまま。
戻り値の型のCat[Cat[U, T], U]
戻り値の型全体としてはtop levelなので+
型引数Cat[U, T]
全体としては、class Cat[-T, +U]の第一型パラメタが-なので+から逆で-
Uはclass Cat[-T, +U]の第一型パラメタが-なので-から逆(flipped)になって+
Tはclass Cat[-T, +U]の第二型パラメタが+なので-のまま。
型引数U
class Cat[-T, +U]の第二型パラメタが+なので+のまま。
** 上の例を全部追えた!!
?? 直感的にはよくわからない。。。
19.5 Lower bounds
Lower bounds
type parameterの下限を指定できる。
[U >: T]
UをTのsuper classのみに制限出来る。
class Queue[T](private val leading: List[T], private val trailing: List[T]) {
...
def append(x: T) = new Queue(leading, x :: trailing)
}
Queue[T]のTはpositive、append(x: T)のTはnegative => Tをcovariantに出来ない。
class Queue[+T](private val leading: List[T], private val trailing: List[T]) {
...
def append[U >: T](x: U) = new Queue[U](leading, x :: trailing)
}
Uがnegativeになるだけなので、Tはcovariantに出来る。
直感的には、xとTの共通のsuper class Uを型推論で導き出すと言う事。
contravariantは最大公約数的な発想。
自分の要素と演算対象の要素の型が異なる場合は共通の演算しかできない。
type-driven design
型インタフェースによって実装の方法をガイドする。
型設計の概念を入れることで設計が整理しやすくなる。
declaration-site variance
scala、variantを明示的に宣言する。
コンパイラでチェックできる。
use-site variance
javaのwildecard、variantは使う側が暗黙的に決める。
コンパイラでチェック出来ない。
variant複雑で間違いやすいので、結局難し過ぎて使えないと言う事になりがち。
19.6 Contravariance
contravarianceの使用が自然である場合の例
trait OutputChannel[-T] {
def write(x: T)
}
OutputChannel[AnyRef]はOutputChannel[String]のsubtype
AnyRefをwrite出来る場合はStirngをwriteしても安全。
OutputChannel[AnyRef]の使用場所でOutputChannel[String]に置き換えても安全。
OutputChannel[AnyRef]型の値をOutputChannel[String]型の変数に代入出来る。
OutputChannel[AnyRef]がsubtypeでOutputChannel[String]がsuper type
代入する側か、される側か
val super: T = sub の使いかたならTとこの表現を含む部分はcovariant
val super = sub: T の使いかたならTとこの表現を含む部分はcontravariant
両方の表現を含む部分はTとnonvariantにしか出来ない。
戻り値か、引数か
f(x: U): T
Tとfはcovariantで、Uとfはcontravariant
引数は関数の要求、条件、制約
戻り値は関数の提供する機能
型システム設計の一般原則 - Liskov Substitution Principle
TがUのsubtypeならUの代わりにTが使える。
TはUと同等以上のオペレーションをサポートしている。
TのオペレーションはUよりも要求(条件、制約)が少ない。
Tのオペレーションの結果はUの結果の同等以上(subtype的)
** 置き換えられるか(substitution, reasoning)が重要。
** 型システムで置き換え可能であることを保証する。
trait Function1[-S, +T] {
def apply(x: S): T
}
subtype的な関数 - どの関数の代わりとしても使える関数
戻り値がどんな事でも実施できる
Tが全てのtraitをmix-inしたような全てのsubtypeであるような型
Tがsubtype的であるほどFunction1はsubtype的になる。
covariant
どんな引数でも受け入れられる
SがAny
Sがsupertype的であるほどFunction1はsubtype的になる。
contravariant
19.7 Object private data
class Queue[+T] private (
private[this] var leading: List[T],
private[this] var trailing: List[T]
) { ... }
leading, trailingをvarにすればQueueの性能を上げられる。
varにしても引数の値のみで関数の戻り値が決まると言う性質は変わらない。
Queueの外側からはQueueがvarを使っていることが完全に隠蔽されている。
leading, trailingがprivate[this]だから
Queueはvarを含んでも純粋関数的(purely functional)と言える。
varがあってもそれはcacheしているだけで毎回計算しているのと同等。
covariantにvarを含んでいても出来る。
leading, trailingがprivate[this]だから
Queueの外側からはvarを使っていない時と同じ振る舞いに見える。
private[this]の場合のみ同じvariantのチェックが省略される。
?? only in positions that have the same variance classification??
privateだとerrorになる。
19.8 Upper bounds
[T <: U]
type parameter TをUのsubtypeに制限したい場合。
def max[T <: Ordered[T]](x: T, y: T) = if (x > y) x else y
x, yの型をOrdered[T]のsubtypeに制限しているので比較演算>が使える。
[T >: S <: U]
lowerとupperを両方指定することも出来る。
?? SもUのsubtypeの必要あり??
?? Tを複数のU1,U2のsubtypeに制限したい場合は??
max(1, 2)は出来ない!!
Int <: Ordered[Int]ではないから。
implicit parameterを使えば出来る。Section 21.6
19.9 Conclusion
information hiding:
private constructors, factory methods, type abstraction, object private members
type variance
variance annotations, lower/upper bounds, private[this] annotation
0 件のコメント:
コメントを投稿