доработка движка для компиляции функций произвольной арности

В данной статье описывается подход автора к решению конкретной технической задачи. Автор не утверждает, что предложенное решение является единственно возможным, оптимальным или наиболее элегантным. Это лишь один из вариантов реализации, представленный в ознакомительных целях, а не руководство к обязательному применению.
Целепологание:
В двнной статье научим двигло собирать произвольные лямбды с возращаемым типом Any и параметрами, например: () => Any, (Any) => Any, (Any, Any) => Any и т.д.
В предыдущей статье в этой секции упоминалось, что одна из целей - создание произвольных Лямбд, однако цели достигнуто не было и собираются только лямбд(ы) типа Map[String, Any] => Any.
Редактура API:
Первая проблема заключается в том, что AST, несмотря на иерархическую структуру, не гарантирует определённого порядка узлов, находящихся в разных ветвях дерева. Это критично для потенциального пользователя, поскольку лямбда-функция может иметь типизированные параметры, порядок которых должен соответствовать порядку аргументов при её вызове.
Под соответствие новых требований к API, заредачимParamNode.
// Старое API параметров
final case class ParamNode(name: String) extends AstNode
// Новое API параметров
final case class ParamNode(name: String, ordered: Int = 0) extends AstNodeДля решения этой проблемы добавим в ParamNode порядковый номер параметра ordered со значением по умолчанию 0.
Такой подход сохраняет обратную совместимость API: существующий код продолжит работать без изменений. Если пользователь не укажет порядок явно, все параметры получат одинаковый номер 0, и их относительный порядок останется неопределённым. Если же порядок имеет значение, пользователь сможет задать его самостоятельно.
Поскольку вводим несколько режимов работы и настроек для кодогенерации, создадим конфиг класс.
И остаётся только подправить старые классы,
которые ранее были залочены на работу с Map[String, Any] => Any.
// Компилирует исходный код Scala и возвращает экземпляр сгенерированного класса
// в типе T, заданном пользователем.
//
// Тип T позволяет использовать результат как функцию с нужной сигнатурой,
// например (Int, Int) => Int или Map[String, Any] => Any.
// Соответствие T фактическому типу скомпилированного объекта
// проверяется только во время выполнения.
object Compiler {
def apply[T](code: String): T = {
val clazz = ScalaCompiler.compile(
source = code,
className = "pico.calc.framework.generated.Formula"
)
clazz
.getDeclaredConstructor()
.newInstance()
.asInstanceOf[T]
}
}
// Вместо фиксированного типа Map[String, Any] => Any
// возвращаем обобщённый тип T, позволяя пользователю самостоятельно
// определить ожидаемую сигнатуру скомпилированной функции.
//
// Конфигурация передаётся через using, чтобы не изменять существующий
// способ вызова API и сохранить возможность задавать настройки
// неявно в контексте вызова.
//
// По мере развития компилятора конфигурация будет расширяться
// дополнительными параметрами.
object ExpressionCompiler {
def apply[T](
literals: Set[LiteralSpec],
functions: Set[FunctionSpec],
constants: Set[ConstantSpec]
)(ast: AstNode)(using config: Config = Config()): T = {
val literalDispatcher = LiteralDispatcher(literals)
val functionDispatcher = FunctionDispatcher(functions)
val constantDispatcher = ConstantDispatcher(constants)
val helpers = CodeGenHelper(
literalDispatcher,
functionDispatcher,
constantDispatcher
)
val source = ScalaCodeGenerator(helpers, config)(ast)
println(source)
Compiler[T](source)
}
}Кодогенерация:
Сначала допишем вспомогательные функции в CodeGenHelper для работы с узлами параметров произвольной арности:
object CodeGenHelper:
// Возращает все узлы парметров из ast дерева.
def getAllParamNodes(ast: AstNode): List[ParamNode] =
ast match
case FunctionNode(_, args) =>
args.iterator
.flatMap(getAllParamNodes)
.toList
case n: ParamNode =>
List(n)
case _ =>
List.empty
// Возращает все узлы парметров из ast дерева
// упорядочено соответсвенно их ordered
def getAllOrderParam(ast: AstNode): List[ParamNode] =
getAllParamNodes(ast).sortBy(_.ordered)Правим кодогенерирующий класс ScalaCodeGenerator и изменяем то, как получаем параметры в сгенеренном коде:
// Старый вариант
case ParamNode(name) =>
f"params(\"${name}\")"
// Новый, зависящий от конфига
case (ParamNode(name, _), Config(MapBased)) =>
f"params(\"${name}\")" // аналогично старому варианту получаем из Map-ы
case (ParamNode(name, _), Config(Positional)) =>
name // в новом сразу обращаемся по имени Теперь надо подправить то, как формируется код класса целиком:
// Класс кодогенерации
object ScalaCodeGenerator:
def apply(
helpers: CodeGenHelper.Helpers,
config: Config
): AstNode => String =
def generateExpression(ast: AstNode): String = ...
ast =>
...
// формирует сигнатуру
val signature: String = if (config.signature == Positional) {
// формируем сигнатру с арностью от количества параметров в AST
// и в заданной последовательности
// (), (Any), (Any, Any), ...
f"(${CodeGenHelper.getAllParamNodes(ast).indices.map(_ => "Any").mkString(", ")})"
} else {
// старый вариант где на вход принимается map
"Map[String, Any]"
}
val applyParameters: String = if (config.signature == Positional) {
// формируем имена параметров и их типы соответственно
// в последовательности заданной пользователем посредством параметра ordered
// (), (x: Any), (x: Any, y: Any)
CodeGenHelper.getAllOrderParam(ast).map(p => s"${p.name}: Any").mkString(", ")
} else {
// старый вариант где на вход принимается map
"params: Map[String, Any]"
}
val body = generateExpression(ast)
s"""package pico.calc.framework.generated
|$imports
|
|class Formula extends ($signature => Any):
| $literalVals
| $functionVals
| $constantVals
|
| override def apply($applyParameters): Any =
| $body
|""".stripMargin
Заключение:
На данном этапе движок позволяет собирать достаточно произвольные лямбда функции, что увеличивает удобство и количество вариантов его использования.
object Example2 extends App {
implicit val config: Config = Config(Positional)
val compile1 = ExpressionCompiler[Int => Any](literals, functions, constants)
val compile2 = ExpressionCompiler[(Int, Int) => Any](literals, functions, constants)
val compile3 = ExpressionCompiler[() => Any](literals, functions, constants)
val ast1 = FunctionNode("+", List(LiteralNode("Int", "1"), ParamNode("a")))
val ast2 = FunctionNode("+", List(ParamNode("a", 1), ParamNode("b", 2)))
val ast3 = FunctionNode("+", List(LiteralNode("Int", "4"), LiteralNode("Int", "6")))
println(compile1(ast1).apply(1)) // 2
println(compile2(ast2).apply(2, 4)) // 6
println(compile3(ast3).apply()) // 10
//старый вариант где все было через Map-у только
println(compile(sumAst).apply(Map("a" -> 2)))
}P.S. код можно посмотреть тут, версия 2.0.0
P.P.S. у меня есть аккаунт на забр карьере
P.P.P.S есть аккаунт в инстаграмм
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.