【问题标题】:How can I create a DSL involving "blocks" where certain functions are in-scope?如何创建涉及某些功能在范围内的“块”的 DSL?
【发布时间】:2018-11-12 03:40:21
【问题描述】:

概述

我有一个基于 Kotlin 的项目,它定义了一个 DSL,但由于下面给出的原因,我现在正在研究用 Scala 编写我的项目是否更好。由于 Scala 似乎不适合使用 as much ease as in Kotlin 创建 DSL,我不完全确定如何在 Scala 中重新创建相同的 DSL。

在它被标记为 this question 的重复之前,我已经看过了,但我的 DSL 要求有些不同,我无法从中找出解决方案。

详情

我正在尝试创建一个基于流程的编程系统来开发自动车辆部件测试程序,在过去的几周里,我一直在 Kotlin 中测试它的实现,因为它似乎支持很多非常适合创建 FBP 系统的功能(原生协程支持、使用类型安全构建器轻松创建 DSL 等)。

尽管 Kotlin 非常棒,但我开始意识到,如果 FBP 的实现语言更实用,它会很有帮助,因为 FBP 似乎与函数式语言有很多共同点。特别是,能够定义和使用类型类对于这样的项目非常有用。

在 Kotlin 中,我创建了一个 DSL,它表示基于流的系统中节点之间的“胶水”语言。例如,假设存在两个黑盒进程 AddSquare,我可以定义一个“复合”节点,将两个数字的总和平方:

@CompositeNode
private fun CompositeOutputtingScalar<Int>.addAndSquare(x: Int, y: Int) {
    val add = create<Add>()
    val square = create<Square>()

    connect {
        input(x) to add.x
        input(y) to add.y
        add.output to square.input
        square.output to output
    }
}

这个想法是 connect 是一个采用 ConnectionContext.() -&gt; Unit 形式的 lambda 的函数,其中 ConnectionContext 定义了中缀函数 to 的各种重载(遮蔽了 Kotlin 中的内置 to 函数stdlib) 允许我定义这些进程(或节点)之间的连接。

这是我尝试在 Scala 中做类似的事情:

class OutputPort[-A] {
  def connectTo(inputPort: InputPort[A]) {}
}

class InputPort[+A]

object connect {
  val connections = new ListBuffer[Connection[_]]()

  case class Connection[A](outputPort: OutputPort[A], inputPort: InputPort[A])

  class ConnectionTracker() {
    def track[A](connection: Connection[A]) {}
  }

  // Cannot make `OutputPort.connectTo` directly return a `Connection[A]` 
  // without sacrificing covariance, so make an implicit wrapper class
  // that does this instead
  implicit class ExtendedPort[A](outputPort: OutputPort[A]) {
    def |>(inputPort: InputPort[A]): Unit = {
      outputPort connectTo inputPort
      connections += Connection(outputPort, inputPort)
    }
  }
}

def someCompositeFunction() {
  val output = new OutputPort[Int]
  val input = new InputPort[Int]
  output |> input // Should not be valid here

  connect {
    output |> input // Should be valid here
  }
}

现在这不会编译,因为ConnectablePort 不在范围内。我可以通过以下方式将其纳入范围:

import connect._
connect {
  output |> input // Should be valid here
}

但是,必须在节点定义中执行此操作是不可取的。

总而言之,我如何在 Scala 中重新创建我在 Kotlin 中制作的 DSL?作为参考,这是我定义 Kotlin DSL 的方式:

interface Composite {
    fun <U : ExecutableNode> create(id: String? = null): U
    fun connect(apply: ConnectionContext.() -> Unit)

    class ConnectionContext {
        val constants = mutableListOf<Constant<*>>()

        fun <T> input(parameter: T): OutputPort<T> = error("Should not actually be invoked after annotation processing")
        fun <T> input(parameterPort: OutputPort<T>) = parameterPort
        fun <T> constant(value: T) = Constant(value.toString(), value)

        infix fun <U, V> U.to(input: InputPort<V>): Nothing = error("Cannot connect value to specified input")
        infix fun <U> OutputPort<U>.to(input: InputPort<U>) = this join input
        infix fun <T, U> T.to(other: U): Nothing = error("Invalid connection")
    }
}

interface CompositeOutputtingScalar<T> : Composite {
    val output: InputPort<T>
}

interface CompositeOutputtingCluster<T : Cluster> : Composite {
    fun <TProperty> output(output: T.() -> TProperty): InputPort<TProperty>
}

【问题讨论】:

标签: scala dsl


【解决方案1】:

如果您使用伴生对象,在 Scala 中打开 |&gt; 非常简单,并且输出端口始终可用

class OutputPort[-A] {
  def connectTo(inputPort: InputPort[A]):Unit = {}
}

class InputPort[+A]

object OutputPort{
    implicit class ConnectablePort[A](outputPort: OutputPort[A]) {
      def |>(inputPort: InputPort[A]): Unit = outputPort connectTo inputPort
    }
}

def someCompositeFunction() {
  val output = new OutputPort[Int]
  val input = new InputPort[Int]
  output |> input // Should be valid here
}

明智地决定在哪里进行导入是 Scala 的核心概念。这是我们在代码中打开implicit 的方式,如下所示,非常常见,因为这是我们打开类型类的方式。

class OutputPort[-A] {
  def connectTo(inputPort: InputPort[A]): Unit = {}
}

class InputPort[+A]

object Converter {
  implicit class ConnectablePort[A](outputPort: OutputPort[A]) {
    def |>(inputPort: InputPort[A]): Unit = outputPort connectTo inputPort
  }
}

def someCompositeFunction() {
  val output = new OutputPort[Int]
  val input = new InputPort[Int]
  import Converter._
  output |> input // Should be valid here
}

现在,我认为这就是您要查找的内容,但仍有一些 importimplicit 需要设置,但这将包含 implicit 行为:

class OutputPort[-A] {
    def connectTo(inputPort: InputPort[A]): Unit = {}
}

class InputPort[+A]

object Converter {

    private class ConnectablePort[A](outputPort: OutputPort[A]) {
        def |>(inputPort: InputPort[A]): Unit = outputPort connectTo
            inputPort
    }

    def convert[A](f: (OutputPort[A] => ConnectablePort[A]) => Unit): Unit = {
        def connectablePortWrapper(x: OutputPort[A]): ConnectablePort[A] = new ConnectablePort[A](x)

        f(connectablePortWrapper _)
    }
}

object MyRunner extends App {

    val output = new OutputPort[Int]
    val input = new InputPort[Int]

    import Converter.convert

    //output |> input  won't work
    convert[Int] { implicit wrapper =>
        output |> input // Should be valid here
    }
}

【讨论】:

  • 赞成投入的努力和有用的提示,但与 Kotlin 等效项相比,底部的 DSL 看起来不太好 - 它涉及导入语句和未使用的隐式参数。此外,我连接连接的块不应该被参数化,因为这个想法是只有两个端口之间每个单独连接的类型需要匹配,而不是块中定义的每个连接。我正在尝试尽可能接近addAndSquare 示例。
  • 你说得对,我可以使用伴随对象使|&gt; 始终可用 - 我已经用一个不太人为的例子更新了我的帖子,其中|&gt; 不仅仅是一个替代品connectTo,因此为什么它可能是我只想在特定范围内可用的东西。就其价值而言,Akka DSL 似乎正在做与我所追求的类似的事情,但我仍在努力了解他们是如何做到的。
  • Akka DSL 和 Akka Streams DSL 将是查找您正在寻找的示例的好方法。 :)
  • 啊,是的,不知道 Akka Streams。尽管(仅从第一印象来看)他们的 DSL 比 Kotlin 中的 DSL 略丑,所以我想底线是我在 Kotlin 中制作的 DSL 可能无法在 Scala 中完全重现,至少作为嵌入式 DSL。至少现在发现这一点是件好事。
  • @TagC 据我了解,Kotlin 没有类型类。 github.com/Kotlin/KEEP/pull/87 并且在 Scala 中拥有这种能力非常棒。我的猜测是,如果 Kotlin 需要让 TypeClasses 成为一种东西,它必须看起来像 Scala,我们可以控制 importimplicit,或者在 Kotlin 的情况下是 extension。但是由于我不是 Kotlin 的专业人士,所以我是出于对 Kotlin 的无知而回答的。 ;)
猜你喜欢
  • 2023-02-23
  • 2011-06-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-06-15
  • 2019-07-03
  • 2020-07-06
  • 1970-01-01
相关资源
最近更新 更多