Scala中隐式参数的好例子?

时间:2012-03-02 09:32:41

标签: scala parameters implicit

到目前为止,Scala中的隐式参数对我来说并不好看 - 它太接近于全局变量,但是由于Scala看起来像是一种相当严格的语言,我开始怀疑自己的看法: - )。

问题:当隐式参数真正起作用时,你能展示一个真实(或接近)的好例子吗? IOW:比showPrompt更严重的东西,可以证明这种语言设计的合理性。

或者相反 - 你能否展示出可靠的语言设计(可能是想象的),这种设计会使隐含的不必要。我认为即使没有机制比implicits更好,因为代码更清晰,没有猜测。

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

更新

全局变量

谢谢你们所有的好答案。也许我澄清了我的“全局变量”异议。考虑这样的功能:

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;
  ...
}

Derek的回答

这是一个很好的例子,但是如果您认为灵活使用不使用implicit发送邮件,请发布一个反例。我对语言设计的纯洁感到好奇; - )。

8 个答案:

答案 0 :(得分:96)

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

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

因此,取出你的全局变量,使它们不可变并在声明站点初始化,并将它们放在名称空间中。它们看起来像全局变量吗?它们看起来仍然有问题吗?

但是,不要止步于此。 Implicits 与类型绑定,它们与类型一样“全局”。类型是否全局困扰你的事实?

至于用例,它们很多,但我们可以根据它们的历史进行简要回顾。原来,afaik,Scala没有暗示。 Scala的观点类型是许多其他语言的特征。每当您编写类似T <% Ordered[T]的内容时,我们仍然可以看到今天,这意味着类型T可以被视为类型Ordered[T]。视图类型是一种在类型参数(泛型)上使用自动强制转换的方法。

Scala然后概括该特征带有implicits。自动转换不再存在,而是具有隐式转换 - 它们只是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

隐式参数只是该模式的泛化,可以传递任何类型的隐式参数,而不仅仅是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就不适合你了。

答案 1 :(得分: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库中闲逛,你会找到一辆卡车。

答案 2 :(得分:9)

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

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

只有在<上有操作A时,才能合理地定义这些内容。因此,在没有暗示的情况下,每次我们想要使用此函数时,我们都必须提供上下文Ordering[B]。 (或者在max内放弃类型静态检查,并冒运行时转换错误。)

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

当然,隐含可能被遮蔽,因此可能存在范围内的实际隐含不够清晰的情况。对于maxsort的简单使用,在trait上使用固定排序Int可能就足够了,并使用一些语法来检查此特征是否可用。但这意味着可能没有附加特征,每一段代码都必须使用最初定义的特征。

增加:
全局变量比较的响应。

我认为你在像

这样的代码中是正确的
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.
它可能闻到腐烂和邪恶的全局变量。然而,关键的一点是,范围内每个类型可能只有一个隐式变量。你的两个Int的示例不起作用。

此外,这意味着实际上,只有在类型不一定是唯一但不同的主要实例时才使用隐式变量。演员的self引用就是这样一个很好的例子。类类示例是另一个示例。对于任何类型,可能存在数十种代数比较,但有一种是特殊的。 (在另一个层面上,代码本身中的实际行号也可能产生一个好的隐式变量,只要它使用非常独特的类型。)

您通常不会将implicit用于日常类型。对于特殊类型(如Ordering[Int]),遮蔽它们的风险并不大。

答案 3 :(得分:4)

隐式参数的另一个很好的一般用法是使方法的返回类型取决于传递给它的一些参数的类型。 Jens提到的一个很好的例子是集合框架,以及像map这样的方法,它们的完整签名通常是:

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

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

有关其他示例,请参阅that answer。在那里,方法Arithmetic.apply的返回类型是根据某个隐式参数类型(BiConverter)确定的。

答案 4 :(得分:4)

这很简单,只需记住:

  • 声明要以隐式方式传入的变量
  • 在单独的()
  • 中声明非隐式参数之后的所有隐式参数

e.g。

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

答案 5 :(得分:4)

根据我的经验,使用implicits参数或暗示转换没有真正好的例子。

使用implicits(不需要显式地编写参数或类型)的小好处与它们创建的问题相比是多余的。

我是一名开发人员已有15年,并且在过去的1。5年里一直在使用scala。

我已经看到很多次错误是由于开发人员没有意识到使用了implicits的事实,并且特定的函数实际上返回的是与指定的类型不同的类型。由于隐式转换。

我也听到过声明说如果你不喜欢暗示,就不要使用它们。 这在现实世界中是不实际的,因为很多时候使用了外部库,并且很多都使用了implicits,所以你的代码使用了implicits,你可能没有意识到这一点。 您可以编写具有以下任一代码的代码:

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

或:

import org.some.common.library._

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

&#39; bug&#39;由此造成的,可能会在编写代码后很长一段时间内发生,以防一些受此转换影响的值最初未使用。

一旦遇到错误,查找原因并非易事。 你必须做一些深入的调查。

即使您在找到错误后感觉自己是scala专家,并通过更改导入语句修复它,实际上您浪费了大量宝贵的时间。

我一般反对暗示的其他原因是:

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

没有选项可以在没有implicits的情况下编译scala(如果有请求更正),如果有选项,则没有任何公共社区scala库可以编译。

由于上述所有原因,我认为隐含是scala语言使用的最糟糕的做法之一。

Scala有很多很棒的功能,很多功能都不太好。

在为新项目选择语言时,implicits是反对scala的原因之一,而不是支持它。在我看来。

答案 6 :(得分:3)

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

没有暗示你要么一直传递这样的东西,这会使正常使用变得麻烦。或者使用不太专业的收藏品,这会令人讨厌,因为这意味着你会失去性能/力量。

答案 7 :(得分:0)

我对这篇文章的评论有点晚了,但我最近开始学习scala。 Daniel和其他人给出了关于隐式关键字的好背景。 从实际使用的角度来看,我会给隐含变量两美分。

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)
}

}
}

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