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.
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.)