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

Good example of implicit parameter in Scala? [closed]

提问人:greenoldman 提问时间:3/2/2012 最后编辑:greenoldman 更新时间:11/29/2018 访问量:23255

问:


想改进这个问题吗?更新问题,以便可以通过编辑这篇文章用事实和引文来回答。

4年前关闭。

到目前为止,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;
  ...
}

Derek's answer(德里克的回答)

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

Scala 参数 隐式

评论

0赞 Derek Wyatt 3/2/2012
如果你做了一个全局隐式(你不能 - 你能做的最好的事情就是包范围的隐式),那么你的语句可能成立,但前提是你选择做这样的事情......不要。而且,最重要的是,该 API 的灵活性来自于隐含物的使用。如果你不使用它们,你就无法获得同样的灵活性。所以你要求删除使它变得伟大的功能,并且仍然让它变得伟大。很奇怪的要求。
0赞 greenoldman 3/2/2012
@Derek怀亚特,最后一句话有点奇怪——你不寻求生活中的优化吗?我愿意。现在,关于全局变量-- 我不是说你必须有全局变量才能使用隐含,我说它们在用法上是相似的。因为它们是由被调用方的名称隐式绑定的,并且它们被从调用方的范围中移除,而不是从实际调用中移除。

答:

3赞 Jens Schauder 3/2/2012 #1

隐式参数在集合 API 中被大量使用。许多函数都具有隐式 CanBuildFrom,这可确保获得“最佳”结果集合实现。

如果没有隐式,你要么一直传递这样的东西,这将使正常使用变得很麻烦。或者使用不太专业的集合,这会很烦人,因为这意味着你会失去性能/功率。

23赞 Derek Wyatt 3/2/2012 #2

确定。Akka在演员方面有一个很好的例子。当你在一个 Actor 的方法中时,你可能想要向另一个 Actor 发送一条消息。执行此操作时,Akka 会(默认)捆绑当前 Actor 作为消息,如下所示:receivesender

trait ScalaActorRef { this: ActorRef =>
  ...

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

  ...
}

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

trait Actor {
  ...

  implicit val self = context.self

  ...
}

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

someOtherActor ! SomeMessage

现在,如果您愿意,您也可以这样做:

someOtherActor.!(SomeMessage)(self)

someOtherActor.!(SomeMessage)(null)

someOtherActor.!(SomeMessage)(anotherActorAltogether)

但通常你不会。您只需保留 Actor 特征中隐式值定义所实现的自然用法即可。还有大约一百万个其他例子。集合类是一个巨大的类。试着在任何一个不平凡的 Scala 库中徘徊,你会发现一大堆。

评论

0赞 Debilski 3/2/2012
我认为这是一个比类型类等更好的例子。Traversable.max
0赞 vertti 3/2/2012
这是一个很好的例子。在某种程度上,我认为隐式变量是一种避免全局变量和“上帝单例”(缺乏更好的词)的方法,但仍然保持您的代码更具可读性,因为您不必显式传递一些基本的管道(上面提到的单例)。再说一次,你仍然可以显式地传递它们,例如在测试时。所以我认为在许多情况下,它们允许更松散的耦合和更干净的代码。
0赞 greenoldman 3/2/2012
@vertti,不完全是。我认为 C++ 的工作方式在这里更好——即每个类的参数和/或默认参数。对我来说,函数本身就吸收了从某个地方获取的论点的想法非常奇怪。
1赞 greenoldman 3/2/2012
@Derek怀亚特,你把这件事看得太个人化了。“在这里效果更好”——我希望我清楚地知道我的指标是什么,它们和你的不一样,所以我的“更好”和你的“更好”不一样。你很高兴你有,我没有 - 你的例子是一个很好的思维难题(我对此表示感谢)如何从另一个角度解决问题。这是我的观点,所以请饶恕我“学习你正在编码的语言”的建议(没错,但不是很有礼貌——任何讨论都不欢迎光顾)。implicits
1赞 Derek Wyatt 3/3/2012
@macias 哦,废话!我真的很抱歉,但我的评论应该读到......我不是光顾你......我真的不是。呃......对不起。
4赞 Jean-Philippe Pellet 3/2/2012 #3

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

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

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

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

评论

0赞 greenoldman 3/2/2012
也许我错过了什么。你不能在这里猜测类型 That,所以你必须指定它,对吧?如果您省略键入 That,只需手动转换结果:map(it => it.foo).toBar() 而不是 map[B,List[Bars]](it => it.foo),那不是一样吗?
0赞 kiritsuku 3/2/2012
@macias:后者不会创建中间集合。当您显式调用 toBar 时,首先必须创建一个 Foo,然后将其转换为 Bar。当存在类型参数时,可以直接创建 Bar。
3赞 Debilski 3/2/2012
@macias:如果你手动转换它,你将在第二步中完成。你可能会得到一个回报,然后需要再次遍历它才能得到一个 .通过使用隐式的“注释”,该方法可以避免首先初始化和填充错误的集合。ListSetmap
1赞 romusz 3/3/2012
@macias:您不必向 map 方法拼写出类型参数 - 可以推断它们。val lf: 列表[Foo] = ...;val sb: Set[Bar] = lf map (_.toBar) //无中间List[Bar]
9赞 Debilski 3/2/2012 #4

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

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

只有当对 进行操作时,才能合理地定义这些。因此,如果没有隐式,我们每次想使用这个函数时都必须提供上下文。(或者放弃内部的类型静态检查,并冒着运行时强制转换错误的风险。<AOrdering[B]max

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

当然,隐式可能会被遮蔽,因此在某些情况下,范围内的实际隐式可能不够清晰。对于简单的用法,或者 可能确实有足够的固定顺序并使用一些语法来检查此特征是否可用。但这意味着不可能有附加特征,每段代码都必须使用最初定义的特征。maxsorttraitInt

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

我认为你是对的,在像

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.

它可能闻起来有腐烂和邪恶的全局变量的味道。然而,关键的一点是,在作用域中,每种类型可能只有一个隐式变量。你用两个 s 的例子是行不通的。Int

此外,这意味着实际上,仅当某个类型存在不一定唯一但不同的主实例时,才会使用隐式变量。演员的参考就是这种事情的一个很好的例子。类型类示例是另一个示例。任何类型都可能有几十种代数比较,但有一种是特殊的。 (在另一个层面上,代码本身的实际行号也可能是一个很好的隐式变量,只要它使用一个非常独特的类型。self

您通常不会将 s 用于日常类型。对于特殊类型(如),隐藏它们的风险不会太大。implicitOrdering[Int]

评论

0赞 greenoldman 3/2/2012
谢谢,但这实际上是反例——这应该是集合实例的“特征”。然后你可以使用 max() 它将使用集合的排序或 max(comparer) 它将使用自定义的集合。
2赞 Debilski 3/2/2012
当然,这是可能的。但这也意味着一个人不能添加另一个特征,例如 或任何其他预定义的类型。(一个经常被引用的例子是一个半群,它可能不是 Int 上的原始特征,也不是 String 上的原始特征——也不可能以固定形式添加这个特征。问题是:没有办法将一种类型概括为所有可能的特征。这些始终是代码(类型注释),必须临时提供,否则您将失去类型安全性。隐式变量只是为此减少了样板代码。Int
0赞 greenoldman 3/2/2012
没有给出 的集合,如 List 或 Array。如果你假设元素是可比较的,并且你写了如上所述的内容,你也可以在类的顶部定义顺序(就像在 C++ 中一样)。在 C++ 中,命名空间不会被任意名称(如“cmp”)污染,因为您传递了值。Int'sInt'simplicit
0赞 greenoldman 3/2/2012
谢谢你的补充,我不能再给你投票了,对不起:-)
0赞 Rotsor 3/5/2013
如果你使用的是 Haskell 术语,你也可以正确使用它。type 的值是类型类实例,而不是类型类Ordering[Int]
98赞 Daniel C. Sobral 3/3/2012 #5

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

另请注意,隐式不在平面命名空间中,这也是全局变量的常见问题。它们显式绑定到类型,因此也绑定到这些类型的包层次结构。

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

但是,我们不要止步于此。隐式与类型相关联,它们与类型一样是“全局”的。类型是全局的这一事实是否困扰着您?

至于用例,它们很多,但我们可以根据它们的历史做一个简短的回顾。最初,afaik,Scala 没有暗示。Scala 拥有的是视图类型,这是许多其他语言所具有的特性。今天我们仍然可以看到,每当你写类似的东西时,这意味着该类型可以被视为一个类型。视图类型是一种使自动强制转换可用于类型参数(泛型)的方法。T <% Ordered[T]TOrdered[T]

然后,Scala 用隐含物概括了该特性。自动转换不再存在,取而代之的是隐式转换 - 这些转换只是值,因此可以作为参数传递。从那时起,意味着隐式转换的值将作为参数传递。由于强制转换是自动的,因此函数的调用方不需要显式传递参数,因此这些参数成为隐式参数Function1T <% 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

后三者主要与集合一起使用,方法如 、 和 。一个广泛使用上下文边界的库是 Scalaz。maxsummap

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

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/3/2012
你正在写的那本书应该是英文的!:-)谢谢你的精彩帖子。
2赞 Chen OT 5/28/2015
为什么 SO 不为这样的答案提供星号选项?真的很棒的帖子!
4赞 samthebest 10/22/2013 #6

这很简单,只要记住:

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

例如

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
6赞 noam 12/3/2016 #7

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

与它们产生的问题相比,使用隐式(不需要显式写入参数或类型)的小好处是多余的。

我做了 15 年的开发人员,在过去的 1.5 年里一直在使用 scala。

我见过很多次错误,这些错误是由于开发人员没有意识到使用了隐式函数这一事实而引起的,并且特定函数实际上返回了指定的不同类型的类型。由于隐式转换。

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

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

艺术

import org.some.common.library._

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

由此引起的“错误”可能会在代码编写后很长一段时间内发生,以防受此转换影响的某些值最初未使用。

一旦遇到错误,找到原因就不是一件容易的事。 你必须做一些深入的调查。

即使你觉得自己是 scala 专家,一旦你发现了这个错误,并通过更改 import 语句来修复它,你实际上也浪费了很多宝贵的时间。

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

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

没有选项可以编译没有隐式的 scala(如果有,请纠正我),如果有选项,没有一个常见的社区 scala 库可以编译。

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

Scala 有许多很棒的功能,也有很多不是那么好。

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

评论

0赞 Akavall 4/1/2017
值得注意的是,Kotlin 摆脱了隐含:kotlinlang.org/docs/reference/comparison-to-scala.html
0赞 anshuman sharma 6/20/2017 #8

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

如果用于编写 Apache Spark 代码,则最适合 Scala。在 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)
}

}
}

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