I'm writing a small custom DSL and interpreter as an exercise in understanding how the language stack (lexer/parser/(interpreter/compiler)) works. Because I want to be able to report the location of semantic errors, the AST needs to retain information about where the elements were in the source.
For this purpose I wrote a data class Point(val line: Int, val column: Int)
. All well and good, until I realized that manually specifying the position of EVERY node in the AST for unit tests on parsing would be prohibitively time consuming (and error prone). (Obviously, some tests need to do so to test the assigning of positions, but some is a lot fewer than all.)
To allow me to write more good unit tests quicker, I decided I wanted to default the position of all my nodes to a wildcard value that would match all valid Point
s. The parsed nodes would have the correct position, but the manually assembled AST to check against would not specify (and thus not test) the positions. This is the solution I came up with.
/**
* DO NOT EXTEND THIS CLASS!!
*
* This is an custom open data class to allow a wildcard value.
*/
open class Point(val line: Int, val column: Int) {
object Wildcard : Point(-1, -1) {
override fun toString() = "Point.Wildcard"
override fun copy() = this
}
override fun equals(other: Any?) = when {
this === other -> true
other === Wildcard -> true
other !is Point -> false
this === Wildcard -> true
line != other.line -> false
column != other.column -> false
else -> true
}
override fun hashCode(): Int { // generated
var result = line
result = 31 * result + column
return result
}
override fun toString() = "Point(line=$line, column=$column)"
operator fun component1() = line
operator fun component2() = column
open fun copy() = Point(line, column)
}
I wanted to keep all the behavior of a data class
, but a data class
cannot be open
and this was the only way I could come up with of implementing the wildcard value. This solution still feels wrong and too much like a hack, however. It could easily be broken by outside code extending Point
(as it must necessarily be available to be used) and so much of this code is just re-implementing what data class
gives us for free. I feel like there must be a solution with a sealed class
, but I haven't figured it out.
Recommended review points: idiomaticness (or not) of code, conventions, alternate solutions (and any other points)
1 Answer 1
Your equals
method is not transitive (e.g. Point(2, 2) == Point.Wildcard
and Point.Wildcard == Point(3, 3)
but Point(2, 2) != Point(3, 3)
. See Any.equals - stdlib - Kotlin Programming Language for more details on the requirements for equals
. You might also consider using EqualsTester
from guava-testlib
for testing your own implementations of equals
.
If you define your own method instead of defining a custom, non-transitive equals
implementation than your existing solution can become much simpler:
data class Point(val line: Int, val column: Int) {
companion object {
val wildcard = Point(-1, -1)
}
infix fun matches(other: Point): Boolean {
return this == other || other === wildcard || this === wildcard
}
}
Notes:
Point(2, 2) matches Point(-1, -1)
will returnfalse
.Point(2, 2) matches Point.wildcard
will returntrue
.- If you want both to return
true
then replace the===
with==
inmatches
. - Marking
matches
asinfix
is purely optional but it seems to me like a good fit.
-
1\$\begingroup\$ Is
~=
a valid operator in Kotlin? That seems like a (somewhat) standard operator that is used for this kind of fuzzy match. \$\endgroup\$CAD97– CAD972017年01月12日 17:59:34 +00:00Commented Jan 12, 2017 at 17:59 -
2\$\begingroup\$ I have not seen such an operator in Kotlin. \$\endgroup\$mfulton26– mfulton262017年01月12日 18:01:29 +00:00Commented Jan 12, 2017 at 18:01
-
1\$\begingroup\$ This is the solution I couldn't find; good catch! I assume that
::matches
would propagate up my AST to allow for fuzzy matching while::equals
checks for the correct equality, then. Is there a way to auto-implement::matches
the way::equals
is (most likely with reflection on an interface)? \$\endgroup\$CAD97– CAD972017年01月12日 18:03:25 +00:00Commented Jan 12, 2017 at 18:03 -
2\$\begingroup\$ If I understand correctly, I think you would simply need
interface Matchable { fun matches(other: Any) }
and then implement it appropriately. Probably with a singleMatchable.wildcard
instance to use across all matchables (instead ofPoint.wildcard
,Blah.wildcard
, etc.). \$\endgroup\$mfulton26– mfulton262017年01月12日 18:14:45 +00:00Commented Jan 12, 2017 at 18:14