Scala中隐式参数的好例子?

gre*_*man 73 parameters scala implicit

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

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

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

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

更新

全局变量

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

max(x : Int,y : Int) : Int
Run Code Online (Sandbox Code Playgroud)

你叫它

max(5,6);
Run Code Online (Sandbox Code Playgroud)

你可以(!)这样做:

max(x:5,y:6);
Run Code Online (Sandbox Code Playgroud)

但在我眼中的implicits作用是这样的:

x = 5;
y = 6;
max()
Run Code Online (Sandbox Code Playgroud)

它与这样的构造(类似PHP)没有太大的不同

max() : Int
{
  global x : Int;
  global y : Int;
  ...
}
Run Code Online (Sandbox Code Playgroud)

德里克的回答

这是一个很好的例子,但如果你能想到不使用发送消息的灵活使用implicit请发一个反例.我对语言设计的纯度非常好奇;-).

Dan*_*ral 97

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

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

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

但是,我们不止于此.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
Run Code Online (Sandbox Code Playgroud)

隐式参数只是该模式的一般化,使得传递任何类型的隐式参数成为可能,而不仅仅是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
Run Code Online (Sandbox Code Playgroud)

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

new Array[Int](size)
Run Code Online (Sandbox Code Playgroud)

它使用类清单的上下文绑定来启用此类数组初始化.我们可以看到这个例子:

def f[T](size: Int) = new Array[T](size) // won't compile!
Run Code Online (Sandbox Code Playgroud)

你可以像这样写:

def f[T: ClassManifest](size: Int) = new Array[T](size)
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

后三者主要用于集合,方法如max,summap.一个广泛使用上下文边界的库是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)
}
Run Code Online (Sandbox Code Playgroud)

然后将其简化为:

withTransaction { implicit txn =>
  op1(data)
  op2(data)
  op3(data)
}
Run Code Online (Sandbox Code Playgroud)

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

我能想到的第三个常见用法是提供有关正在传递的类型的证据,这使得在编译时检测会导致运行时异常的事情成为可能.例如,请参阅以下定义Option:

def flatten[B](implicit ev: A <:< Option[B]): Option[B]
Run Code Online (Sandbox Code Playgroud)

这使得这成为可能:

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!
                        ^
Run Code Online (Sandbox Code Playgroud)

一个广泛使用该功能的库是Shapeless.

我不认为Akka库的例子适用于这四个类别中的任何一个,但这是通用特征的全部要点:人们可以以各种方式使用它,而不是语言设计者规定的方式.

如果你喜欢被处方(比方说,像Python那样),那么Scala就不适合你了.

  • 你正在写的那本书应该是英文的!:-)谢谢你的精彩帖子. (6认同)
  • 为什么SO不为这样的答案提供星级选项?真的很棒的帖子! (2认同)

Der*_*att 23

当然.就其演员而言,Akka有一个很好的例子.当你在Actor的receive方法中时,你可能想要向另一个Actor发送消息.当你这样做时,Akka会捆绑(默认情况下)当前的Actor作为sender消息,如下所示:

trait ScalaActorRef { this: ActorRef =>
  ...

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

  ...
}
Run Code Online (Sandbox Code Playgroud)

sender是隐含的.在Actor中有一个看起来像这样的定义:

trait Actor {
  ...

  implicit val self = context.self

  ...
}
Run Code Online (Sandbox Code Playgroud)

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

someOtherActor ! SomeMessage
Run Code Online (Sandbox Code Playgroud)

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

someOtherActor.!(SomeMessage)(self)
Run Code Online (Sandbox Code Playgroud)

要么

someOtherActor.!(SomeMessage)(null)
Run Code Online (Sandbox Code Playgroud)

要么

someOtherActor.!(SomeMessage)(anotherActorAltogether)
Run Code Online (Sandbox Code Playgroud)

但通常你不这样做.您只需保留Actor特征中隐式值定义即可实现的自然使用.还有大约一百万个其他例子.集合类是一个巨大的类.尝试在任何非平凡的Scala库中闲逛,你会找到一辆卡车.


Deb*_*ski 9

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

def max[B >: A](implicit cmp: Ordering[B]) : A
Run Code Online (Sandbox Code Playgroud)

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

但是,如果隐式比较类型类在范围内,例如一些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.
Run Code Online (Sandbox Code Playgroud)

它可能闻到腐败和邪恶的全局变量.然而,关键的一点是,范围内每种类型可能只有一个隐式变量.你的两个Ints的例子不会起作用.

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

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

  • 当然,这是可能的.但这也意味着无论何时需要,都无法添加其他特征,例如"Int"或任何其他预定义类型.(一种经常被引用的例子是这可能不是在诠释的原始特征,也没有对字符串的半组 - 既不将有可能在一个固定的形式添加该性状.)的问题是:没有办法在所有可能的特征上推广一种类型.这些始终是代码(类型注释),必须临时给出或者您失去类型安全性.隐式变量只是减少了样板代码. (2认同)

noa*_*oam 6

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

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

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

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

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

import org.some.common.library.{TypeA, TypeB}
Run Code Online (Sandbox Code Playgroud)

要么:

import org.some.common.library._
Run Code Online (Sandbox Code Playgroud)

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

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

遇到错误后,找到原因就不容易了。您必须进行一些深入的调查。

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

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

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

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

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

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

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