【问题标题】:Algebraic Expressions - Kotlin way to model attributes that are both static and dynamic代数表达式 - Kotlin 建模静态和动态属性的方法
【发布时间】:2019-11-25 16:17:46
【问题描述】:

对于冗长的介绍,我很抱歉,我想不出办法让它更短。

问题

我们正在构建一个简单的应用程序,该应用程序通过应用模式转换来操作代数表达式 - 目标是能够说“采用2*x+2 并应用'分解'”。最终目标是能够确定两个表达式在一组模式转换下是否等价(即是 2*(x+1)^2 'reachable' from 2x^2+4x+2)。

我们创建了一组对域进行建模的类并定义了“规范”形式,这基本上意味着构造(符号上)相同的表达式只有一种方式:x+y+z 将始终表示为 @ 987654325@) 而不是Plus(y, x, z)Plus(x, Plus(y, z)),即使它是通过调用Plus(Plus(x, z), y) 创建的。

许多规范转换是相应运算符的代数属性的直接结果 - 关联性允许上面看到的“扁平化”等。这些属性也对模式操作有直接影响 - 例如,Power(\_, 2) 是不可交换,因此它只匹配以2 作为第二个子项(参数)的表达式,但Plus(_, 2) 是可交换的,因此它可以匹配以2 作为其任何子项的表达式。由于这些原因,我们希望将这些代数属性表示为类上的显式数据,然后在应用程序的各个部分进行解析。

棘手的部分是我们需要这些数据既可以静态使用(我们在构造表达式以应用适当的规范转换时需要它,但由于尚未构造表达式,我们无法动态访问它)和动态(在模式匹配期间,我们必须能够做到expr.hasAttribute(Associative))。由于这些属性对于特定的表达式类型(类)是固定的,因此我们也试图避免直接在类上定义它,例如

class Plus(children: List<Expression>): Expression(children) {
    val attributes = setOf(Associative, Commutative, IdentityElement(Number(0.0)))
    ...
}

这是因为我们不希望每次创建表达式时都创建新的 Set - 我们希望在类声明期间创建一次 Set,因为这是唯一一次需要创建。

解决方案

我们决定解决这个问题的方法是创建一个全局 HashMap&lt;KClass&lt;out Expr&gt;, Set&lt;Attribute&gt;&gt; 来保存属性,然后创建一个 Builder 对象,该对象负责解析属性并在创建表达式时应用适当的规范转换。

我将包含我们正在使用的代码的简化版本。一些高级 cmets:

  • HashMapExpression.Companion 上定义
  • 所有表达式构造函数都受到保护,而是通过companion object 上定义的invoke 函数创建实例。这是因为规范转换可能会导致与正在创建的对象不同,例如 Plus(Symbol(x)) 创建 Symbol(x)
  • 构建器主要构建一个运算符(函数)列表,然后将其应用于正在构建的表达式的子级 - 在本例中为展平和排序。

我的问题有两个:

  1. 您会采用这种方式吗?我们尝试了其他几种方法,但所有这些方法都需要大量重复代码(实际上速度更慢)。
  2. 你能给我一些关于如何使这个更清洁的代码审查技巧吗?我对 Kotlin 还很陌生。如您所见,我最终创建了一种半构建器语义来与Builder 中的Attributes 一起使用。这实际上不是我的目标,它只是因为试图使代码更干净。而且我确信可以做得更多。

谢谢!

import kotlin.reflect.KClass

/**
 * ATTRIBUTES
 */
abstract class Attribute
object Associative: Attribute()
object Commutative: Attribute()
class Identity(val element: Expr) : Attribute()
/**
 * END ATTRIBUTES
 */


/**
 * BUILDER
 */
// So there are basically two things we can do in canonical form -> we can operate on children (flattening, canonical ordering)
// or we can operate on the final expression as a whole (transforming Plus(x) to x, for example)
// So, every attribute may or may not contribute a functional operator that transforms either the children or the expression
// as a whole. These operators must be composed in a certain order (e.g. flattening must occur before ordering) and then
// applied to the result.
// The operators that change the expressions type short-circuit the function (because there is nothing else to do)
object Builder {

    inline fun<reified A: Attribute> Set<Attribute>.withAttribute(block: A.() -> Any?) =
            this.filterIsInstance<A>().firstOrNull()?.run(block)

    inline fun<reified E: Expr> withAttributes(block: Set<Attribute>.() -> Any?)
            = Expr.attributes[E::class]?.run(block)

    inline fun<reified E: Expr> canonizeByAttributes(crossinline constructor: (List<Expr>) -> E, children: List<Expr>): Expr {

        val childrenOperators = mutableListOf<(List<Expr>) -> List<Expr>>()

        withAttributes<E> {
            withAttribute<Identity> {
                when(children.size) {
                    0 -> return element
                    1 -> return children.first()
                    else -> {}
                }
            }

            withAttribute<Associative> {
                childrenOperators.add {
                    children -> children.flatMap { if(it is E) it.children else listOf(it) }
                }
            }

            withAttribute<Commutative> {
                childrenOperators.add {
                    children -> children.sortedBy { it.toString() }
                }
            }
        }

        return constructor(childrenOperators.fold(children) { children, op -> op(children)})
    }
}
/**
 * END BUILDER
 */


/**
 * EXPRESSIONS
 */
abstract class Expr protected constructor(val children: List<Expr>) {
    val attributes: Set<Attribute>? get() = Companion.attributes[this::class]

    override fun toString(): String = "${this::class.simpleName}(${children.joinToString(", ")})"

    companion object {
        val attributes: HashMap<KClass<out Expr>, Set<Attribute>> = hashMapOf()
    }
}

Expr.attributes[Plus::class] = setOf(Identity(Symbol("0")), Commutative, Associative)
class Plus private constructor(children: List<Expr>): Expr(children) {

    companion object {
        operator fun invoke(children: List<Expr>) = Builder.canonizeByAttributes(::Plus, children)
        operator fun invoke(vararg children: Expr) = invoke(children.toList())
    }

}

class Symbol(val name: String): Expr(listOf()) {
    override fun toString(): String = "Symbol($name)"
}
/**
 * END EXPRESSIONS
 */


/**
 * EVALUATION, TESTS
 */
Plus(Symbol("x")).attributes

(Plus(Symbol("x")) as Symbol).name == "x"

(Plus(Symbol("x"), Plus(Symbol("x"), Symbol("y"))))

(Plus() as Symbol).name == "0"

(Plus(Plus()) as Symbol).name == "0"

Plus(Symbol("2"), Symbol("y"), Plus(Symbol("x"), Symbol("1")))
/**
 * END EVALUATION, TESTS
 */

【问题讨论】:

    标签: generics kotlin dynamic static expression


    【解决方案1】:

    你可以拥有一个 set 属性,而无需每次都创建一个新实例,例如:

    private val plusAttributes = setOf(Associative, Commutative, IdentityElement(Number(0.0)))
    
    class Plus(children: List<Expression>): Expression(children) {
        val attributes = plusAttributes
        // ...
    }
    

    这样,Plus 的所有实例都引用了同一个集合。每个实例中仍然存在额外引用的开销,但远低于集合(可以有一个大对象树)。

    更好的是,将attributes 设置为Expression 类中的抽象属性,并在每个实现类中重写。这样,您可以在不知道表达式类型的情况下访问属性。

    您可能会考虑的另一种方法是使用相关子类实现的标记接口,以及扩展 Expression。这根本没有开销,而且测试起来非常容易和清晰:if (myExpression is Associative),尽管它不会一次性给你一张地图。 (如果有任何与属性相关的行为,您可以在相关接口中添加一个方法,这将朝着更传统的 OO 设计方向发展。)

    【讨论】:

    • 感谢您的提示,但是在这两个提示中,我不清楚如何实现静态访问​​,即在 Builder 中 - 那时没有实例,所以我无法访问它们。我能想到两种解决方案:创建一个“虚拟”表达式以允许动态访问,这似乎是错误的,并且也违背了“每个表达式始终为规范形式”的保证,或者 2)在类上都有“属性”在 Companion 对象上。这是我之前提到的“可怕的代码重复”,忽略了 Companion 对象不继承的事实
    • @GabrielShanahan 啊,我现在明白您所说的“静态和动态”是什么意思了……如果在我的示例代码中将 plusAttributes 移动到同伴中,也许它不会涉及太多重复对象,并将其重命名为attributes?然后你可以“静态地”访问它。 (请注意,在 Kotlin 中,伴生对象 可以 扩展另一个类和/或实现接口。因此您需要以多态方式访问它,它可以实现一些通用接口。)但我恐怕我可能错过了太多大局,无法提供太多帮助。
    • 感谢您的回复。我明白你的意思,但是即使我让伴生对象扩展了一个公共接口,我仍然无法向编译器保证在派生表达式的类上存在伴生对象。每个子类(Plus、Times、Symbol、Pow)都必须定义它自己的伴侣,但即使它扩展了一个公共接口,编译器也不允许我在没有明确指定类型的情况下访问它:即。 Plus.attributes 有效,但 T::attributes 无效,这使我无法抽象代码。唯一的解决方案是反射。
    • @GabrielShanahan 我认为一种不需要反思的方式。如果所有同伴都实现了一个带有名为attributes 的属性的HasAttributes 接口,那么您只需一个演员表就可以做到:(T::class.companionObjectInstance as HasAttributes).attributes。 (它仍然很丑,很遗憾你不能通过在超类中指定一个限制来避免强制转换,当然,但也许这总比没有好?)
    • 另外,感谢您的讨论,知道我与其他人的想法相同,这会有所帮助。查看我发布的代码,您是否看到我以这种特定方式所做的任何可能的改进?例如,我真的很想解决在 canonizeByAttributes 中到处都有返回的丑陋问题,但我唯一能想到的是将返回语句转换为 lambda,然后在最后执行而不是调用到 constructor() (基本上类似于我对 childrenOperators 所做的事情)。你怎么看?
    猜你喜欢
    • 2022-07-22
    • 2014-02-18
    • 2013-03-28
    • 2017-09-05
    • 2012-03-30
    • 2017-10-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多