0

Context

Writing a URL Query Parameter parser for Go language library

Problem

Only structs have a form of inheritance in Go, that I am aware of. One can use the reflect package to discern the kind of an entity, i.e. it's fundamental storage class within the type system, and one can probe the type of said element. So I can discern that the entity is of Kind string, and Type Title, as an arbitrary example, assuming something like this exists:

type Title string

Better, for structs, I can use anonymous members to gain a limited type of inheritance:

type Foo struct { name string }
func (f Foo) Hello() string { return f.name; }

type Bar struct { Foo }

func main() {
  b := Bar{ Foo{"Simon"} }
  fmt.Println(b.Hello())
}

Live example

The point being, Go allows me to extend a Foo as Bar but inherit / reuse Foo functions at least for the portion that is a Foo.

However, for a non-struct type - I am unaware as to how to approach this problem for an encoding/decoding library similar to json or xml - where I want to be able to decode query params into a struct's members, and crucially, be able to support user-defined types without requiring that every one of them defines a specialized decoder or encoder for my purposes, so long as they're derivatives of a type that supports an interface that I can utilize.

In the concrete, I would like to support user-defined types that are stored as a Google uuid.UUID, or a time.Time (or time.Duration would be useful as well).

Here's a simplistic example:

type TransactionID uuid.UUID

As declared, this inherits zero behaviors of uuid.UUID, and I'm not sure if, or how, to use reflect to intentionally make this happen in my encoder/decoder library?

Go itself won't provide any sort of inheritance or relationship between them. My TransactionID is NOT a uuid.UUID, but they share the same Kind - a [16]byte array, and to the best of my current knowledge, that is all they share.

This is my conundrum

How to allow a user defined type that I wish to support w/o requiring my users now define those same encoder or decoder functions for their type that I've already defined for the "fundamental" type?

Some More Context

My decoder library makes this interface available:

// Parser is an interface for types that RequestParser can parse from a URL parameter
type Parser interface {
    ParseParameter(value string) error
}

And I have defined a specialized extension for uuid.UUID and time.Time and a few other useful non-struct types to decode them from query parameter strings. If the entity type I'm decoding into is literally a uuid.UUID or a time.Time and not a user-defined type based on those, then things work properly. Similarly, user aliases work because they're not genuine new types.

Aliases are too limited for my purposes - they do no provide any meaningful distinction from the aliased type, which severely limits their actual utility. I will not respond to suggestions that require their use instead. Thank you.

UPDATE

Ideally, a user should be able to define TransactionID for their purpose and I would like to know that it is a "kind of" UUID and hence - by default - use a UUID parser.

It is already trivially possible for a user to define:

func (id *TransactionID) ParseParameter(value string) (err error) {
   id_, err := uuid.Parse(value)
   if err != nil {
      return
   }
   *id = TransactionID(id_)
   return
}

This will force my decoder to choose their well-defined interface - and decode the input exactly as they wish for their type.

What I wish - is to have a way to know that their type is a derivative type of ____ what?, and if I have a parser for a ____ what - then -- in the absence of a user-defined ParseParameter -- use my default supplied smart one (which knows that a [16]byte is not the same thing as a UUID, and understands more sophisticated ways to decode a UUID).

Addendum

I will expand this question and provide more details as I get thoughtful and useful responses. I'm not sure at this point what else I might provide?

Thank you for taking the time to thoughtfully respond, should you choose to do so.

Mordachai
  • 8,657
  • 4
  • 52
  • 96
  • 2
    When you embed one type in another, the enclosing type gets the same methods as the embedded type. When you define a new named type based on another, the new type does not get the methods of the base type. The second one is not type aliasing. Your question is not clear. what do you mean by "relationships"? – Burak Serdar Apr 27 '21 at 17:58
  • 3
    "Only structs have a form of inheritance in Go, that I am aware of." -- Well, not really. There is no inheretence at all in Go. But there is embedding, and maybe that's what you mean by "a form of". But you can embed _any_ type, not just structs, but the can only be embeded _into_ structs. – Flimzy Apr 27 '21 at 17:59
  • Burak - yes - I tried to edit that a bit to make it more clear... but basically - I want an easy way to know "hey, user type TransactionID is a type of uuid.UUID, so I can use my uuid.UUID parser to decode it on their behalf directly into the underlying [16]byte array using value.Set(bytes)... – Mordachai Apr 27 '21 at 18:00
  • 1
    Embed it: `type TransactionID struct {uuid.UUID}`. A new named type is a new type. It means you have a type that shares the representation of another, not its methods. – Burak Serdar Apr 27 '21 at 18:03
  • you can ask if type is array and if it has element of type byte, in that case you can safely cast this type to desired alias and use methods – Jakub Dóka Apr 27 '21 at 18:06
  • 2
    if you want to support more types, then encapsulate a logic for casting and encoding in a interface and store handlers in parser then loop trough them, or store just one and make user decide witch backend they want to use. – Jakub Dóka Apr 27 '21 at 18:13
  • @JakubDóka that doesn't work if there is a chain of types with different serialization semantics. – Burak Serdar Apr 27 '21 at 18:17
  • Well lets put it simply. Very thing you want is not suported. You have to choos which implementation of distinct type you will use. Why would you even care if there is a chain? If you need type to preserve functionality you have to embedd it. That is only explicit way to do this. How do you even expect to refer to methods of original type if they shadow each other? Istnt this a very reason why distinct types does not allow you to access method of the base type? – Jakub Dóka Apr 27 '21 at 18:23
  • Cerise - that's essentially what I'm doing now. The Q is - for user types that are a Kind such as [16]byte - but really aren't just an array - they're a UUID - how to distinguish and use the UUID parser, not a mindless 16-byte parser (I want to support smarter decoding for various UUID formats, for example, out of the box) – Mordachai Apr 27 '21 at 18:35
  • can i know what is this good for at the end? If you want to preserve all methods then use different programing language. Java would help you out in this matter XD. – Jakub Dóka Apr 27 '21 at 18:37
  • 1
    `type TransactionID uuid.UUID` defines a new type `TransactionID` that shares the same underlying type as `uuid.UUID`. There is no other relationship between `TransactionID` and `uuid.UUID` at compile time or runtime. Why are you defining `TransactionID` instead of using `uuid.UUID` directly? The problem may be that you are defining types unnecessarily. – Cerise Limón Apr 27 '21 at 18:38
  • Cerise - when you have an application with a dozen different kinds of fields that happen to be encoded as a UUID - it is very helpful and useful to correctly use distinct types for those fields - so that the compiler - can help you avoid misusing the wrong ID in the wrong slot (e.g. a TransactionID vs. a SessionID). This is one of the fundamental values of a strongly typed language - which Go claims to be. – Mordachai Apr 27 '21 at 18:44
  • Other nail to the coffin is that if user wants to use `UUID` encoding he can just embed UUID in his struct and then when he is passing it to your parser, he will just say `maStruct.UUID` and thats it. i dont see anything wrong with this, it is actually very readable. – Jakub Dóka Apr 27 '21 at 18:45
  • C++ or Java would be trivial languages to define such a thing. Objective-C, or even Smalltalk would support this. I need this for Go, and am evaluating / investigating my options. Worse case scenario - reflect cannot inform me that type A was defined as a type of B. If that is impossible to know through reflect, so be it. But if it is possible to know this through reflect - that is my real question! – Mordachai Apr 27 '21 at 18:47
  • 2
    The answer to the real question: The reflect API cannot tell you that A was defined as type B because there is no relationship between types A and B other than the shared underlying type. Use struct embedding to declare a new type that shares methods with some other type. – Cerise Limón Apr 27 '21 at 18:52
  • I think someone already mentioned that. Huraaa!! – Jakub Dóka Apr 27 '21 at 18:55
  • 1
    The solution used by all the stdlib encoding packages is to have a type switch for the basic types, and interfaces for the marshal/ummarshal methods. Anything that implements the interfaces gets to choose its behavior, anything that doesn't gets the default behavior for its underlying type. That's pretty much the only solution. – Adrian Apr 27 '21 at 18:57
  • Cerise/ Jakub - Well, not quite true... at least, the compiler knows. I am allowed to do this: `var id TransactionID = TransactionID(u)` where u is a uuid.UUID. So, in fact, there is a relationship - question is - does it exist at runtime accessible through the reflect package? (going to investigate `Type().ConvertibleTo()`) – Mordachai Apr 27 '21 at 18:58
  • 2
    @Mordachai The conversion is allowed because the underlying types are identical. There is no other relationship between TransactionID and uuid UUID. – Cerise Limón Apr 27 '21 at 19:16
  • @CeriseLimón is that true? e.g. - I cannot cast any struct to any other struct. I'm probing this possibility now. If it is as you say - nothing but reflect.Kind that matter (memory layout) - then this is a worthless avenue and I should abandon it. – Mordachai Apr 27 '21 at 19:23
  • "Only structs have a form of inheritance in Go" This statement is plain wrong. There is absolutely no inheritance in Go and any attempt to mimic inheritance are doomed to fail miserabely. – Volker Apr 27 '21 at 20:59
  • 1
    @Mordachai You are constructing a relationship between `A` and `B` from `type A B` that simply _is_ _not_ there and if e.g. Cerise rightly tells you that this made-up relationship is nonexistent you question him. This is not helpful. If you do `type T struct{I int; B bool; F float32; A [4]float64}` You create a type `T` and the right part of that definition is (almost) **only** explaining the "memory layout" of T. This description is not 100% correct, but I hope it helps you overcome your misconception about the relationship introduced between A and B in `type A B`. ... – Volker Apr 28 '21 at 05:36
  • 1
    ... If you now declare a new type S like `type S T` all you tell the compiler is basically: "I want a new named type S and its memory layout is that of T." And that's the _only_ relation. Peek at https://play.golang.org/p/CHsHLos91nP where a S2 is convertible to a S simply because "convertible" basically just means "has same memory layout". Also note that there are no "casts" in Go, only type conversions. And no, reflection doesn't help here. – Volker Apr 28 '21 at 05:46
  • @Volker yes - I investigated this on playground as well. And you're quite right that there is no relationship but memory layout. And, either the compiler is smart enough to fold identical such layouts, or evaluate the sameness of two independent declarations of the same layout - either way - I can assign from one to the other with no declared relationship between them (just the underlying memory layout). Thanks for amending your comments and helping to clarify. Cheers. – Mordachai Apr 28 '21 at 14:17

1 Answers1

2

It is not possible to use the reflect API to discern that one type was created from another type.

The specification says:

A type definition creates a new, distinct type with the same underlying type and operations as the given type, and binds an identifier to it.

and

The new type is called a defined type. It is different from any other type, including the type it is created from.

The specification does not define any other relationship between the defined type and the type it was created from. There is not a "created from" relationship for the reflect API to expose.

In the example

type A B

the reflect API cannot tell you that type A was created from type B because there is no relationship between type A and type B except for the shared underlying type.

Type conversions from A to B and B to A are allowed because A and B have identical underlying types.

You can use the reflect API to determine if two types have identical underlying types, but that does not help with the problem. It could be that A was created from B, B was created from A or neither was created from from the other. In this example, type A and B have identical underlying types:

 type A int
 type B int

The question states that the parser has a builtin decoder for uuid.UID. The goal is to use that parser for type TransactionID uuid.UUID. Here are some ways to solve that problem:

Embedding Declare the type as type TransactionID struct { uuid.UID }. Check for struct with single known embedded field and handle accordingly.

Registry Add function function to register mappings between convertible types.

 var decodeAs  = map[reflect.Type]reflect.Type{}
 
 func RegisterDecodeAs(target, decode reflect.Type) {
     if !decode.ConvertibleTo(target) {
        panic("type must be convertible")
     }
     decodeAs[target] = decode
 }

When decoding, check for decode type in decodeAs. If present, decode to the decoding type, convert to the target type and assign.

Call the registration function at program startup:

 func init() {
     RegisterDecodeAs(reflect.TypeOf(TransactionID(nil)), reflect.TypeOf(uuid.UUID(nil))
 }
I Love Reflection
  • 2,124
  • 2
  • 9
  • Yes, this is what I landed on as well (both of those ideas - using a registry, or embedding UUIDs instead of creating new types off of them). I'm going to provide users a UUID which is a struct with an embedded uuid.UUID and a default parser for that. This then is trivial to create new user-types from, while retaining the default behaviors that are common to UUIDs (or they can define their own overrides, trivially). Thanks for this response - I think it is well written and may help others going forward. Cheers. – Mordachai Apr 28 '21 at 14:11
  • I may actually do both. It would offer more flex / choices for end-users to support either one, and may have uses I don't immediately foresee, but nonetheless would be useful. – Mordachai Apr 28 '21 at 14:18