Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DISCUSSION] Exposing the underlying struct #100

Open
bigs opened this issue Apr 2, 2019 · 18 comments
Open

[DISCUSSION] Exposing the underlying struct #100

bigs opened this issue Apr 2, 2019 · 18 comments

Comments

@bigs
Copy link
Contributor

bigs commented Apr 2, 2019

In my opinion, hiding the underlying multiaddr struct is a hinderance, particularly when working with serializations in golang (looking at flags and encoding/json). Is there any reason we couldn't simply expose the struct and keep its member hidden? This would enable us to implement interfaces that require zero values.

@bigs
Copy link
Contributor Author

bigs commented Apr 2, 2019

ping @Kubuxu @raulk

@ghost
Copy link

ghost commented Apr 3, 2019

Why not -- are you looking for more serializations than the existing MarshalBinary, MarshalText MarshalJSON?

@Stebalien
Copy link
Member

So, this is an issue for deserializing because we need a concrete struct to deserialize into. However, I'm not sure what the issue is for serialization.

Note: there's also the tricky fact that we now have two concrete implementations: Component and multiaddr.

@raulk
Copy link
Member

raulk commented Apr 3, 2019

Yeah, I've encountered this hindrance in the past. I get the impression the goal was to enable diversity of multiaddr implementations, but I can't see a use case for that.

@Stebalien
Copy link
Member

Yeah, I've encountered this hindrance in the past. I get the impression the goal was to enable diversity of multiaddr implementations, but I can't see a use case for that.

IMO, we should implement Multiaddr for net.TCPAddr, etc. However, I'm not sure how well that would work with how we currently use multiaddrs (it would probably lead to a bunch of allocations).

@raulk
Copy link
Member

raulk commented Apr 3, 2019

Ah, I see what you mean. Treating Multiaddrs as a canonical format with conversions from/to other formats. Unlike Rust, I don't think this is feasible in Go by making TCPAddr implement Multiaddr from outside the owning package. We could, of course, provide translation util functions, but that's what go-multiaddr-net does already, right?

@Stebalien
Copy link
Member

Hm, no, you're right. We could add newtypes but we can't implement it directly (I sometimes blissfully forget about go's type system).

@bigs
Copy link
Contributor Author

bigs commented Apr 3, 2019

i think if we keep the interface and just expose the strict as well, it should solve our problem

@Stebalien
Copy link
Member

@bigs could you explain how this solves the problem? Having to type assert to the concrete type is going to be annoying.

@bigs
Copy link
Contributor Author

bigs commented Apr 3, 2019

@Stebalien i misspoke earlier when i said serialization was the issue. it's only deserialization.

there's a somewhat prevalent pattern, for example in the datastores, of having something like

NewBadgerDatastore(...) *BadgerDatastore

where * BadgerDatastore is the receiver for methods implementing the Datastore interface. i think we could do that for Multiaddr. we could even expose the concrete type in another package if we wanted to have the name preserved.

being able to to say

var bytes []byte
var addr *multiaddr.Multiaddr
if err := addr.UnmarshalJSON(bytes); err != nil {
  panic(err)
}

makes it possible for users to seamlessly read in json objects with string representations of multiaddrs. we could keep the signature of NewMultiaddr the same if we so pleased. furthermore, any functions accepting the Multiaddr interface should have no problem.

@Stebalien
Copy link
Member

So, the issue I have with that is that the struct would need to be:

type Thing struct {
  ID peer.ID
  Addrs []*ma.MultiaddrConcrete
}

But now, when I want to create this thing, I have to convert from my ma.Multiaddr to a *ma.MultiaddrConcrete. Basically, this is going to suck either way: it's either going to be annoying when serializing/deserializing or at a higher layer when creating these structs.

I wonder if using refmt would help? refmt allows specifying explicit transforms so it may be possible to specify how to decode a multiaddr.

@bigs
Copy link
Contributor Author

bigs commented Apr 3, 2019

ah, you know, that's a fair point. i think it's still probably worth it, because it would only be adding functionality, not removing or hindering anything. that said, it would be wonderful to have some multiaddr magics for refmt! could be a convenience package.

@hsanjuan
Copy link
Contributor

I am going to bump this. I have custom code to wrap multiaddresses into a type that knows how to deserialize itself. Having to do that is worse than anything.

Could I convince you to remove the interface(s) altogether and use a concrete struct type instead, since nowhere this is used as an interface with alternate implementations, and right now it is a pain? (This would be backwards compatible as this type would have all the methods from the interface).

@raulk
Copy link
Member

raulk commented Aug 30, 2019

@hsanjuan I'm convinced. Multiaddrs are used everywhere, this indirection has proven useless over time (we have zero alternative implementations), adds a performance cost, and multiaddrs are a data containers per spec, not an abstraction over alternative data structures.

The above use case mentioned by @Stebalien -- using the Multiaddr as a canonical interface to things like TCPAddr -- is not feasible in go, because we can't extend types beyond their definition site. We are going to need translation/wrapper types/methods either way.

I'm very annoyed because the way it's implemented, we are totally unable to unmarshal multiaddrs, for obvious reasons. In fact, the unit tests blissfully use the private type. This is BOA (broken on arrival), in my opinion:

var addr2 multiaddr
if err = addr2.UnmarshalJSON(b); err != nil {
t.Fatal(err)
}

Something basic I'd like to be able to do:

	ma, err := multiaddr.NewMultiaddr("/ip4/127.0.0.1/tcp/5001")
	if err != nil {
		panic(err)
	}

	bytes, err := json.Marshal(ma)
	if err != nil {
		panic(err)
	}

	fmt.Println((string)(bytes))

	var addr multiaddr.Multiaddr
	err = json.Unmarshal(bytes, &addr)
	if err != nil {
		panic(err)
	}
	fmt.Println(addr)
"/ip4/127.0.0.1/tcp/5001"
panic: json: cannot unmarshal string into Go value of type multiaddr.Multiaddr

@Stebalien
Copy link
Member

So, we are now using multiple implementations. Specifically, we have Component and multiaddr to make parsing multiaddrs easier.

However, we can just export multiaddr as GenericMultiaddr (or something like that). That would allow us to unmarshal as:

type Bar struct {
	Addr ma.Multiaddr
}

func main() {
	b := Bar{Addr: new(ma.GenericMultiaddr)}
	json.Unmarshal([]byte(`{"Addr": "/ip4/1.2.3.4"}`), &b)
	fmt.Println(b)
}

But that's still kind of crappy as we need to partially initialize the struct.


An alternative is to use refmt which allows specifying per-type decoders.

@jfietz
Copy link

jfietz commented Jun 17, 2020

As I just ran into this issue, has there been any further development on this? Specifically, I was trying to exchange host addresses in testground between different servers without relying on discovery mechanisms.

@mg98
Copy link

mg98 commented Jan 29, 2023

I'll just make a dumb proposal:

We could refactor the Multiaddr interface to something else, like IMultiaddr but less Java-flavored,
and then refactor the multiaddr struct to Multiaddr.
Furthermore, NewMultiaddr should return *Multiaddr (the struct).

That's the most straightforward but also least-harmful way I can think of doing it. I will be happy to create a PR. Waiting for comments first, though.

@jcace
Copy link

jcace commented Feb 1, 2023

Hey guys, ran into this error recently too while trying to unmarshall Multiaddrs from json.
cannot unmarshal string into Go struct field MinerChainInfo.chain_info.addresses of type multiaddr.Multiaddr

Ended up having to redefine it as string and manually convert it over with NewMultiAddr() function. It would be nice if there was a more straightforward way of doing it 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants