Scala中隐式参数的好例子?[关闭]


75

到目前为止,Scala中的隐式参数对我而言并不好-它与全局变量过于接近,但是由于Scala似乎是相当严格的语言,所以我开始怀疑:

问题:当隐式参数真正起作用时,您能否显示一个真实的(或接近的)良好示例?IOW:比严重的事情showPrompt证明了这种语言设计的合理性。

或相反-您能展示可靠的语言设计(可以是虚构的)吗?我认为,甚至没有一种机制比隐式机制更好,因为代码更清晰,没有猜测。

请注意,我是在问参数,而不是隐式函数(转换)!

更新

全局变量

感谢您提供的所有出色答案。也许我澄清我的“全局变量”异议。考虑这样的功能:

max(x : Int,y : Int) : Int

你叫它

max(5,6);

您可以(!)这样做:

max(x:5,y:6);

但在我眼里implicits是这样的:

x = 5;
y = 6;
max()

它与这种构造(类似于PHP)没有太大区别

max() : Int
{
  global x : Int;
  global y : Int;
  ...
}

德里克的答案

这是一个很好的例子,但是,如果您可以认为灵活地使用不使用发送消息的方式,implicit请发布反例。我真的对语言设计的纯正很好奇;-)。


如果您创建一个全局隐式变量(您不能-最好-可以做的是一个包范围的隐式变量),那么您的语句可能成立,但前提是您选择这样做。而且,最重要的是该API的灵活性来自使用隐式函数。如果不使用它们,将无法获得相同的灵活性。因此,您要删除使它很棒的功能,并且仍然要使其出色。非常奇怪的要求。
德里克·怀亚特

@Derek Wyatt,最后的评论有些奇怪-您不寻求生活中的优化吗?我做。现在,关于全局变量-我并不是说您必须具有全局变量才能使用隐式变量,而是说它们的用法相似。因为它们隐式地按被叫者的名字绑定,所以它们不在调用者的范围之内,而不是从实际调用中移除。
greenoldman'3

Answers:


99

从某种意义上说,是的,隐式表示全局状态。但是,它们不是可变的,这是全局变量的真正问题-您不会看到人们抱怨全局常量,是吗?实际上,编码标准通常要求您将代码中的任何常量转换为通常是全局的常量或枚举。

还要注意,隐式不在平面名称空间中,这也是全局变量的常见问题。它们明确地与类型相关联,因此与这些类型的包层次结构相关联。

因此,使用您的全局变量,使它们在声明站点不变并初始化,然后将它们放在命名空间中。他们仍然看起来像全局变量吗?他们看起来仍然有问题吗?

但是,我们不要就此止步。Implicits捆绑的类型,和他们一样多的“全球性”的类型。类型是全局的事实困扰您吗?

至于用例,有很多,但是我们可以根据它们的历史进行简要回顾。最初,Afaik,Scala没有隐式。Scala拥有的是视图类型,这是许多其他语言所具有的功能。今天,我们仍然可以看到,只要您编写类似的内容T <% Ordered[T]T就可以将类型视为类型Ordered[T]。视图类型是一种对类型参数(泛型)进行自动强制转换的方法。

然后,Scala用隐式泛化了该功能。自动转换不再存在,而是具有隐式转换-这些转换只是Function1值,因此可以作为参数传递。从那时起,T <% Ordered[T]意味着隐式转换的值将作为参数传递。由于强制转换是自动进行的,因此不需要函数的调用者显式传递参数-因此这些参数成为隐式参数

请注意,有两个概念-隐式转换和隐式参数-非常接近,但并不完全重叠。

无论如何,视图类型成为隐式转换隐式传递的语法糖。它们将被这样重写:

def max[T <% Ordered[T]](a: T, b: T): T = if (a < b) b else a
def max[T](a: T, b: T)(implicit $ev1: Function1[T, Ordered[T]]): T = if ($ev1(a) < b) b else a

隐式参数只是该模式的一般化,因此可以传递任何种类的隐式参数,而不是just Function1。然后是它们的实际用法,后来是这些用法的语法糖。

其中之一是Context Bounds,用于实现类型类模式(该模式不是内置功能,而是一种使用与Haskell的类型类提供类似功能的语言的方式)。上下文绑定用于提供适配器,该适配器实现类中固有的功能,但不是由类声明的。它提供了继承和接口的优点,而没有缺点。例如:

def max[T](a: T, b: T)(implicit $ev1: Ordering[T]): T = if ($ev1.lt(a, b)) b else a
// latter followed by the syntactic sugar
def max[T: Ordering](a: T, b: T): T = if (implicitly[Ordering[T]].lt(a, b)) b else a

您可能已经使用过了-人们通常不会注意到一个常见的用例。它是这个:

new Array[Int](size)

它使用类清单的上下文绑定来启用此类数组初始化。通过以下示例可以看到:

def f[T](size: Int) = new Array[T](size) // won't compile!

您可以这样写:

def f[T: ClassManifest](size: Int) = new Array[T](size)

在标准库上,最常用的上下文边界是:

Manifest      // Provides reflection on a type
ClassManifest // Provides reflection on a type after erasure
Ordering      // Total ordering of elements
Numeric       // Basic arithmetic of elements
CanBuildFrom  // Collection creation

后三个大多与集合使用,方法如maxsummap。Scalaz是一种广泛使用上下文范围的库。

另一个常见用法是减少必须共享一个公共参数的操作的样板。例如,交易:

def withTransaction(f: Transaction => Unit) = {
  val txn = new Transaction

  try { f(txn); txn.commit() }
  catch { case ex => txn.rollback(); throw ex }
}

withTransaction { txn =>
  op1(data)(txn)
  op2(data)(txn)
  op3(data)(txn)
}

然后将其简化为:

withTransaction { implicit txn =>
  op1(data)
  op2(data)
  op3(data)
}

这种模式与事务性内存一起使用,我认为(但不确定)Scala I / O库也使用它。

我可以想到的第三个常见用法是对要传递的类型进行证明,这使在编译时检测可能导致运行时异常的事情成为可能。例如,请参阅以下定义Option

def flatten[B](implicit ev: A <:< Option[B]): Option[B]

这使得这成为可能:

scala> Option(Option(2)).flatten // compiles
res0: Option[Int] = Some(2)

scala> Option(2).flatten // does not compile!
<console>:8: error: Cannot prove that Int <:< Option[B].
              Option(2).flatten // does not compile!
                        ^

大量使用该功能的库是Shapeless。

我认为Akka库的示例不适合这四个类别中的任何一个,但这就是通用功能的全部要点:人们可以以各种方式使用它,而不是由语言设计师规定的方式使用。

如果您喜欢被要求开处方(例如Python这样做),那么Scala并不适合您。


7
您正在写的那本书一定要用英语!:-)谢谢你的精彩帖子。
greenoldman'3

2
为什么SO不给星号这样的答案呢?真的很棒!
陈OT

23

当然。就其演员而言,Akka就是一个很好的例子。当您处于Actor的receive方法内部时,您可能希望向另一Actor发送消息。当您执行此操作时,Akka将(默认情况下)将当前Actor作为sender消息的束捆绑在一起,如下所示:

trait ScalaActorRef { this: ActorRef =>
  ...

  def !(message: Any)(implicit sender: ActorRef = null): Unit

  ...
}

sender是隐含的。在Actor中,有一个定义如下:

trait Actor {
  ...

  implicit val self = context.self

  ...
}

这将在您自己的代码范围内创建隐式值,并允许您执行以下简单操作:

someOtherActor ! SomeMessage

现在,如果您愿意,也可以执行此操作:

someOtherActor.!(SomeMessage)(self)

要么

someOtherActor.!(SomeMessage)(null)

要么

someOtherActor.!(SomeMessage)(anotherActorAltogether)

但是通常你不会。您只需保留Actor特性中隐式值定义所能实现的自然用法。还有大约一百万个其他示例。收集类是一个巨大的类。尝试在任何非平凡的Scala库中四处逛逛,您会发现很多东西。


我认为这是比Traversable.max类型类之类更好的示例。
Debilski'3

这是一个很好的例子。在某种程度上,我认为隐式变量是避免全局变量和“神单例”(缺少更好的词)的一种方法,但是由于不必显式传递一些基本的参数(上面提到的,但仍使代码更具可读性)单身人士)。再一次,您仍然可以在测试期间将它们明确地传递给他们。因此,我认为在许多情况下,它们允许更宽松的耦合和更简洁的代码。
vertti 2012年

@vertti,不完全是。我认为C ++的工作方式在这里更好-即每个类的参数和/或默认参数。对我来说,函数本身会吸收某个地方的参数是很奇怪的。
greenoldman'3

1
@Derek Wyatt,您太个人了。“在这里工作得更好”-我希望自己弄清楚我的指标是什么,它们与您的不同,因此我的“更好”与您的“更好”不同。您很高兴有implicits,但我没有。您的榜样非常出色,令人费解(我很感激)如何从另一个角度解决问题。那是我的观点,所以请避免给我“学习您正在编写的语言”的建议(是的,但不是很礼貌-不欢迎任何讨论。
greenoldman'3

1
@macias哦,废话!真的很抱歉,但是我的评论应该已经读过……我不是在光顾你……我真的不是。gh ...对此感到抱歉。
Derek Wyatt'3

9

一个示例是对的比较操作Traversable[A]。例如maxsort

def max[B >: A](implicit cmp: Ordering[B]) : A

这些可以仅在有操作被合理地定义<A。因此,在没有隐式的情况下,Ordering[B]我们每次使用此功能时都必须提供上下文。(或者放弃内部的类型静态检查,max并冒着运行时强制转换错误的风险。)

但是,如果范围内包含隐式比较类型类,例如some Ordering[Int],我们可以立即使用它,或者通过为隐式参数提供其他值来简单地更改比较方法。

当然,隐式可能会被遮盖,因此在某些情况下,范围内的实际隐式不够清楚。对于的简单使用,max或者sort具有固定的顺序可能就足够了traitInt,并使用一些语法检查这种特质是否可用。但这意味着没有附加特性,每段代码都必须使用最初定义的特性。

另外:
全局变量比较的。

我认为您是正确的,例如

implicit val num = 2
implicit val item = "Orange"
def shopping(implicit num: Int, item: String) = {
  "I’m buying "+num+" "+item+(if(num==1) "." else "s.")
}

scala> shopping
res: java.lang.String = I’m buying 2 Oranges.

它可能会闻到腐烂和邪恶的全局变量。但是,关键的一点是,范围内每种类型可能只有一个隐式变量。您带有两个Ints的示例将不起作用。

同样,这意味着实际上,仅在类型不一定存在唯一但又不同的主要实例时才使用隐式变量。self演员的引用就是一个很好的例子。类型类示例是另一个示例。任何类型都可能有数十个代数比较,但是有一个比较特殊。(换句话说,代码本身中的实际行号也可以构成一个很好的隐式变量,只要它使用非常独特的类型即可。)

通常implicit,日常类型不使用。对于特殊类型(如Ordering[Int]),将它们隐藏起来不会有太大的风险。


谢谢,但是实际上这是一个反例-这应该是collection实例的“特征”。然后,您可以使用max()(使用集合的排序)或max(comparer)(使用定制的排序)。
greenoldman'3

2
当然可以。但这也意味着,Int无论何时都无法在例如其他任何预定义类型上添加其他特征。(一个经常被引用的例子是一个半组,它可能不是Int或String上的原始特征,也不能以固定形式添加该特征。)问题是:没办法概括所有可能特征的类型。这些始终是必须临时提供的代码(类型注释),否则您将失去类型安全性。隐式变量只是减少了样板代码。
Debilski'3

Int's提供的集合Int's,例如List或Array。如果您认为这些元素是可比较的,并且您implicit可以像上面那样编写,那么也可以在类的顶部定义顺序(如C ++)。在C ++中,名称空间不会被此处的“ cmp”之类的任意名称污染,因为您可以传递值。
greenoldman'3

谢谢您的补充,对不起,我无法再给您
投票

如果您使用的是Haskell术语,则可能会正确使用。type的值Ordering[Int]类型类实例,而不是类型类
Rotsor 2013年

6

根据我的经验,没有一个真正好的使用隐式参数或隐式转换的例子。

与隐式创建的问题相比,使用隐式(无需显式编写参数或类型)的小好处是多余的。

我从事开发工作已有15年,在过去的1.5年中一直与scala合作。

我已经见过很多次由开发人员引起的错误,这些错误是由开发人员不知道使用隐式的事实引起的,并且特定的函数实际上返回的类型与指定的类型不同。由于隐式转换。

我还听到有声明说,如果您不喜欢隐式,请不要使用它们。在现实世界中,这是不切实际的,因为很多时候都使用外部库,并且很多外部库都使用隐式,因此您的代码使用隐式,您可能不知道这一点。您可以编写具有以下任一功能的代码:

import org.some.common.library.{TypeA, TypeB}

要么:

import org.some.common.library._

这两个代码都将编译并运行。但是它们不会总是产生相同的结果,因为第二个版本会导入隐式转换,这将使代码的行为有所不同。

如果最初并未使用受此转换影响的某些值,则在编写代码后很长一段时间内可能会因此而引起“错误”。

一旦遇到错误,查找原因就非易事。您必须进行一些深入的调查。

即使您发现bug并通过更改import语句进行了修复,就觉得自己是scala的专家,但实际上却浪费了很多宝贵的时间。

我通常反对隐式的其他原因是:

  • 它们使代码难以理解(代码更少,但您不知道他在做什么)
  • 编译时间。使用隐式时,scala代码的编译速度要慢得多。
  • 实际上,它将语言从静态类型更改为动态类型。确实,一旦遵循非常严格的编码准则,您就可以避免这种情况,但在现实世界中,情况并非总是如此。即使使用IDE“删除未使用的导入”,也可能导致代码仍可编译和运行,但与删除“未使用的”导入之前的代码不同。

没有没有隐式编译scala的选项(如果有,请指正我),并且如果有一个选项,那么所有通用社区的scala库都不会编译。

由于上述所有原因,我认为隐式是Scala语言使用的最差的做法之一。

Scala具有许多出色的功能,但很多还没有那么出色。

在为新项目选择语言时,隐式是反对scala的原因之一,而不是赞成它。在我看来。


值得注意的是Kotlin摆脱了隐式:kotlinlang.org/docs/reference/comparison-to-scala.html
Akavall

4

隐式参数的另一种很好的通用用法是使方法的返回类型取决于传递给该方法的某些参数的类型。Jens提到的一个很好的例子是collections框架和诸如之类的方法map,其完整签名通常为:

def map[B, That](f: (A) ⇒ B)(implicit bf: CanBuildFrom[GenSeq[A], B, That]): That

请注意,返回类型ThatCanBuildFrom编译器可以找到的最佳拟合确定。

有关此示例的另一个示例,请参见答案。在此,该方法的返回类型Arithmetic.apply根据某个隐式参数类型(BiConverter)确定。


也许我想念一些东西。您无法在此处猜到类型That,因此您必须指定它,对吗?如果您省略了That类型,而只是手动转换结果,结果是否会相同:map(it => it.foo).toBar()而不是map [B,List [Bars]](it => it.foo)?
greenoldman'3

@macias:后一个不会创建中间集合。当您显式调用toBar时,首先必须创建Foo,然后将其转换为Bar。当存在类型参数时,可以直接创建Bar。
kiritsuku 2012年

3
@macias:如果您手动进行转换,则需要进行第二步。您可能会得到一个List回报,然后需要再次遍历它以获取一个Set。通过使用隐式“注释”,该map方法可以避免首先初始化和填充错误的集合。
Debilski'3

1
@macias:您不必在map方法中拼写出类型参数-可以推断出它们。val lf:List [Foo] =…; val sb:Set [Bar] = lf map(_.toBar)//无中间列表[Bar]
romusz 2012年

4

很简单,只要记住:

  • 将变量也声明为隐式
  • 在单独的()中声明非隐式参数之后的所有隐式参数

例如

def myFunction(): Int = {
  implicit val y: Int = 33
  implicit val z: Double = 3.3

  functionWithImplicit("foo") // calls functionWithImplicit("foo")(y, z)
}

def functionWithImplicit(foo: String)(implicit x: Int, d: Double) = // blar blar

3

隐式参数在集合API中大量使用。许多函数都有一个隐式的CanBuildFrom,它可以确保您获得“最佳”结果集合实现。

如果没有隐式,您将一直都通过这样的事情,这会使正常使用变得麻烦。或者使用不太专业的集合,这会很烦人,因为这将意味着您失去了性能/力量。


0

我对此帖子发表评论有些晚,但是最近我已经开始学习scala。Daniel和其他人为隐式关键字提供了很好的背景知识。从实际使用的角度来看,我将为隐性变量提供2美分的费用。

Scala最适合用于编写Apache Spark代码。在Spark中,我们确实具有Spark上下文,并且很可能具有可以从配置文件中获取配置键/值的配置类。

现在,如果我有一个抽象类,并且如果我声明一个配置对象和Spark上下文,则如下:

abstract class myImplicitClass {

implicit val config = new myConfigClass()

val conf = new SparkConf().setMaster().setAppName()
implicit val sc = new SparkContext(conf)

def overrideThisMethod(implicit sc: SparkContext, config: Config) : Unit
}

class MyClass extends myImplicitClass {

override def overrideThisMethod(implicit sc: SparkContext, config: Config){

/*I can provide here n number of methods where I can pass the sc and config 
objects, what are implicit*/
def firstFn(firstParam: Int) (implicit sc: SparkContext, config: Config){ 
    /*I can use "sc" and "config" as I wish: making rdd or getting data from cassandra, for e.g.*/
    val myRdd = sc.parallelize(List("abc","123"))
}
def secondFn(firstParam: Int) (implicit sc: SparkContext, config: Config){
 /*following are the ways we can use "sc" and "config" */

        val keyspace = config.getString("keyspace")
        val tableName = config.getString("table")
        val hostName = config.getString("host")
        val userName = config.getString("username")
        val pswd = config.getString("password")

    implicit val cassandraConnectorObj = CassandraConnector(....)
    val cassandraRdd = sc.cassandraTable(keyspace, tableName)
}

}
}

如上面的代码所示,我的抽象类中有两个隐式对象,并且已将这两个隐式变量作为​​函数/方法/定义隐式参数传递。我认为这是我们可以用隐式变量来描述的最佳用例。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.