作者macbuntu (邀怪)
看板PLT
標題Re: [問題] Scala 的 Covariant/Contravariant/Inv …
時間Tue Mar 17 21:38:20 2009
※ 引述《sbrhsieh (sbr)》之銘言:
: : interface A<+T> {
: : public T get(); // OK
: : }
: : interface C<-T> {
: : public void set(T t); // OK
: : }
: : interface E<T> {
: : public T get(); // OK
: : public void set(T t); // OK
: : }
: 我認為 generic type 大部分會是上述的 interface E 這種型態居多,這種類型
: 的 generic type 在 scala 裡只能定義為 non-variant subtyping,那麼用上此
: feature 的機會不多。
我覺得因為 Java type parameter 是 invariant 的, 所以看不出 A 和 C 的好處.
舉個使用 covariant type 的例子 (pseudo java):
interface Set<+T> {
public T get();
}
class Algorithm {
public static Number findMax(Set<Number> s) { .... }
}
// 下面都可以用, 因為 type parameter 是 covariant
Set<Integer> si = /* new ... */;
Integer i = Algorithm.findMax(si);
Set<Double> sd = /* new ... */;
Double d =Algorithm.findMax(sd);
實務上常常會有很多東西可以被 model 成 immutable object, 只能讀取不能修改,
如果使用 generics 來設計這些東西, parameterized type 之間能有繼承的關係,
譬如可以設計成 IPod 繼承 Product 則 Parts<IPod> 繼承 Parts<Product>,
我覺得很多時候會蠻直覺的.
: 第二,我認為不應該是由 generic type 來決定 subtyping variance,而是由
: client code 來決定。
: ,,,
: 如果一個 Java programmer 沒有對 wildcard/bounded wildcard 有足夠的觀念,
: 那麼即使把 scala Variance Annotations 加入 Java PL,對他們不會帶來多大的
: 好處(因為 Variable 這種類型的 generic type 佔大多數)。
這就很難說哪樣比較好了... 兩種方法雖然表面上看起來都可以做到一樣的事,
譬如我上面舉的例子您可能說宣告 findMax(Set<? extends Number> a) 就好了,
但這兩種方法有一個微妙的差別:
Java 的方法表示宣告 variance 的責任落在使用型別的人,
而 Scala 的方法表示宣告 variance 的責任落在設計型別的人.
我本來以為 Java 的方法比較保守, 但後來仔細看了 Scala 後, 反而覺得
Java 的方法比較亂, 雖然可以活用, 但這樣活用的代價是型別會被允許處在一個
語意不正確的狀態. 我來舉個例子:
interface A
<T> {
public
T get();
public void set(
T t);
}
static void func(A
<? extends Number> a) {
Number n = a.get(); // OK
a.set(
n); // compile time error
a.set(
123); // compile time error
a.set(
new Object()); // compile time error
a.set(
null); // only this is OK
}
上面那個 func() 裡面, 如果沒有前三個 set() 呼叫, 是可以 compile 沒問題的.
但是 T 用在 parameter type 的時候根本不該允許 <? extends Number>
這種 covariant binding, 也就是說, Java compiler 允許 A 處在一個不正確
的狀態, 讓 A.set() 變成完全無用, 只能放 null 進去. 只允許 s.set(null)
從語意上就變得說不通了, 原先的 A<T> 可以 a.set(a.get()) 為什麼
進到 func() 裡後不能 a.set(a.get())? 是因為 A 裡的 T 只該允許 invariant.
Scala 的方法讓整個 type polymorphism 變得很一致, 即使有 type parameter
還是可以有繼承的關係. Type parameter variance 的能力跟本來就 T 在型別裡面
使用的位置有直接的關係, 所以宣告 variance 的責任落在設計型別的人身上,
感覺既合理又清楚. 如此一來, 使用型別的人沒有辦法以錯誤的方法使用它,
所以上面的例子用 Scala 的方法來寫的話, 設計 A 的人當初如果宣告 A<T>,
是 invariant, 則沒有變成 covariant/contravariant 的機會:
interface A
<T> { .... }
void func(A
<Number> a) { ... }
A<Integer> i = /* new something */
func(
i);
// compile time error
A<Object> o = /* new something */
func(
o);
// compile time error
A<Number> n = /* new something */
func(
n); // OK
如果需要 covariant 或是 invariant, 當初設計 A 的人就要設計一個是可以允許
+T 或是 -T 的介面, 將來才可以這樣用:
interface A
<+T> { .... }
void func(A
<Number> a) { ... }
A<Integer> i = /* new something */
func(
i); // OK
A<Object> o = /* new something */
func(
o);
// compile time error
A<Number> n = /* new something */
func(
n); // OK
在使用型別的地方不需要用 <? extends X> 這種東西, 因為這已經定義在 A 裡了,
型別不會像 Java 那樣處在不正確的狀態. 習慣 Java 的人會說 Java 用法比較靈活,
但我自己覺得 Scala 的 variant 方法是往前更進了一步, 語意也更乾淨漂亮.
(哈, 語意而已... Scala 的語法我就很難習慣了... Java 中毒太深 :P )
--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 60.251.144.115