使用Scala 3进行显式项推理

2020-11-10 15:24:45

对于采用Scala3的开发人员来说,最显著的变化之一是引入了新的语法来取代以前Scala版本中使用的隐式机制。

新语法背后的动机是,相同的隐式关键字用于不同的目的和模式,因此它成为表达如何实现模式的一种方式。这意味着,当用户遇到这个模棱两可的咒语时,需要破译开发者的意图:这是一种转换吗?这是否避免了参数重复?这是类型的扩展吗?这是打字机吗?我怎么导入这个?

看到隐含含义在库和项目中变得如此普遍,Scala3的目标是通过使用传达开发人员意图的新关键字来减少困惑和认知负担。

本文通过分析最常见的用例和模式:扩展方法、隐式参数、隐式转换和类型类,简要介绍了Scala3程序员可以使用的新语法和语义。

虽然我们认为新的语法代表了一种改进,但需要强调的是,使用隐式的旧代码在Scala 3.0编译器中仍然完全有效,即使它将在未来的版本中被弃用。您不需要立即移植代码库,这可能是一个渐进的过程。

当您无法控制某个类型,但需要使用新方法扩展其功能时,Scala3允许您定义扩展方法。

假设您正在使用List[try[String]],并且经常需要检索计算成功的元素。

//ListTryOps.scala导入scala.util。{try,uccess}扩展名(ls:list[try[string]])def Collect成功:list[string]=ls。收集{案例成功(X)=>;x}

要记住语法,请注意集合成功是如何跟在它将在其上可用的对象之后的:ls.Collection tSucceede.Extension方法也可以有类型参数:

扩展名[A](ls:List[Try[A]]):def集合成功:List[A]=ls。收集{Case Success(X)=>;x}def getIndexOfFirstFailure:Option[Int]=ls。ZipWithIndex。找到((t,_)=>;t.。IsFailure)。地图(_.。(2)。

//Main.scala导入scala.util.Try//导入ListTryOps。_//使用通配符导入ListTryOps.ColltSucccessdef getLastTweet(用户名:字符串):try[String]=?@main def main=val niceTwets=list(getLastTweet(";Scala_lang&34;),getLastTweet(";odersky。集合成功打印(NiceTwets)。

在Scala3之前,这个模式的实现特别麻烦。典型的方法如下:

隐式类ListTryExtension[A](私有Val in:List[Try[A]])扩展AnyVal{def Collect Succcessed:List[A]=ls。收集{案例成功(A)=>;a}定义getIndexOfFirstFailure:OPTION[Int]=in。ZipWithIndex。查找{case(t,_)=>;t.。IsFailure}。MAP{case(_,index)=>;index}}。

请注意,您需要为类定义一个名称,即使除了IMPORT语句之外可能永远不会使用这个名称。您还需要了解AnyVal是什么,为什么扩展它是一种好的做法,以及它的限制是什么。

阅读这段代码时需要一些经验,才能理解它的唯一目标是添加几个方法来列出[try[A]]。

您可以在专门的文档页面上找到更多关于扩展方法的信息。我们还建议您阅读它们如何补充Scala 3的另一个新特性:不透明类型。在本文的后面,我们将看到它们如何简化一个非常常见的模式:类型类。

与其他编程语言类似,Scala允许您省略变量的类型,因为编译器可以执行类型推断。例如,我们可以写。

在本例中,我们声明了值,然后编译器推断出相应的类型。Scala的一个独特特性是,当指定类型时能够推断出值。

考虑一下标准库中的未来抽象。每次通过提供计算来创建Future时,都需要指定计算将在哪个ExecutionContext上求值:

导入scala.conflic._def阶乘(n:int):int=?Def Fibonacci(n:int):int=?@main def main=val Executor:ExecutionContext=ExecutionContext。全局Val fact100=Future(阶乘(100))(执行者)Val fibo100=Future(斐波纳奇(100))(执行者)//...。

如您所见,Executor的重复很快就变成了一项乏味的任务。我们可以声明此参数是当前上下文通用的,并避免其重复:

@main def main=给定的执行程序为ExecutionContext=ExecutionContext。全局Val fact100=未来(阶乘(100))Val fibo100=未来(Fibonacci(100))。

//scala.concurrent.Future.scala对象未来{//.。Def Apply[T](Body:=>;T)(使用Executor:ExecutionContext):Future[T]=//...}。

我们在这里看到了语法的另一半:如果我们将一个参数声明为USING,那么编译器将在调用点的当前作用域中搜索具有兼容类型的给定值。

另一个重要的方面是我们如何导入这些值。例如,如果您有多个文件中使用的ExecutionContext的更多相关定义,则可以将其重构为不同的文件:

您可以在文档中了解有关使用/给定以及解析规则的更多信息。

在Scala2中,这种模式是通过用IMPLICIT关键字标记值和参数来实现的。上一个示例如下所示:

//Main.scala导入scala.consist._Object Main扩展App隐式Val EC:ExecutionContext=ExecutionContext。Global Future(println(";Hello World&34;))}//scala.concurrent.Future.scala对象未来{//.。Def Apply[T](Body:=>;T)(隐式Executor:ExecutionContext):Future[T]=//...}。

我们再次注意到,我们必须提供一个可能不会用于变量的名称。Scala 2中的隐式函数在Scala 3中被重命名为call。

最后,特殊的导入语法允许用户按类型而不是按名称显式导入给定的实例。这更有意义,因为我们通常也用类型来指代它们。

隐式转换功能很危险。出于这个原因,在Scala3中,每次使用编译器时都会发出警告。您可以通过将功能显式导入当前范围来禁用警告,风险自负。

Java标准库提供了一个与Options非常相似的可选类型。如果您正在使用的Java库生成了大量具有此类型的对象,但您也有很多选项,那么您可能希望定义一个自动转换。

//OptionalConversion.scala导入java.util.OptionalConversion:给定[A]作为转换[Optional[A],Option[A]]:def Apply(in:Optional[A]):Option[A]=if in。如果有的话,就会有一些(在……中)。Get())否则无。

应通过添加IMPORT子句或通过设置编译器选项-LANGUAGE:IMPLICTICT CONVERSIONS来启用对象OptionalConversion中给定的_CONVERSION_OPTIONAL_OPTION隐式转换类。有关为何应显式启用该功能的讨论,请参阅Scala文档中的值scala.language.implitConversions。

IMPORT java.util.Optional Object OptionalConversion{IMPLICIT def optionalToOption[A](in:可选[A]):Option[A]=if(in.。存在的)一些(在.)。Get())否则无}。

请注意,乍一看,并不清楚该定义是否旨在自动转换为预期类型:

该方法的名称可能是一个提示,但这只是其他开发人员可能不同意的约定。

隐式def可能意在为您提供其参数类型的扩展方法,具体取决于其结果。

没有直接的方法来验证此函数是否不会用作具有隐式参数列表的函数的隐含参数。

该模式是Cats等Scala函数式编程库的基础。在Scala2中,我们过去严重依赖隐式转换来添加方法,依赖隐式参数来传播实例,这对于可能已经在函数编程中纠结于新概念的初学者来说有点神秘。

在Scala3中,由于有了新的语法,这个模式变得更加简单。让我们考虑一个简单的类型类,比如Show,它描述了类型具有字符串表示的能力。

然后,我们可以添加一个配套对象来提供两个辅助方法和实例,以减少创建实例的繁琐:

//Show.scala对象Show:def from[A](f:A=>;string):show[A]=new Show[A]:Extension(a:A)def show:String=f(A)给定[A:Show]as Show[List[A]]:Extension(ls:List[A])def show:String=ls。地图(_.。显示)。MkString(";,";)。

//Main.scala导入显示。{_,给定}案例类Mountain(名称:字符串,高度:int)给定Show[Mountain]=Show。从((m:Mountain)=>;s&34;${m.name}为${m.Height}米高&34;)@main def main=Val Mountain=list(Mountain(";Mont Blanc";,4808),Mountain(";Matterhorn";,4478))println(Mountain。(秀场)。

因为扩展方法是在特征中定义的,所以不需要额外的导入语句,这与Scala 2中过去发生的情况相反,您将在下面的小节中看到这一点。

在Scala2中,涉及到更多的样板代码。这一切都要从定义接口开始:

//ShowOps.scala对象ShowOps{IMPLICIT类showOps[A](in:A)扩展AnyVal{def show(IMPLICIT INSTANCE:SHOW[A]):String=Instance。显示(输入)}}。

//Show.scala Object Show{def from[A](f:A=>;string):show[A]=new Show[A]{def show(a:a):string=f(A)}//此选项或导入ShowOps并使用上下文绑定的隐式def showList[A](ls:list[A])(隐式实例:show[A])=ls。贴图(实例。显示)。MkList(";,";)}

//Main.scala导入Show._import ShowOps._Case类Mountain(Name:String,Height:int)Object Main扩展App{隐式Val Mountain Show:Show[Mountain]=Show。FromFunction((m:Mountain)=>;s";${m.name}是${m.Height}米高&34;)Val Mountain=list(Mountain(";Mont Blanc";,4808),Mountain(";Matterhon";,4478))println(Mountain。显示)}。

我们认为,这个例子展示了隐含如何被用来实现不同的目标,而在这样做的过程中,往往更令人困惑:

如果要提供隐式实例,请使用GISTEN声明该值现在可用。

如果您永远不会按名称指定值,请不要提供该值。

Scala3中与上下文抽象相关的另一个非常有趣但更高级的特性是上下文函数。

我们回顾了Scala 2中隐式的主要用例,并提供了它们在Scala 3中的外观。虽然最终结果几乎相同,但代码更加明确和可读性更好,这样您就可以专注于解决业务问题,而不是语法。

这是Scala 3的一系列更大的可用性和人体工程学改进的一部分,我们相信这些改进将使Scala 3使用起来更容易、更有趣,我们非常兴奋地看到社区将用它们创造什么。