Another one of the many reasons why I like c# … it’s definitely an “editor first” language. Having come to Python after C#, I find Python’s syntax for something like:
words_on_lines = [line.split() for line in text.splitlines()]
to be frustratingly backwards, almost like the designers were just being whimsical with their order of operations. The “fluent” C# syntax for reference is similar to the Rust syntax show in the post;
There are definitely times when it's useful to have "languages" which are not constrained by the limitations of text, but it's also useful to have languages that can be fully embedded within a printable document.
This same problem exists in sql. First I type select *, then from table, then I go back to the select list and replace * with the list of fields that I can now see through autocomplete
First I type select *, then from table, then I go back to the select list and replace * with the list of fields that I can now see through autocomplete
The UI in the SQL management studio could bring up a list of tables as soon as you type select and then let you pick the list of columns. It wouldn't even be hard for them to make that change. Auto complete doesn't need to work in a strictly linear fashion just because the statements in the language do.
Auto-complete is generally designed to allow a user to start typing an identifier to constrain the list, or allow a user to type a statement while ignoring auto-type suggestions. I suppose that if a user types "SELECT WOO", auto-complete could offer as suggestions "* FROM WOOZLE", "* FROM WOODY", "SELECT * FROM WOOLY", etc. and if one of those is chosen select the * and respond to changes to that by offering auto-complete suggestions for the field name(s), but having auto-complete move the selection point can often be annoying.
Depends on framing. It's the same concept but uses SQL-style naming, which isn't bad - it's just different. You could also argue that filter is bad because grep exists.
Well ignoring the naming, what about LINQ makes it special? I always see C# people gooning to LINQ all the time, but if it’s just basic functional programming that every other language has…?
The idea with LINQ is that the expressions themselves can be compiled into abstract trees that can be converted to SQL or executed as functional programming or parallelized or whatever execution framework we wanted. Which was honestly a great idea. Declaratively expressing the computation we want like that and letting compilers figure out how best to fit that to the data is great. And yes functional languages had the same ideas before, but LINQ expressions were a very elegant way to embed that aspect into an existing imperative language.
Though I do think the SQL-like syntax were a mistake and they should've just stuck with the familiar chained method syntax. But thankfully that was optional.
Though I do think the SQL-like syntax were a mistake and they should've just stuck with the familiar chained method syntax. But thankfully that was optional.
LINQ queries are just syntactic sugar for those chained methods. And not every method has a counterpart in the query-syntax
Though I do think the SQL-like syntax were a mistake and they should've just stuck with the familiar chained method syntax. But thankfully that was optional.
I always found it strange that this is the only DSL baked into the language and it's just sugar for the LINQ method syntax. There are some opertations (like joins) that are more elegant using the SQL syntax but I still don't enjoy using it.
LINQ is designed in a way that lets strongly-typed queries written using it be translated to SQL for frameworks that support it. Because of this ORMs and some lighter-weight alternatives can offer, for example, something like context.Orders.Where(o => o.cost > 100).Select(o => o.Customer) and it will execute something like select customer from orders where cost > 100, which feels a bit like magic given o => o.cost > 100 still looks and behaves like a regular delegate/anonymous function.
The only problem with that is when it suddenly breaks in runtime with something like - you can't use this or that in lambda converted to sql. Still remember running into this kind of bug all this years later.
It has gotten a lot better in the latest Entity Framework versions but yeah, it's still an issue. I will say though, if you have a basic idea of what the final SQL should roughly look like (and don't try to make EF do something near impossible for SQL) it almost always works.
Even better, if you can bother with pre-compiled queries, they should fail as soon as their instantiated, rather than when you get around to running the query.
Oh I never claimed it to be special. It is exactly what you describe. I do like their "sql, but IDE friendly" approach though. Despite having lots of experience in languages with map/grep/filter stuff, it did come pretty naturally to me. And to their credit, their library of utility methods is vastly better than Java streams.
Yeah I (unfortunately) had to use C# at work, and LINQ seemed to be a lot nicer than Java streams for sure, although I was really confused by how much my coworkers talked it up as some revolutionary library
It was a little bit revolutionary at the time (back in 2007). It was a functional language feature that no other commonly used imperative language had at the time. Javascript got it later.
Today it's an expected feature. However, I'm not sure how many other languages can do this as expressions, which enable stuff like LINQ to SQL, LINQ to JSON, etc. Using the same syntax for normal code and for querying a database is neat.
I think this is the entire reason object orientation ever took off in the first place.
People don't care about the patterns, academic reasonings, maybe a little about inheritance. They want OVS so the editor can auto complete.
The main draw is entering the dot and seeing the methods. This is the data I have, reasonably I expect the method I want to be on this one, show me the methods at my disposal, there it is, problem solved. No docs required. (Until your API inevitably throws some curve balls)
The use of target.verb(...optional other objects or values...) syntax allows one to locate the code for the verb using the type of the target. By contrast, if overloads can be based on something other than the first parameter, it's unclear whether the code for verb(object1, object2) will be associated with the type of object1 or object2, and namespace resolution only works in situations where code doesn't need to simultaneously use things from namespaces that define the same symbols.
One could have a language which doesn't support target.verb() syntax but requires function overloads to be defined within the definition of the first argument's type, but even there I don't see how having the location of a function determined by an argument which appears after the verb is somehow better than having it determined by something that appears before the verb.
I get what this talks about and is something to consider, but also consider that readability matters even when the parser doesn't help. You want to see the most important thing first and then the less important details. What the languages here where trying to do was push developers to push what goes first rather than something else. When we talk about readability it's always for the human, anything done by a parser/IDE/software is really an aide to write. Even the author struggles to explain the problem beyond the auto-complete not helping as much.
Now python list comprehensions has its issues in how it works with the rest of the language and it's easy for the whole thing to get ugly, but this isn't the case with all implementations.
That said that doesn't mean there isn't a way to get what you want, just realize that you are making a language that you would want to read backwards of how you write it. That is I look at the end of the line for the most important thing. Think about variable assignment. So in this example we could do something like:
text.lines().map(|line| line.split_whitespace()) |> var words_on_lines
So here I just look at the end of the line to make it work. It's going to be awkward at first, but you do get used to it in languages that do this. With practice you just see that I just stored a variable words_on_lines that has a iterator of strings which had split_whitespace() on a line. I don't need to know that we had a single text and split it, I mean I could keep reading but it doesn't give me more insight. I already understand we split across whitespace and it's strings, do we need to know more?
It may seem dumb, but in the rust example I could append a filter that looks like this:
let words_on_lines = text.lines().flat_map(|l| l.split_whitespace()).filter(|w| w.parse<i32>.is_some_and(|x| x > 10));
Now tell me, quickly what is the type of the elements of words_on_lines? What's the rule to know where to look on where the actual values of them are defined? So sure we can do a bit more of whitespace here:
let words_on_lines = text.lines().flat_map(|l| l.split_whitespace())
.filter(|w| w.parse<i32>.is_some_and(|x| x > 10));
Did you notice the gorilla of bug there on the first read though? Or did you have to go back and look at it more carefully? I switched things, and it is a bit easier to read on the first iteration but not on the second. Also realize that switching back to how the code is supposed to be adds a new bug as now the filter is on the wrong thing.
It gets messy because I have to jump around the line to understand what I am getting, the values I get are defined in the middle, they are used in the end to further define how the whole looks (but not the elements). This jumping around makes it hard to catch small issues or little details that matter. with list comprehensions it's clear that I am generating a list of lists, and that this is the goal. The filtering happens later, but when I change it to make types happy (which is what I imagined our little Timmy the intern did here) it's clear that I am deeply changing the meaning of what is being done here.
And yeah that matters because it what makes code reviews easy or a pain. Even in a small code I look at things to see there isn't an issue and hope that tests cover the issue. But it can be the case that code that is wrong happens to work now, but will fail later when we do some other change, or that I'll realize the type change when adding some other feature and have to go back and fix it.
So there's an argument for backwards code, where we put the important bits at the end (like in a FORTH like language). But there's a reason we don't see it. The brain seems to parse as it writes, and writing one way and reading another feels a bit weird, even with practice. I mean there's a reason that FORTH and it's derivatives had limited success, while reverse polish notation has not become the standard way of writing arithmetic (even if it is superior in ever non-human-subjective aspect).
So we get to the issue again: we can make our code weird to read (requiring relearning how we normally read in the western world) or alternatively we could offload that weirdness to writing (basically you jump around a bit while writing). It's easy to do an IDE that is smart letting us "jump" around and define first the source, and then the output independent of the actual ordering of the write.
And that leads us to why we end up in this weird place. Maybe we should just support people to type [ for line in text.splitlines()]⇆ (where that last symbol represents me typing tab, or enter, or some other autocomplete key) and then the cursor jumps to the start of the definition to let me write that part.
I do wonder, there may be a third path I am not seeing here, but I don't know of any language that has used it. But then again not sure how to best handle this. And none of this takes away from what the author said, I just don't see it as straight forward as its put. Again python comprehensions have issues, but there's many languages that do it way better IMHO, such as haskell where you can even use it for monads and do stuff like
[ x+y | x <- [1..10], y <- [1..x], x `mod` y != 0, then take 5 ]
and it's just a different set of compromises with different pros and cons.
The SQL point is spot on and it's always been one of my biggest frustrations with the language. You're essentially forced to declare what you want before you've established where it comes from, which is backwards from how you actually think through a query.
Elixir's pipe operator handles this really well imo. You start with your data source and chain transformations left to right, which maps much more naturally to how you reason about the problem. It's one of those things where once you've used it, going back to nested function calls feels genuinely painful.
The pipe operator is genuinely one of the best things to happen to readable code. I write a lot of TypeScript at work and the lack of native piping means you end up with either deeply nested function calls or temporary variables everywhere, both of which obscure the actual data transformation.
Elixir gets this right. You write the pipeline top to bottom, each step is clear, and you can add or remove transformations without restructuring the entire expression. Python comprehensions have always felt backwards to me for exactly the reason the article describes.
The SQL example resonated too. Every time I start a query I type SELECT * just to get the FROM clause so the editor knows what columns exist. Minor thing but it adds up across thousands of queries.
the logic in them always seem to go backwards, and given stupid enough programmer, he crams in it too much logic, closing on the unreadability of perl.
I mean from my experience I find that they read almost like natural language, which is super nice.
Also yeah bad programmers can make it bad, but bad programmers will make everything bad. You shouldn’t optimize for bad people that don’t want to improve
In an obscure language I once overloaded the >= operator to be assignment with the left and right hand sides swapped. x >= y read as "x goes into y". I did this because I'd written a huge comment explaining how some memory layout worked, and then I realized I could just convert the diagram into code with a bit of metaprogramming, and the comment was no longer necessary.
In C, you can’t have methods on structs. This means that any function that could be myStruct.function(args) has to be function(myStruct, args).
I'm gonna sidetrack a bit here but this is false. myStruct.function(args) is valid in C as long as function is a function pointer of an appropriate type declared inside the struct declaration of the type of myStruct.
Hey, some people actually even made a profit from crypto, so that's almost insulting to the crypto bros. AI makes me look back in nostalgia for the crypto spam years.
Finally someone dunking on list comprehensions. Pythonistas always looked at me funny when I said that the syntax is really awkward and not composable.
Some nitpicks though:
While Python gets some points for using a first-class function
Having functions not attached to classes is a feature now? We've come full circle.
Haskell, of course, solos with map len $ words text
Veneration of Haskell as the ultimate braniac language here is a bit much when good old work-camel Perl has pretty much the same syntax: map length, split / /, $text.
I work in Python and generally like it, but trying to compose list comprehensions always takes me a couple of minutes thinking about how to do it right.
[x for y in z for x in y]
or is it
[x for x in y for y in z]
I still don't really get why it's the former and not the latter.
(Yes, yes, I know itertools.chain.from_iterable(z) is the right way to do this)
It doesn't give you a one-liner, and it does sometimes make me nostalgic for Ruby one-liners, but it's usually good enough, and people are often already doing stuff like this with list comprehensions anyway.
Definitely not the most maintainable thing, and you tell me if it's really readable. But Python really resists being bent into that shape. I end up doing this instead, which is definitely more readable:
rows = []
with open('ints.csv') as f:
for line in f:
rows.append([int(s.strip()) for s in line.split(',')])
If I was gonna check that in, I might split it into a few more lines, because that comprehension still has the awkward right-to-left logic OP was complaining about. I guess what I'm nostalgic for is how much I could get away with in a single line in a REPL just to test stuff out.
Yes, I can see that ... except that in the comprehension version, we also use x before it is defined. So we've kind of already crossed that particular bridge.
i argue that when you type that list comprehension, you don't type
words_on_lines = [line.split() for line in ...
bit by bit, but wonder what to type next. Either you type the entire thing out because the expression is already in your head, or you don't really know what or how to do it, and is just typing characters to fill in the blanks in the hopes of getting somewhere.
For me personally, i type:
words_on_lines = []
as the first step. Then
words_on_lines = [text.splitlines()]
then
... line.split() for line in ...
This follows my chain of thought to split a text blob into words.
SQL really is the final boss of 'wait, I forgot the table name' loops. Nothing kills my vibe more than jumping back to the top of a query just to fix a column name because autocomplete didn't know what I was doing. This is why pipe operators in literally any language are a total godsend for my sanity. 🫠
That's because it's not even namespaced (like for example `Vec\map` in Hack). Not even prefixed (for example not all array methods begin with `array_`). And that's because PHP started as simple, procedural language and many things weren't fixed when it shifted towards OOP.
In other OOP languages (like Java, C#, TS), when you put a dot after an array (or any other value), you see a list of all the methods, with type hints. If you think about it, honestly, it's so much better language design.
This is why I strongly think that
1. all operators should be postfix. Like (condition).if {true-block} else {false-block} or (value).match {cases}
2. function calls should be postfix, like in bash. Something like arg1 |> function_name.
3. Assignments should be flipped my_long_chain_of_operations =: variable_name
Then there's me, who has a mini parsing lib that lets me write this in two lines and a for loop. I like C# and linq for these types of simple loops & array
As much as people say that Python's ordering is backwards and unintuitive, if they flipped it to their preferred way, you'd get the same amount of people saying its backwards and initiative because it isn't like set builder notation.
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)))
I understand it's just to illustrate the author's point, but for anyone who is learning Python, here's some information.
len(list(...)) always builds up a list in memory sum(1 for _ in iterable) gives you the length in constant memory usage.
You don't need to build lists to pass to all(), as that builds a list in memory and doesn't allow for short circuiting. Generally pass the generator.
That gets you to
sum(1 for _ in 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)
Now it's become a bit more obvious that we're incrementing a counter based on some condition, we can just cast the condition to an integer and remove the filtering logic.
sum(int(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))) for line in diffs)
Python allows for combining comparisons, which removes an extraneous call to abs in one branch.
sum(int(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))) for line in diffs)
Personally, I would prefer for the comparisons that don't involve a function call to short circuit the function call, and also removing some parentheses.
sum(int(all(1 <= abs(x) <= 3 for x in line)) for line in diffs if all(x > 0 for x in line) or all(x < 0 for x in line))
If someone submitted this to me, I would still prefer they use temporary variables and a flatter structure, but this is probably fine.
If diffs is a million elements long, and each row is a hundred elements, all equal to -2, the original codes does something like this:
Grab the first row. Build a list of a hundred elements all equal to False. Build a list of a hundred elements all equal to True. Build a list of a hundred elements all equal to True.
Push the full list of 100 elements to a new list.
HOPEFULLY do garbage collection here.
Repeat a million times until you have a second list with a million rows.
Actually writing this out makes it obvious there's an even simpler solution:
sum(int(all(1 <= x <= 3 for x in line) or all(-3 <= x <= -1 for x in line)) for line in diffs)
I agree in disliking the order of Python list comprehensions, but autocomplete is a strange thing to pin the argument on, since there's nothing that strictly requires autocomplete to operate left-to-right.
In the C example, you could have an editor that lets you type fi tab ctrl-enter and it would auto-complete the variable file and then pull up a list of functions that take typeof(file) as their first argument for you to peruse, then replace the whole expression with fopen(file) when you select it. I used to write extensions like that for Vim and Emacs. If editors aren't being ergonomic enough, we can fix the editors.
But from a basic readability perspective, I agree with the argument. Even in natural language, it would be the difference between...
"Please wash the knife that has a red handle that's in the drawer in the sink."
Versus
"Please go to the drawer, get the red-handled knife, take it to the sink, and wash it."
Easier to understand if the steps are presented in the same order they have to be followed in.
Pascal is mostly left to right, partly because it's single-pass.
In C, you can’t have methods on structs. This means that any function that could be myStruct.function(args) has to be function(myStruct, args)
In Free Pascal you can have "advanced records" (structs):
{$ModeSwitch AdvancedRecords}
//...
const Bit5 = 1 SHL 5; Bits5 = Bit5 - 1; type u5 = 0..Bits5;
//...
type
uint = u32;
Color_RGB555 = bitpacked record
procedure Swap; inline;
var
R, G, B : u5;
private
var
_reserved : u1;
end;
procedure Color_RGB555.Swap; inline;
var
u : uint;
begin
u := R;
R := B;
B := u;
end;
Chris_Codes@reddit
Another one of the many reasons why I like c# … it’s definitely an “editor first” language. Having come to Python after C#, I find Python’s syntax for something like:
words_on_lines = [line.split() for line in text.splitlines()]
to be frustratingly backwards, almost like the designers were just being whimsical with their order of operations. The “fluent” C# syntax for reference is similar to the Rust syntax show in the post;
words_on_lines = text.Split(“\n”).Select(line => line.Split(“ “))
tyrannomachy@reddit
It's because that's the order for set builder notation
BigHandLittleSlap@reddit
Mathematics was designed for pencil & paper.
Copying it blindly into a computer system is the same mistake early CAD software made.
Nobody wants "electronic paper". They want a parametric 3D solid modelling system that can project arbitrary 2D views.
flatfinger@reddit
There are definitely times when it's useful to have "languages" which are not constrained by the limitations of text, but it's also useful to have languages that can be fully embedded within a printable document.
Cautious_Abies_6202@reddit
19F here and playful as always, snap isabellaad.19
programming-ModTeam@reddit
Your comment was removed for being off topic for the /r/programming community.
Zenimax322@reddit
This same problem exists in sql. First I type select *, then from table, then I go back to the select list and replace * with the list of fields that I can now see through autocomplete
Suppafly@reddit
The UI in the SQL management studio could bring up a list of tables as soon as you type select and then let you pick the list of columns. It wouldn't even be hard for them to make that change. Auto complete doesn't need to work in a strictly linear fashion just because the statements in the language do.
flatfinger@reddit
Auto-complete is generally designed to allow a user to start typing an identifier to constrain the list, or allow a user to type a statement while ignoring auto-type suggestions. I suppose that if a user types "SELECT WOO", auto-complete could offer as suggestions "* FROM WOOZLE", "* FROM WOODY", "SELECT * FROM WOOLY", etc. and if one of those is chosen select the * and respond to changes to that by offering auto-complete suggestions for the field name(s), but having auto-complete move the selection point can often be annoying.
Hellball911@reddit
The NewRelic NRQL language allows the FROM and SELECT to be ordered such to improve autocomplete
aanzeijar@reddit
Which C# fixes in LINQ, and the designer quoted auto-completability as the design choice there.
tav_stuff@reddit
Isnt LINQ just glorified map/filter/etc. with bad names?
aanzeijar@reddit
Depends on framing. It's the same concept but uses SQL-style naming, which isn't bad - it's just different. You could also argue that filter is bad because grep exists.
tav_stuff@reddit
Well ignoring the naming, what about LINQ makes it special? I always see C# people gooning to LINQ all the time, but if it’s just basic functional programming that every other language has…?
hippyup@reddit
The idea with LINQ is that the expressions themselves can be compiled into abstract trees that can be converted to SQL or executed as functional programming or parallelized or whatever execution framework we wanted. Which was honestly a great idea. Declaratively expressing the computation we want like that and letting compilers figure out how best to fit that to the data is great. And yes functional languages had the same ideas before, but LINQ expressions were a very elegant way to embed that aspect into an existing imperative language.
Though I do think the SQL-like syntax were a mistake and they should've just stuck with the familiar chained method syntax. But thankfully that was optional.
danielcw189@reddit
LINQ queries are just syntactic sugar for those chained methods. And not every method has a counterpart in the query-syntax
Mechakoopa@reddit
LINQ queries were the only way joins made sense to me for the longest time. I still find functional joins to be clunky.
crozone@reddit
I always found it strange that this is the only DSL baked into the language and it's just sugar for the LINQ method syntax. There are some opertations (like joins) that are more elegant using the SQL syntax but I still don't enjoy using it.
Sprudling@reddit
I use the method syntax almost always, but once in a blue moon I want "let" and/or "join" and the LINQ syntax becomes the only sensible choice.
tav_stuff@reddit
Ah that makes more sense, thanks!
aloha2436@reddit
LINQ is designed in a way that lets strongly-typed queries written using it be translated to SQL for frameworks that support it. Because of this ORMs and some lighter-weight alternatives can offer, for example, something like
context.Orders.Where(o => o.cost > 100).Select(o => o.Customer)and it will execute something likeselect customer from orders where cost > 100, which feels a bit like magic giveno => o.cost > 100still looks and behaves like a regular delegate/anonymous function.LucasVanOstrea@reddit
The only problem with that is when it suddenly breaks in runtime with something like - you can't use this or that in lambda converted to sql. Still remember running into this kind of bug all this years later.
crozone@reddit
It has gotten a lot better in the latest Entity Framework versions but yeah, it's still an issue. I will say though, if you have a basic idea of what the final SQL should roughly look like (and don't try to make EF do something near impossible for SQL) it almost always works.
Even better, if you can bother with pre-compiled queries, they should fail as soon as their instantiated, rather than when you get around to running the query.
aanzeijar@reddit
Oh I never claimed it to be special. It is exactly what you describe. I do like their "sql, but IDE friendly" approach though. Despite having lots of experience in languages with map/grep/filter stuff, it did come pretty naturally to me. And to their credit, their library of utility methods is vastly better than Java streams.
tav_stuff@reddit
Yeah I (unfortunately) had to use C# at work, and LINQ seemed to be a lot nicer than Java streams for sure, although I was really confused by how much my coworkers talked it up as some revolutionary library
Sprudling@reddit
It was a little bit revolutionary at the time (back in 2007). It was a functional language feature that no other commonly used imperative language had at the time. Javascript got it later.
Today it's an expected feature. However, I'm not sure how many other languages can do this as expressions, which enable stuff like LINQ to SQL, LINQ to JSON, etc. Using the same syntax for normal code and for querying a database is neat.
TheAtro@reddit
First class support - functional design, fluent syntax so easy to write.
In combination with entity framework can be translated directly to SQL.
Can be used for other things like XML / JSON aswell.
BigHandLittleSlap@reddit
Kusto Query Language (KQL) used in Azure Log Analytics is also a great example of this left-to-right incremental approach.
Cualkiera67@reddit
Yep because SQL is a garbage language
geon@reddit
Extremely poor composability. Different syntax for each of select, insert and update.
cbarrick@reddit
Google is pushing a new pipe syntax for SQL to fix this.
It's supported on BigQuery and also outside of GCP in things like Spark and Databricks.
https://research.google/pubs/sql-has-problems-we-can-fix-them-pipe-syntax-in-sql/
edave64@reddit
I think this is the entire reason object orientation ever took off in the first place.
People don't care about the patterns, academic reasonings, maybe a little about inheritance. They want OVS so the editor can auto complete.
The main draw is entering the dot and seeing the methods. This is the data I have, reasonably I expect the method I want to be on this one, show me the methods at my disposal, there it is, problem solved. No docs required. (Until your API inevitably throws some curve balls)
mccoyn@reddit
Object oriented programming was already popular before auto-complete was common.
flatfinger@reddit
True, but it eliminated the need to come up with different names for functions that did the same kind of thing but on different types of objects.
MrRufsvold@reddit
So does overload resolution or namespaces
flatfinger@reddit
The use of
target.verb(...optional other objects or values...)syntax allows one to locate the code for the verb using the type of the target. By contrast, if overloads can be based on something other than the first parameter, it's unclear whether the code forverb(object1, object2)will be associated with the type ofobject1orobject2, and namespace resolution only works in situations where code doesn't need to simultaneously use things from namespaces that define the same symbols.One could have a language which doesn't support
target.verb()syntax but requires function overloads to be defined within the definition of the first argument's type, but even there I don't see how having the location of a function determined by an argument which appears after the verb is somehow better than having it determined by something that appears before the verb.lookmeat@reddit
I get what this talks about and is something to consider, but also consider that readability matters even when the parser doesn't help. You want to see the most important thing first and then the less important details. What the languages here where trying to do was push developers to push what goes first rather than something else. When we talk about readability it's always for the human, anything done by a parser/IDE/software is really an aide to write. Even the author struggles to explain the problem beyond the auto-complete not helping as much.
Now python list comprehensions has its issues in how it works with the rest of the language and it's easy for the whole thing to get ugly, but this isn't the case with all implementations.
That said that doesn't mean there isn't a way to get what you want, just realize that you are making a language that you would want to read backwards of how you write it. That is I look at the end of the line for the most important thing. Think about variable assignment. So in this example we could do something like:
text.lines().map(|line| line.split_whitespace()) |> var words_on_lines
So here I just look at the end of the line to make it work. It's going to be awkward at first, but you do get used to it in languages that do this. With practice you just see that I just stored a variable
words_on_linesthat has a iterator of strings which hadsplit_whitespace()on aline. I don't need to know that we had a single text and split it, I mean I could keep reading but it doesn't give me more insight. I already understand we split across whitespace and it's strings, do we need to know more?It may seem dumb, but in the rust example I could append a filter that looks like this:
Now tell me, quickly what is the type of the elements of
words_on_lines? What's the rule to know where to look on where the actual values of them are defined? So sure we can do a bit more of whitespace here:Did you notice the gorilla of bug there on the first read though? Or did you have to go back and look at it more carefully? I switched things, and it is a bit easier to read on the first iteration but not on the second. Also realize that switching back to how the code is supposed to be adds a new bug as now the
filteris on the wrong thing.It gets messy because I have to jump around the line to understand what I am getting, the values I get are defined in the middle, they are used in the end to further define how the whole looks (but not the elements). This jumping around makes it hard to catch small issues or little details that matter. with list comprehensions it's clear that I am generating a list of lists, and that this is the goal. The filtering happens later, but when I change it to make types happy (which is what I imagined our little Timmy the intern did here) it's clear that I am deeply changing the meaning of what is being done here.
And yeah that matters because it what makes code reviews easy or a pain. Even in a small code I look at things to see there isn't an issue and hope that tests cover the issue. But it can be the case that code that is wrong happens to work now, but will fail later when we do some other change, or that I'll realize the type change when adding some other feature and have to go back and fix it.
So there's an argument for backwards code, where we put the important bits at the end (like in a FORTH like language). But there's a reason we don't see it. The brain seems to parse as it writes, and writing one way and reading another feels a bit weird, even with practice. I mean there's a reason that FORTH and it's derivatives had limited success, while reverse polish notation has not become the standard way of writing arithmetic (even if it is superior in ever non-human-subjective aspect).
So we get to the issue again: we can make our code weird to read (requiring relearning how we normally read in the western world) or alternatively we could offload that weirdness to writing (basically you jump around a bit while writing). It's easy to do an IDE that is smart letting us "jump" around and define first the source, and then the output independent of the actual ordering of the write.
And that leads us to why we end up in this weird place. Maybe we should just support people to type
[ for line in text.splitlines()]⇆(where that last symbol represents me typing tab, or enter, or some other autocomplete key) and then the cursor jumps to the start of the definition to let me write that part.I do wonder, there may be a third path I am not seeing here, but I don't know of any language that has used it. But then again not sure how to best handle this. And none of this takes away from what the author said, I just don't see it as straight forward as its put. Again python comprehensions have issues, but there's many languages that do it way better IMHO, such as haskell where you can even use it for monads and do stuff like
and it's just a different set of compromises with different pros and cons.
HateFlyingThough@reddit
The SQL point is spot on and it's always been one of my biggest frustrations with the language. You're essentially forced to declare what you want before you've established where it comes from, which is backwards from how you actually think through a query.
Elixir's pipe operator handles this really well imo. You start with your data source and chain transformations left to right, which maps much more naturally to how you reason about the problem. It's one of those things where once you've used it, going back to nested function calls feels genuinely painful.
HateFlyingThough@reddit
The pipe operator is genuinely one of the best things to happen to readable code. I write a lot of TypeScript at work and the lack of native piping means you end up with either deeply nested function calls or temporary variables everywhere, both of which obscure the actual data transformation.
Elixir gets this right. You write the pipeline top to bottom, each step is clear, and you can add or remove transformations without restructuring the entire expression. Python comprehensions have always felt backwards to me for exactly the reason the article describes.
The SQL example resonated too. Every time I start a query I type SELECT * just to get the FROM clause so the editor knows what columns exist. Minor thing but it adds up across thousands of queries.
middayc@reddit
Example from the blogpost:
Would be:
in ryelang.org
Krafty_Kev@reddit
Code is read more often than it's written. Optimising for readability over writability is a trade-off I'm more than happy to make.
Hot_Slice@reddit
Python list comprehensions aren't readable either.
tav_stuff@reddit
What about them isn’t readable?
SnooFoxes782@reddit
the variables are used before they are introduced. Especially when nested
Aro00oo@reddit
If you nest in a list comprehension, I hope your reviewers call that out.
Simple over complex.
Fenreh@reddit
But is that just because the syntax is poor? Perhaps if it had a more readable syntax it might not be considered unpythonic.
Aro00oo@reddit
I guess you have a point, but in any language you can contrive up some super unreadable code that the syntax supports, no?
Fenreh@reddit
Yeah, that's true.
kRkthOr@reddit
What a nice, friendly conversation.
Good job, you two 🏅
tilitatti@reddit
the logic in them always seem to go backwards, and given stupid enough programmer, he crams in it too much logic, closing on the unreadability of perl.
aanzeijar@reddit
Weird comparison because composed list processing in perl is decades ahead of its time in readability:
tav_stuff@reddit
I mean from my experience I find that they read almost like natural language, which is super nice.
Also yeah bad programmers can make it bad, but bad programmers will make everything bad. You shouldn’t optimize for bad people that don’t want to improve
ThumbPivot@reddit
In an obscure language I once overloaded the
>=operator to be assignment with the left and right hand sides swapped.x >= yread as "x goes into y". I did this because I'd written a huge comment explaining how some memory layout worked, and then I realized I could just convert the diagram into code with a bit of metaprogramming, and the comment was no longer necessary.magnomagna@reddit
I'm gonna sidetrack a bit here but this is false.
myStruct.function(args)is valid in C as long asfunctionis a function pointer of an appropriate type declared inside the struct declaration of the type ofmyStruct.orbiteapot@reddit
Additionally, C libraries often prefix the functions to the object they operate on. So, one would have:
The autocomplete the author mentions would work just fine as soon as would write the prefix. Not everything needs to be OOP-like.
danielcw189@reddit
So, static methods?
Absolute_Enema@reddit
Functions.
orbiteapot@reddit
Yes, in the C++ terminology.
meowsqueak@reddit
Except with LLM auto-completion the right side is already inferred by the context and it tends to get it right anyway.
Typing out code left to right is now an anachronism. Even typing out code is quaint.
That doesn’t mean I like it, but this is how it is now.
BlueGoliath@reddit
AI bros really are the new crypto and NFT bros.
Full-Spectral@reddit
Hey, some people actually even made a profit from crypto, so that's almost insulting to the crypto bros. AI makes me look back in nostalgia for the crypto spam years.
edave64@reddit
Even the fanciest LLM code competition gets signing better if it knows what data you actually want to operate on
deleted_by_reddit@reddit
[removed]
programming-ModTeam@reddit
Your post or comment was removed for the following reason or reasons:
Your post or comment was overly uncivil.
Farados55@reddit
Sorry mods
gmes78@reddit
LLM full line completion is incredibly annoying, and even when the suggestion is correct, it still slows you down.
AsIAm@reddit
Left-to-right no-precedence is the only way forward in syntax design.
aanzeijar@reddit
Finally someone dunking on list comprehensions. Pythonistas always looked at me funny when I said that the syntax is really awkward and not composable.
Some nitpicks though:
Having functions not attached to classes is a feature now? We've come full circle.
Veneration of Haskell as the ultimate braniac language here is a bit much when good old work-camel Perl has pretty much the same syntax:
map length, split / /, $text.codesnik@reddit
i completely forgot perl's map can work with bare expressions. Blockless form seems weird.
Conscious-Ball8373@reddit
I work in Python and generally like it, but trying to compose list comprehensions always takes me a couple of minutes thinking about how to do it right.
or is it
I still don't really get why it's the former and not the latter.
(Yes, yes, I know
itertools.chain.from_iterable(z)is the right way to do this)SanityInAnarchy@reddit
I tend to just use generator comprehensions:
It doesn't give you a one-liner, and it does sometimes make me nostalgic for Ruby one-liners, but it's usually good enough, and people are often already doing stuff like this with list comprehensions anyway.
elperroborrachotoo@reddit
that should be
xs = (x for x in ys), right?SanityInAnarchy@reddit
Whoops. Yep, edited.
elperroborrachotoo@reddit
Faith in the universe restored :)
darkpaladin@reddit
IMO there's a lot of code out there which would be better and more maintainable split over multiple lines. Nested ternaries come to mind.
SanityInAnarchy@reddit
Oh, absolutely, and it's a balance, but what I miss is stuff like:
Definitely not the most maintainable thing, and you tell me if it's really readable. But Python really resists being bent into that shape. I end up doing this instead, which is definitely more readable:
If I was gonna check that in, I might split it into a few more lines, because that comprehension still has the awkward right-to-left logic OP was complaining about. I guess what I'm nostalgic for is how much I could get away with in a single line in a REPL just to test stuff out.
Zahand@reddit
If you were to write it as regular for-loops, which iterable would you iterate over first? Would you write
or
Clearly the second version doesnt work as y isn't even defined yet until the next line.
Conscious-Ball8373@reddit
Yes, I can see that ... except that in the comprehension version, we also use x before it is defined. So we've kind of already crossed that particular bridge.
tokland@reddit
What I do is visualize the equivalent generator:
for y in z: for z in y: yield x
Chii@reddit
i argue that when you type that list comprehension, you don't type
words_on_lines = [line.split() for line in ...
bit by bit, but wonder what to type next. Either you type the entire thing out because the expression is already in your head, or you don't really know what or how to do it, and is just typing characters to fill in the blanks in the hopes of getting somewhere.
For me personally, i type:
as the first step. Then
then
This follows my chain of thought to split a text blob into words.
correct-me-plz@reddit
That's the point!
guoruiqiubai@reddit
SQL really is the final boss of 'wait, I forgot the table name' loops. Nothing kills my vibe more than jumping back to the top of a query just to fix a column name because autocomplete didn't know what I was doing. This is why pipe operators in literally any language are a total godsend for my sanity. 🫠
burnsnewman@reddit
This is the same thing I hated in PHP, which is using detached functions, like
array_map(), instead of doingsomeArray.map().neondirt@reddit
First time I've heard them called "detached". Usually just (global) functions vs (bound) methods.
burnsnewman@reddit
That's because it's not even namespaced (like for example `Vec\map` in Hack). Not even prefixed (for example not all array methods begin with `array_`). And that's because PHP started as simple, procedural language and many things weren't fixed when it shifted towards OOP.
In other OOP languages (like Java, C#, TS), when you put a dot after an array (or any other value), you see a list of all the methods, with type hints. If you think about it, honestly, it's so much better language design.
norude1@reddit
This is why I strongly think that
1. all operators should be postfix. Like
(condition).if {true-block} else {false-block}or(value).match {cases}2. function calls should be postfix, like in bash. Something like
arg1 |> function_name.3. Assignments should be flipped
my_long_chain_of_operations =: variable_namerooktakesqueen@reddit
Never have I said these words before, but... You might like writing in Forth
norude1@reddit
I know forth, no I don't like it
levodelellis@reddit
Then there's me, who has a mini parsing lib that lets me write this in two lines and a for loop. I like C# and linq for these types of simple loops & array
MinimumPrior3121@reddit
Stop these non sense articles and just use Claude to generate code, we don't care about all that bullcrap anymore
somebodddy@reddit
The reason for this is mimicking the mathematical set-builder notation.
Norphesius@reddit
As much as people say that Python's ordering is backwards and unintuitive, if they flipped it to their preferred way, you'd get the same amount of people saying its backwards and initiative because it isn't like set builder notation.
GameCounter@reddit
Side note, this python is kind of bad
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)))I understand it's just to illustrate the author's point, but for anyone who is learning Python, here's some information.
len(list(...))always builds up a list in memorysum(1 for _ in iterable)gives you the length in constant memory usage.You don't need to build lists to pass to all(), as that builds a list in memory and doesn't allow for short circuiting. Generally pass the generator.
That gets you to
sum(1 for _ in 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)Now it's become a bit more obvious that we're incrementing a counter based on some condition, we can just cast the condition to an integer and remove the filtering logic.
sum(int(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))) for line in diffs)Python allows for combining comparisons, which removes an extraneous call to abs in one branch.
sum(int(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))) for line in diffs)Personally, I would prefer for the comparisons that don't involve a function call to short circuit the function call, and also removing some parentheses.
sum(int(all(1 <= abs(x) <= 3 for x in line)) for line in diffs if all(x > 0 for x in line) or all(x < 0 for x in line))If someone submitted this to me, I would still prefer they use temporary variables and a flatter structure, but this is probably fine.
GameCounter@reddit
If diffs is a million elements long, and each row is a hundred elements, all equal to -2, the original codes does something like this:
Grab the first row. Build a list of a hundred elements all equal to False. Build a list of a hundred elements all equal to True. Build a list of a hundred elements all equal to True.
Push the full list of 100 elements to a new list.
HOPEFULLY do garbage collection here.
Repeat a million times until you have a second list with a million rows.
Actually writing this out makes it obvious there's an even simpler solution:
sum(int(all(1 <= x <= 3 for x in line) or all(-3 <= x <= -1 for x in line)) for line in diffs)rooktakesqueen@reddit
I agree in disliking the order of Python list comprehensions, but autocomplete is a strange thing to pin the argument on, since there's nothing that strictly requires autocomplete to operate left-to-right.
In the C example, you could have an editor that lets you type
fi tab ctrl-enterand it would auto-complete the variablefileand then pull up a list of functions that taketypeof(file)as their first argument for you to peruse, then replace the whole expression withfopen(file)when you select it. I used to write extensions like that for Vim and Emacs. If editors aren't being ergonomic enough, we can fix the editors.But from a basic readability perspective, I agree with the argument. Even in natural language, it would be the difference between...
"Please wash the knife that has a red handle that's in the drawer in the sink."
Versus
"Please go to the drawer, get the red-handled knife, take it to the sink, and wash it."
Easier to understand if the steps are presented in the same order they have to be followed in.
taelor@reddit
Piping in elixir is so nice for this.
ShinyHappyREM@reddit
Pascal is mostly left to right, partly because it's single-pass.
In Free Pascal you can have "advanced records" (structs):
AxisFlip@reddit
This always grinds my gears when I have to write PHP. Seriously not enjoying that.
ymonad@reddit
I don't know why Ruby is so underestimated
crozone@reddit
So, LINQ