Removing Type Dependency

202 views
Skip to first unread message

shan...@gmail.com

unread,
Nov 25, 2021, 6:44:35 PM11/25/21
to golang-dev
First, apologies if this has been spoken about previously, I couldn't find anything relevant, but that could be a search-fu limitation

Secondly, apologies if this isn't the correct mailing list, it feels  more go-dev than go-user as I'm looking for/at the way data is currently transmitted across architectural boundaries

One of the greatest things about Go (IMO) is the implicit implementation of interfaces, a type can implement an interface without ever even knowing that the interface even exists. This property decouples the type from the interface, there's no explicit dependency.

However when complex data needs to be transferred between the implementation, and the user of the implementation, a Data Transfer Object (type) is (normally) created.

Allow me to delineate the possibilities to demonstrate the problem

Firstly, interface{} is NOT an option, not only is it slow, the producer and consumer have no way to communicate what lies within, and, there's no compile time check to ensure that and particular field actually exists within the type. eg. Is there an X in that object, or not.

We could just return a list of different types from the standard library, but after 2 or 3 it becomes cumbersome, return int, int, float, bool, int, string, error is not something people would think of as ideal, it leads to "dogsledding" _, _, f, _, _, err := horrible(), and, worse, consumers/users need to memorise which field represents what value, eg. is the first int the X, the Y, or the Z.

We could use a map, but that limits the return types to a list of one type, map[string]T - generics aside as I'm not across them at this point, but my understanding is that a map[string]T isn't going to hold a int and string and float as values. Further, the lack of communication between the consumer and producer on what fields are in the map gives the same problem as interface{}, what fields does the map need to contain, and how is that going to be enforced.

And, we could return a struct, this allows multiple fields with different types, and they're named, and they're checked at compile time, *but* we can only have two possibilities, an Anonymous struct, which needs to be defined every time we want to produce one, in the signature, in the function, and so on.

Or Named structs, which are the usual choice they avoid all of the above problems *but* bring with them an explicit dependency. In order to use it, both the producer and consumer must know of its existence and definition. This breaks my favourite feature, the implicit implementation of an interface becomes an implicit implementation, with an explicit dependency on the type(s) that will be returned by the method(s) in the implementation.

The implementing types must return type example.com/foo.T, and therefore have to import example.com/foo to gain knowledge about it. This is usually, but not always placed near the interface definition.

I have a possible solution, and I want to hear opinions (/me dons fireproof jacket)

I think that some sort of implicit typing could work here, I think that the compiler can at compile time (not runtime!) check if a returned type matches an expected type, such that all of the fields defined in the expected type exist (by name!) and all of the types of those fields match (exactly!) and by this I mean
type Expected struct {
    A int64
    B int
}

type Success struct {
    A int64
    B int
    C bool // doesn't matter as it's not defined in the expected type
}

type Fail struct {
    A int64
    B int32 // wrong type
}

Sure, this means that the reality is the author(s) of the implementation need to know how the types in the interface's method are composed, but it's not an explicit dependency, and the method can then be (re)used for another interface, with no need to change the type(s) or repeat the code - this removes, say, a problem where my type is implementing a complex Stringer like interface, which locks the String() method name on my type to that interface, but if I am allowed to return a struct with two different fields, one that is used by one interface, and one that is used by another, I am free again

I use the name Stringer here purely as an example, this idea wouldn't work because the Stringer interface expects string, not struct{ s string}, so please allow me a little licence :)


The downsides that I can think of
Nothing comes without cost, this would make compilation slower and more complicated, as the types being transferred and expected have to be checked for compatibility. It's close to duck typing, where effectively the question is, can this type be /used/ as the expected type. And there exists a problem where a type contains a type that contains... that's very deep. Customised named types, would fall into the explicit dependency category, so I'd expect developers to avoid them (but, let's be honest, they will creep in), but, again, the same rule could apply, field names and types must match in order for them to be used.
Reply all
Reply to author
Forward
0 new messages