InquirerUrdaneta gets new leaders after dismissal of mayor, vice mayorRTP Desporto12h30 Samu muito elogiado e os pequenos que foram grandes na TaçaESPNBears CB Kyler Gordon returns with vengeance, answers criticsPunchUNILORIN graduates 16,152 students at 41st convocation ceremonyESPN DeportesDestacan audiencia y asistencia de WNBAThe Jerusalem PostHamas planned to detonate bombs at Gaza refugee camps to cover terrorists' escape, IDF revealsBollywood HungamaTiger Shroff Firing Incident: Three accused arrested by Kota police while travelling on Pune-Jaipur expressMintFSSAI proposes ban on added sugars in infant foods: Why it wants to act and how sucrose, fructose can harm babiesLa TerceraUcrania afirma que una reunión de Putin con Xi y Trump solo servirá para continuar la guerraRTL BoulevardVan Douzen Kroes tot Imaan Hammam: deze 5 Nederlanders schitteren bij Victoria's Secret Fashion ShowZDF heuteEntdecken Sie das ZDF-NachrichtenstudioThe South AfricanGovernment pays tribute to media on ‘Black Wednesday’
The Daily Newsstand · Free, Always
Monday, October 19, 2026

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

Translate

В данной статье описывается подход автора к решению конкретной технической задачи. Автор не утверждает, что предложенное решение является единственно возможным, оптимальным или наиболее элегантным. Это лишь один из вариантов реализации, представленный в ознакомительных целях, а не руководство к обязательному применению.

Целепологание:

В двнной статье научим двигло собирать произвольные лямбды с возращаемым типом 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 есть аккаунт в инстаграмм

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.