Ремарка:
В данной статье описывается подход автора к решению конкретной технической задачи. Автор не утверждает, что предложенное решение является единственно возможным, оптимальным или наиболее элегантным. Это лишь один из вариантов реализации, представленный в ознакомительных целях, а не руководство к обязательному применению.
Целепологание:
В двнной статье научим двигло собирать произвольные лямбды с возращаемым типом 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 есть аккаунт в инстаграмм