Scala 中隐式参数的一个好例子?

到目前为止,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请张贴反例。我真的很好奇语言设计中的纯粹性; ——)。

23130 次浏览

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

如果没有隐式,您可能会一直传递这样的内容,这会使正常的使用变得非常麻烦。或者使用专门化程度较低的集合,这会很烦人,因为这意味着您会失去性能/能力。

当然。阿卡有一个很好的例子,关于它的演员。当您在 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 trait 中的隐式值定义所带来的自然用法即可。还有大约一百万个其他的例子。集合类是一个巨大的类。尝试在任何非平凡的 Scala 库中漫游,你会发现一卡车的东西。

隐式参数的另一个好的通用用法是使方法的返回类型依赖于传递给它的一些参数的类型。Jens 提到的一个很好的例子是集合框架和类似 map的方法,它的完整签名通常是:

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

请注意,返回类型 That是由编译器能够找到的最适合的 CanBuildFrom决定的。

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

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

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

只有在 A上有操作 <时才能明智地定义这些。因此,在不隐含的情况下,我们必须在每次使用这个函数时提供上下文 Ordering[B]。(或者放弃 max内部的类型静态检查,冒着发生运行时强制转换错误的风险。)

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

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

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

我想你是对的

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]) ,跟踪它们不会有太大的风险。

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

还要注意,在平面名称空间中隐含的是 没有,这也是全局的常见问题。它们显式地绑定到类型,因此绑定到这些类型的包层次结构。

因此,使用全局变量,使它们不可变并在声明站点初始化,并将它们放在名称空间中。它们看起来还像全球性的吗?他们看起来还有问题吗?

但我们不能就此止步。隐式 与类型相关联,它们与类型一样具有“全局性”。类型是全局的这一事实是否让你感到困扰?

至于用例,它们很多,但是我们可以根据它们的历史做一个简短的回顾。最初,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

隐式参数只是该模式的一种推广,使得可以传递 任何类型的隐式参数,而不仅仅是 Function1。然后才是它们的实际用途,后来才是 那些用途的句法糖。

其中之一是 上下文界限,用于实现 类型类型模式类型模式(模式,因为它不是一个内置特性,只是一种使用语言的方法,该语言提供类似于 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!
^

一个广泛使用这个特性的库是 Shapless。

我不认为阿卡图书馆的例子符合这四种类型中的任何一种,但这就是通用特性的全部意义: 人们可以以各种方式使用它,而不是由语言设计师规定的方式。

如果你喜欢被规定(比如说,Python 喜欢) ,那么 Scala 就不适合你。

很简单,记住:

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

例如:。

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

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

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

我是一个开发人员已经15年了,并且在过去的1.5年里一直在使用 scala。

我已经见过很多次由于开发人员没有意识到隐式被使用的事实而导致的 bug,并且一个特定的函数实际上返回一个指定的不同类型。由于隐式转换。

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

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

或:

import org.some.common.library._

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

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

一旦你遇到了这个问题,找到原因就不是一件容易的事情了。 你得做些深入调查。

尽管一旦发现了 bug,并通过更改 import 语句修复了它,您就会觉得自己是 Scala 方面的专家,但实际上您浪费了大量宝贵的时间。

我通常反对暗示的其他原因还有:

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

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

基于以上原因,我认为隐含是 Scala 语言使用的最糟糕的实践之一。

Scala 有很多很棒的特性,但也有很多不那么棒的。

当为一个新项目选择一种语言时,隐式是反对 scala 的原因之一,而不是支持 Scala 的原因之一。在我看来。

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

Scala 最适合用于编写 ApacheSpark 代码。在 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)
}


}
}

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