如何在某些功能范围内创建包含“块”的DSL?

时间:2018-11-11 11:09:34

标签: scala dsl

概述

我有一个基于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.() -> Unit形式的lambda函数,其中ConnectionContext定义了infix函数to的各种重载(隐藏了在Kotlin stdlib的to函数中),允许我定义这些进程(或节点)之间的连接。

这是我在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>
}

1 个答案:

答案 0 :(得分:1)

在Scala中,如果您使用伴随对象,只需打开|>就很简单了,并且在输出端口上总是可用的

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的行为:< / p>

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
    }
}