Although the subtitle was “programs should be valid as they are typed”, it’s weakened to “somewhat valid” at this point. And yes, it is valid enough that tooling can help, a lot of the time (but not all) at full capability. But there’s also interesting discussion to be had about environments where programs are valid as they are typed. Syntactically, especially, which requires (necessary but not sufficient) either eschewing delimition, or only inserting opening and closing delimiters together.
That would be nice if devs always wrote code sequentially, i.e. left to right, one character at a time, one line at a time. But the reality is that we often jump around, filling in some things while leaving other things unfinished until we get back to them. Sometimes I'll write code that operates on a variable, then a minute later go back and declare that variable (perhaps assigning it a test value).
If i decide to add a new field to some class, i won't necessarily go to the class definition first, I'll probably write the code using that field because that's where the IDE was when i got the idea.
If I want to enhance some condition checking, i'll go through a phase where the piece of code isn't valid while I'm rearranging ifs and elses.
Reading through the article, the author makes the argument for the philosophy of progressive disclosure. The last paragraph brings it together and it's a reasonable take:
> When you’ve typed text, the program is valid. When you’ve typed text.split(" "), the program is valid. When you’ve typed text.split(" ").map(word => word.length), the program is valid. Since the program is valid as you build it up, your editor is able to help you out. If you had a REPL, you could even see the result as you type your program out.
In the age of CoPilot and agent coders I'm not so sure how important the ergonomics still are, though I dare say coding an LSP would certainly make one happy with the argument.
But some languages just won't let you do that, because they put in errors for missing returns or unused variables.
> Here, your program is constructed left to right. The first time you type line is the declaration of the variable. As soon as you type line., your editor is able to suggest available methods.
Yeah, having LSP autocomplete here does feel nice.
But it also makes the code harder to scan than Python. Quick readability at a glance seems like the bigger win than just better autocomplete.
It depends a lot on what you’re accustomed to. You get used to whichever style. Just like different languages use different sentence order: subject, object and verb appear in all possible orders in different languages, and their speakers get along just fine. There are some situations where one is clearly superior to the other, and vice versa.
`text.lines().map(|line| line.split_whitespace())` can be read loosely as “take text; take its lines; map each line, split it on whitespace”. Straightforward and matching execution flow.
`[line.split() for line in text.splitlines()]` doesn’t read so elegantly left-to-right, but so long as it’s small enough you spot the `for` token, realise you’re dealing with a list comprehension, and read it loosely from left to right as “we have a list made up of splitting each line, where lines come from text, split”. Execution-wise, you execute `text.splitlines()`, then `for line in`, then `line.split()`. It’s a bunch of left-to-rights embedded in a right-to-left. This has long been noted as a hazard of list comprehensions, especially the confusion you end up with with nested ones. Now you could quibble over my division of `for line in text.splitlines()` into two runs; but I think it’s fair. Consider how in Rust you get both `for line in text.split_lines() { … }` and `text.split_lines().for_each(|line| { … })`. Sometimes the for block reads better, sometimes .for_each() or .map() or whatever does. (But map(lambda …: …, …) never really does.)
Python was my preferred language from 2009–2013 and I still use it not infrequently, but Rust has been my preferred language ever since. I can say: I find the Rust version significantly easier to read, in this particular case. I think the fact there are two levels of split contributes to this.
.unwrap().unwrap().into::<&&&Vec<Box<&&&&&mut String>>>()
which kind of ruins the elegance :P len(list(filter(lambda line: all([abs(x) >= 1 and abs(x) <= 3 for x in line]) and (all([x > 0 for x in line]) or all([x < 0 for x in line])), diffs)))
This really isn’t fair on Python. Python isn’t very much not designed for this style of functional programming. Plus you haven’t broken lines where you could. Rewrite it as a list comprehension and add line breaks, and turn the inner list comprehensions into generator expressions (`all([…])` → `all(…)`), and change `abs(x) >= 1 and abs(x) <= 3` to `1 <= abs(x) <= 3` (thanks, Jtsummers), and it’s much better, though it still has the jumping around noted, and I do prefer the functional programming approach. I’m just saying the presentation isn’t fair on Python. len([line for line in diffs
if all(1 <= abs(x) <= 3 for x in line)
and (all(x > 0 for x in line) or all(x < 0 for x in line))])
(Aside: change the first line to `sum(1 for line in diffs` and drop the final `]`, and it will probably perform better.)I also want to note, in the JS… Math.abs(x) instead of x.abs() (as seen in Rust).
And, because nerd sniping, two Rust implementations, one a direct port of the JS:
diffs.iter().filter(|line| {
line.iter().all(|x| x.abs() >= 1 && x.abs() <= 3) &&
(line.iter().all(|x| x > 0) || line.iter().all(|x| x < 0))
}).count()
(`x.abs() >= 1 && x.abs() <= 3` would be better as `(1..=3).contains(x.abs())` or `matches!(x.abs(), 1..=3)`.)And one optimised to only do a single pass:
diffs.iter().filter(|line| {
let mut iter = line.iter();
let range = match iter.next() {
Some(-3..=-1) => -3..=-1,
Some(1..=3) => 1..=3,
Some(_) => return false,
None => return true,
};
iter.all(|x| range.contains(x))
}).count()
abs(x) >= 1 and abs(x) <= 3
Is also unidiomatic in Python. 1 <= abs(x) <= 3
Means the same thing and tightens it up a bit, and reads better since it's indicating that you're testing if something is in a range more clearly.EDIT: To add:
The filter, list construction, and len aren't needed either. It's just:
sum(map(predicate, diffs)) # this counts the number of elements in diffs which satisfy predicate, map is lazy so no big memory overhead
Or alternatively: sum(predicate(diff) for diff in diffs)
The predicate is complex enough and used twice, so it warrants extraction to its own named function (or lambda assigned to a variable), but even if it were still embedded this form would be slightly clearer (along with adding the line breaks and removing the extra list generations): sum(map(lambda line: all(1 <= abs(x) <= 3 for x in line)
and (all(x > 0 for x in line) or all(x < 0 for x in line)),
diffs))
Verb-final languages like PostScript and Forth are oddballs.
Embedded verbs are a thing of course, with the largest contribution coming from arithmetic expressions with infix operators, followed by certain common syntax like "else" being in the of an "if" statement, followed by things like these Python comprehensions and <action> if <condition> syntactic experiments and whatnot.
table.where(...).select(...)
that is typical in many OO interfaces, though you sometimes see a pipe syntax which would be autocomplete friendly in languages like Clojurehttps://clojuredocs.org/clojure.core/-%3E
or F#
https://stackoverflow.com/questions/12921197/why-does-the-pi...
https://en.wikipedia.org/wiki/Non-English-based_programming_...
I've seen some SQL-derived things that let you switch it. They should all let you switch it.
For a language where there is supposed to be only one way to do things, there are an awful lot of ways to do things.
Don’t get me wrong, writing a list comprehension can be very satisfying and golf-y But if there should be one way to do things, they do not belong.
I would say unless you have a good reason to do so, features such as meta classes or monkey patching would be top of list to avoid in shared codebases.
It's not. The author gives objective reasons why Python's syntax is inferior – namely, that it makes IDE support in the form of discoverability and auto-completion more difficult.
To make a long story short, we added features for "incomplete" programs in the language and tools, so that your program was always valid and could not be invalid. It was a reasonable concept, and I think could have been a game changer if AI didn't first change the game.
Queries should start by the `FROM` clause, that way which entities are involved can be quickly resolved and a smart editor can aid you in writing a sensible query faster.
The order should be FROM -> SELECT -> WHERE, since SELECT commonly gives names to columns, which WHERE will reference.
You could even avoid crap like `SELECT * FROM table`, and just write `FROM table` and have the select clause implied.
Never mind me, I'm just an old man with a grudge, I'll go back to my cave...
Check out the DuckDB community extensions:
[0]: https://duckdb.org/community_extensions/extensions/psql.html
[1]: https://duckdb.org/community_extensions/extensions/prql.html
C++ has this issue too due to the split between header declarations and implementations. Change a function name? You're updating it in the implementation file, and the header file, and then you can start wondering if there are callers that need to be updated also. Then you add in templates and the situation becomes even more fun (does this code live in a .cc file? An .h file? Oh, is your firm one of the ones that does .hh files and/or .hpp files also? Have fun with that).
{3} for {2} in {1}
which would give you code completion for {3} based on the {1} and {2} that would be filled in first.There is generally a trade-off between syntax that is nice to read vs. nice to type, and I’m a fan of having nice-to-read syntax out of the box (i.e. not requiring tool support) at the cost of having to use tooling to also make it nice to type.
This is not meant as an argument for the above for-in syntax, but as an argument that left-to-right typing isn’t a strict necessity.
For example, using "rm" on the command line, or an SQL "delete". I would very much like those short programs to be invalid, until someone provides more detail about what should be destroyed in a way that is accident-resistant.
If I had my 'druthers, the left-to-right prefix of "delete from table" would be invalid, and it would require "where true" as a safety mechanism.
Your editor can’t help you out as you write it.
90% of programming language design nowadays is aimed at users whose entire demand is "make squiggles show up! MAKE SQUIGGLES SHOW UP NAOW!!!!!!"If you can't keep hitting Tab and drooling, it's not easy enough.
taeric•2h ago
nathan_compton•2h ago
I believe there are some strongly typed stack based languages where you really always do have something very close to a syntactically correct program as you type. But now that LLMs exist to paper over our awful intuitions, we're stuck with bad syntax like python forever.
taeric•1h ago
On one level, I do prefer my code to be readable left to right and top to bottom. This, typically, means the big "narrative" functions up top and any supporting functions will come after they were used. Ideally, you could read those as "details" after you have understood the overall flow.
On another level, though, it isn't like this is how most things are done. Yes, you want a general flow that makes sense in one direction through text. But this often has major compromises and is not the norm. Directly to programming, trying to make things context free is just not something that works in life.
Directly to this discussion, I'm just not sure how much I care about small context-free parts of the code?
Tangential to this discussion, I oddly hate comprehensions in python. I have yet to get where I can type those directly. And, though I'm asking if LLM tools are making this OBE, I don't use those myself. :(
mrguyorama•1h ago
I've had some minor success with claude, but enabling the AI plugin in intellij has literally made my experience worse, even without using any AI interactions.