Scala 的語言設計有哪些缺陷?


開一個吐槽貼。以下主要是我自己編程實踐中遇到的問題。

雖然吐槽了這麼多,Scala 還是甩 Java、C#、Python、JS、Ruby 等等好幾條街的。

1. 高階類型 lambda 表達式語法醜陋

FactoryEv[({type λ[K] = Multiset[K, R]})#λ, Eq]

簡直是不能忍。如果能像下面這樣多好:

FactoryEv[K =&> Multiset[K, R], Eq]

GitHub - non/kind-projector 能部分緩解這個問題。

2. 類型組合的 with 操作符不滿足交換律

trait A { def f: A }
trait B { def f: B }
trait AB extends A with B // AB.f 的類型是 B
trait BA extends B with A // BA.f 的類型是 A

此問題有待 Dotty 中加入的交類型(intersection type)和並類型(union type)解決。

3. 不允許自定義含多個域的值類型(類似 C# 的 struct)

這個應該是 JVM 的缺陷導致的。這個問題導致 Option[+A]、Complex 這些小的不可變的對象每次都要產生額外的解引用操作,影響效率。

4. 沒有 C#、Python 里的 yield 操作符

有了這個構造惰性流會方便得多。

寫一個惰性的深度優先搜索要這樣:

def depthFirstSearch[N](start: N, succ: N =&> Traversable[N]) = new Iterable[N] {
def iterator = new Iterator[N] {
private[this] val s = ArrayStack[N](start);
def hasNext = !s.isEmpty
def next() = {
val curr = s.pop()
for (x &<- succ(curr)) s.push(x) curr } }

而 C#:

IEnumerable& DepthFirstSearch(N start, Func&&> succ) {
var s = new Stack&();
s.Push(start);
while (!s.IsEmpty()) {
var curr = s.Pop();
yield return curr;
for (N next in succ(curr)) s.Push(next);
}
}

5. 不能單獨構造沒有 getter 的 setter


謝邀。

這個問題挺難回答的,因為功力不夠;對編程語言認識廣度和深度上都不夠。

並且程序語言的設計又是個哲學和審美的問題,一千個人有一千個哈姆雷特,從來就沒有一門完美的語言。所以僅從個人的喜好來回答幾點。

1)類型系統是把雙刃劍

我在blog里寫過很多偏有關scala類型系統的,因為這部分是我學習scala過程中耗費最多精力去了解的。我非常理解為什麼Jetbrains為什麼要設計一門 kotlin 語言,他們的理由是scala很好很強大,但也有些過了,所以我們採取了折中的設計,借鑒scala但去掉它非常複雜的地方(主要是類型系統)。

要從設計上來證明它的不好之處,不太容易,有些不好的點,並不一定是設計層面的,而是實現層面的問題,比如類型推導為何不是用全局式的HM推導,而採用局部的基於流的推導;導致有些情況下不如haskell的推導那麼強大。另外還有一些看上去奇奇怪怪的符號,也可以說是實現風格的問題。

對於設計上的爭論,這裡有一個例子,scala霧中風景(15): class A { type T }與class A[T] {}

你會覺得這兩種是否等價,或者那種更好?既然scala這麼追求統一和一致性,為何在類型上不用一種方式呢?看看martin自己的解釋:

https://github.com/wecite/papers/blob/master/An-Overview-of-the-Scala-Programming-Language/5.Abstraction.md#53–%E7%94%A8%E6%8A%BD%E8%B1%A1%E7%B1%BB%E5%9E%8B%E5%BB%BA%E7%AB%8B%E6%B3%9B%E5%9E%8B%E6%A8%A1%E5%9E%8Bmodeling-generics-with-abstract-types

&>兩種抽象模式之間可以轉換,對於一種語言還是有價值的,因為可以降低其內在的概念複雜性。例如,Scala 的泛型,實際上就是一種語法糖,完全可以被抽象類型替代掉。既然如此,也許會有人問,這種語法糖有沒有必要性?或者說為什麼不只用抽象類型呢,這樣可以使語法本身簡化很多。實際上,Scala 中引入泛型有兩重意義:首先,手工把泛型轉化為成為抽象類型表達形式並不那麼簡單,不僅會喪失語法的簡潔性,而且還可能帶來前述的命名衝突等問題。其次,泛型和抽象類型在 Scala 中一般扮演不同的角色,泛型一般用於類型的實例化,而抽象類型主要用於在調用者代碼中對相應的抽象類型進行引用。後者主要來自於兩個場合:一個是有人需要在客戶代碼中隱藏相關類型信息,用於構造類似於SML模式的模塊系統。另一個是在子類中協變地繼承父類的類型,從而獲得族多態。

&>可能有人會問,那麼是否可以反過來用泛型來替代抽象類型呢?一些對於兩種抽象方式都支持的系統進行的研究 [27] 證實,這樣做要困難得多,至少整個程序都需要重寫。不僅如此,如果系統要實現受限多態的話,重寫類型上/下界的部分會呈平方級增長 [8]。實際上這一點也不奇怪,因為這兩種類型體系的理論基礎就不同,泛型(不帶 F-界的)可以用 F&<: 系統來表達 [11],而抽象類型則建立在類型依賴的基礎之上。後者比前者的表現力更強,例如,帶路徑依賴類型的 νObj 演算是可以涵蓋 F&<: 的。

2)靈活性也是把雙刃劍(eg: 操作符重載)

我個人不排斥這個特性,但團隊協作上需要剋制。當年有段採訪Java之父Gosling在回答為何不支持無符號類型(unsigned int)時,說非常多的語言都是因為它們太過靈活而死亡的,所以他要儘可能保持簡單。現在來看,是一種大智若愚的做法。

3) Martin的野心太大

即想支持腳本式開發,又想擔當系統語言;即想要動態語言的靈活性,又想要靜態語言的安全性;即想運行在JVM上,又想運行在.NET上;總之是想得非常宏大,實際能力卻不匹配(背後沒有大財團支持),在實現上有很多bug(編譯器和類庫),還有兼容性的問題,都給用戶一種不夠可靠的感覺(相對於java)。


隨便說說,可能有誤1. 編譯效率低,生成文件眾多,但這不見得是語言缺陷,跟客觀限制有關

2. ( {語義不明確,常常可以混用(註:這和允許空格代替括弧和允許{}省略有關)

3. 有些語法優先順序不明,例如for() yield {}.map是錯誤的,需要括弧

4. 類型可以省略加上implicit conversion,看代碼的時候沒有IDE容易讓人跟丟

5. 允許定義運算符,加上允許類型隱式轉換,造成對初學者很不友好。符號表達眾多,&<:, &<:&<之類讓人不知所云

6. 為了解決JVM type erasure的manifest設計API一改再改,反射似乎現在還是beta

7. 轉到akka前actor設計有內存泄露問題

8. 允許用空格代替括弧隔開參數,結果代碼一複雜編譯就會報錯

9. Seq有Nil表示空,Map和Set沒有


Martin Odersky是個教授 手下帶了不少研究生 研究生為了畢業 一定得搞點什麼成果 主要就是給scala加feature 等研究生畢業了 這些feature基本上就沒人管了 最後就成了現在這個樣子


1. 同名同位置默認參數不能overload

def bar(i:Int,s:String="a"){}
def bar(i:String,s:String="b"){}

編譯錯誤: .... multiple overloaded alternatives of method bar define default arguments.

因為scala編譯後,按默認的參數位置,生成類似如下這樣的方法,導致重載衝突。

public String bar$default$2() {return "a";}
public String bar$default$2() {return "b";}

解決方案:部分解決,類似java的重載。

def bar(i:Int,s:String){}
def bar(i:String,s:String){}
def bar(i:Int) = bar(i,"a")
def bar(i:String) = bar(i,"b")

2. 可變參數的類型,類型擦除有關

@varargs def bar(a:String*){}
@varargs def bar(a:Int*){}

編譯錯誤:... double definition ... have same type after erasure

因為scala編譯後,會生成以下代碼,導致類型擦除後衝突。

public static void bar(String[] arg0){}
public static void bar(int[] arg0){}
public static void bar(Seq& seq) {} // 會擦除
public static void bar(Seq& seq) {} // 會擦除

相似的,java代碼可以滿足要求。

public void bar(String ... obj){}
public void bar(int ... obj){}

解決方案: (implicit d: DummyImplicit)

@varargs def bar(a:String*){}
@varargs def bar(a:Int*)(implicit d: DummyImplicit){}


只有過一點Scala,有愛的語言,但是我不是大愛。見別人用的多一些,自己用得少一些。

1、首先縱觀所有強類型函數語言的設計,基本採用的都是類型聲明與定義分開的方式,Miranda、ML、F#、Ocaml都是如此,把類型與函數參數的聲明混在一起顯得太零亂了,可讀性直線下降。

def sum[A](xs: List[A])(implicit m: Monoid[A]): A =
if (xs.isEmpty) m.unit
else m.add(xs.head, sum(xs.tail))

而Haskell的實現同樣的東西。

sum :: Monoid a =&> [a] -&> a
sum [] = mempty
sum (x:xs) = x `mappend` sum xs

Scala是不是設計成下面這樣更好?並且支持把類型簽名跟定義分開。你看這裡你就知道設計Haskell的這群人有多重視細節有多智(ji)慧(zei),類型、類型類在Haskell必須大寫,類型變數小寫。所以根本沒必要在sum後面寫[A]這種東西,小寫的自動就是多態類型變數了。不設計成這樣怎麼樣,a是一個monoid,則直接用a.unit、 a.add表示就好了。

def sum :: Monoid[a] =&> List[a] -&> a
sum(xs) = if (xs.isEmpty) a.unit
else a.add(xs.head, sum(xs.tail)

同樣都是兩三行的東西,Haskell就可以這麼美,不是沒有原因的,這些原因隱藏在了各種看似無所謂的細節里。

2、上面還體現出了一個問題,就是Scala中的所有特性必須是modular的,各種封閉是獨立的,所以你要引入m。即限定在對象中的,沒有頂層的設計(至少我沒發現),這樣一來,對於不同類型沒有辦法做到全局的限定。與Java的重載並無兩樣。當然我不是說modular的設計不好,因為非modular也會引起問題,只是原始的Scala庫的設計並沒有特別精心,如果你看了Haskell數字類型的設計有多嚴謹你就知道了。可能也是這個原因所以我在Scala里也沒看到Ord、Eq這樣的abstract類在頂層限定所有其他類型。不過見到JVM上能做到這樣的語言我已經跪笑了。歡迎大家觀摩一下EK大神的scalaz庫,我看了他演講的視頻,覺得設計很不錯。

3、兼容性問題

Scala兼容性的確是一個問題,學並發的時候老師給了叫cso的包,只能運行在2.9特定的某個子版本下,否則無法使用,而坑爹的的是,當時我安新版本eclipse里的插件好像已經不支持那個scala版本,WTF!Haskell演化面臨很多地方面臨跟Scala一樣的問題。但是Haskell得到了一定程度上解決,比如Typeable導出是7.7之後支持的,之前的要手寫,所以這時候就可以藉助宏。並不確定Scala是如何做的。另外一點就是Haskell標準確定了之後就沒有人再改動了,無論GHC要加什麼特性都是使用編譯器擴展來完成,這樣保證了一個語言的基礎是不會變的,餘下的可以大刀闊斧的進化,所以GHC目前為止有比較好的兼容性。希望Scala也能這樣,有一個基礎後就不要再改動了。這個過程Haskell花了9年才定製出Haskell98。

#if __GLASGOW_HASKELL__ &>= 707
deriving (Typeable)

4、Scala的類型系統

有幾次,聲明的函數返回了一個值,那個值的類型我覺得很好確定,但是Scala就是不以推斷,一定要我手寫。我一個初學者感覺挺奇怪的,有的時候可以有的時候不行,這是最崩潰的,索性就都要寫了。(寫畢業設計的時候也遇到過為什麼我用for循環來new對象不行,但是一行一行new就可以!好像是有類型錯誤,WTF!,為此我把代碼用for列印了出來貼上去的)Haskell在設計之初用的是HM類型系統,後又對其進行了擴展。中間語言使用System F,都是有強大理論支持的,這種積澱不是幾年,而是幾十年的,我不知道Scala類型系統是怎樣的,但顯然我用之前常用的2.9的問題挺多的。不知道現在的2.11怎麼樣。

最後,我覺得在JVM設計出一個有函數式特性、類型系統嚴格、兼容Java、又要照顧面向對象程序員的語言絕對是世界級難題,而Scala逆流而上在解決,除此之外目前還無人敢上,單憑這一點大家就要包容一些。不過目前scala跟我心中的state-of-art還差一些,但我會抱樂觀態度拭目以待,希望Martin等大神們好好設計。


以後這個地方就是吐槽地了。。。

--

帶高階類型參數的方法無法重載(因為類型擦除),然後產生了magnet模式(一堆冗餘),面向對象和函數式結合,卻採用了Java常規的重載方式(不可帶類型參數),希望3.0再創奇蹟

implicit class OptionApplicativeOp[A](opt1: Option[A]) {
def apply[B](opt2: Option[A =&> B]): Option[B] =
optionApplicative.map2(opt1, opt2)((opt1x, opt2f) =&> opt2f(opt1x))
def apply[B,C](opt2: Option[(A, B) =&> C]): Option[B =&> C] =
optionApplicative.map2(opt1, opt2)((opt1x, opt2f) =&> opt2f(opt1x, _))
}
def add2(x: Int, y: Int) = x + y
val s = Some(1) apply (Some(2) apply Some(add2 _))

編譯不過,無法重載,要我寫apply2~apply23,我寫大段隱轉不是添亂嗎!

--------------

掙扎過後,解決方案也是皆大歡喜 Magnet 大法好

implicit class OptionApplicativeOp1[A,B](opt1: Option[A =&> B]) {
def apply(opt2: Option[A]): Option[B] =
optionApplicative.map2(opt2, opt1)((opt2x, opt1f) =&> opt1f(opt2x))
}
implicit class OptionApplicativeOp2[A,B,C](opt1: Option[(A, B) =&> C]) {
def apply(opt2: Option[A]): Option[B =&> C] =
optionApplicative.map2(opt2, opt1)((opt2x, opt1f) =&> opt1f(opt2x, _))
}
def add2(x: Int, y: Int) = x + y
val s = Some(add2 _) apply Some(2) apply Some(1)

這樣是能編譯過的,更像Haskell了^ ^

--------------------更新 2015-9-9

Scala 的函數根本不是多態(泛型)的,方法才是多態的:

scala&> def const[A, B](a: A, b: B): B = b
const: [A, B](a: A, b: B)B
scala&> val f = const _
f: (Any, Nothing) =&> Nothing = &
scala&> val f2 = const[Int, Int] _
f2: (Int, Int) =&> Int = &

方法轉函數後,要麼推導為食物鏈頂端和底端(Any, Nothing),要麼就變單態的(f2),所以在compose 函數時要注意。也怪不得shapeless 定義個函數要繼承個奇怪的 "~&>" (PolyFunction)


沒想到cpl組組長會問這個問題。

設計問題不敢亂說,只說說自己的感覺吧。

第一點是複雜。(也比較合題主的口味吧)

跟能在編譯時執行代碼的D語言一樣複雜,同時還帶了一個比D語言還複雜的類型系統。

第二點就是

特別是省略掉括弧和點號以後,看上去很像Lisp / Haskell,但是卻完全是另外一回事兒。

還有張淞說的類型簽名,亂糟糟的堆在一起。

第三點給人的感覺就像是在拼湊語言特性

把OO和FP扭在一起還好,但後面又一點點加上Reflection、Macro等,總覺得有些地方太過生硬,不夠協調。

估計真的要等到Scala 3出來才可能有個統一吧。

我覺的這三點就能嚇跑很多人了。


編譯太慢…………


面向對象和函數式編程的融合要等scala3.0再看martin有什麼天才設想。目前階段不覺得語言設計層面有什麼缺陷。

編譯器實現上由於種種限制,類型推斷能力不夠強,有時候會發生代碼怎麼看都是正確的,但是編譯器就是報編譯不過的問題。

在3.0之前不期待語言本身再有什麼改進,只希望編譯器能繼續加強。


1. 語法過於複雜。

2. 關鍵詞多義。


過於靈活複雜,學習成本高,學習成本高不是指學習時難理解,而是一段時間不用就唧唧了,忘了咋寫了


不能定義public field


Scala 里的var用的特別不爽,舉個例子一個case class A(...) 結構存到內存里,裡面有需要改變狀態的成員用了var a = "start", 第一次改狀態 A.a = b, 內存里的a成了b的狀態, 後續如果繼續改變b的話,A.a也會被錯誤改變狀態。 這就需要clone(),而且如果A中包含著更多的case class就需要一層一層的clone下去


Thinking in Java 的作者在一次演講上說,Scala 被認為是 最沒有主見的語言 non-opinionated,他列舉了Java,C++ 等等,每一種語言似乎都有一種特定的目的,特定的用法,而Scala就有各種用法,沒有特定的,同時Scala的版本之間,1 到 2 是不兼容的,然後據說 2 到 3 也不兼容。這是我聽到的Scala的缺點。不過當時作者還在學習Scala,所以也不完全代表現在的觀點了。


謝謝樓主開的主題,起碼可以看出scala不適合用於生產。java拋開別的不說,IDE對代碼支持簡直太完美了,即使是經常抽風的eclipse都很難找得到對手,更別友好的IDEA了。


語言上有什麼設計缺陷不敢說,但是借用一句話:

scala可能是唯一一個編譯器和IDE對代碼有不同理解的語言。

當你開始用scala的高級特性的時候,他們的分歧特別的大,以至於現在,intellij上的scala插件已經不敢對可能編譯不通過的代碼標記錯誤了。


Scala的優點與缺陷:深沉而有趣的繼承者

2009-08-06 14:01 司馬牽牛 編譯 51CTO.comScala的優點與缺陷:深沉而有趣的繼承者字型大小:T | T

自從Twitter開始採用Scala做後台開發以來,Scala編程語言便開始吸引眾多開發者的目光了。本文對Scala的優點以及現存的一些不足做了一番總結,並認為Scala非常適合Java王位繼承者這一角色。

AD:51CTO 網+ 第十二期沙龍:大話數據之美_如何用數據驅動用戶體驗

【51CTO精選譯文】一段時間以來,Java 是唯一運行在 JVM 上的語言。而 Scala 最近因用於擴展 Twitter 後台(詳情可參考51CTO之前發布的《一位Twitter工程師的Scala探秘之旅》)而吸引了眾多目光。與許多運行在 JVM 上的語言不同,如 Groovy、Jruby 和 Jython,Scala 它是靜態類型的。這表示,與 Java 和 C# 類似,類型必須在編譯時是已知的。通常,人們介紹 Scala 時會說它即是面向對象的(OO)又是函數式的(functional)。雖然這種說法是正確的(並且令人畏懼,很多人對於這些辭彙很不爽),但並沒有抓住 Scala 的重點。

以下 Scala 的各種特性能夠為你帶來最直接的好處:

兼容 Java。這點很明顯(其他 200 多種 JVM 上的語言也兼容 Java),但它是如此重要的一個功能,因此不可小視。它意味著 Scala 可以使用所有 Java 庫和框架愛。這也是對那些投資該技術的人員和公司的表達敬意。

聯合編譯(Joint Compilation)。這表示與 Groovy 類似,Scala 類被編譯為 Java 類,因此可以在 Java 項目中使用(甚至在他們被定義的同一項目中的 java 類 使用)。即使你的團隊覺得完全轉向 Scala,對於通過 JSR 223 整合動態語言,這依然很有用。

類型推斷(Type Inference)。如果編譯器能猜到類型(通常它可以做到),你就不必告訴它。這使得 Scala 代碼具有動態語言的簡潔性,而同時保持類型安全。

隱式轉換(Implicit conversion),使 Scala 具有類型安全性,正如擴展方法(extension method)之於 C#,開放類(open class)之於 ruby。即:向你未曾定義的類型添加方法(如字元串、列表、整數)。這是使得 Scala 符合 DSL(特定領域語言)模型的特性之一。

鼓勵使用對象不變性,並且容易實現。Scala 甚至提供了內置的不變對象垃圾收集器。對於Scala有這樣一種說法:「每當提到不變性時,有人就會開始擔心性能的問題,對於某些情況,這種擔憂並非毫無來由,但對於 Scala,最終結果卻與這一擔憂相反。不可變的數據結構相對於可變的數據結構,更有助於獲得較高的效率。其原因之一在於強大的垃圾收集器(garbage collector),與 JVM 中的垃圾收集器類似。」

自動生成 Getter 和 Setter,如果你不需要(比如你只需 Setter),你必須顯示地將他們私有化(private)。這不是問題,因為通常情況都需要。

Scala 具有第一等級(first-order)函數並通過 iterable trait 實現了枚舉協議(enumeration protocol),這有助於讓代碼更加乾淨、更加簡潔,並能夠帶來其他一些好處。

Actor 編程模式讓高度並行應用程序的開發更加簡單。

不必顯示抓取或拋出(try-catch)異常。可以認為使用已檢查異常(checked exception)的害處多於好處。

有關Scala更多的好處,可以參考51CTO之前發布的Ruby高手點評Scala編程語言十大絕招一文。不過,單單這些特性就已足夠——足夠令 Scala 成為一個非常有趣的語言,足夠讓 JRuby 創建者之一 Charles Nutter 宣稱它就是 Java 王位的合法繼承人,甚至足夠讓 Groovy 的創建人 James Stracha 以及 Java 的創建人 James Gosling 為其背書。儘管如此,Scala 是一個具有深度的語言,還有幾項高級特性,可以讓開發者更具生產力。但在掌握基本知識之前就學習這些功能會讓人感覺非常困難,如果沒有很好的支持文檔(比如 IBM、 Aritma、Jonas Bonér、Daniel Spiewak、Sven Efftinge 以及官方和其他網站提供的資料)會更加困難。不過,這不僅僅是令人興奮,在你需要時,它確實可以用來進一步挖掘更具深度的一些概念。

51CTO編輯推薦:Scala編程語言專題

即使 Scala 具有學術性的根源(正如在它的論文網頁上顯示的那樣,以及它涉及的某些高深概念),它已成功應用在企業項目上,除 Twitter 之外,還有西門子、法國電力集團(électricité de France)和 WattzOn 網站。

在所有這些優點之外,Scala 的確還存在一些粗糙的地方。雖然很多正在努力解決這些弱點,但近期內它們還是有影響的:

◆剛剛起步的 IDE 支持。正如 Lift 的作者所言,Scala 的 IDE 支持,雖然進行了很多開發,還沒有做到 Java 那種地步。重構支持、代碼完整以及單元測試整合都很差。更不必說大多數框架支持工具不能很好地與 Scala 兼容。由於 IDE 能夠幫助人們學習這種語言,這可能嚇退那些新手。另一方面,Martin Folwer 認為這種 IDE 狀況是相對的,一種讓你更具生產力的語言足以彌補缺乏高級工具的弱點。

◆大多數 IDE 不支持聯合編譯。同樣,當 Scala 更加普及之後,這一點會有所改變。

◆類的不變性並非真的不變性,因為引用對象自身可能不是不變的。並且目前沒有方法可以確保整個對象圖譜是不變的。

◆讓 JSR 223 完美地兼容 Scala 非常困難。但另一方面,取得足夠好的兼容還是相當容易的。

◆Scala 不支持元編程(metaprogramming)。通過將其與動態語言結合,如 Ruby,可以繞過這個問題,但如果你是元編程的重度使用者,使用一個完全不同的語言是一種較好的解決辦法(Fan 是另一個運行在 JVM 上的靜態類型語言,與 Scala 類似,但支持元編程)。

◆使用 Java 資源的框架,如客戶端 GWT,不能很好地兼容 Scala(雖然有人已經在伺服器端讓 Scala 與 GWT 兼容)。不過,有一個項目正在進行,將能夠使 Scala 轉化為 Java 資源。

◆語法和某些概念與 Java 有點不同,比如:顛倒的類型聲明順序、使用下劃線而不是通配符、星號和預設值,太多種類的空概念(nothing)、沒有靜態方法(你需要使用單例對象 singleton object 作為替代)。文檔對這些問題有很好的解釋,但是,請留意,這不是 Java 代碼到 Scala 代碼的自動轉換。

正如 Joe Amstrong 所說,隨著 CPU 變得更加廉價,具有越來越多的內核,開發者能夠更加簡便地使用多核 CPU 的語言需求,將會不斷增加。Scala 恰好滿足了這種需求,而同時 Java 的開發停滯不前,糾纏於廣泛部署所帶來的問題,以及未來有多開放的不確定性和某些主要貢獻者的政治問題。根據這種情況來看,Scala 非常適合 Java 王位繼承者這一角色。

【編輯推薦】


推薦閱讀:

TAG:後端技術 | Scala | Java虛擬機JVM | 設計語言 | Dotty |