Interfaces all the way down
jjain.substack.com
jjain.substack.com
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.
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.
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.
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.)
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.
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.
What are interfaces if not ways to communicate?
Basically, the opposite of a modern microwave.
The lag on key/touch presses. The stupid and overcomplication of the controls. The simple made difficult. The hard made impossible.
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.
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.
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.
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...
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.
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 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.
> 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".
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.
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).
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
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.
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
> 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.
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...
Sounds like "you should wear a straightjacket because you might fall down when running with scissors" kind of a solution.
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.
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.
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.
Good interfaces are retroactively obvious. Not like my modern microwave.
(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 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.
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.
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.
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.