Как я могу создать DSL, включающий «блоки», в которые входят определенные функции? - PullRequest
0 голосов
/ 11 ноября 2018

Обзор

У меня есть проект на основе Kotlin, который определяет DSL, но по причинам, указанным ниже, я сейчас выясняю, лучше ли было бы написать мой проект в Scala. Поскольку Scala, похоже, не позволяет создавать DSL с с такой же легкостью, как в Kotlin , я не совсем уверен, как воссоздать тот же DSL в Scala.

До того, как это будет помечено как дубликат этого вопроса , я посмотрел на него, но мои требования к DSL несколько отличаются, и я не смог найти решение из этого.

Подробнее

Я пытаюсь создать систему основанного на потоке программирования для разработки автоматизированных процедур проверки деталей транспортных средств, и в течение последних нескольких недель я тестировал реализацию этого в Котлине, поскольку, похоже, он много поддерживает функций, которые действительно хороши для создания систем FBP (встроенная поддержка сопрограмм, простое создание DSL с использованием безопасных типов типов и т. д.).

Как бы ни был хорош Kotlin, я начинаю понимать, что было бы очень полезно, если бы язык реализации для FBP был более функциональным, поскольку, похоже, FBP имеет много общего с функциональными языками. В частности, возможность определять и использовать классы типов будет очень полезна для такого проекта.

В Kotlin я создал DSL, представляющий «склеенный» язык между узлами в системе на основе потоков. Например, учитывая существование двух процессов черного ящика Add и Square, я могу определить «составной» узел, который возводит в квадрат сумму двух чисел:

@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, где ConnectionContext определяет различные перегрузки инфиксной функции to (скрывая встроенную функцию to в Kotlin 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
}

Однако нежелательно делать это в пределах определения узла.

Подводя итог, как я могу воссоздать DSL, который я сделал в Kotlin в Scala? Для справки, вот как я определил свой DSL Kotlin:

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 голосов
/ 11 ноября 2018

Просто включить |> довольно просто в 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
}

Теперь, я думаю, это то, что вы ищете, но все еще есть некоторые import и implicit, которые необходимо настроить, но это будет включать в себя поведение 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
    }
}
...