提问人:greenoldman 提问时间:3/2/2012 最后编辑:greenoldman 更新时间:11/29/2018 访问量:23255
Scala 中隐式参数的好例子?[关闭]
Good example of implicit parameter in Scala? [closed]
问:
到目前为止,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
答:
隐式参数在集合 API 中被大量使用。许多函数都具有隐式 CanBuildFrom,这可确保获得“最佳”结果集合实现。
如果没有隐式,你要么一直传递这样的东西,这将使正常使用变得很麻烦。或者使用不太专业的集合,这会很烦人,因为这意味着你会失去性能/功率。
确定。Akka在演员方面有一个很好的例子。当你在一个 Actor 的方法中时,你可能想要向另一个 Actor 发送一条消息。执行此操作时,Akka 会(默认)捆绑当前 Actor 作为消息,如下所示:receive
sender
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 库中徘徊,你会发现一大堆。
评论
Traversable.max
implicits
隐式参数的另一个很好的一般用法是使方法的返回类型取决于传递给它的某些参数的类型。Jens 提到的一个很好的例子是集合框架,以及像 这样的方法,其完整签名通常是:map
def map[B, That](f: (A) ⇒ B)(implicit bf: CanBuildFrom[GenSeq[A], B, That]): That
请注意,返回类型由编译器可以找到的最佳拟合确定。That
CanBuildFrom
有关此示例的另一个示例,请参阅该答案。在那里,方法的返回类型是根据某个隐式参数类型()确定的。Arithmetic.apply
BiConverter
评论
List
Set
map
一个例子是 上的比较操作。例如 或:Traversable[A]
max
sort
def max[B >: A](implicit cmp: Ordering[B]) : A
只有当对 进行操作时,才能合理地定义这些。因此,如果没有隐式,我们每次想使用这个函数时都必须提供上下文。(或者放弃内部的类型静态检查,并冒着运行时强制转换错误的风险。<
A
Ordering[B]
max
但是,如果隐式比较类型类在范围内,例如一些,我们可以立即使用它,或者通过为隐式参数提供一些其他值来简单地更改比较方法。Ordering[Int]
当然,隐式可能会被遮蔽,因此在某些情况下,范围内的实际隐式可能不够清晰。对于简单的用法,或者 可能确实有足够的固定顺序并使用一些语法来检查此特征是否可用。但这意味着不可能有附加特征,每段代码都必须使用最初定义的特征。max
sort
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.
它可能闻起来有腐烂和邪恶的全局变量的味道。然而,关键的一点是,在作用域中,每种类型可能只有一个隐式变量。你用两个 s 的例子是行不通的。Int
此外,这意味着实际上,仅当某个类型存在不一定唯一但不同的主实例时,才会使用隐式变量。演员的参考就是这种事情的一个很好的例子。类型类示例是另一个示例。任何类型都可能有几十种代数比较,但有一种是特殊的。
(在另一个层面上,代码本身的实际行号也可能是一个很好的隐式变量,只要它使用一个非常独特的类型。self
您通常不会将 s 用于日常类型。对于特殊类型(如),隐藏它们的风险不会太大。implicit
Ordering[Int]
评论
Int
Int's
Int's
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
其中之一是 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。max
sum
map
另一个常见用法是减少必须共享公共参数的操作的样板。例如,事务:
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 不适合你。
评论
这很简单,只要记住:
- 将要传入的变量也声明为隐式变量
- 在单独的 () 中声明非隐式参数之后的所有隐式参数
例如
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。
我见过很多次错误,这些错误是由于开发人员没有意识到使用了隐式函数这一事实而引起的,并且特定函数实际上返回了指定的不同类型的类型。由于隐式转换。
我还听到有人说,如果你不喜欢暗示,就不要使用它们。 这在现实世界中是不切实际的,因为很多时候使用外部库,而且其中很多都使用隐含,所以你的代码使用隐含,你可能没有意识到这一点。 您可以编写具有以下任一功能的代码:
import org.some.common.library.{TypeA, TypeB}
艺术
import org.some.common.library._
这两个代码都将编译并运行。 但它们并不总是产生相同的结果,因为第二个版本导入了隐式转换,这将使代码的行为不同。
由此引起的“错误”可能会在代码编写后很长一段时间内发生,以防受此转换影响的某些值最初未使用。
一旦遇到错误,找到原因就不是一件容易的事。 你必须做一些深入的调查。
即使你觉得自己是 scala 专家,一旦你发现了这个错误,并通过更改 import 语句来修复它,你实际上也浪费了很多宝贵的时间。
我通常反对隐式的其他原因是:
- 它们使代码难以理解(代码较少,但你不知道他在做什么)
- 编译时间。使用隐式时,Scala 代码的编译速度要慢得多。
- 在实践中,它将语言从静态类型更改为动态类型。的确,一旦遵循非常严格的编码准则,您就可以避免这种情况,但在现实世界中,情况并非总是如此。即使使用 IDE“删除未使用的导入”,也可能导致代码仍可编译和运行,但与删除“未使用”导入之前不同。
没有选项可以编译没有隐式的 scala(如果有,请纠正我),如果有选项,没有一个常见的社区 scala 库可以编译。
由于上述所有原因,我认为隐式是 scala 语言使用的最糟糕的做法之一。
Scala 有许多很棒的功能,也有很多不是那么好。
在为新项目选择语言时,隐式是反对 scala 的原因之一,而不是赞成它。在我看来。
评论
我对这篇文章的评论有点晚了,但我最近开始学习 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)
}
}
}
正如我们在上面的代码中看到的,我的抽象类中有两个隐式对象,并且我已将这两个隐式变量作为函数/方法/定义隐式参数传递。 我认为这是我们可以用隐式变量的使用来描述的最佳用例。
评论