I wrote a toy DSL for tree structure/property matc...
# dsl
c
I wrote a toy DSL for tree structure/property matching. this lets me write something like
Copy code
val matcher = plus { num { where { it.value > 2 } } variable {} }
val ast = Addition(Number(3), Variable("y"))
val matched = matcher.matches(ast)
I'm hoping to also support property extraction. I implemented something that works, but it feels gross using nullable vars like this:
Copy code
var value: Int? = null
var name: String? = null
val extracter = plus { num { extract { value = it.value } } variable { extract { name = it.name } } }
val ast = Addition(Number(3), Variable("y"))
extracter.extractFrom(ast)
println("$value $name") // 3 y
Is there a pattern I could use to implement something more like this?
Copy code
val (value: Int, name: String) = extracter.extractFrom(ast) ?: return null
🆒 1
m
Something, that I was able to make work is this:
Copy code
val extraction = extractor.extract(ast)

val value: Int by extraction
val variable: String by extraction
    
println("value: $value")
println("variable: $variable")
But you would loose some compile-time safety, that you have with the variables right now 🤔. Is that something, that you need?
Here you have the code, if you want to have a look: https://pl.kotl.in/IyEHVhOeC Probably you can't just copy-and-paste it into your project, since my definition of the DSL is slightly different. This shouldn't matter though, since your question is more about the extraction than about the creation of the intermediate types 😄
c
This is a great little demo, thank you for writing it. I think static checking is probably important for ease of writing these more than anything.
I'm still pretty new to these language features like delegates. I'll play around with your example. It seems like maybe the extract calls could take a type parameter to add a little more static checking?
m
I think the problem is, that the only connection between the extract calls and the variable is the name, so the compiler doesn't really know that they are related. It's all happening at runtime through string equality.
Maybe to understand more of what you want to achieve: How does your program behave if the AST doesn't have the expected values? For example you're expecting a number, but there is a variable instead. Is it throwing a runtime exception? If not, what should be the extracted value?
c
I'm building this as an extension to a matcher DSL, and I am happy with getting null if the AST structure doesn't match.
maybe there's enough information for the compiler in a call like this?
val (name, value) = extracter.extract<String, Int>(ast) ?: return null
the
extract
calls inside the dsl could specify the type too, like
variable { extract<String> { it.name } }
this idea is all in service of avoiding the detailed imperative style conditional checks of parts of an AST and then getting some properties from those bits of the tree after confirming that they're the right type/meet the right criteria
m
I came up with a "slightly" different DSL:
Copy code
val ast = Addition(listOf(Number(3), Variable("y")))

val extractor = object : plus() {
    val operands = extracting(precondition = { it.isNotEmpty() }) { it.values }

    val num = object : num(precondition = { it.value > 2 }) {}
    val variable = object : variable() {
        val isY = extracting { it.name == "y" }
        val isX = extracting { it.name == "x" }
    }
    val otherVariable = object : variable(precondition = { it.name.count() == 1 }) {
        val isX = extracting { it.name == "x" }
    }
}

val operands = extractor.operands(ast)
val value = extractor.num(ast)?.value
val customVariableName = extractor.variable(ast)?.name
val isY = extractor.variable.isY(ast)
val isX = extractor.variable.isX(ast)
val isOtherX = extractor.otherVariable.isX(ast)
Unfortunately it has a few
object :
and
val
keywords, but it has a lot of compile-time safety. You're not able to reference a value, that doesn't exist and you can't mess up the type of the variable. Additionally it even supports type inference when reading from the AST. The syntax isn't as nice as your initial DSL, but how much of it, do you want to have? Also, how flexible is this matcher DSL of yours? You might be able to use it instead of this custom precondition "thing", that I created here. If you want to check it out anyway, here is the link: https://pl.kotl.in/2xIC85wzo
maybe there's enough information for the compiler in a call like this?
val (name, value) = extracter.extract<String, Int>(ast) ?: return null
the
extract
calls inside the dsl could specify the type too, like
variable { extract<String> { it.name } }
You might be able to use some kind of type modeling, as explained here https://kt.academy/article/type-modelling-kotlin, but I feel like this would lead to an explosion of symbols, since you're not working with simple lists and probably want to make extraction part compile-time safe.
c
interesting! very cool to see your new example. for very complicated matchers that might be worth the extra syntactic overhead. in most cases the imperative checks would be less verbose, I think
Some of the patterns in that type modeling article do indeed look like what I'm hoping for. I'm probably just having an active imagination here but it did remind me that maybe I can do something useful with scoping here, by setting the fields of a member of Extractor.
Copy code
data class Values(name: String, value: Int)
val extractor = extractor<Values> {
  plus {
    variable { values.name = it.name }
    number { values.value = it.value }
  }
}
val ast = Addition(Variable("x"), Number(2))
val (name, value) = extractor.extract(ast) ?: return null
println("$name $value") // x 2
I think the only hope of something like that working would be if reified generics are applicable here; not sure about that yet. I'll look at the article in more detail too!