Hacker News new | past | comments | ask | show | jobs | submit login
Interfaces all the way down (jjain.substack.com)
94 points by jinay on July 23, 2023 | hide | past | favorite | 67 comments



I agree that interface is key to not just software design, but design in general. Designing parts in isolation, you can do pretty much whatever you want. But people being able to gracefully handle where and when two parts collide is what makes the software world go round. One of my favorite bits on interfaces is this Rust Koan[0], which has a funny twist of pragmatism at the end.

It's a shame too many developers think this huge idea is just the interface keyword, or OOP, or even just the `object.method()` syntax. I hear dot-autocomplete come up in conversation almost every day now. When I was in college people asked me "How can you even use C? It doesn't even have classes...?", the implication being you couldn't encapsulate your code at all, and I get the same shrinking feeling when people talk about dot-autocomplete as if it's synonymous with interface discoverability. But it's really just one particular implementation (heh) of a much broader and more abstract (heh) idea. It's like calling all tissues Kleenex or all sodas Coke.

0. https://users.rust-lang.org/t/rust-koans/2408/3


Dot autocomplete is only one option, but I’ve never seen anything work as well for discoverability.

Just talking about it in terms of functions, you usually have at least one argument. Then you’re trying to find the right functions and remaining arguments. Single dispatch and the dot syntax support this very well! I’ve never seen it, but maybe you could do the same thing with functions. Just put in the first arg, maybe more, and then the function name.


I’m not sure in C but in Python and JS the encapsulation can be at the module level rather than using a class. So you still get dot hunting by importing the module.


>Great product designs require no manual, and similarly, great interfaces need no documentation. Imagine having to read a manual on how to use a coffee mug.

This could not be more wrong.

Not everything is easy. If a library is addressing a complicated domain, solving by definition a complicated problem, it is fine if it requires some learning.

When did expertise and learning become bad things? If software is an engineering discipline, why would people in it ever promulgate the idea that any random cog can step in to any “engineer”s shoes?

Rich Hickey analogizes this mentality to the world of music, where it taken for granted that learning an instrument requires a lot of study:

“ We start with the cello. Should we make cellos that auto tune? Like, no matter where you put your finger, it's just going to play something good, play a good note.

“[Audience laughter]

“Like, you're good. We'll just fix that.

“ Should we have cellos with, like, red and green lights? Like, if you're playing the wrong note, you know, it's red. You slide around, and it's green. You're like, great! I'm good. I'm playing the right song. Right?

“ Or maybe we should have cellos that don't make any sound at all. Until you get it right, there's nothing.

“ [Audience laughter]”

https://github.com/matthiasn/talk-transcripts/blob/master/Hi...

If something can be made easier without undermining its integrity, great. Not everything can be made as easy as drinking from a cup, something most 3 year olds can handle. If you think hitting dot in your IDE and choosing among the options is as much as you should be required to learn, you are asking to use NERF toys instead of power tools. Sometimes you need to read things, welcome to adulthood.


Opining that "everything should be simple" is a sign of having been in the management class for too long and not being in the weeds.


Most people would do well to learn more than they have, but also most things could be a lot simpler than they are.

Honestly, the best way to achieve the latter is probably to encourage the former.

(Which will never happen as long as companies prefer to hire N interchangable people than M well-trained people, even with M ≪ N.)


Just to note simple does not mean less complicated [1], easy is an enemy more often.

[1]: https://youtu.be/LKtk3HCgTa8


I get where Rich Hickey is coming from, but his analogy does have gaps. Not every piece of music is for a cello, and in fact, much of it is for discretized and not continuously pitched instruments such as pianos and guitars, which also take out the variable of bowing.

The innovation of discrete pitch was practical -- for many songwriters, the point is to get out the song and not focus on "implementation details" -- and I think there are a lot of similarities there to software.


> If you think hitting dot in your IDE and choosing among the options is as much as you should be required to learn, you are asking to use NERF toys instead of power tools.

It depends. It's really great when that does work out. It's traditionally my first step (copilot frequently beats me to the correct use of an unfamiliar API today), second step being the documentation if that's not enough. Step 1 is almost always sufficient.


> His advice contradicted my idea that a great senior dev should learn better communication or management.

What are interfaces if not ways to communicate?


Yes, and not just that. A lot of management boils down to aligning teams' responsibilities with their interfaces, and committing the teams to a baseline of usability of their interfaces. See also Stevey's Platforms Rant (https://gist.github.com/chitchcock/1281611)


Indeed. A way to communicate, reduce mistakes, all the while; hopefully; without restricting the task at hand.

Basically, the opposite of a modern microwave.


Sounds like you've got a personal vendetta against modern microwaves.


I have a personal vendetta against almost all modern UIs, coffee machine and printers included, like the sibling comment joked about.

The lag on key/touch presses. The stupid and overcomplication of the controls. The simple made difficult. The hard made impossible.


If they have something against modern microwaves, I hope they never discover office printer interfaces. It may be all too much.


Nailed it right here. Lots of people don't think of it this way though :)


also they aren't mutually exclusive lol


You never come up with the right interface at first because there is no right interfaces. There are better interfaces, but you usually need to encounter more situations to find it. Balancing up front time designing an interface with velocity to test it is a principle problem of software development.


I think good software interfaces are essentially always extracted from already-working systems, rather than being designed. The tricky part is figuring out when to do that and how to successfully advocate for it.


Agree with this. Starting with a "well-designed set of interfaces" usually results in half of them being unused having only one implementation and the other half being rewritten because the requirements changed (worst offenders are interfaces which only have one implementation, not even used for mocks in tests, the runner-up being interfaces accepting multiple boolean parameters).

On the other hand, finding an opportunity to introduce a common interface to a familiar codebase feels like leaving behind a mental burden. Sounds crazy I know, but introducing order into a chaotic mess is just relaxing to me, especially when I know I will have to maintain it.


There's another style of programming that has much of the required interfaces predefined in the language itself. A set of universal interfaces that conceptually work universally for all programming.

It's mathematical interfaces.

Commutativity, Identity, Associativity, Ordinality and more. For these interfaces, it becomes less about "designing" interfaces with gut intuition, guessing and checking... but more about finding the final design via calculation.

Math is known to be universal so it makes sense why mathematical interfaces have such wide application. When you use mathematical interfaces and compose everything along those parameters... it's no longer about "you never come up with the right interface at first"... That concept becomes less relevant.

It's hard to agree with me when I'm just saying it here. It has to "click" after you tried it with a language that supports this type of programming first class.


I have never come across a codebase written in this style that is any good. I think people inclined in this direction usually seem to fail to reify the domain they're working within, preferring to leave everything in terms of the mathematical primitives. This is much harder to work with than systems that have thought through the concrete interfaces useful in their concrete domain. (And sure, implementing those using "mathematical interfaces" is great.)


I actually somewhat agree. I think there's a gradient here between modularity and useability. The more modular something is, the less user friendly it becomes.

The problem the GP is talking about here is a modularity problem right? He designs (aka guesses) the interface and finds out later that his guess was wrong. Mathematical interfaces deal with this problem better.

However mathematical interfaces are less user friendly and less intuitive, especially for someone not familiar with mathematical interfaces.

The reconciling solution is that the public interface can be very domain specific and made narrow in usage. The logic underneath this public interface can remain mathematical and therefore more amenable to future changes.


Yep!

> However mathematical interfaces are less user friendly and less intuitive, especially for someone not familiar with mathematical interfaces.

Actually I think it's less useful for people who are very familiar with the mathematical techniques as well.

Pure math also specializes things by domain. Certainly experienced mathematicians are capable of seeing the generalism underneath the specialization and are able to re-derive it, but to make progress they mostly don't do that, they build on top of the specific "interface".


I hear where you're coming from, but I also have had to operate a poorly written Haskell codebase that used these concepts well and still has poorly written code. I once had similar views, but eventually, I let them go after finding my zeal for functional purity a panacea or a self-salving truism meant to retain my sanity after dealing with monstrosities written in traditional imperative languages.

I think that the sad truth is that ugly code reflects an impedence mismatch between the model in the codebase and the reality of how that software would be used, which wouldn't be obviated by taking a more formal or calculated approach to constructing the code. I've seen bad code written in every language and good code written in most languages. Moving state mutation to edges just lifts and shifts the hard part, it doesn't make it any easier.

But to conclude this dissent with a point of agreement, I that the style of programming you're talking about already exists and is common, is called SQL, and is built on very concrete primitives in set theory. You get ACID properties which give transactional isolation, at-rest data integrity based on normalization and uniqueness constraints and purely functional data transformation that can live in a purely functional language (SQL) rather than inside whatever application level language is chosen. And for what it's worth, I have seen enormous, universal improvements in codebase quality by "lifting and shifting" computation that could more comfortably and ergonomically live inside SQL to occurring there.

To your point, I've seen this occur much more successfully when utilizing an ORM approach that is more functional (query builder flavor) than imperative (object mapper flavor).


> Math is known to be universal

Is this really the case? Or what do you mean by this exactly? Gödel’s incompleteness does apply to it as well, and we can for example only determine the Busy Beaver number up to a fix point no matter what. Though it probably doesn’t matter from a practical perspective, I would just like to know in what sense do you mean universality.


Quite so. I’m currently reading Algebra Driven Design by Smart McGuire. It’s a valuable perspective.


Which languages support mathematical interfaces? Is this similar to functional programming?


What's not mentioned in the comment you're replying to is the postgraduate-level type theory you need to learn and constantly refine to work with these languages at a practical level. So be aware that even if you put the time into learning them, it's unlikely you'll get to use them at work.

Look up Coq, Agda, Lean, and Idris. I would start with Coq, it's the most used. Idris is more like Haskell and programmer-oriented.

Edit: Nevermind, apparently they were just talking about Haskell...


I'm more thinking in terms of logical primitives for the design of modules and components that can be composed, decomposed and recombined. I'm thinking less about proof based correctness.


On some level, all of them do. Programming lives in a grey area between human language and discrete, symbolic logic.

The way in which we get to an "application" is in designing interfaces that look more like the domain, and less like the implementation. If you can define the math you're using symbolically, you can apply it directly to express ideas from, e.g. linear algebra, set theory, graph theory. And libraries exist for all of those things - you can make the interface more convenient with additional syntax and compiler assistance, and you can frame the program in terms of theorem-proving logic(which is the realm of stuff like Coq) which provides an extra degree of assurance that the program does the thing you defined it to do by adding more detail to that definition, but often the problem requirements fit in the realm of "just tell the computer to do things" - and so imperative code is the default, everywhere.

But we can also take concepts like "name", "job", "age", "ethnicity", "gender", and enter them into a computer. And all of those are human ideas, socially constructed and philosophical in some degree. Mathematics doesn't help us express the essence of those ideas, it just tells us of ways to symbolize the tokens involved, which can be made relatively general and flexible but all of which ultimately stem from a predesigned enumeration of options like the codepoints available in UTF-8 or the range of values in a 32-bit integer.

And a lot of the mathematical stuff is subsumed by the social/philosophical in practical application: we agree that the data has some kind of truth to inform us, because it's compatible with our framework for understanding it. And if you have a setup that fits the model of computing, something like taking a sensor that emits numeric values at a regular frequency and processing the output into some kind of signal - then you can program mathematically all throughout. But if you're mostly dealing with human language, you're constantly hammered with interface problems for other reasons.


Human concepts can be placed under mathematical interfaces.

Inversion for gender

   ~male = female
Ordinality for human hierarchies:

   CEO > manager > worker
Commutativity for human action:

   Punch human + kick human = damaged human
   Kick human + punch human = damaged human
Mathematical interfaces are different from mathematical primitives which I believe you have mistakenly combined into a singular concept in your response.

By fitting human concepts into mathematical interfaces you develop a sort of algebra dsl for the language allowing you to apply all mathematical theorems of the equivalent algebra to the domain. Those theorems are the generalities that improve design by improving modularity.

Suddenly for ordinal concepts I can use a general min or max function across all domains. By using mathematical interfaces I am in the realm of the ultimate generality. Normally people would be writing some form of equivalent logic to derive the lowest ranking human in a hierarchy when really the concept of min covers it.

These basic mathematical interfaces that apply to basic numerical logic are found to be expandable across domains. There's no proof or logic as to why these interfaces happen to be more universal. It's just a gut feeling after using this interfaces more that they happen to be extremely universal. Thus there's no way I can prove to you what I'm saying is correct, you ultimately have to try it.


The language of math itself is functional. In fact "functional programming" is basically programming as if everything was math. Think about it, does math allow for variable mutation inside a mathematical expression? No.

Interfaces that mutate internal values do not exist in mathematics. So, in essence, yes. Mathematical interfaces only support functional operations.

Haskell would be the language.


Is this really true? Have you written idiomatic Haskell before? Not trying to question your familiarity if so, but a large part of doing so is utilizing interfaces that do just that.

One of the most critical parts necessary to fully grok idiomatic Haskell is how it uses mathematical interfaces, specifically category theoretic interfaces to structure internal state mutations -- specifically Monads and other structures. This specific interface utilizes two properties, identity and associativity, to do this and create one-way, sequenced computations still couched in formal rigor and laws.

Contrasted to other languages, in order to do any useful I/O in Haskell, one needs to understand and use Monads. Of course, the utility of the Monad includes but is not limited to just this use case.


>You never come up with the right interface at first because there is no right interfaces. There are better interfaces, but you usually need to encounter more situations to find it. Balancing up front time designing an interface with velocity to test it is a principle problem of software development.

You never come up with the right medicine at first because there is no right medicine. There are better medicines, but you usually need to encounter more outcomes to find it. Balancing up front time designing a medicine with velocity to test it is a principle problem of medicine development.

Thank you for your template


I agree that there is no absolute best interface, and a good interface will be battle-tested and motivated by real problems. As with all design, there are certain intuitions you develop over time that are widely applicable. You can eliminate a lot of errors up front that way.


Totally. Frankly, you will never find the perfect interface. It’s an ever evolving process that changes as the market and user behaviors and expectations change.


One of my favorite relevant quotes, whether you’re talking about code or user interfaces:

The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise. — Edsger Dijkstra


I love citing this one (from https://spacecraft.ssl.umd.edu/akins_laws.html):

> 15. (Shea's Law) The ability to improve a design occurs primarily at the interfaces. This is also the prime location for screwing it up.

While this is about hardware interfaces, software interface fulfill the same exact role, and the same exact principle applies. The design is in the interface. The implementation is just grinding until it's done, because the decision of how something will be implemented are already determined by the interfaces and the information we have on the infrastructure we'll be doing the implementation in.


It took me until the last 3 paragraphs to realize they meant ux and not Java interfaces. Maybe I've been writing too much Java recently but I feel like it could be clearer.


In a way, I meant both. The buttons and sliders of a UI are the parameters and functions of a Java interface. I appreciate the feedback nonetheless.


I totally read it as both! And indeed, I think the insights apply to both.


A good interface guides the user to doing the right thing. To quote Brad Adams (who borrowed it from Rico Mariani)[1]:

    Rico called this the Pit of Success. That concept really resonated with me. More generalized, it is the key point of good API design. We should build APIs that steer and point developers in the right direction. Types should be defined with a clear contact that communicates effectively how they are to be used (and how not to).
This concept is also tied closely with the concept of "making illegal states unrepresntable," popularized by Yaron Minsky[2].

For example, a document workflow system might naively model Document as a single entity that has all possible fields for all possible states. This could result in the need to raise exceptions (or return error codes) when the Document state is invalid, e.g. you can't approve a draft document until it's submitted. A better API models each state separately (e.g. using a union type, if your language supports it); so you'd have `DraftDocument`, `SubmittedDocument`, `ApprovedDocument`, `RejectedDocument`, where only `DraftDocument` would offer a `submit` method, and `SubmittedDocument` offer a `approve` and `reject` methods, returning an `ApprovedDocument` or `RejectedDocument`, respectively.

[1] https://learn.microsoft.com/en-gb/archive/blogs/brada/the-pi...

[2] https://blog.janestreet.com/effective-ml-revisited/


And what happens when you have orthogonal traits ? You have to implement a matrix of all valid permutations ?

Sounds like "you should wear a straightjacket because you might fall down when running with scissors" kind of a solution.


> You have to implement a matrix of all valid permutations ?

I think the example was pretty bad, however the union type being referenced is that matrix, afaik.

Let's start with a different example. I have a business process that runs off a plan-document. The plan-document includes things like caps for number of times things can happen over a period (including, all-time), or day parting specifying when things can happen at all (only saturdays), and absolute controls like "active" or "inactive", on top of the creation/deletion paradigm.

When the process runs, what is the state of the business process at any given time? "inactive because capped out" or "inactive because day parting"? This set of labels will grow, in permutations, over time as well as discoverable business needs. eg Was it "inactive" because it was created that way or someone manually deactivated it with an update? Now the program needs to reference a history of changes as well as referencing run-state.

A business that is building a new product, especially within a domain that few people understand, requires more than building a set of states and assume they will always meet the needs. This is a recipe for lots of large-scale rework and bugs. A set of states (be it a bitfield or json blob or whatever) fed into a rules engine (or component) will likely be more extensible over time than looking at simple labels.

Granted, this is predicated on the software being a non-trivial system.


> And what happens when you have orthogonal traits ? You have to implement a matrix of all valid permutations ?

No, you'd use polymorphism. As a silly example (but it demonstrates the technique), look at the phantom-typed builder pattern, where you can set the fields in any order you like but you can't build until you've set all of them.


Then you do something else. Engineering is a matter of tradeoffs. There is no silver bullet, we don't throw off an approach because it doesn't solve everything.


> Great product designs require no manual, and similarly, great interfaces need no documentation. Imagine having to read a manual on how to use a coffee mug.

It's a fallacy to assume a perfect interface needs no documentation, just as it's a fallacy to assume perfect code needs no comments. Most interfaces are far more complex than a coffee mug. In reality, the more complex something is, the more documentation is needed to describe how it works, how to use it, how not to use it, and why it works that way.


Yes and no to this, IMO. I think the best interfaces have a "happy path" simple usage that doesn't require documentation to intuit one's way through without any dangerous footguns but also doesn't do very much, and then an arbitrarily deep path into more powerful use cases requiring more detailed documentation.


I wrote an ebook [1] about interface design for engineers after seeing time and time again that the best interfaces I worked on all came about when engineers and designers both contributed to the process.

[1] https://uidesignforengineers.com/


I would add that interface design is so hard you have to assume you will get it wrong, and think about adaptability as a design goal.


Have we reached adaptive interfaces yet? The kind that mold to the users preference based on the setup of other interfaces they us? Would be nice to have one interface to rule them all, tailored for everyone.


This sounds awful


You must design interfaces.


In my experience, applying the same mindset and efforts to interfaces for which the principal user is not a developer also yield similar benefits.

Good interfaces are retroactively obvious. Not like my modern microwave.


Maybe not just designing the interfaces but their boundaries?


I should also point out that comments are a code smell. The vast majority of the time you feel the need to add a comment, you should probably refactor your code into a well-named function — whether a closure or a method. Your code should read well even without comments!

(There are some exceptions, that have to do with notes eg for security implications, or optimization techniques, or side effects. And also to document parameters in duck-typed languages. But even those should be put into structured comments, like DocBlock or YUIdoc or whatever kids use these days.)

I have learned this with time. Like when you are procrastinating and putting off doing something, that’s a sign you need to attract partners.


---I should also point out that comments are a code smell.

I would encourage you to ponder this a little more. I work on scientific code bases and there's just some processes and math that requires line by line exposition beyond what the code can offer.

There's also nothing worse than digging into a library to view the code and just being left with a ton of small classes and no notes on how it's all meant to interact. Comments here and there are helpful.


The book the post mentions, A philosophy of Software Design, has a lot to say about having lots of small classes. It’s not in favour. That book is also strongly in favour of comments, lots of them. This book is well worth reading.


Instead of here and there, why not write a manual that ties it all together? As you said, “a ton of small classes” might not be enhanced so much by “a ton of small comments”


I started out writing Java. I have seen a lot of people complaining about the java docs, but to me they are the best docs I've ever used. Python isn't even close, JS is just a bad language in general, C# docs are pretty good but I still prefer the java docs even though I use C# professionally.

My point is, the java docs are just comments. It's a html page (or you can explore it directly from an IDE) generated from the code and its comments, it describes the code and how to use it. Almost anything I want to achieve I can achieve by exploring the java docs. The type system itself tells me which things fit together and if anything isn't obvious there are helpful comments detailing almost anything I could wish to know.

In my opinion, this is the way to do it. You don't have to painstakingly document every little class you write, just the "public facing" stuff. Not only does this help you or whoever else will be working with it in the future, it also documents the intent of the class/method much better than decent names can. People talk about "self documenting code", I haven't seen much of it. Most of the code I've seen fails miserably at being self documenting.

Keep the "self documenting" internal. If you write a library to be used by other code, document the external API and leave the internals more loosely defined. At least that's what I want when I use someone else's library. I don't need some user manual that explains a few use cases I don't have, I just want simple, to the point, java docs that explain exactly what the code does, what the parameters represent etc. I can figure out how to put it together on my own if I just know what all the pieces do.


What a terrible advice (sorry). Great comments are not about "what does this code do?". They are about "why does it do it?".

In some domains like scientific or high performance computing they are absolutely necessary to a) provide context to others b) make you not forget c) prevent unintended consequences and regressions.

You reorder some operations or do seemingly weird things for improved performance, numerical stability, or address some not easily testable bug? If you don't comment it, this knowledge will be lost and someone might obliterate it during a refactor.


You are overgeneralising. There is bad commenting, yes, but you yourself listed several examples of when comments are needed: if we continued, this could be a long thread.

Sometimes you need comments explaining an API that you are using, because its workings are not immediately obvious. Then the smell is not coming from your code but from the code you're calling.


That was actually another thing I gathered from the same internship. We had obnoxiously long variable and function names, but that meant our code read like English. Any comment would just be rephrasing how the code read aloud.


Comments aren't for "what does this code do?", they are for "why does this code do this?".


Agreed. I don't advocate for no comments in code, just that many comments tend to explain "what" as a consequence of poor naming.


Indeed. We agree. People shouldn't do that :)




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

Search: