On Scaling Mental Models(buttondown.email) |
On Scaling Mental Models(buttondown.email) |
- it introduces its own vocabulary, it looks like gibberish until you build up the context
- since the group is small and gifted they have no problem creating and using the power tools of their own
- there is no documentation so your only way is through experimentation and direct communication
- the folks who have built it had to move on to solve another (bigger) problem, leaving no traces of what the vocabulary is, where the design was headed, what should be changed with the new requirements and again next to zero documentation
Some of the best ergonomic improvements to my own code have come from trying to explain why my code is so stupid and figuring out that there is a fix that’s easier than this apology. When it’s three people there are no apologies even offered, and almost no feedback.
Current modern languages are built around the false dicotomy that you have to choose between one of two representations, like those in the provided example:
1. mydict1.merge(mydict2, handle_duplicates=(key, v1, v2) => v1 + v2)
2. mydict1 + mydict2
A mid-level language will use the first style so that programmers will know what's going on, but then the team will be stuck to using that style everywhere even when they already understand that it's "oh, just adding two dictionaries". Powerful languages let you use the second style but then, as the article explains, it's hard for a new programmer to tell what's the mental model behind that complex operation.
A good powerful Lisp-like language should allow you to build the second abstraction in terms of the first, and use the simple syntax everywhere, but then switch between both representations easily.
I.e. you should be able to inspect the meaning of the more abstract syntax by expanding its definition in place and see how it works in context. Programmers typically address this shortcoming with support of powerful (yet ad-hoc) IDE's runtime debugging tools. But why that approach is not integrated into the language itself, I have no idea.
The problem as I see it is in how they represent the data in a way that makes one think of linear algebra. Refinements brought the naive original implementation down to cubic time, and the literature, as far as I can tell, stops there.
But here’s the thing, when I tried to unpack the logic to figure out what is actually going on under the hood, I realized that a quadratic subproblem could be restated as a sorting problem. Sorting is very much nlogn these days, if not lower due to radix sort being an option.
Which means that this heuristic has the same computational complexity as the naive solution. I’m not saying I’ve discovered anything new, except perhaps that this algorithm is homomorphic to another one. It’s just that it’s so stuck in one representation that you can’t see it for what it is unless you stare pretty hard at it.
But don’t abuse math. Like redefining summation to mean any old thing under the sun. Least of all when it’s a fricking union, for which the math symbol is ‘∪’, not ‘+’.
Otherwise, article is basically arguing we should drag everyone down to the lowest-common denominator “to be safe”, rather than allowing individuals and groups to raise themselves up as appropriate in order to tackle their given problems more effectively.
That’s group-think, authoritarianism, and reducing expert knowledge workers to anonymous plug-n-play “human resources”. What’s the software equivalent to “Churnalism”? Because that’s how you get the software equivalent to “Churnalism”.
Now, if you want to talk about the criticality of Human Communications, and how utterly atrocious many programmers are at it, even amongst their own (never mind with “outsider” stakeholders like users and management), then I’m all on board with that discussion. Good and bad use of abstraction (trade jargon), effective knowledge “transfer” (really reconstruction), and successful team collaboration (especially heterogeneous teams, which are vital to solving the actual problem at hand). This is epically important stuff, and this profession has so much yet to learn.
But rationalizing away “this is how we’ve always done it so this is how we will always do it”? Don’t be surprised when the rest of the world treats you as cheap, easy, disposable code-monkeys, paying you peanuts and zero respect. That’s the deal you bought into yourselves, and you have no-one to blame but yourselves.
Basically anything consumer-facing from the past 15 years, especially if it's "mobile"
Are you asking why _all_ programming languages don't make their own Abstract Syntax Tree a 1st-class concept for self-reflection and manipulation of its own syntax like Lisp?
Because there are tradeoffs to that approach. If one thinks there are zero tradeoffs to Lisp's philosophy of "code is data", I contend one doesn't fully understand Lisp. Yes, self-inspection is a powerful device but even Lisp programmers[1] keep inventing new languages for others to use that do not have Lisp-like ASTs as 1st-class concepts. I ask people to really think deeply on why that's been happening for decades and comment with any insights.
[1] e.g. Guy Steele a Lisp expert, works on creating the new Java language at Sun in 1994.
No, I'm asking why there isn't _any_ programming language (that I'm aware of) with an integrated IDE where you can make static code rewrite expansions (replace an function call instance with its definition) at writing time, and instead depend on waiting until runtime to have an in-memory stack and navigate between functions to see how a function call will run.
In my Alma Mater, at the time, there was a third year course in programming languages that had Scheme exposure. The follow-up fourth year course in compiler construction was all C hacking with Lex and Yacc, and that was that.
I was able to wriggle out of taking the prerequisite, too.
In a dynamically-typed language you’d get some kind of runtime error.
Just think of (+) like any other function and what would happen in your language if you call a function with types that the function doesn’t support.
Taking the example of adding maps, you (where "you" means the language designers or language community) just need to define what `+` means for primitive types and the composition follows naturally. What does adding maps mean then? It means adding together the primitive types found under each key. If everyone agrees 1) there is a `+` operation and 2) what it means for primitives, there isn't much room for confusion. At some point you might want to give this concept a name and then you have basically reinvented monoids.
This is one of the differences between my experience in untyped functional languages (primarily Scheme) and inexpressive languages (primarily Java) and typed functional languages (primarily Scala). In the former each abstraction was a perfect snowflake crafted to the specific situation. In the latter we just reuse existing abstractions. The problem with the former is you have to learn the meaning and idiosyncrasies of each new abstraction. In the latter you can leverage your existing knowledge in new domains.
That's because it has tons of funding and support from Google, not because people did experiments and found that it worked better.
Disclaimer - I do not use Go.
I have heard of teams using "communication" to bridge these gaps but I know little of that technology.
I've never done pair programming (other than one project when I was in school and it was required -- bit of a nightmare IMO), but I thought the way it typically worked was you switch off who "drives" (i.e. actually types the code) in long intervals. So why couldn't each coder just use their own preferred programming environment? Do they not each have their own machines?
> Each of you can open files, navigate, edit code, highlight, or refactor - and changes are instantly reflected. As you edit you can see your teammate’s cursor, jump to the location of your teammate’s carat, or follow their actions.
https://code.visualstudio.com/blogs/2017/11/15/live-share
I've found that letting both participants inspect and edit code together collaboratively is useful when one participant is acting in more of a teaching role. When the student gets stuck, you can jump in and demo right in the editor instead of dictating code or ideas. It's useful if it's not overused, like the second brake on the instructor's side in a driving lesson.
But if both participants are of equal ability, it just becomes annoying to have dual controls in my experience. Hell is watching other people use computers, but jumping in to effectively grab your colleague's keyboard and mouse and say, “look, I'll do it” can be irritating.
The other main advantage is that, because you're streaming actions and not video, you get a high definition view at all times instead of an image that sometimes breaks up.
I think the OP is possibly talking about pairing in the same physical location on the same machine, though, where custom vim setups can definitely get in the way if you're swapping driver every hour or two.
The "scale" he's talking about doesn't seem to be "of traffic" or "of data" but rather "of programmers required to work on it".
> it’s hard for other people to work with you. They don’t share your mental model, and they don’t come in with all your initial assumptions. This is somewhat addressable if you all start working on the project together but falls apart when people join on later. The expressivity doesn’t scale.
So that's a failure of documentation, eh?
People who build systems that only make sense to them are never going to provide good documentation, no matter how often you ask for it. And it’s always describing something that is already ‘done’, so there is very little value as a feedback loop. You just stop asking when you either realize this is all you’re gonna get or figure out how batshit what they’ve described is.
Sunlight is the best disinfectant, they say. Ex post facto documentation provides almost none.
During the implementation process some knowledge is lost. What the author is describing is a kind of merge operation. If you would know the semantic meaning of the data that is being represented, it is rather obvious what choice should be made. When a number represent a quantity, it is obvious they should be added, but when a number is a kind of identifier, like an article number, it is obvious that they should not be added. And yes, maybe in that case the number should have been better represented as a string. In the process of implementing it is often smart to represent it like a number, because it is much easier to deal with a 64-bit number than a variable length string.
Architectural astronauts are fond of their own terminology. It’s nuts. Especially if it’s from someone like my most recent perpetrator who tries to use big words all the time and either gets them wrong or uses obscure meanings that nobody else ever uses conversationally. You may be smart pal, but you’re the biggest idiot I know.
At least the last astronaut was personable and had a hint of humility.
Hence the idea/label of Domain Driven Design [0].
IMO like unit-testing it's valuable but you can't hope for 100%. There will always be some concepts that are unique to how the data is being packaged or calculated which aren't part of the business-domain, but hopefully they'll be safely locked down beneath domain-centered abstractions.
I think we need to be careful here with what you mean by "abstraction." One kind, which we can call "syntactic abstraction" means the following: given a specific language, what portion of its syntactic patterns can be factored into a new construct that "abstracts" all of its instantiations. Languages with macros score very highly here. Another kind, which has been more studied mathematically, is what we can call "algorithmic abstraction" or "behavioral abstraction", which means that given a certain specification of a desirable observable behavior, what portion of the implementations of that behavior can be exposed with the same syntactic construct (the same API). On this front, Haskell has worse abstraction than Java (e.g. if you want to change the implementation of a map to support big data with disk writes, you have to change the syntactic construct), and a language like Rust has terrible algorithmic abstraction, as even different implementation details like memory lifetimes require different constructs. Every additional piece of implementation detail that is exposed in the syntax level (e.g. in the type) hurts abstraction. I don't think that proponents of exposing as much as possible in the type think that's a bad thing, though.
Both of these kinds of abstraction allow you to "leverage existing knowledge in new domains", and both have a certain "mental" cost. I don't think it is possible to relate "abstraction" in general to any bottom-line result, especially as no language choice seems to show any big impact one way or another, but mostly cater to different personal aesthetic preferences.
The former is Scheme and Java. The latter is Scala.
Isn't the crux of why programming is a technical activity second and a social one first?
When I read that example the semantics were obvious to me: adding a map (applying a monoid operation on a map) should add the elements (apply the monoid operation for the elements).
My opinion is that terse notation only gains consensus when it's divinely inspired. That's the word we've traditionally used to describe the np-complete social phenemonon that happens every so often, where some guy's arbitrary mental model manages to allure a lasting self-evident appeal, e.g. LISP, C, and Python. So in some ways, we can think of guys like John McCarthy as modern programming prophets.
One interesting thing I noticed, reading John McCarthy's original paper, is he was actually trying his darndest to not introduce a new notation. He wanted LISP code to look normal, since it's usually not a good thing to come across to other smart people as claiming divine inspiration (see Dunning Kruger).
I disagree with the conventional wisdom though. If you'd want to take the more powerful approach of crafting a shared mental model, my advice would be one of encouragement. You can absolutely be the next John McCarthy. Folks only use wordy notation like HashInsert(hTable,key,value) because they want to go the safer route of being a small successful part of a large corporate machine.
In a modern typed language you wouldn't represent an ID as an integer or a string, you'd represent it is an ID. Then define addition as whatever makes sense for IDs. This can mean not defining the operation at all, in which case you can't compile code that tries to add IDs. This is the viewpoint that statically typed FP people take, and the viewpoint that languages like Rust take.
Your view is more inline with untyped languages or languages with weak type systems like Java or Go. (Arguably Java is changing as it adopts more modern features but I don't think the culture is changing as rapidly as the language.)
I realize that this requires a different mode of thinking about software engineering than is common among software engineers. I also wonder whether most software engineers could be called engineers, because it looks like it is more a craftsmanship than engineering, like mechanical engineering where they are able to accurately predict the properties of a certain object before it is actually being made with the help of standard methods, such as finite element analyses.
I have to admit that we are working in a far more complex domain, but it seems to me that not much progress is made in the field of software engineering. Probably also because technologies are moving too fast to build a solid foundation and that the demand for solutions is too high.
I am surprised how few software engineer understand that main problem boils down to the fact that computers are too slow for our demands. See: https://www.iwriteiam.nl/AoP.html
And yes, you can argue that using "+" implies commutative, and I can see that, but I think it's still a plausible meaning in this context.
(I'm probably rabbit-holing here, but it's interesting.)
The usual implementation technique is type classes, and the issue of having multiple type classes (implementations of, say, monoid) for a given type is known as "type class coherence".
In Scala you could just pass a type class instance explicitly. If Haskell you'd have to change the type.
Plus, my comment about stringly-typed Haskell demonstrates that it's a property of programs more than languages anyway.
However, with that disclaimer, noting that both operations form monoids and both are commutative justifies the "+".
Sure, make it easy to type then we can use it instead of +
However, anyone who has been programming for a while should be able to puzzle out what + is doing.
Funny that software engineers can figure out complex parallelism and algorithms, but typing ∪ as a function name is next to blasphemy.