https://kotlinlang.org logo
Title
e

Emilio

09/11/2019, 7:26 PM
I'm trying to create some sort of compute graph and I'm trying to make my nodes a sealed class to exhaustively match on them with
when
. So I have something like (class internals omitted)
sealed class Node
sealed class SourceNode : Node() // No parents
sealed class SinkNode : Node() // No children
sealed class OpNode : Node()
// classes that extend the sealed classes here
I would like to distinguish at the type level nodes that have parents and nodes that have children so I could write something like:
fun foo(x: ParentNode): ParentNode {
    // Do something to only parent nodes, possibly exhaustively match on them, and return a ParentNode
}
Where
ParentNode
is somehow a subtype of
Node
The problem is
OpNode
is both a parent node and a child node which calls for multiple inheritance via interfaces, but I would like to be within the land of sealed classes for exhaustive matching. Is there a solution to this in kotlin?
b

bbaldino

09/12/2019, 4:31 AM
could you maybe define the methods for parent nodes and child nodes in separate (from the node sealed class) interfaces? that way things can still all derive from Node, but the contracts around parent/child nodes is just defined separately?
something like:
interface Parent {
    fun getChildren(): List<Node>
}

interface Child {
    fun getParent(): Node
}

sealed class Node
class SourceNode : Node()
class SinkNode : Node()
class OpNode : Node(), Parent, Child {
    override fun getChildren(): List<Node> {
        TODO()
    }
    override fun getParent(): Node {
        TODO()
    }
}
e

Emilio

09/12/2019, 8:10 PM
I've tried something similar, but I would like to be able to do things with
Parent
and
Child
treating them as nodes, without the run-time casting to
Node
. For instance, being able to define the interfaces like:
interface Parent {
    fun getChildren(): List<Child>
}
fun ConsumeParent(x: Parent) {
    // Access some node specific fields of x without casting
    val children = x.getChildren()
}
b

bbaldino

09/12/2019, 8:11 PM
what about extracting the node methods you need to an interface as well?
which parent and child could inherit from
e

Emilio

09/12/2019, 9:14 PM
If I extract nodes to an interface, say
interface INode
then I lose exhaustive
when
matching on Nodes, which would be nice to keep.
b

bbaldino

09/12/2019, 9:14 PM
i was thinking you could still keep the sealed class structure
just move the contracts out of the sealed class into the interfaces
but hard to say without knowing specifics
e

Emilio

09/12/2019, 10:43 PM
Hmm, perhaps I can make this more specific then, building off the idea of lifting the contracts into the interfaces:
interface INode {
    val type : String
}
interface IParentNode : INode {
    val children : List<IChildNode>
    fun addChild(child: IChildNode)
}
interface IChildNode : INode
interface IAddNode : IParentNode, IChildNode {
    val parent0 : IParentNode
    val parent1 : IParentNode
}
sealed class Node
sealed class SourceNode : Node(), IParentNode
sealed class SinkNode : Node(), IChildNode
sealed class OpNode : Node(), IParentNode, IChildNode
class InputNode : SourceNode() {
    override val type = "Input"
    override val children: List<IChildNode>
        get() = TODO()
    override fun addChild(child: IChildNode) {
        TODO()
    }
}
class OutputNode : SinkNode() {
    override val type: String = "Output"
}
class AddNode : OpNode(), IAddNode {
    override val parent0: IParentNode
        get() = TODO()
    override val parent1: IParentNode
        get() = TODO()
    override val type: String = "+"
    override val children: List<IChildNode>
        get() = TODO()
    override fun addChild(child: IChildNode) {
        TODO()
    }
}
fun foo(x: List<Node>) { 
    x.map { when(it) {
        // TODO
    }.let { } }
}
// Got from a Parent Nodes Children
fun bar(x: List<IChildNode>) {
    foo(x) // will have to do an unchecked cast
}
Since IChildNode isn't in the sealed class of Node, I will have to litter unchecked casts in many places.
b

bbaldino

09/12/2019, 10:46 PM
could you create a common parent for the sealed classes which implement the child interface?
e

Emilio

09/12/2019, 10:48 PM
I could but I would also like the dual, to have a common parent for classes that implement the parent interface. I guess I have to choose only 1 of the 2?
b

bbaldino

09/12/2019, 10:49 PM
yeah, i was just trying it out and think i ran into that
fails when getting to
OpNode
hmm, but looks like this might work?
sealed class ChildNode : Node(), IChildNode
sealed class ParentNode : Node(), IParentNode
sealed class ParentChildNode : Node(), IChildNode, IParentNode
sealed class SourceNode : ParentNode()
sealed class SinkNode : ChildNode()
sealed class OpNode : ParentChildNode()
maybe not pretty
but doesn't seem like a terrible workaround
e

Emilio

09/12/2019, 10:54 PM
Suppose I wanted to write a function that returned a
ParentNode
(functions are covariant in return type), how would I type this function now that
ParentChildNode
isn't a subtype of
ParentNode
?
b

bbaldino

09/12/2019, 10:55 PM
yup, fair point, sounds like you'd be screwed there šŸ˜›
i've actually written what sounds like a similar pipeline. i didn't try and accomplish this exactly--i do have a sealed class of Nodes, but i don't try and enforce these sorts of properties--it would be nice to be able to accomplish this though
i have things like
ConsumerNode
and
DemuxerNode
but don't need to manipulate them much after they're created, so didn't need the generic typing really
i also played a bit with trying to enforce that 2 nodes could only be connected if the output type of the first one matched the input type of the second, but ultimately gave up on it
e

Emilio

09/12/2019, 11:03 PM
I see, I guess it is rather uncommon to enforce Parent/Child status on the type level. I think what I'm trying to accomplish is something similar to that line of thinking, only allowing nodes to connect to certain other types of nodes. It just becomes hard when you want all nodes to be a subtype of a general
Node
b

bbaldino

09/12/2019, 11:03 PM
yeah
which i think is necessary for them to be reasonably useful
e

Emilio

09/12/2019, 11:12 PM
Yup, just that I think this may be impossible currently. I think if we had something like sealed interfaces (https://youtrack.jetbrains.com/issue/KT-20423) we could do something like:
sealed interface Node
sealed interface ParentNode : Node
sealed interface ChildNode : Node
sealed interface SourceNode : ParentNode
sealed interface OpNode : ParentNode, ChildNode
sealed interface SinkNode : ChildNode
Although maybe this has issues I havent thought of. Sadly seems like this is on the backburner - I'm not sure if without this or some other new language feature this type of idea may be impossible to express in kotlin?
b

bbaldino

09/12/2019, 11:13 PM
yeah, that'd be nice. i'll star it, as i'd find this helpful as well
r

rook

09/13/2019, 4:08 PM
Just a quick call out. I noticed in all your
sealed class
children, you also defined them as
sealed class
. Which is a little strange unless you intend for them to also have their own internal classes.
b

bbaldino

09/13/2019, 5:24 PM
i think that's what he intended, but it's a good point as that does solve the inheritance problem (could then do
class OpNode : Node(), ChildNode(), ParentNode()
)
he does have subclasses of OpNode, though, so i think he wanted to be able to enforce exhaustive checks
āœ”ļø 1
e

Emilio

09/17/2019, 6:26 PM
Yes, I intended to have their own internal classes, just didn't show it for brevity.