Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

It's great to see this approach to core API design, and I wish other platforms adopted it. There are many advantages to having a rich ecosystem of third-party libraries, but composability becomes a problem when they define different types for the same concepts. This is the best of both worlds - you get multiple competing implementations of the interesting bits, but everything "just works" with any of them.


I couldn't agree more. Although, as someone mostly in to C++, I think data types like this should be standardised as concepts rather than added as concrete types. The HeaderMap described in this crate sounds like it is quite complex. I'd rather have a concept with some kind of trait mechanism for determining if e.g. the type preserved insertion order (which defaulted to false)

The Beast HTTP library that just got accepted in to Boost follows this approach. E.g. the following Fields concept specifies the requirements the library has on its any HeaderMap-like type you feed it. Writing an adapter for your own types then becomes straightforward

http://vinniefalco.github.io/beast/beast/concepts/Fields.htm...


Defining the type as a trait would prevent link-time optimization of library-consumer-side manipulations of data of that type, no?

Really, you want a concept that's like a trait, but is explicit about the fact that there's exactly one implementation of the trait, and that that implementation is discoverable at compile time. Less like an interface, more like a C typedef in a third-party-library header file—just without the "header file" part.


Most Rust compilation is non-incremental but rather one shot with all type information available. Thus, because of LTO optimization either in Rust or LLVM the compiler should discover what the concrete type of the trait is and be able to optimize / inline accordingly.


Can't wait for concepts...


Counterexamples: XML DOM APIs, as interfaced and implemented (separately) in, say, Java; or OpenGL, with its extension functions.

One problem is construction. When you need to interact with a library at one remove using interfaces everywhere, you can't simply new up instances; you have to go via factories. And that leads to ugly alien code.

Another is extension (OpenGL). If your super duper implementation has nifty new features, how do you expose them if you're living behind an interface? Does the interface enable extension in a usable way? Or is it indirect and awkward again?

In practice, you code and test against one or two implementations (bug compatibility), and other implementations are only supported by accident or effort by the implementors.

(I'm fully on board with the idea that a language ecosystem is constrained by the highest level of abstraction in the type system that's shared across all ecosystem libraries. It's the strongest argument for a large standard library.)


One problem is construction. When you need to interact with a library at one remove using interfaces everywhere, you can't simply new up instances; you have to go via factories. And that leads to ugly alien code.

That's mostly a Java problem, though, which lacks first-class types.


I think there might be a typo in your second paragraph ("at one remove"), but this is very interesting to me, so I would love to pick your brain.

Why is a factory more alien than simply newing up an instance? Shouldn't they have basically the same interface? Is it just a matter of giving people rope to hang themselves... because they can do an arbitrary interface, someone inevitably will make a weird one, and then someone else will think it's cute, and the fashion gets perverted?

That does seem like a problem. It could be solved with culture, but solving problems with culture is hard so I understand your decision to write it off as a bad direction.

I am writing a lot of code with this kind of structure lately though, so you've got me a little worried. One of the things I dislike about a library is when it exposes raw data structures to the user, and then presumes other libraries will understand that structure. For example, if I have a client library that is sending HTTP requests, and then another separate library on the server that handles them, there is an opportunity there for miscommunication in that data layer.

On the other hand, if I have one library which gives me both a factory and an interface which consumes that factory object, then the library handles both sides of the data structure management. I am forced to only work through the interface, which means as long as my libs are in order, everything should be able to communicate.

Sorry if that's vague, but I'm programming in JavaScript and this post is about Rust and you're referencing Java and OpenGL, so I'm not sure exactly where the ground is. Maybe I only like my approach because it's JavaScript, so I can't ever assume the type of anything is correct. #stockholmsyndrome


No, I meant "at one remove". When you're interacting with a library using interfaces defined by a third party, you're intermediated; you're at one remove[1] from the library.

Needing to use a factory means you don't have ambient authority to create instances. Code that constructs needs to be parameterized by the factory, irrespective of how far down a call chain it is. Annotating the call stack with a handle to the library adds clutter and clumsiness throughout.

You can kind of get around this using module systems of various kinds, and the de facto module system in JS of putting your entire program in a function parameterized by the modules it uses - it's far from the worst way of doing things, and it's a lot less clumsy than many alternatives. There's a bonus in that it's the typical idiom in JS. But it isn't in many other languages, so the benefits of the API design pattern needs to be traded off against how it clashes with the language culture. It's not an unalloyed good.

[1] https://www.collinsdictionary.com/dictionary/english/at-one-...


I agree, Rust leadership continues to make well thought out decisions. Coming from Java, the Servlet spec is comparable (though broader in scope) and was really successful.


Go's interface{} gets heavy criticism, a lot of it deservingly too, but this is one area where I think Go works really well. You can have a standard API structure and use that across multiple domains.

I really do need to give Rust a try though. I'd been holding back because my impressions of it was the core libraries were still in flux so code that compiled today might fail 2 months down the line. Is this still the case now?


It's all good now! Rust has been stable with backwards-compatibility guarantees since 1.0 was released two years ago.


Right, but I think when they say "core libraries" they probably mean things like hyper, which are still unstable (though getting closer to stability). That said, a lot of big rust crates have stabilized in the last few months.


yeah, but you still end up needing nightly all the time.


The vast majority of our users are on stable.

It's true that some cases still use nightly, as some people want to opt into cutting-edge stuff. But it's much much less than it was a while back.


I believe you, but at the same time it's an issue every time I go to use it. I want to use this crate, it has a feature flag and now I'm on nightly.

Now, I'm willing to use the bleeding edge, since I'm just just learning the language, but it still just bugs me in that way that code smells always do.


Which crates are you running into, out of curiosity?


No bullshit: interface{} is almost the same level hack as void*. And is not well, no matter how much Stockholm syndrome you have.


Like I said, much of the criticism against interface{} is deserving, but genuinely it works really well for passing generic APIs like Reader / Writer.

The way how interface{} works there is you actually write your own named structure that is bootstrapped by interface{} rather than dealing with interface{} as a generic type that needs casting. So what you're actually working with is an io.Reader / io.Writer structure but which can be transferred to any domain providing your specific implementation of Reader / Writer supports the same methods (since it's the methods that define the interface you avoid all the horrible hacks than normally trouble interface{}). This means you can transfer data from a gzip archive to a base64 encoder or JSON marshaller to a OS STD* file or network device all as if they were the same logical interface and without having to write a lot of additional layers encoding / decoding the data nor describing the interface. It all works surprisingly painlessly. In fact it's literally the only time when working with interface{} that the process isn't painful.

So it's going nothing to do with fanboyism nor stockholm syndrome, interface{} just behaves quite differently to it's usual behavior and works really well in this specific situation in my personal opinion. If the Go developers left interface{} there instead of using it as a hacky alternative to generics then I doubt there would be the same backlash against it. But sadly they didn't.


It's a lot like the Boost libraries are for C++. They are curated, and easily composable by design. I don't see the big leap here, but you are right, it is a very good way of working.

On the other hand, this is the Nth time that somebody implemented a HTTP library on some platform. It seems that something can be improved still, software-engineering wise.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: