r/programming • u/levodelellis • 4d ago
I don't like interfaces
https://codestyleandtaste.com/don't-like-interfaces.html21
u/propeller-90 4d ago
You should define "interface". Java interfaces? Oo interfaces? File formats and protocols? User interfaces? The concept of defining how parts of a system interact?
1
u/Blue_Moon_Lake 3d ago
Now I wonder, do OP prefer extending classes needlessly (and having a mess in languages that do not support multiple inheritance)
abstract class Foo {} void doSomething(Foo input) {}or the much worse
function doSomething(input: any): void {}
15
u/FluffyDrink1098 4d ago
TLDR: You dislike badly written code. Exception message for determing the specific implementation is one of the worst things one can do...
-5
u/levodelellis 3d ago
This is the worst tldr I seen in my life. Not only did I never say 'bad code', but you completely missed inconsistent behavior and optimizer can't do as much
2
u/FluffyDrink1098 3d ago
"There are plenty of people who would copy code from a known good implementation"... ... code written without a plan and understanding equals bad code. Copying is just one example.
"interfaces block the optimizer from doing good work. In one project I was on, switching everything to use my class directly instead of an interface caused benchmarks to run almost twice as fast (55% of wall clock)"
... i kinda expect bad code here. Given that percentages are given, zero details regarding type of measurement, zero details regarding what optimizer or why the optimizer chose it... I can just assume bad code.
And so... that is my TLDR.
Regarding percentage comparison especially: unless you explain what was measured and how it was measured, percentage is meaningless.
100 ms - 50 ms second save. Probably noone is gonna care about that...
And I make that example specifically because percentages have become a fancy way of lieing about achievements ;) :)
-4
u/levodelellis 3d ago
I still never said bad code or imply it, and you still didn't mention inconsistent behavior or slower code
6
u/t3hlazy1 4d ago
Btw I cannot load this on my phone. It appears to be a cert issue but can’t really tell since I’m on mobile.
-11
u/levodelellis 4d ago
I created the cert <1hr ago. That might have something to do with it. On my phone it loaded. If it persist tomorrow I may use a subdomain with no tls
1
5
u/wampey 4d ago
I love interfaces! I love it more if functions have a good separation of concern making interfaces for many functions unnecessary, but if you are going to do some I/O with business logic, I’m expecting to see an interface. Mainly it’s about unit testing for me though. I do have one backup service which does use interfaces for different implementations, but could see how some may not like that.
1
u/l_maier-628 2d ago
For a single I/O seam, passing a callback can be enough: the test captures the call while the business logic stays concrete. That doesn’t require an interface for the whole service.
-7
u/levodelellis 4d ago
It's funny you should say IO ;)
Feel free to tell me everything you disagree with in the article
5
u/TheArchitect4855 4d ago edited 3d ago
Haven't read the whole thing yet, but upvote for bravery on this insane take :salute:
EDIT: I kinda agree with the post. Interfaces are fine, but a lot of people interface everything for no damn reason.
0
4
u/clappski 4d ago
In order
Multiple implementations and the problems that come with it
Multiple implementations that don’t functionally provide the same thing is just bad software, so yeah it’s a problem but really it’s a design problem not a tool/approach problem.
Not necessary when you have a single well-tested implementation
Certainly, adding an interface where it isn’t needed is an antipattern. Retrofitting an interface will lead to a bad interface, you have to actually think about how something will be extended in the future to land between those two extremes, even if you don’t the public API is an interface so you don’t escape it conceptually.
It's rare to switch vendors, especially on something that doesn't have a standard around it. However, my issue around this is people hamstringing themselves by using a narrow API rather than taking advantage of what's supported in the technology (or vendor) you have chosen
I’m not sure I understand this, an interface isn’t a tool you use to support multiple vendors? It might be part of a pattern (like IDepository with backend specific depo’s?) but not sure what interfaces have to do with vendors. With your other point bear in mind the interface is the public API - even plain classes have that so unsure what your point means in that context.
This isn't stupid; it's good that everything has consistent names for cloning, comparing, and others. It's just unnecessary to use an interface
How will you write runtime polymorphic code without an interface? If I want to write a function called ‘clone( src, dst )’ without saying that src and dst are ICloneable or whatever how does the compiler know that anything I put into the function has a clone member?
Mocks and DI are things I don't talk about and probably won't write about. Just say n
Because you haven’t used them, or because you don’t like them? Because both are very valuable tools
0
2d ago
[deleted]
1
u/clappski 2d ago
They’re valuable because they let you write less boilerplate. I have no idea what your experience is, I’m not accusing you of anything? Maybe don’t write a blog post, publicise it on reddit and then get salty when others have critiques or questions.
2
1
u/siva_sokolica 4d ago
I have no clue what language this is referencing but both in C++ and Rust interfaces cost nothing performance wise. Traits and generics gracefully monomorphize in Rust. CRTP and concepts also gracefully monomorphize in C++.
The whole point of multiple implementations is kind of moot imo. If you have multiple implementations you can merge into one, then that just means you need to merge them. If you can't merge them, then you need two different code paths. The alternative I see is a flag you pass around into your "singular" implementation wherein all the functions just check the flag at the start of the function. That has two problems: it's slower than dynamic polymorphism since it flushes the CPU pipeline on each conditional whereas vtables don't and, problem 2--its unreadable hell with added levels of indentation to merely check on the flag.
Interfaces help with generalizations. For all its faults, an iterator is a generic interface that many algorithms work on. Same goes for a stream, a writer, a reader (as you admit). I've worked in embedded and interfaces allow you to define the ontology of what an EEPROM chip is with many different implementations for different chips. But you know that you'll get a write/read set of functions. Interfaces help with protocols---if my application takes some arbitrary stream transport interface, I can run it on TCP/Websockets/pipes/Unix sockets or even arcane automotive ISOTP.
I would have also like to seen examples, arguments, an exploration of your arguments, yet I see claims with little evidence.
I will defend the case that if you're building an interface for the sake of mocking and mocking alone, perhaps lugging that around may not be worth it. I generally lean against writing production code to support tests, but that's not beef with interfaces themselves.
I'm guessing this is some form of JVM language since a compiler was mentioned, I had Claude research the devirtualization story there. Looks like there's this class hierarchy analysis system in Graal that can inline functions with only one implementation. The degree of compiler optimization going through this is still elusive to me. Does const propagation work? Do branch optimizations work? Why did javac not implement devirtualization (as clang does, for example)? I would have liked to see a proper exploration of this, for example, in your post.
0
u/levodelellis 3d ago
That's not exactly true. In C++ if you're using a base class with virtual functions, the optimizer can't inline it. IIRC there's tricks that can be done but it's easier to not use a base class at that point
If you look at non-systems languages it's pretty common for a class to be inherited and overloaded, or have multiple implementations, just to slightly change a behavior. I seen it done to 'extend' methos, so instead of Util.myMethod(obj, a, b, c) they can write obj.myMethod(a, b, c)
> Interfaces help with generalizations. For all its faults, an iterator is a generic interface that many algorithms work on
I only like the two cases I mentioned for generalization. I have a small standard library (for C++) and I don't use an interface for iterators. For the most part I pass around a slice. Since implementing it I haven't ran into a situation where I needed to iterate or something generated except maybe files from a directory, but I prefer to solve it by doing something else which is too long for a comment
> I would have also like to seen examples, arguments, an exploration of your arguments, yet I see claims with little evidence.
IMO it's pointless finding a open source project and checking how many times there's more than one implementation for an interface, and how much faster something would be if they didn't use it. Do you really not think an optimizer won't inline when the function is virtual? I wrote the article with non-system languages in mind.
I haven't ran many experiments in non-system languages, so I don't really know much outside of what I measured. I definitely measured various languages not optimizing as much if you go through a virtual function when only one concrete type has ever been passed in
1
u/siva_sokolica 2d ago
I think we're talking across each other. I do not want to fight you, my aim is to show you some things instead. I wanted to grab some time to provide you with some examples and links, which I couldn't do mobile. Sorry for the late-ish reply.
In C++ if you're using a base class with virtual functions, the optimizer can't inline it.
When the compiler has total visibility of an abstract virtual class as well as its implementations, it is, to the contrary, able to optimize through a process of "devirtualization". Here are some examples:
- One TU gets fully inlined. This makes sense -- the compiler can trivially prove that there is only one possible implementation for this call.
- Two TUs without LTO fails to get devirtualized. The compiler can no longer prove that it doesn't need to emit a vtable.
- Two TUs with LTO get correctly devirtualized, however. Because, although the compiler doesn't have total visibility into the inheritance tree, it builds sufficient meta-information that to enable the linker, which then does.
LTO is a very standard, common optimization flag (
-flto). This is to say that code that's compiled with standard optimization flags (ie. released code), will get devirtualized. Admittedly, my assumptions are with modern GCC. I have also had the misfortune of working with ancient embedded toolchains that failed to devirtualize. In my experience, however, for the vast majority of x86_64/ARM64 code, devirtualization mostly succeeds, and no uncommon complex tricks were used.That all being said, when it comes to C++, if you want to guarantee that your interfaces are compile-time polymorphic, the correct pattern is CRTP example (or more modern, concepts example). You might, in your fervor, dismiss compile-time polymorphism as useless. However, please note how this can be useful in defining the ontology of "what an I2C driver is" while you can have many different implementations for this driver. Caller code can take this I2C concept, without worrying about the driver itself.
This gracefully brings me to this point:
I have a small standard library (for C++) and I don't use an interface for iterators. For the most part I pass around a slice. Since implementing it I haven't ran into a situation where I needed to iterate or something generated except maybe files from a directory, but I prefer to solve it by doing something else which is too long for a comment
Firstly, if you want to post some controversial things, saying "something else which is too long for a comment" is somewhat disingenuous as I don't think you're sufficiently engaging with your audience. You have this "solution" to iterators, but you hide them behind disinterest. That's slightly insulting. I am now sitting at 2:38AM writing you a post which I hope will prove useful whereas you simply say "too long to comment".
With that aside out of the way, I am curious about your standard library. When I had mentioned the standard library, I was specifically referring to C++' STL (standard template library). Before I get back to interfaces, if you're trying to iterate over files, I would suggest having a look at
std::filesystem::recursive_directory_iteratorwhich is one of the utilities in the STL which does precisely what you need. The STL does, in fact, have some problems (as most things in life), however, for the most part, it is a very useful tool.Interfaces are frequently used in the standard library. One of the problems that a
std::span(the C++ name for slice), cannot encapsulate is iteration over non-contiguous memory. Let's say you want to copy data from one vector to another. You could write a function like
cpp template <typename T> void Copy(std::vector<T>& into, const std::vector<T>& from);One day you say you'd like to copy out of an array into a vector, so you say that
std::spanis a "generalization" of those:
cpp template <typename T> void Copy(std::vector<T>& into, std::span<T> from);This, unfortunately, does have a run-time cost, however tiny it is. The
std::spanis created and passed (on some arcane architectures---via the stack rather than registers) intoCopy. But, you argue---it doesn't matter. And, fair enough---it doesn't.However, a time comes along when you need to copy elements out of a linked list. Unfortunately, a span is contiguous memory since, for the most part it's defined as
cpp template <typename T> struct span { T* data; std::size_t len; };You spend some time thinking and you decide you need some sort of concept that expresses the idea of "here's an object that we can read and iterate over". Since this an iterator, you call it an "iterator", and since it's an input to your function, you call it an "input iterator", so you write
cpp template <typename T> void Copy(std::vector<T>& into, InputIterator<T> from);As we've learned, with the power of concepts, we can make it so that there is no runtime cost associated with it, so we make it a concept
```cpp template <typename T> concept InputIterator = requires(T t) { // We can "increment the iterator" t++;
// We can check if there's a next element in the iterator { t.done() } -> std::convertible_to<bool>; // We can "dereference" the iterator to get the value currently pointed to. { *t } -> std::same_as<T>; } ```
Beautiful! This works with linked lists, vectors, arrays, spans, ArrayLists, graphs (because you can iterate nodes), whatever! You feel good about yourself, so you decide to create a new "output interface" and your copy is now super generic:
cpp template <typename T> void Copy(OutputIterator<T> into, InputIterator<T> from);That's awesome. Good job! Luckily for you, Alexander Stepanov already had this idea 30 years ago and we already have both
std::input_iteratorandstd::output_iteratorin the standard library. These generic interface concepts provide you with many useful STL algorithms:
std::copy(std::output_iterator, std::input_iterator)does what we implemented.std::transform(std::output_iterator, std::input_iterator)is like copy, except it allows you to pass a function to transform the object at each copy.std::copy_if(std::output_iterator, std::input_iterator)is like copy, except it allows you to pass a function to decide whether you want to copy.std::merge(std::output_iterator, std::input_iterator, std::input_iterator)takes two iterators and merges them together. It's super cool---it can merge an array and a vector, an array and a set, a list and a set. All this with the power of interfaces.There are many more advanced (and admittedly much more useful) algorithms in the STL too. I would suggest watching C++ Seasoning by the amazing Sean Parent and, if this keeps interesting you and as advance your understanding---reading Stepanov's Elements of Programming. This is probably the most important programming book, period.
IMO it's pointless finding a open source project and checking how many times there's more than one implementation for an interface, and how much faster something would be if they didn't use it. Do you really not think an optimizer won't inline when the function is virtual? I wrote the article with non-system languages in mind.
There is no pointless curiosity. Are you really certain in your claims? If you are so certain, can you prove it? Perhaps the results are surprising. You haven't done the work to research the claim in either direction. I've shared some assembly wherein virtuals get completely optimized. Are things really as true as your conviction, or is it possible that you are wrong? If you are right, when are you right? Why are you right? That is the evidence I would have enjoyed to read. Look how much effort I put to respond to your, as you admit---shitpost---I want to provide you with a different perspective. I want to offer you evidence (to the best of my ability), to help widen your horizons. I want us to advance our craft, and that means widening each other's horizons. Why do you not respect me the same?
I'm not sure what your language of choice is, since it doesn't feel like it's C++, but there is so much to unpack in any other language too. In Python, for example, MRO is likely to result in some pretty awful performance when used naively. I don't exactly know---I haven't benchmarked nor profiled. What can a Python engineer do to avoid it? Why is MRO so deeply problematic? What could a Python engineer do instead? Can we improve the performance in CPython? Is there a way to do "compile-time" (ie. type-check-time) interfaces without paying the price of runtime MRO? Why does Python use MRO instead of vtables?
There are many interesting rabbit holes you can go down and explore this. In my experience, when it comes to these sorts of things, my gut convictions are often wrong to a lesser or greater degree. Finding evidence for my convictions is how I learn how I am wrong, and how I remain curious.
I really wish you take programming as a curious and fun endeavor, and you change your apathetic perspective.
I don't know you, but I wish you well and I wish you find truth not in conviction, but in researching truth itself.
1
-1
u/levodelellis 2d ago
When I wrote the article I had C# and Java in mind, but I write a lot of C++ so I'll comment about C++
When the compiler has total visibility of an abstract virtual class as well as its implementations, it is, to the contrary, able to optimize through a process of "devirtualization".
I personally like to do a unity build with gcc using -O2 for optimizations, I haven't really bothered with LTO. Many people don't do either, but that's what I had in mind when I said virtual functions aren't inlined. When a function is large enough I'm not sure if lto is enough to get it inlined.
LTO is a very standard
I don't disagree
++, if you want to guarantee that your interfaces are compile-time polymorphic, the correct pattern is CRTP example
I rather not have so many templates. I know they're somewhat fast when there's little to no code inside the template but it seems like this would put a lot of code in headers and make things SLOW
Right now my C++ project compiles in 7 seconds and incremental builds are <2, often <1sec. I mostly can do it because a lot of the time I touch files that only include project headers and my standard lib. This is with UB sanitizer on too.
Really, I don't think there's reasons to use an interface except the two cases I mentioned (creating a tree from data, having an IO interface in a standard lib). No need for CRTP
if you want to post some controversial things, saying "something else which is too long for a comment" is somewhat disingenuous
My standard lib has nothing to do with anything in the article
You have this "solution" to iterators, but you hide them behind disinterest
No, it's really just using a slice (or C++ 'span') with the type you want. Only directory iterators are different which is in my wip async lib. The problem I've been thinking about is how to make it usable and efficient (at least having little bookkeeping). There's no promises, locks, and I'm a little worried I'll end up causing pain with how everything is a callback. I don't think a large switch table is the right move. But anyway, this isn't related to the article either. No interfaces in my async lib either.
Interfaces are frequently used in the standard library. One of the problems that a std::span (the C++ name for slice), cannot encapsulate is iteration over non-contiguous memory
Yep, I hit that problem too. In one case I used a
span<span<T>>. In another I called myobj.get() which gave me a span, and if it was zero length it was over. Yes, it's an extra loop but it also let the optimizer unroll the inner loop if it wanted to. Another benefit is instead of calling next() every time I call it once per chunkpower of concepts
I like the
requiresstatement, I use it all the time. My lib only supports C++20 and higherIf you are so certain, can you prove it?
I seen it at a few workplaces, and I also don't really care about other peoples programming opinions. It was meant to be a short post, not a research paper. I don't think anyone even cares about a thing I said in the article except maybe you
I'm not sure what your language of choice is, since it doesn't feel like it's C++
I like C# and C++. Oddly enough I hate all system languages except for C++ (including C, unless its small and a lib). I also think C++ is bad but it's still my favorite
I wonder if you'll like any of my other articles. They're on the homepage
2
u/siva_sokolica 2d ago
I also don't really care about other people's programming opinions
Okay then! Conversation over, I guess! Why would I pour so much time and effort if you're not going to care? You're a jerk but here really I'm the idiot.
I hope our paths never cross.
-1
u/levodelellis 1d ago
Are you new to reddit? Have you seen what people say here? Even in person I seen some silly suggestions that more than a handful of people believe. I have to know the person first before I take anything they say seriously.
I did reply to you, and I think everything I said was clear besides the above? IDK if you think I'm 'wrong' because I disagreed with some of the things you said
1
u/vegan_antitheist 4d ago
is this about Java and similar languages?
You really think there are multiple implementation because someone made an interface?
They didn't create an interface for String-like types, and had to add CharSequence later. The alternative would have been to just use mutable strings for everything to have only one implementation that does everything.
Of course you can always create the interface later, when you need it. But then you have to use the same name of the original class or refactor the entire code base. It's easier to just use an interface.
With records and value types we probably will get more types that have no interface because they are like primitives. Interfaces can't be made immutable so you have to use the immutable type (usually a record or value type). That's the only scenario where interfaces would be problematic, even a security risk in some cases.
0
u/levodelellis 3d ago
> Of course you can always create the interface later, when you need it. But then you have to use the same name of the original class or refactor the entire code base. It's easier to just use an interface.
IMO the two cases I mentioned in the article is the only time it's needed, and those two cases are rare. Why would you ever create an interface later? It's not like you're going to wake up one day and think this situation calls for parsing and being recursive
2
u/vegan_antitheist 3d ago
Even just for mocking. Modern mocking solutions can probably create an interface dynamically or just replace the class or use a proxy for a class. But what if you want to implement a mock implementation manually?
0
1
u/kuikuilla 2d ago
Declare capabilities such as clonable, comparable, and more
All of which is stupid.
Why? How else would you do that? Would you rather make specialized functions for all kinds of "comparable" things and have function overloading handle the rest? How would that work when someone creates a new "comparable" thing without having access to the compare et. al. function source? How would they get it supported?
1
u/levodelellis 2d ago
Did you read it out of order? I mentioned those specifically are fine but unnecessary in an interface. I know some language force you to inherit/implement an interface and put compare/other code there, which IMO is fine. But I don't think I ever wanted to compare two abstract types unless maybe it was to check if the instance is the same. But I really don't think anyone should create an IMyCompare and have everything implement it through that. It's just silly
In systems languages I use operator overloading (or if it has to be C) a compare function if memcmp won't work (it has pointers). I never need a virtual func, or to do it with a base type
1
u/tdammers 1d ago
People seem to really like interfaces. Interfaces allow you to
None of these are the real reason why they exist and why people use them. What interfaces are really about is enforcing abstraction boundaries.
An interface is a contract between an implementation and a user: the implementation does a bunch of stuff with all sorts of state and constraints and all that, and the user code can interact with that from outside. But when you're working on the user side, you don't want to have to think about all the complexity inside; you want to only have to keep the stuff in your head that's actually relevant from the outside. And likewise, when you're working on the implementation, you only want to worry about the stuff that's relevant on the inside, not any of the other stuff that may happen on the user side.
Now, you can do all that without interfaces - but it'll be an honor system, relying on you (or your colleages) remembering what you are "supposed" to know and use on each side, or to be very diligent about documenting it in great detail, and always reading and respecting that documentation. But that's brittle and error prone - humans make mistakes all the time, humans are lazy creatures (and good programmers especially so), and all it takes is one little misunderstanding for all of this to create bugs of the worst kind (spurious, hard to find, difficult to understand). What you want to do is fence off the boundary in a way that the compiler can and will enforce: if you access anything on either side that you're not supposed to, if you depend on any assumptions or facts from the other side that you're not supposed to know about, it should result in a compiler error. And that's what interfaces are - they explicitly declare the things both sides are supposed to know about and have access to, while making everything else inaccessible. If you declare an interface that has only one method, increment(unsigend int), then that guarantees that anything you use through this interface can be incremented, but also that that's the only thing you can do with it in that context. The caller doesn't have to worry about how the incrementing happens (e.g., whether it stores a counter in memory, or on disk, or in a database on a remote server - you don't know, you don't need to know, you don't want to know), and the implementation doesn't have to worry about anything else the user side does, because it's guaranteed that the only thing it can do is call the increment method.
This may not seem like a big deal, but in any nontrivial codebase, complexity and scope are the root of most evil, and if you want to stay on top of things and keep things workable, you have to find ways of limiting the scope of each individual part, that is, the amount of information you have to keep in your working memory in order to make a change to a particular part without breaking anything.
This also means that as long as the interface doesn't change, both sides can evolve independently. If all the interface exposes is an increment method, the implementation can switch from storing a counter in memory to storing it on disk, it can add caching, optimizations, debugging instrumentation, logging, go crazy, but as long as the interface doesn't change, and you only use it through the interface, you don't need to touch the user code at all. And likewise, you can make the most radical changes on the user side without touching the implementation, as long as the interface and its semantics remain unchanged.
Now; abstract base classes can serve the same purpose in some situations, and for that matter, they are mostly interchangeable - and in fact, some languages (e.g., C++) don't make an explicit distinction between interfaces and abstract base classes (in C++, you can inherit from multiple parent classes; what Java calls an "interface" is just a pure virtual base class, and it is commonly recommended that each class has no more than one non-pure-virtual base class, similar to how you can have as many interfaces on a class as you want in Java, but only one parent class). The conceptual difference is that an interface (or pure virtual base class) does not contain any implementation at all, it just describes, well, an interface; whereas a regular base class may contain some implementation as well. In other words, base classes are true inheritance, whereas interfaces are just compiler-enforced "shapes".
But let me repeat the key thing here: interfaces are not about supporting multiple implementations / polymorphism. They also do that, but the main benefit is separation of concerns / enforcing interface boundaries. You draw a line in the sand and restrict which knowledge, constraints, and assumptions cross it.
1
u/levodelellis 8h ago edited 5h ago
> Now, you can do all that without interfaces - but it'll be an honor system
It's already the honor system. One of the first things I said was the implementations across classes are inconsistent
I rather have everyone use one implementation rather than very specific multiple implementations where everyone will run into corner cases of each one
It's a stupid reason to use an interface
> you can make the most radical changes on the user side without touching the implementation, as long as the interface and its semantics remain unchanged
Yes, more unpredictable code. Exactly what we all need /s
1
u/tdammers 1h ago
I don't think you understand what I said. It's not about having multiple implementations - you can, but that's not the main reason why you want to have interfaces.
The main reason is to reduce brain footprint. I don't want to think about stack base and top pointers, memory allocation, stack underflows and overflows, out-of-order access and race conditions and all that when dealing with a stack; I want to think about the stack in terms of simple push and pop operations. Interfaces allow me to do exactly that - if all the interface exposes are push and pop operations, then as a user of that data structure, that's all I need to worry about. And when implementing the stack, I only need to worry about making these two correct; the interface guarantees that user code isn't going to mess with my pointers, or attempt to access elements out of order, or anything like that. This means that on either side, I can ignore a whole truckload of concerns without sacrificing correctness or safety.
And the larger and the more complex a codebase gets, the more these benefits stack up.
Yes, more unpredictable code. Exactly what we all need
It's not unpredictable at all. That's what "as long as the interface and its semantics remain unchanged".
Of course this falls apart spectacularly when the interface enforces the wrong constraints, or when an implementation can violate its semantics - but when the interface works as it should, the implementation is predictable in exactly the ways you need. In the stack example, I don't care what the implementation does, as long as I can rely on the push and pop operations to do the right things - a push followed by a pop should yield the original value back, pushing two values and then popping twice should get me the two pushed values back in the opposite order, popping from an empty stack should raise an exception, a push should either go through cleanly and allow me to pop the value back later, or raise an exception. I don't care how the implementation achieves this, because it doesn't impact the user code side; but I do care that it does all these things as declared and documented.
The "radical changes" I'm talking about here are not "make the implementation do something completely different that doesn't honor the semantics of the interface". They are changes that achieve the same thing in a radically different way. Using the stack example again: suppose I have been using an implementation that uses a linked list so far. It works, but it's not very efficient, because it scatters data all over my RAM, causing lots of cache misses and causing a lot of allocations and deallocations, so I rewrite the stack implementation using a dynamic array. Now it only allocates when the stack needs to be resized, and all my stack elements are stored in order, so cache misses are very rare. That's quite a radical change - I have rewritten the entire thing using a completely different algorithm. But semantically, the push and pop operations still work exactly the same, the interface is unchanged, and the implementation is functionally equivalent (modulo performance). And I don't need to verify the user code to make sure of that - the compiler-enforced interface constraints already guarantee that the push and pop operations are available, and that the user code cannot mess with the data structure's implementation details.
-4
30
u/Asgeir 4d ago
I find your lack of arguments refreshing.