我刚刚开始研究即将发布的2.8版本中的Scala集合库重新实现。熟悉2.7版本的库的人会注意到,从使用角度来看,库的变化很小。例如
> List("Paris", "London").map(_.length)
res0: List[Int] List(5, 6)
…两种版本都可以。这个图书馆非常有用:事实上它非常棒。然而,那些以前不熟悉Scala并四处摸索以了解该语言的人现在必须理解方法签名,如:
def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That
对于这样简单的功能,这是一个令人望而生畏的签名,我发现自己很难理解。我并不认为Scala有可能成为下一个Java(或/C/C++/C#)-我不相信它的创造者是瞄准这个市场的-但我认为Scala成为下一代Ruby或Python(即获得大量商业用户)是可行的
这会让人们不去斯卡拉吗?这会不会让斯卡拉在商业界名声扫地,因为它是一种只有专业的博士生才能理解的学术游戏?首席技术官和软件负责人会被吓跑吗?图书馆重新设计是否明智?如果你在商业上使用Scala,你会担心吗?您是否计划立即采用2.8版本,还是等待结果?
Steve Yegge曾攻击Scala(在我看来是错误的),因为他认为Scala的类型系统过于复杂。我担心有人会用这个API来传播FUD(类似于Josh Bloch如何吓得JCP不敢向Java添加闭包)。
注意-我应该清楚,虽然我认为约书亚·布洛赫在拒绝BGGA关闭提案方面有影响力,但我不认为这是因为他诚实地认为该提案代表了错误。
尽管我的妻子和同事一直在告诉我,我并不认为自己是个白痴:我在牛津大学获得了很好的数学学位,我已经在商业编程近12年,在Scala编程大约一年(也是商业编程)。
请注意,煽动性主题标题引用了20世纪80年代初英国一个政党的宣言。这个问题是主观的,但这是一个真实的问题,我已经把它改成了CW,我想就此事发表一些意见。
我希望这不是“遗书”,但我明白你的意思。您发现了Scala的优点和问题:它的可扩展性。这使我们能够实现库中的大多数主要功能。在其他一些语言中,带有map或collect之类的序列将被内置,没有人需要了解编译器要使其顺利工作所需的所有步骤。在Scala中,它都在一个库中,因此是公开的。
事实上,复杂类型支持的地图功能非常先进。考虑一下:
scala> import collection.immutable.BitSet
import collection.immutable.BitSet
scala> val bits = BitSet(1, 2, 3)
bits: scala.collection.immutable.BitSet = BitSet(1, 2, 3)
scala> val shifted = bits map { _ + 1 }
shifted: scala.collection.immutable.BitSet = BitSet(2, 3, 4)
scala> val displayed = bits map { _.toString + "!" }
displayed: scala.collection.immutable.Set[java.lang.String] = Set(1!, 2!, 3!)
看看你总是如何得到最好的类型?如果将Ints映射到Ints,则会再次获得一个BitSet,但如果将Int映射到Strings,则将获得一个通用Set。映射结果的静态类型和运行时表示都取决于传递给它的函数的结果类型。即使集合为空,这也有效,因此函数永远不会被应用!据我所知,没有其他集合框架具有同等功能。然而,从用户的角度来看,这就是事情应该如何工作的。
我们面临的问题是,所有实现这一点的巧妙技术都会泄露到类型签名中,而这些签名变得又大又吓人。但也许默认情况下不应该向用户显示地图的完整类型签名?如果她在BitSet中查找地图,她会得到:
map(f: Int => Int): BitSet (click here for more general type)
在这种情况下,文档不会说谎,因为从用户的角度来看,map确实具有类型(Int=>Int)=>BitSet。但地图也有一个更通用的类型,可以通过单击另一个链接来查看。
我们还没有在工具中实现这样的功能。但我认为我们需要这样做,以避免吓跑人们,并提供更多有用的信息。有了这样的工具,希望智能框架和库不会成为自杀笔记。
嗯,我可以理解你的痛苦,但坦率地说,像你和我这样的人——或者几乎任何一个普通的StackOverflow用户——都不是规则。
我的意思是。。。大多数程序员都不会在意类型签名,因为他们永远不会看到它们!他们不阅读文档。
只要他们看到了代码如何工作的一些示例,并且代码不会使他们无法产生他们期望的结果,他们就永远不会查看文档。当失败时,他们将查看文档并期望在顶部看到用法示例。
考虑到这些,我认为:
任何人(像大多数人一样)遇到这种类型签名时,如果事先对Scala进行了处理,就会对其进行无限的嘲笑,如果他们喜欢Scala,就会认为它是Scala力量的象征。如果文档没有得到增强以提供使用示例,并清楚地解释方法的用途和使用方法,那么可能会有点影响Scala的采用。从长远来看,这无关紧要。Scala可以做这样的事情,这将使为Scala编写的库更加强大,使用起来更加安全。这些库和框架将吸引熟悉强大工具的程序员。喜欢简单和直接的程序员将继续使用PHP或类似的语言。
唉,Java程序员非常热衷于强大的工具,所以,在回答这个问题时,我刚刚修正了我对主流Scala采用的期望。我毫不怀疑Scala会成为主流语言。不是C主流,但可能是Perl主流或PHP主流。
说到Java,您曾经替换过类加载器吗?你有没有调查过这涉及到什么?如果你看看框架编写者所做的事情,Java可能会很可怕。只是大多数人都不知道。这同样适用于Scala,IMHO,但早期采用者倾向于在他们遇到的每一块岩石下寻找,看看是否有什么隐藏在那里。
我没有博士学位,也没有任何其他学位,既没有计算机科学,也没有数学,也没有其他领域。我以前没有使用Scala或任何其他类似语言的经验。我甚至没有使用远程可比类型系统的经验。事实上,我所掌握的不仅仅是表面知识,甚至还有一种类型系统的语言是Pascal,它并不完全以其复杂的类型系统而闻名。(虽然它确实有范围类型,而AFAIK几乎没有其他语言有,但这在这里并不重要。)我知道的其他三种语言是BASIC、Smalltalk和Ruby,它们都没有类型系统。
然而,我完全不难理解你发布的地图函数的签名。在我看来,这与地图上我见过的其他语言的签名几乎相同。不同的是,这个版本更通用。它看起来更像C++STL,而不是Haskell。特别是,它通过仅要求参数为IterableLike来抽象具体的集合类型,并且通过仅要求存在隐式转换函数来抽象具体返回类型,该函数可以从结果值集合中构建一些东西。是的,这很复杂,但它实际上只是泛型编程的一般范式的一种表达:不要假设任何实际上不需要的东西。
在这种情况下,map实际上不需要集合是列表,也不需要被排序或可排序等。地图唯一关心的是,它可以一个接一个地访问集合的所有元素,但没有特定的顺序。它不需要知道生成的集合是什么,只需要知道如何构建它。因此,这就是它的类型签名所要求的。
所以
map :: (a → b) → [a] → [b]
这是映射的传统类型签名,它一般不需要具体的List,而只需要IterableLike数据结构
map :: (IterableLike i, IterableLike j) ⇒ (a → b) → i → j
然后通过仅要求存在能够将结果转换为用户想要的任何数据结构的函数来进一步概括:
map :: IterableLike i ⇒ (a → b) → i → ([b] → c) → c
我承认语法有点笨拙,但语义是一样的。基本上,它从
def map[B](f: (A) ⇒ B): List[B]
这是地图的传统签名。(注意,由于Scala的面向对象特性,输入列表参数是如何消失的,因为它现在是单个分派OO系统中每个方法都具有的隐式接收器参数。)
def map[B](f: (A) ⇒ B): IterableLike[B]
现在,它用一个函数替换了IterableLike结果集合,这个函数实际上可以产生任何东西。
def map[B, That](f: A ⇒ B)(implicit bf: CanBuildFrom[Repr, B, That]): That
我真的相信这并不难理解。你只需要几个智能工具:
你需要知道(大致)map是什么。我承认,如果你只给出了类型签名,而没有给出方法的名称,那么很难弄清楚到底发生了什么。但既然你已经知道map应该做什么,而且你知道它的类型签名应该是什么,你就可以快速扫描签名并关注异常情况,比如“为什么这个映射将两个函数作为参数,而不是一个?”您需要能够实际读取类型签名。但是,即使您以前从未见过Scala,这也应该很容易,因为它实际上是您从其他语言中已经知道的类型语法的混合:VB.NET使用方括号来表示参数多态性,使用箭头表示返回类型,使用冒号来分隔名称和类型,这实际上是一种规范。您需要大致了解泛型编程是什么。(这并不难理解,因为它基本上都是用名称拼写的:它实际上只是以通用方式编程)。
这三项都不应该让任何专业程序员甚至业余程序员头疼。在过去的50年中,map已经成为几乎每一种语言的标准功能,不同的语言都有不同的语法,这一事实对于任何使用HTML和CSS设计网站的人来说都是显而易见的,如果没有一些来自圣教堂的讨厌的C++粉丝,你就无法订阅甚至远程编程相关的邮件列表。Stepanov解释泛型编程的优点。
是的,Scala很复杂。是的,Scala是人类所知的最复杂的类型系统之一,可以媲美甚至超越Haskell、Miranda、Clean或Cyclone等语言。但如果复杂性是一种编程语言成功的理由,C++早就死了,我们都会写Scheme。Scala很可能不会成功的原因有很多,但程序员在坐在键盘前不必费心动脑这一事实可能不是主要原因。
这会让人们不去斯卡拉吗?
我不认为这是影响Scala流行程度的主要因素,因为Scala具有强大的功能,其语法对于Java/C++/PHP程序员来说不像Haskell、OCaml、SML、Lisps等那样陌生。。
但我确实认为Scala的流行程度将比现在的Java要低,因为我也认为下一个主流语言必须要简化得多,而我认为实现这一点的唯一方法是纯不变性,即声明性的HTML,但图灵是完整的。然而,我有偏见,因为我正在开发这样一种语言,但我只是在几个月的研究中排除了Scala不能满足我的需求后才这么做的。
这会不会让斯卡拉在商业界名声扫地,因为它是一种只有专业的博士生才能理解的学术游戏?首席技术官和软件负责人会被吓跑吗?
我认为斯卡拉的声誉不会因为哈斯克尔情结而受损。但我认为有些人会推迟学习,因为对于大多数程序员来说,我还没有看到一个用例迫使他们使用Scala,他们会拖延学习。也许高度可扩展的服务器端是最引人注目的用例。
而且,对于主流市场来说,首先学习Scala并不是“新鲜空气”,而是立即编写程序,比如首先使用HTML或Python。当一个人从一开始就了解了所有的细节之后,Scala会逐渐发展壮大。然而,如果我从一开始就阅读了Scala编程,我对学习曲线的经验和看法可能会有所不同。
图书馆重新设计是否明智?
肯定
如果你在商业上使用Scala,你会担心吗?您是否计划立即采用2.8版本,还是等待结果?
我使用Scala作为我新语言的初始平台。如果我在商业上使用Scala,我可能不会在Scala的集合库上构建代码。我会创建我自己的基于类别理论的库,因为有一次我发现Scalaz的类型签名比Scala的集合库更加冗长和笨拙。问题的一部分可能是Scala实现类型类的方式,这是我创建自己语言的一个次要原因。
我决定写这个答案,因为我想强迫自己研究并比较Scala的集合类设计和我为自己的语言所做的设计。不妨分享一下我的思考过程。
2.8 Scala集合使用生成器抽象是一个合理的设计原则。我想探讨以下两个设计权衡。
只写代码:写完本节后,我阅读了卡尔·斯莫特里茨的评论,这与我期望的权衡一致。James Strachan和davetron5000的评论一致认为,that(甚至不是that[B])的含义和隐含的机制不容易直观地理解。参见我在下面第2期中对幺半群的使用,我认为这一点更加明确。Derek Mahar的评论是关于编写Scala的,但阅读其他人的Scala又如何呢。我读过的关于Scala的一个批评是,写它比读别人写的代码更容易。我发现,由于各种原因(例如,编写函数的许多方法、自动闭包、DSL的单元等),这偶尔是正确的,但我不确定这是否是主要因素。在这里,隐式函数参数的使用有利有弊。另一方面,它减少了冗长的内容,并自动化了构建器对象的选择。在Odersky的示例中,从BitSet(即Set[Int])到Set[String]的转换是隐式的。不熟悉代码的读者可能不太清楚集合的类型,除非他们能够很好地解释当前包范围中可能存在的所有潜在的不可见隐式构建器候选项。当然,经验丰富的程序员和代码编写者会知道BitSet仅限于Int,因此到String的映射必须转换为不同的集合类型。但哪种集合类型?未明确指定。AD-HOC系列设计:写完本节后,我阅读了托尼·莫里斯的评论,意识到我的观点几乎相同。也许我更详细的阐述会让这一点更清楚。在Odersky&Moors的“用类型对抗比特腐烂”中,介绍了两个用例。它们是BitSet对Int元素和Map对元组元素的限制,并且是通用元素映射函数A=>B必须能够构建替代目标集合类型的原因。然而,从范畴论的角度来看,这是有缺陷的。为了在范畴理论中保持一致,从而避免角落情况,这些集合类型是函子,其中每个态射A=>B必须映射在同一函子类别中的对象之间,List[A]=>List[B],BitSet[A]=>BitSet[B]。例如,Option是一个函子,可以视为一个Some(对象)和None的集合的集合。没有从Option的None或List的Nil到其他没有“空”状态的函子的一般映射。这里有一个折衷的设计选择。在我的新语言的集合库的设计中,我选择将所有东西都变成一个函子,这意味着如果我实现了一个BitSet,它需要支持所有元素类型,在使用非整数类型参数时使用非位字段内部表示,并且该功能已经存在于它在Scala中继承的Set中。在我的设计中,Map只需要映射它的值,它可以提供一个单独的非函子方法来映射它的(键,值)对元组。一个优点是每个函子通常也是一个应用的,也许也是一个单元。因此,元素类型之间的所有函数,例如A=>B=>C=>D=>。。。,自动提升至提升应用类型之间的功能,例如列表[A]=>列表[B]=>列表[C]=>列表[D]=>。。。。对于从一个函子到另一个集合类的映射,我提供了一个映射重载,它接受一个幺半群,例如Nil、None、0、“”、Array()等。因此,生成器抽象函数是幺半群的append方法,并作为必要的输入参数显式提供,因此没有不可见的隐式转换。(Tangent:这个输入参数还允许附加到非空幺半群,这是Scala的映射设计无法做到的。)这样的转换是同一迭代过程中的映射和折叠。此外,我还提供了一个可遍历的,从类别意义上讲,“带效果的应用编程”McBride&Patterson,它还允许在从任何可遍历到任何应用的单个迭代过程中进行map+折叠,其中大多数集合类都是两者。此外,状态monad是一个应用程序,因此是一个来自任何可遍历的完全通用的构建器抽象。因此,Scala集合是“ad-hoc”的,因为它不是基于范畴理论的,而范畴理论是更高级指称语义的本质。尽管Scala的隐式构造器一开始看起来比函子模型+幺半群构造器+可遍历->应用性“更通用”,但它们并没有被证明与任何类别一致,因此我们不知道它们在最一般的意义上遵循什么规则,也不知道会给出什么样的角情况——它们可能不遵守任何类别模型。添加更多的变量会使事情变得更一般,这是不正确的,这是范畴理论的一个巨大好处,因为它提供了在提升到更高级语义的同时保持通用性的规则。集合是一个类别。我在某个地方读到,我认为这是Odersky,作为库设计的另一个理由,纯函数式编程的代价是在不使用尾部递归的情况下,递归和速度有限。到目前为止,我发现在我遇到的每一种情况下使用尾部递归并不困难。
此外,我的脑海中还带着一个不完整的想法,即Scala的一些权衡是由于试图成为一种可变和不可变的语言,而不像Haskell或我正在开发的语言。这与托尼·莫里斯关于理解的评论一致。在我的语言中,没有循环和可变构造。我的语言将位于Scala之上(目前),这在很大程度上归功于它,如果Scala没有通用的类型系统和可变性,这是不可能的。不过,这可能不是真的,因为我认为Odersky&Moors(“用类型对抗比特烂”)说Scala是唯一一种具有更高种类的OOP语言是不正确的,因为(我自己和通过Bob Harper)验证了Standard ML拥有它们。此外,SML的类型系统可能相当灵活(自20世纪80年代以来),这可能并不容易理解,因为语法与Java(和C++/PHP)的相似性不如Scala。无论如何,这不是对Scala的批评,而是试图对权衡进行不完整的分析,我希望这与问题密切相关。Scala和SML不受Haskell无法执行菱形多重继承的影响,这一点很关键,我理解为什么Haskell Prelude中的许多函数会针对不同类型重复。