1

I have the following definition of a directed graph in Kotlin. (I'm still learning Kotlin so please forgive any shortcomings. Improvements and suggestions are always welcome.) My goal is to have a method, reverse, which maintains the vertices and loops but swaps the directions of the other edges.

// We use an edge list because it makes it the easiest to swap.
data class ReversibleDirectedGraph<T>(val vertices: Set<T>, val edgeList: List<Pair<T,T>>) {

    // This should be a self-inverting function.
    fun reverse(): ReversibleDirectedGraph<T> {
        // Make sure all vertices in edgeList are in vertices.
        val allVertices = edgeList.flatMap { it.toList() }
        require(allVertices.all { it in vertices }) { "Illegal graph specification" }

        // Swap the edges.
        val newEdgeList = edgeList.map { it.second to it.first }
        return ReversibleDirectedGraph(allVertices.toSet(), newEdgeList)
    }
}

fun main() {
    // Example test: works correctly. Double edge reversal results in original graph.
    val g = ReversibleDirectedGraph(setOf(0, 1, 2, 3),
        listOf(0 to 1, 2 to 1, 3 to 2, 3 to 0, 1 to 3))
    println(g)
    val gr = g.reverse()
    println(gr)
    val grr = gr.reverse()
    println(grr)
    println(grr == g)
}

I'd like to use property-based testing to test this code using KotinTest, but I'm having trouble structuring it to properly produce random samples of undirected graphs. If I can achieve that point, I can reverse the edge direction twice and then ensure that the original graph is achieved.

I'm familiar with Gen.list, Gen.choose, etc, but I can't seem to fit the pieces together to get the final product, i.e. the random undirecteed graph.

I've gotten up to this, but this is clearly missing pieces, and I was hoping that someone might be able to assist. I suspect I could do it in Scala since I have more experience there, but I am determined to learn Kotlin. Ultimately, something along the lines of:

class ReversibleDirectedGraphTest: StringSpec() {
    init {
        "reversibleDirectedGraphTest" {
            forAll { g: ReversibleDirectedGraph<Int> ->
                assertEqual(g.reverse().reverse() == g) }
            }
        }
    }
}

Any help / suggestions would be greatly appreciated. Thanks!

Sebastian
  • 715
  • 6
  • 13
  • I assume that ReversibleDirectedGraph is a recursive structure. This usually requires a recursive generator. Can you show RDGs constructors or factories; I might be able to suggest something concrete then. – johanneslink Nov 11 '19 at 06:32
  • Hi johanneslink. No recursion necessary: it's just a basic graph representation, comprising a list of vertices and then an edge list, so what I want to do is generate a vertices: List and then an edge: List> where the Ints in the pair come from the list of vertices. – Sebastian Nov 11 '19 at 17:23
  • 1
    You could write a Gen< ReversibleDirectedGraph> or something, which inside it generates a graph from random values. Then you just need to pass that to the forAll method ? – sksamuel Nov 11 '19 at 17:59
  • @monkjack That seems to have been a step that I needed to take, but now I'm getting that my generator isn't used and an exception "Cannot infer generator for dcp.day218.ReversibleDirectedGraph; specify generators explicitly." Is there a way to specify the generator as implicit? – Sebastian Nov 11 '19 at 19:43
  • 1
    Not as implicit no, Kotlin doesn't have implicits like scala. You can pass it explicitly itno the forAll. Something like `forAll(MyCustomGen) { graph -> // test graph here }` – sksamuel Nov 11 '19 at 20:00
  • And if you pass in the Gen yourself, then you don't need to specify the type on the lambda argument as it can be inferred from the Gen itself. – sksamuel Nov 11 '19 at 20:00

1 Answers1

0

I ended up following the suggestion of @monkjack and creating my own Gen. I had to explicitly provide the Gen to the forAll, and a rare exception would come up with "bound must be greater than origin", but this works and the vast majority of test cases that are produced are valid and do not need to be intercepted by the try...catch.

class GraphGen: Gen<ReversibleDirectedGraph<Int>> {
    override fun constants() =
        listOf(
            ReversibleDirectedGraph(emptySet(), emptySet()),
            ReversibleDirectedGraph(setOf(0), setOf(0 to 0)),
            ReversibleDirectedGraph(setOf(0, 1), emptySet()),
            ReversibleDirectedGraph(setOf(0, 1), setOf(0 to 1))
        )

    override fun random(): Sequence<ReversibleDirectedGraph<Int>> = generateSequence {
        val vertexGen = Gen.choose(0, 20)
        val vertices = Gen.set(vertexGen).random().first()
        val vertexList = vertices.toList()
        val edgeGen = Gen.set(Gen.pair(Gen.from(vertexList), Gen.from(vertexList))).random()

        // On rare occasions, this throws an exception with origin and bound, probably due
        // to the number of sets. In those cases, we use an emptySet instead as a placeholder.
        val edges = try { edgeGen.first() } catch (e: IllegalArgumentException) { null }
        ReversibleDirectedGraph(vertices, edges?: emptySet())
    }
}

class ReversibleDirectedGraphTest: StringSpec() {
    init {
        "reversibleDirectedGraphTest" {
            forAll(GraphGen()) { g -> g.reverse().reverse() == g }
        }
    }
}
Sebastian
  • 715
  • 6
  • 13
  • 1
    Just from looking at the code it seems to me that you might generate edges from one vertix to itself as well as duplicate edges, which might not be what you want. – johanneslink Nov 12 '19 at 06:35
  • Hi Johanneslink: I thought about loops (edges (a,a)), but they shouldn't be problematic, so I left them in since whether they are a subclass of directed graphs is questionable. Wouldn't using the `Gen.set(Gen.pair....)` prevent the duplicate edges? – Sebastian Nov 12 '19 at 13:07
  • Oh, I should note that I switched from list to set, so that solves your second concern. Oooops. No more repeated edges. – Sebastian Nov 12 '19 at 13:09
  • 1
    The important question you have to ask about any generator: Is it diverse enough to produce all the different kinds of data that might reveal problems in my properties? In your case that might be: Isolated vertices, circles with just one or many involved edges, very deep graphs etc. It often pays to look at the generated data with some statistical tooling. Most PBT frameworks provide something for that purpose - I don’t know about KotlinTest though. – johanneslink Nov 13 '19 at 06:27