【问题标题】:Functions / methods in Scala. How does this work?Scala 中的函数/方法。这是如何运作的?
【发布时间】:2019-02-11 13:34:21
【问题描述】:

我是 Scala 新手,很难理解声明和使用函数的所有方式。有人可以逐步解释这里发生了什么吗?

我正在学习一门介绍 Akka HTTP 的课程。代码有效,但是路由方法看不懂:

import akka.http.scaladsl.server.Directives._

def route = path("hello") {
    get {
      complete("Hello, World!")
    }
  }

我们正在定义一个方法route,它被声明为path 的值(从上面的行导入),但是在path 函数中我们有一个我不明白的叫做get 的东西.

当我将path 声明为一种方法时,我是在覆盖它,还是发生了什么?

如果有人可以逐行解释发生了什么,我会很高兴。并且不要介意它涉及 Akka。我想了解 Scala 语法。

================================================ ==================

感谢所有出色的答案。我想我明白了!

所以总结一下我的版本。

path() 是一个需要字符串的函数。它返回另一个需要Directive 的函数。而在 Scala 术语中,我们可以做某种柯里化来直接向返回的函数发送指令。

因此,块 {} 中的所有内容都会发送到 path() 返回的函数。由于 Scala 中的块总是返回最后一行,因此我们返回 get,这与我们使用 complete 调用的原理相同。

get 也是一个函数,它接受一个参数并且可以写成一个块。这相当于只写get(complete("Hello, world"))

再次感谢!

【问题讨论】:

  • 你能澄清你的问题吗?您询问声明函数,但您没有在发布的示例中声明任何函数。

标签: scala function akka-http


【解决方案1】:

您不一定需要了解此答案中的所有内容才能有效地使用 akka-http,但我向您保证,有时(可能迟早)您将与编译器发生争执,并且只想要所有花哨的语法糖要消失了,好消息是有工具可以做到这一点(坏消息是,一旦你摆脱了花哨的语​​法,现实可能会变得一团糟)。

首先要注意的是,虽然这里的花括号可能看起来很像 Java 或其他语言的范围或定义分隔符,但它们实际上只是将方法应用于参数。你可以用括号做同样的事情:

scala> import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Directives._

scala> val route = path("hello")(get(complete("Hello, World!")))
route: akka.http.scaladsl.server.Route = ...

虽然这些 getcomplete 可能看起来像关键字或其他东西,但它们实际上只是 Directives 上的静态方法(大约 - 阅读全文以获得完整故事),因此以下内容也是等效:

scala> import akka.http.scaladsl.server.Directives
import akka.http.scaladsl.server.Directives

scala> val route = Directives.path("hello")(
     |   Directives.get(Directives.complete("Hello, World!"))
     | )
route: akka.http.scaladsl.server.Route = ...

这有望解释一些语法,但这里仍然有很多不可见的东西。如果你在 REPL 中,你可以使用 scala-reflect 的 reify 作为一个非常有用的工具来帮助使这些东西可见。

从一个简单(不相关的)示例开始,您可能想知道当您看到像 "a" * 3 这样的 Scala 代码时发生了什么,尤其是如果您知道 Java 字符串没有 * 运算符,因此您打开了 REPL :

scala> import scala.reflect.runtime.universe.reify
import scala.reflect.runtime.universe.reify

scala> reify("a" * 3).tree
res6: reflect.runtime.universe.Tree = Predef.augmentString("a").$times(3)

还有一个脱糖版本,显示了应用于字符串的隐式方法,以赋予它* 运算符。

在你的情况下,你可以这样写:

scala> import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Directives._

scala> import scala.reflect.runtime.universe.reify
import scala.reflect.runtime.universe.reify

scala> reify(path("hello")(get(complete("Hello, World!")))).tree
res0: reflect.runtime.universe.Tree = Directive.addByNameNullaryApply(Directives.path(Directives._segmentStringToPathMatcher("hello"))).apply(Directive.addByNameNullaryApply(Directives.get).apply(Directives.complete(ToResponseMarshallable.apply("Hello, World!")(Marshaller.liftMarshaller(Marshaller.StringMarshaller)))))

我们可以重新格式化具体化的表达式以提高可读性:

Directive.addByNameNullaryApply(
  Directives.path(
    Directives._segmentStringToPathMatcher("hello")
  )
).apply(
  Directive.addByNameNullaryApply(Directives.get).apply(
    Directives.complete(
      ToResponseMarshallable.apply("Hello, World!")(
        Marshaller.liftMarshaller(Marshaller.StringMarshaller)
      )
    )
  )
)

如果添加几个导入,这也是完全合法的 Scala 代码:

scala> import akka.http.scaladsl.server.{ Directive, Directives }
import akka.http.scaladsl.server.{Directive, Directives}

scala> import akka.http.scaladsl.marshalling.{ Marshaller, ToResponseMarshaller }
import akka.http.scaladsl.marshalling.{Marshaller, ToResponseMarshaller}

scala> val route = Directive.addByNameNullaryApply(
     |   Directives.path(
     |     Directives._segmentStringToPathMatcher("hello")
     |   )
     | ).apply(
     |   Directive.addByNameNullaryApply(Directives.get).apply(
     |     Directives.complete(
     |       ToResponseMarshallable.apply("Hello, World!")(
     |         Marshaller.liftMarshaller(Marshaller.StringMarshaller)
     |       )
     |     )
     |   )
     | )
route: akka.http.scaladsl.server.Route = ...

要逐步解释这一点,我们可以从path("hello")开始。我们可以从the API docs 看到Directives.path 不使用字符串,而是使用PathMatcher,因此我们知道从StringPathMatcher 的隐式转换正在启动,并且在我们完全脱糖的版本中,我们可以在这里看到:

  Directives.path(
    Directives._segmentStringToPathMatcher("hello")
  )

如果我们查看文档,可以肯定,_segmentStringToPathMatcher 是适当类型的隐式转换。

complete("Hello, World!") 也发生了类似的事情。 Directives.complete 采用 ToMarshallableResponse,而不是 String,因此必须进行隐式转换。在这种情况下,它是 ToResponseMarshallable.apply,它还需要隐式 Marshaller 实例,在这种情况下它通过从ToEntityMarshallerToResponseMarshallable 的隐式转换,其中ToEntityMarshaller 实例为Marshaller.StringMarshaller,转换器为Marshaller.liftMarshaller 部分:

    Directives.complete(
      ToResponseMarshallable.apply("Hello, World!")(
        Marshaller.liftMarshaller(Marshaller.StringMarshaller)
      )
    )

还记得上面我说过get 只是Directives 上的静态方法吗?这是一个谎言,虽然它是Directives 上的静态方法,但我们在编写get(...) 时并没有调用它。相反,这个get 实际上是一个返回Directive0 的无参数方法。 Directive0Directive[Unit] 的类型别名,虽然Directive[Unit] 没有apply 方法,但它可以通过Directive 上的addByNameNullaryApply 方法隐式转换为可以使用的东西。因此,当您编写get(...) 时,Scala 会将其脱糖为get.apply(...),然后将get 值转换为Route => Route 函数,该函数具有适当的apply 方法。 path("hello")(...) 部分也发生了完全相同的事情。

这种事情可能看起来像是一场噩梦,作为一个长期使用 Scala 的用户,我可以告诉你,它肯定经常发生。不过,reify 和 API 文档之类的工具可以让它变得不那么可怕。

【讨论】:

    【解决方案2】:

    在你的sn-ps里面有Scala语言和编译器的几个特性,我分析一下我知道的:

    def route = ...
    

    定义一个不带参数的函数,其结果类型由函数体的返回值决定。

    path("hello") {
      ...
    }
    

    我不熟悉 path 函数本身,但似乎该 sn-p 中发生了三件事:

    我不想花时间描述所有这些,因为互联网上有很多资源可以很好地解释它们。但我想至少链接this great introductory article,这在我早期对我有很大帮助。

    链接的文章向您展示了一个完整的示例,说明如何使用所有三个功能来构建您自己的控制结构,就像您正在使用的代码之一。

    继续前进

    get {
      ...  
    }
    

    同样是上述几点的应用,但这次没有柯里化,所以花括号是函数的唯一参数。

    complete("Hello, World!")
    

    只是一个普通的旧函数调用。

    简而言之,该代码使用了一些“技巧”,将函数调用转换为类似于特殊语言结构的东西,这会给初学者带来困惑。

    这种技术经常用于在 Scala 中编写 Domani-Specific Languages (DSL)。


    【讨论】:

      【解决方案3】:

      这里发生了很多事情,这是一个理解 scala 的非常复杂的示例。但我会试试的。

      route 的类型是Route,这是一个定义为type Route = RequestContext ⇒ Future[RouteResult] 的类型别名,其中RequestContext ⇒ Future[RouteResult] 是一个使用RequestContext 并产生Future[RouteResult] 的函数。

      path 是一种创建Directive[Unit] 的方法。有一个隐式转换将Directive[Unit] 转换为函数Route => Route(简化)。可以通过方法apply 或使用编译器糖(???){???} 调用函数。

      get 也是一种创建Directive[Unit] 的方法,类似的方法也适用于它。

      completeStandardRoute 类型,它扩展了 Route

      知道了这一切,我们可以丑化你的例子,写成

      path("hello").apply { ctx =>
          val inner: Route = { ctx =>
            ctx.complete("done")
          }
          get.apply(inner).apply(ctx)
      }
      

      【讨论】:

        【解决方案4】:

        如果您看到这样的代码 sn-p 可能会有所帮助:

        导入 akka.http.scaladsl.server.Directives._

        def route: Route = path("hello") {
            get {
              complete("Hello, World!")
            }
          }
        

        我添加了Route 类型,以表明您只是使用 Akka HTTP 提供的语法构建路由,该语法允许您在更高级别定义通用匹配条件,并将特定条件嵌套在该部分中。在这里,您正在使用 Akka HTTP 中的路由 DSL 功能。路由 DSL 带来了一些隐含的作用。使用 path 方法可确保您能够处理进入主机的路径 host/hello 的请求,这意味着您的主机现在可以处理路径 /hello 的获取请求。 path 指令内的代码主体表示额外的匹配条件,以检查我们何时有正确的路径匹配。完整的方法知道如何转换为HttpResponse。在这里,您将使用纯文本“hello world”完成。

        您可能有其他 HTTP 方法标准请求,如 post、put、delete,视具体情况而定。甚至是自定义 HTTP 方法。

        这是一个方便的 DSL,用于在 Akka-HTTP 中处理 HTTP 请求。检查 Akka-HTTP 文档 here

        【讨论】:

          猜你喜欢
          • 2018-03-25
          • 2023-03-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-12-31
          • 1970-01-01
          相关资源
          最近更新 更多