I got an interesting quirk I try to fix with the n...
# compiler
o
I got an interesting quirk I try to fix with the new collection literal feature. I love that this could make my DSLs so much more readable, but I have a hard time getting this to work. Here's a tiny example of a builder I work on:
Copy code
class TinyBuilder {
    val data: Map<String, Any?>
        field = mutableMapOf<String, Any?>()

    infix fun <http://String.to|String.to>(value: Any?) {
        data[this] = value
    }

    infix fun <http://String.to|String.to>(list: TinyList1D) {
        data[this] = list.elements
    }

    // Adding this 2D builder adds functionality for deeper nesting, but will kill the option to set
    // `to []` due to overload resolution ambiguity, interestingly for all three functions
    // value: Any?, list: TinyList1D and list: TinyList2D!
    infix fun <http://String.to|String.to>(list: TinyList2D) {
        data[this] = list.elements
    }

    // this is a "fix" I tried, making clear that [] can be assigned only here, but it makes things worse!
//    infix fun <http://String.to|String.to>(emptyList: List<Nothing>) {
//        data[this] = emptyList<Any?>()
//    }

}

class TinyList1D(val elements: List<Map<String, Any?>>) {
    companion object {
        operator fun of(vararg builders: TinyBuilder.() -> Unit) =
            TinyList1D(builders.map { build(it) })
    }
}

class TinyList2D(val rows: List<TinyList1D>) {
    val elements by lazy { rows.map { it.elements} }
    companion object {
        operator fun of(vararg row: TinyList1D) = TinyList2D(row.toList())
    }
}

fun build(builder: TinyBuilder.() -> Unit): Map<String, Any?> =
    TinyBuilder().apply(builder).data

fun main() {
    println(
        build {
            "foo" to "bar"
        }
    )  // prints {foo=bar}

    println(
        build {
            "foo" to [1, 2, 3, 4]
            "empty" to []
        }
    )  // prints {foo=[1, 2, 3, 4],empty=[]}

    println(
        build {
            "foo" to [1, 2, 3, 4]
            "nested" to [{"deeper" to "yes"}]
        }
    )  // prints {foo=[1, 2, 3, 4],nested=[{deeper=yes}]}

    println(
        build {
            "2d nested" to [[{"deepest" to "yeah"}]]
        }
    ) // prints {2d nested=[[{deepest=yeah}]]}
}
You need to copy this to a project with -Xcollection-literals enabled! So, I want to make use of collection literals to make my JSON builder DSL look even more like natural JSON. Of course, the
:
in real JSON are not reproducible, but the rest is getting quite close. A user can now add lists of scalars like this:
Copy code
build {
  "foo" to [1, 2, 3, 4]
}
And thanks to the new operator fun of, I even managed a list of objects without much trouble, thanks to one
to
function that accepts the `TinyList1D`:
Copy code
build {
  "foo" to [
    {"bar" to 1}, {"bar" to 2}
  ]
}
But here's where things get strange. Empty arrays are supported indirectly through this first `TinyBuilder1D`:
"foo" to []
is resolved here, and creates an empty list (even though the overload chosen is the one for deeper object nesting!) When I add a second
to
overload for 2D arrays that are about to enable
"foo" to [[{"deeper" to "yes"}]]
, the 2D nesting works just fine, but the empty array now breaks down, signaling "overload resolution ambiguity" for all three functions: the plain Any?, the TinyList1D and TinyList2D! I wanted to fix this by adding a new overload specifically for empty lists:
List<Nothing>
I thought must be 100% compatible to
[]
- but no, this one makes things even worse. So, overall: I was only able to play this game for 1D lists of objects. Not further. And I don't know if this is something that could be fixed before the collection literals become stable?!? Thanks for any input!
🧵 2
I forgot to also note that the limitation that going for more dimensional arrays with builder is quite an act and requires more and more classes and functions for each dimension...
g
• Compiler error for
[]
is expected: indeed, all three candidates for
to
(with
Any?
,
TinyList1D
, and
TinyList2D
) are considered successful: `TinyList`s declare their own operator
of
and
Any?
can fallback to
List
. ◦ Among these candidates, neither
TinyList1D
is more specific than
TinyList2D
nor
TinyList2D
is more specific than
TinyList1D
. That's why ambiguity is reported. ◦ Both of these candidates are more specific than one with
Any?
. However, in the diagnostic message all successful candidates are included. • Good news is that there is a nice workaround for your case: you can modify a signature of
TinyList2D.Companion.of
slightly:
Copy code
class TinyList2D(val rows: List<TinyList1D>) {
    val elements by lazy { rows.map { it.elements} }
    companion object {
        operator fun of(first: TinyList1D, vararg row: TinyList1D) = TinyList2D(row.toList())
    }
}
That way, empty collection literal will be inferred to
TinyList1D
(as it is more specific than
Any?
). If you want it to be inferred specifically to
List<Nothing>
you can also modify
TinyList1D
in the same way and add an overload with
List<Nothing>
.
❤️ 1
o
Wow. The workaround is really, really good news! Thanks! How is specificity measured? Is there something like a score that is compared?
g
If we add a parameter to
TinyList2D.Companion.of
then for empty collection literal the corresponding
to
candidate just becomes unsuccessful, so it is not actually about specificity (it is filtered out before choosing the most specific candidate)