" shadowofneptune 1 day ago
parent | prev | next [–] |
The same information can be communicated in different ways, trading one form of noise for another. I have a personal preference for Pascal-like or PL/I syntax. Instead of int *char x or int&& x, there's x: byte ptr ptr. It's more to type and read, sure, but sometimes having an english-like keyword really helps clarify what's going on.
...
(someone else replying) Ideally, the language should have enough syntax-bending facilities so that you can still simulate what you want, this is mostly just operator overloading and not treating custom types like second class citizens. For example, your example of byte ptr ptr can be easily done in C++ by a bytePtrPtr struct, or even better, a Ptr<Ptr<Byte>> instantiated class from the template Ptr<T> for any T. Overloading the dereference and conversion operators will completely hide any trace of the fact it's not a built in type, and compiler optimization and inlinning will (hopefully, fingers crossed) ensure that no extra overhead is being introduced by the abstraction.
As for the 'byte ptr ptr' syntax specifically, in F# generic instantiation can be done by whitespace concatenation of type names in reversed C++/Java/C# order, so the above C++ type would (if translated to F# somehow) literally be written out as you want it to be, so even what seems like it would require language support (whitespace between related identifiers, generally a tricky thing in PL design) can actually be accomplished with clever and free minded syntax. "
---
hawski 1 day ago
parent | prev | next [–] |
I find that Rust tends to have code that goes sideways more than downward. I prefer the latter and most C code bases, that I find elegant are like that.
It is like that, because of all the chaining that one can do. It is also just a feeling.
reply
est31 1 day ago
root | parent | next [–] |
There are two upcoming features, let chains and let else, to counter the sideways drift.
Sometimes it's also the formatter though that outputs intensely sideways drifting code: https://github.com/rust-lang/rust/blob/1.60.0/compiler/rustc...
reply
kzrdude 1 day ago
root | parent | prev | next [–] |
I think it's because of the expression focus. Isn't it easier to make the code flow like a waterfall when it's imperative, but is harder to reason about values and state.
reply
---
... I find that Rust tends to have code that goes sideways more than downward. I prefer the latter and most C code bases, that I find elegant are like that. ... aaron_m04 1 day ago
root | parent | prev | next [–] |
I have noticed this tendency as well.
To counteract it, I write exit-early code like this:
let foo_result = foo(); if let Err(e) = foo_result { return Bar::Fail(e); } let foo_result = foo_result.unwrap(); ...
reply
mathstuf 1 day ago
root | parent | next [–] |
Any reason why this wasn't preferred?
let foo_result = foo() .map_err(Bar::Fail)?;
reply
veber-alex 1 day ago
root | parent | next [–] |
Bar::Fail is not wrapped in a Result type, so you can't use '?' with it (on stable at least).
You can write it like this:
let foo_result = match foo() { Ok(v) => v, Err(e) => return Bar::Fail(e) };
reply
loeg 1 day ago
root | parent | next [–] |
The result type is the return from Foo -- Bar::Fail does not need to wrap Result. Foo is Result<T, E> and map_err() would convert it to Result<T, Bar::Fail>. I think GP's `map_err()?` is the most straightforward way of writing this idea (and it's generally speaking how I would suggest writing Rust code).
reply
veber-alex 1 day ago
root | parent | next [–] |
GP's code will return Result<_, Bar>, the original code we are trying to fix just returns Bar.
reply
loeg 1 day ago
root | parent | next [–] |
If you are writing code to handle Results, it’s going to be a lot less painful to just return Result.
reply
ntoskrnl 1 day ago
root | parent | prev | next [–] |
I do this too, with a different spin on it:
let foo = match foo() { Ok(foo) => foo, Err(err) => return Err(err), };
I was typing it out so often I made editor aliases `lmo` and `lmr` for Option and Result
reply
veber-alex 1 day ago
root | parent | next [–] |
let me introduce you to the famous '?' operator.
The code above can be written as:
let foo = foo()?;
reply
ntoskrnl 1 day ago
root | parent | next [–] |
LOL you're right! I just pasted the template here, but my defaults are mostly equivalent to plain old `?`. I don't use the match if `?` would work.
reply
gardaani 1 day ago
root | parent | prev | next [–] |
Early exit code would be easier to write if Rust supported guard let.
reply
veber-alex 1 day ago
root | parent | next [–] |
its coming soon, already available on nightly.
let Some(x) = foo() else { return 42 };
reply
inferiorhuman 1 day ago
root | parent | next [–] |
I'd suggest that something like that is already achievable by having foo return an Option and combining it with unwrap_or.
reply
klodolph 1 day ago
root | parent | prev | next [–] |
Like this?
if let Ok(x) = my_func() { // ... }
Or do you mean something else?
reply
metaltyphoon 1 day ago
root | parent | prev | next [–] |
Isn’t that a good general practice todo? Exit early
reply
icedchai 1 day ago
root | parent | next [–] |
You'd be surprised. For every person that things exit early is good, you'll run into another that prefers a single exit. At worked at a C++ shop that preferred "single exit", and some methods with an ungodly amount of conditions just to make this possible. Ugh.
reply
boolemancer 22 hours ago
root | parent | next [–] |
In my experience, a preference for single exit comes from C where you always need to make sure to clean up any resources, and an early exit is a great way to have to duplicate a bunch of cleanup logic or accidentally forget to clean things up.
Of course, that's what goto is actually good for.
reply
---
... Rust is a mostly-expression language (therefore, semicolons have meaning), while JavaScript?/Python/Go aren't.
The conventional example is conditional assignment to variable, which in Rust can be performed via if/else, which in JS/Python/Go can't (and require alternative syntax). ...
kibwen 1 day ago
root | parent | next [–] |
Not the parent, but you can certainly have an expression-oriented language without explicit statement delimiters. In the context of Rust, having explicit delimiters works well. In a language more willing to trade off a little explicitness for a little convenience, some form of ASI would be nice. The lesson is just to not extrapolate Rust's decisions as being the best decision for every domain, while also keeping the inverse in mind. Case in point, I actually quite like exceptions... but in Rust, I prefer its explicit error values.
reply
steveklabnik 1 day ago
root | parent | next [–] |
Ruby is a great example of a language that’s expression oriented, where terminators aren’t the norm, but optionally do exist.
reply
...
Some constructs are incompatible with optional semicolons, as semicolons change the expression semantics (I've given an example); comparison with languages that don't support such constructs is an apple-to-oranges comparison.
An apple-to-apple comparison is probably with Ruby, which does have optional semicolons and is also expression oriented at the same time. In the if/else specific case, it solves the problem by introducing inconsistency, in the empty statement, making it semantically ambiguous.
reply
---
bobbylarrybobby 1 day ago
root | parent | prev | next [–] |
In practice though, getting the AST from the text is a computational task in and of itself and the grammar affects the runtime of that. For instance, Rust's "turbofish" syntax, `f::<T>(args)`, is used to specify the generic type for f when calling it; this is instead of the perhaps more obvious `f<T>(args)`, which is what the definition looks like. Why the extra colons? Because parsing `f<T>(args)` in an expression position would require unbounded lookahead to determine the meaning of the left angle bracket -- is it the beginning of generics or less-than? Therefore, even though Rust could be modified to accept`f<T>(args)` as a valid syntax when calling the function, the language team decided to require the colons in order to improve worst case parser performance.
reply
colejohnson66 7 hours ago
root | parent | next [–] |
How does C# manage to handle without the turbo fish syntax? What’s different in Rust?
reply
bobbylarrybobby 5 hours ago
root | parent | next [–] |
It's not impossible to handle the ambiguity, it's just that you may have to look arbitrarily far ahead to resolve it. Perhaps C# simply does this. Or perhaps it limits expressions to 2^(large-ish number) bytes.
reply
xigoi 19 hours ago
root | parent | prev | next [–] |
This is why using the same character as a delimiter and operator is bad.
reply
---
tene 1 day ago
root | parent | prev | next [–] |
That's really cool that you think Rust syntax could be significantly improved. I'd really love to hear some details.
Here's the example from the post:
Trying::to_read::<&'a heavy>(syntax, |like| { this. can_be( maddening ) }).map(|_| ())?;
How would you prefer to write this?
reply
chmln 1 day ago
root | parent | next [–] |
That whole example feels like a strawman, from my (maybe limited) experience something that's rather the exception than the norm.
First, lifetimes are elided in most cases.
Second, the curly braces for the closure are not needed and rustfmt gets rid of them.
Finally, the "map" on result can be replaced with a return statement below.
So, in the end we get something like:
Trying::to_read(syntax, |like| this.can_be(maddening))?; Ok(())
reply
---
codebje 1 day ago
parent | prev | next [–] |
> That said, I've mostly reached the conclusion that much of this is unavoidable. Systems languages need to have lots of detail you just don't need in higher level languages like Haskell or Python, …
I am not convinced that there's so more to Rust than there is to GHC Haskell to justify so much dense syntax.
There's many syntax choices made in Rust based, I assume, on its aim to appeal to C/C++ developers that add a lot of syntactic noise - parentheses and angle brackets for function and type application, double colons for namespace separation, curly braces for block delineation, etc. There are more syntax choices made to avoid being too strange, like the tons of syntax added to avoid higher kinded types in general and monads in particular (Result<> and ()?, async, "builder" APIs, etc).
Rewriting the example with more haskell-like syntax:
Trying::to_read::<&'a heavy>(syntax, |like| { this. can_be( maddening ) }).map(|_| ())?;
Trying.to_read @('a heavy) syntax (\like -> can_be this maddening) >> pure ()
It's a tortuous example in either language, but it still serves to show how Rust has made explicit choices that lead to denser syntax.
Making a more Haskell-like syntax perhaps would have hampered adoption of Rust by the C/C++ crowd, though, so maybe not much could have been done about it without costing Rust a lot of adoption by people used to throwing symbols throughout their code.
(And I find it a funny place to be saying _Haskell_ is less dense than another language given how Haskell rapidly turns into operator soup, particularly when using optics).
skavi 1 day ago
root | parent | next [–] |
> In more plain terms, the line above does something like invoke a method called “to_read” on the object (actually `struct`) “Trying”...
In fact, this invokes an associated function, `to_read`, implemented for the `Trying` type. If `Trying` was an instance `Trying.to_read...` would be correct (though instances are typically snake_cased in Rust).
I'll rewrite the line, assuming `syntax` is the self parameter:
syntax .to_read::<&'a heavy>(|like| this.can_be(maddening)) .map(|_| ())?;
In my opinion, this is honestly not bad.
reply
zozbot234 19 hours ago
root | parent | prev | next [–] |
> like the tons of syntax added to avoid higher kinded types in general and monads in particular (Result<> and ()?, async, "builder" APIs, etc).
Rust is not "avoiding" HKT in any real sense. The feature is being worked on, but there are interactions with lifetime checking that might make, e.g. a monad abstraction less generally useful compared to Haskell.
reply
---
flohofwoe 1 day ago
root | parent | next [–] |
This just plasters over the underlying problem, which in case of Rust is IMO that features that should go into the language as syntax sugar instead are implemented as generic types in the standard library (exact same problem of why modern C++ source code looks so messy). This is of course my subjective opinion, but I find Zig's syntax sugar for optional values and error handling a lot nicer than Rust's implementation of the same concepts. The difference is (mostly): language feature versus stdlib feature.
reply
vlovich123 1 day ago
root | parent | prev | next [–] |
I’m not familiar with zig. Can you give some examples to illustrate your point?
reply
flohofwoe 1 day ago
root | parent | next [–] |
An optional is just a '?' before the type:
For instance a function which returns an optional pointer to a 'Bla':
fn make_bla() ?*Bla { // this would either return a valid *Bla, or null }
A null pointer can't be used accidentally, it must be unwrapped first, and in Zig this is implemented as language syntax, for instance you can unwrap with an if:
if (make_bla()) |bla| { // bla is now the unwrapped valid pointer } else { // make_bla() returned null }
...or with an orelse:
const bla = make_bla() orelse { return error.InvalidBla };
...or if you know for sure that bla should be valid, and otherwise want a panic:
const bla = make_bla().?;
...error handling with error unions has similar syntax sugar.
It's probably not perfect, but I feel that for real-world code, working with optionals and errors in Zig leads to more readable code on average than Rust, while providing the same set of features.
reply
veber-alex 1 day ago
root | parent | next [–] |
I don't see how that is all that different from Rust.
The main difference I see is that in Rust it will also work with your own custom types, not just optional.
fn make_bla() -> Option<Bla> { // this either returns a valid Bla, or None }
if let Some(bla) = make_bla() { // bla is now the unwrapped valid type } else { // make_bla() returned None }
..or with the '?' operator (early return)
let bla = make_bla().ok_or(InvalidBla)?;
..or with let_else (nightly only but should be stable Soon(tm))
let Some(bla) = make_bla() else { return Err(InvalidBla) }
..or panic on None
let bla = make_bla().unwrap();
reply
wtetzner 1 day ago
root | parent | prev | next [–] |
How does Zig represent Option<Option<i32>>? Would it be something like this?
??i32
reply
flohofwoe 22 hours ago
root | parent | next [–] |
I had to try it out first, but yep, that's how it works:
const assert = @import("std").debug.assert;
fn get12() i32 { const opt_opt_val: ??i32 = 12; const val = opt_opt_val.?.?; comptime assert(val == 12); return val; }
reply
---
>when I revisit old mostly forgoten code, I love that boilerplate. I rarely have to do any puzzling about how to infer what from the current file, it's just all right there for me.
This is going to sound absurd, but the only other language I had this experience with was Objective-C.
Verbosity is super underrated in programming. When I need to come back to something long after the fact, yes, please give me every bit of information necessary to understand it.
-- [2]
---
carlmr 2 days ago
root | parent | prev | next [–] |
That's true, I found this writing F# with an IDE vs reading F# in a PR without IDE it really becomes easier to read if you at least have the types on the function boundary.
F# can infer almost everything. It's easier to read when you do document some of the types though.
reply
eropple 1 day ago
root | parent | next [–] |
> F# can infer almost everything. It's easier to read when you do document some of the types though.
F# is also easier to avoid breaking in materially useful ways if (like TypeScript?) you annotate return types even if they can be inferred. You'll get a more useful error message saying "hey stupid, you broke this here" instead of a type error on consumption.
reply
---
bilkow 2 days ago
prev | next [–] |
It looks like I'm on the minority here, but I generally like Rust's syntax and think it's pretty readable.
Of course, when you use generics, lifetimes, closures, etc, all on the same line it can become hard to read. But on my experience on "high level" application code, it isn't usually like that. The hardest thing to grep at first for me, coming from python, was the :: for navigating namespaces/modules.
I also find functional style a lot easier to read than Python, because of chaining (dot notation) and the closure syntax.
Python:
array = [1, 0, 2, 3] new_array = map( lambda x: x * 2, filter( lambda x: x != 0, array ) )
Rust:
let array = [1, 0, 2, 3]; let new_vec: Vec<_> = array.into_iter() .filter(|&x| x != 0) .map(|x| x * 2) .collect();
I mean, I kind of agree to the criticism, specially when it comes to macros and lifetimes, but I also feel like that's more applicable for low level code or code that uses lots of features that just aren't available in e.g. C, Python or Go.
Edit: Collected iterator into Vec
reply
klodolph 2 days ago
parent | next [–] |
There are people who write Python code like that, but it's an extreme minority. Here's the more likely way:
array = [1, 0, 2, 3] new_array = [x * 2 for x in array if x != 0]
Just as a matter of style, few Python programmers will use lambda outside something like this:
array = [...] arry.sort(key=lambda ...)
reply
foolfoolz 1 day ago
root | parent | next [–] |
i have always felt the backwards nature of list comprehensions makes them very hard to read
reply
zanellato19 1 day ago
root | parent | next [–] |
me too. Its one of the things that I kinda dislike in Python.
reply
bilkow 2 days ago | root | parent | prev | next [–]
I guess you're right, list/generator comprehensions are the idiomatic way to filter and map in python, with the caveat of needing to have it all in a single expression (the same goes for lambda, actually).
I still feel like chained methods are easier to read/understand, but list comprehensions aren't that bad.
reply
dralley 2 days ago
root | parent | next [–] |
Even in Rust I don't like chains that go beyond ~4 operations. At some point it becomes clearer when expressed as a loop.
reply
Iwan-Zotow 2 days ago
root | parent | prev | next [–] |
> with the caveat of needing to have it all in a single expression (the same goes for lambda, actually).
one could use multiple expressions in lambda in (modern) Python
reply
vgel 2 days ago
root | parent | next [–] |
Do you mean using the walrus operator? Because unless I missed a recent PEP, I don't know of a way to do this without something hacky like that.
reply
Iwan-Zotow 1 day ago
root | parent | next [–] |
yes
x = 1 y = 2
q = list(map(lambda t: ( tx := tx, ty := ty, tx+ty )[-1], [1, 2, 3]))
print(q)
reply
nemothekid 2 days ago
parent | prev | next [–] |
1. I don't think your Python example if fair. I think
new_array = [x*2 for x in array if x != 0]
is much more common.
2. In your example, `new_array` is an iterator; if you need to transform that into an actual container, your rust code becomes:
let new_array = array.into_iter() .filter(|&x| x != 0) .map(|x| x * 2) .collect::<Vec<_>>();
And there your generic types rear their ugly head, compared to the one liner in python.
reply
bilkow 2 days ago
root | parent | next [–] |
Oh, yeah, you're right! If you want to collect into a Vec you may need to specify the type, but usually, you can just call `.collect()` and the compiler will infer the correct type (as I suppose you're collecting it to use or return).
If it can't infer, it's idiomatic to just give it a hint (no need for turbofish):
let new_vec: Vec<_> = array.into_iter() .filter(|&x| x != 0) .map(|x| x * 2) .collect();
I don't think that's ugly or unreadable.
About the Python list comprehension, I answered your sibling, I think you're both right but it also does have it's limitations and that may be personal, but I find chained methods easier to read/understand.
reply
veber-alex 2 days ago
root | parent | prev | next [–] |
They maybe rear their ugly head but they also allow you to collect the iterator into any collection written by you, by the standard library or by any other crate.
While in python you have list/dict/set/generator comprehension and that's it.
reply
nemothekid 2 days ago
root | parent | next [–] |
I don't think it's bad thing. In fact one of my favorite features is that you can do `.collect::<Result<Vec<_>, _>>()` to turn an interators of Results, into a Result of just the Vec if all items succeed or the first error. That is a feature you just can't express in Python.
But you have to admit that is a pretty noisy line that could be difficult to parse.
rajman187 1 day ago | parent | prev | next [–]
minor point but your python code creates a generator here not an array, you'd have to wrap it in a `list()` to get the same data type and be able to for example assert its length (of course you can just iterate over the generator)
reply
---
robonerd 2 days ago | root | parent | prev | next [–]
> Back when I wrote C and C++ for a living I'd occasionally meet someone who thought their ability to employ the spiral rule or parse a particularly dense template construct meant they were a genius. I get the same vibe from certain other groups in this industry, most recently from functional programmers and Rust afficionados
Perl one-liner guys used to exemplify this. But I don't really agree that functional programmers do, except for Haskell and people who use lots of the car and cdr compositions, or those who use too much metaprogramming, or... okay maybe you're right. But at least the fundamental premise of functional programming is simple..
reply
---
titzer 2 days ago
parent | prev | next [–] |
I find Rust code hard to read...to the point where I don't feel motivated to learn it anymore. Line noise is confusing and a distraction. Random syntactic "innovations" I find are just friction in picking up a language.
For example, in the first versions of Virgil I introduced new keywords for declaring fields: "field", "method" and then "local". There was a different syntax for switch statements, a slightly different syntax for array accesses. Then I looked at the code I was writing and realized that the different keywords didn't add anything, the array subscripting syntax was just a bother; in fact, all my "innovations" just took things away and made it harder to learn.
For better or for worse, the world is starting to converge on something that looks like an amalgam of Java, JavaScript?, and Scala. At least IMHO; that's kind of what Virgil has started to look like, heh :)
reply
---
19 kornel edited 2 days ago | link | flag |
Because I don’t think that’s the fault of the syntax. Huge part of criticism is expectations/preferences and lack of understanding of the trade-offs that made it the way it is. When Rust is different than whatever other language someone is used to, they compare familiar with unfamiliar (see Stroustrup’s Rule). But it’s like saying the Korean alphabet is unreadable, because you can’t read any of it.
People who don’t like Rust’s syntax usually can’t propose anything better than a bikeshed-level tweak that has other downsides that someone else would equally strongly dislike.
For example, <> for generics is an eyesore. But if Rust used [] for generics, it’d make array syntax either ambiguous (objectively a big problem) or seem pointlessly weird to anyone used to C-family languages. Whatever else you pick is either ambiguous, clashes with meaning in other languages, or isn’t available in all keyboard layouts.
The closure syntax
expr may seem like line noise, but in practice it’s important for closures to be easy to write and make it easy to focus on their body. JS went from function { return expr } to () => expr. Double arrow closures aren’t objectively better, and JS users criticize them too. A real serious failure of Rust regarding closures is that they have lifetime elision rules surprisingly different than standalone functions, and that is a problem deeper than the syntax. |
Rust initially didn’t have the ? shortcut for if err != nil { return nil, err } pattern, and it had a problem of a low signal-to-noise ratio. Rust then tried removing boilerplate with a try!() macro, but it worked poorly with chains of fallible function calls (you’d have a line starting with try!(try!(try!(… and then have to figure out where each of them have the other paren). Syntax has lots of trade-offs, and even if the current one isn’t ideal in all aspects, it doesn’t mean alternatives would be better.
And there are lots of things that Rust got right about the syntax. if doesn’t have a “goto fail” problem. Function definitions are greppable. Syntax of nested types is easy to follow, especially compared to C’s “spiral rule” types.
---
" Integer Types and Casting
Because we’re writing a compiler, we deal with a lot of integer math, and we essentially need to use every integer type that Rust provides: both signed and unsigned values, 8, 16, 32, and 64 bits wide. We also frequently need to perform operations involving integers of different types. Unlike C, Rust won’t automatically promote integer types to wider types. It forces you to manually cast any mismatching integer types for every operation. It also forces you to use the usize type (akin to C’s size_t) wherever you need to index into an array or slice.
It seems to me that the way Rust handles integer casting leaves something to be desired, and I know I’m not the only one who feels this way as there have been discussions pertaining to these issues dating up to several years back. It can be frustrating for programmers, because a priori, there's no reason why you couldn’t safely promote a u8, u16, or u32 into a usize, and asking programmers to manually cast integers everywhere makes the code more noisy and verbose.
In my opinion, Rust’s insistence on manual casting everywhere encourages people to write inefficient code, because writing verbose code feels uncomfortable and adds friction. You can make your code less verbose by reducing the number of integer casts, and you can reduce the number of casts by using the widest integer types possible everywhere. If you do that, your code will superficially look nicer, but it will also be less efficient.
In many cases, you can probably afford to use 64-bit integers instead of 8-bit integers. We’re talking about mere bytes of space savings, right? Well, maybe not. We care because JIT compilers can allocate tens or even hundreds of millions of objects, and the smaller these objects are, the better they fit into the data cache. Compactness matters for performance, and it will still matter for as long as processors have caches and limited memory bandwidth. By reducing the friction around integer casts, Rust could actually help programmers write more efficient code. " [3]
---
https://www.swiftbysundell.com/articles/swifts-new-shorthand-optional-unwrapping-syntax/
" if let animation { Perform updates ... }
"
---
Good languages with simple grammar
---
" We can get rid of the semicolons the same way Lua does. We can write function calls the ML/Haskell way, like f a b instead of f(a, b). (But then we need the semicolons back, or significant whitespace like Python. We can make the brackets of control structures mandatory, and the parentheses optional. That way we can write if cond { … } else { …}. We can make sure curly brackets are mostly indentation punctuation like in idiomatic C, or get rid of them almost entirely if we go the significant whitespace route. " -- https://lobste.rs/s/ay2bww/why_not_oberon#c_0s1fgp
---
" mananaysiempre 3 days ago
root | parent | next [–] |
[1] https://news.ycombinator.com/item?id=31384528
[2] https://www.aosabook.org/en/ghc.html -- https://news.ycombinator.com/item?id=32823870
---
from https://github.com/hsutter/cppfront
---
0z____ for hex constants in 'little endian' order, with most-significant-digits on the right, eg 0zA0 is decimal 10, not 160, and is the same as 0zA00. Decimal 160 is 0z0A (which is the same as 0z0A0).
So 0xA0 = 0z0A
---
kouteiheika 1 day ago
next [–] |
I don't necessarily agree with the step of putting the code in a separate function; that often works, but just as often makes it so that the code can't be read top-to-bottom anymore which hurts readability.
In this case there's, I think, a better alternative; the equivalent-ish code in Ruby for the example code here would be something like this:
values = s .partition('?')[-1] .split('&') .map { |key_value| key_value.partition('=')[-1] }
You can write these nice functional pipelines where you just read the code top-to-bottom and see step-by-step what is being done to the data on each line. You don't have to jump up-and-down around the code when reading it, and you don't have to keep too much context in your head when reading it.
This is one of the reasons why I vastly prefer Ruby over Python for most data processing tasks. I wish more languages would support this style of programming.
reply
---
https://gleam.run/news/v0.25-introducing-use-expressions/
---
i havent read these links yet:
rigoleto 56 minutes ago
parent | next [–] |
I wish OCaml had something like F#'s lightweight syntax: https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref... https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/verbose-syntax
reply
bitbckt 18 minutes ago
root | parent | next [–] |
It's been tried https://people.csail.mit.edu/mikelin/ocaml+twt/
reply
runevault 43 minutes ago
root | parent | prev | next [–] |
I'd never seen the verbose syntax for f#, I thought you had to write it the whitespace dependent way. Huh.
reply
---
" I like the new arrow but I don’t like the
characters. |
Suggest an alternative character that could be used to delimit the left side of the lambda.
Since we’re using the arrow to delimit the arguments from the return value, the second
is redundant, so remove it. |
let add = :x, y -> x + y let sum = add(1, 2)
Here, we have removed the second : character after the arguments list, since it is not necessary for the syntax or functionality of the lambda. This makes the syntax a little simpler and easier to read.
This is interesting. I haven’t even considered this syntax, but it surprisingly works! Let’s keep going and add some more nontypical features. " -- https://judehunter.dev/blog/chatgpt-helped-me-design-a-brand-new-programming-language
---
amw-zero 11 hours ago
link | flag |
Effects are the obvious feature of Koka. But, it actually also has a lot of interesting syntactic features. It has universal call syntax, which it calls dot selection (i.e. f(a, b) is equivalent to a.f(b)). It has trailing closure syntax, similar to Ruby and Swift. You can even drop braces for trailing closures with indentation. Similarly, it supports a with statement which cleans up passing trailing closures as well. The example they use in the docs for that is:
with x <- list(1,10).foreach println(x)
which is equivalent to this in Ruby:
(1..10).each {
i | puts i } |
I think these features are interesting, and it makes it a very complete language vs. just a research language based on effects.
---
" And, finally, $ doesn’t guarantee any interface at all – it just tells you (and the compiler) to think of the variable as a single entity (technical name: Scalar).
What does it mean to treat a variable as a single entity? Well, imagine I’ve got a grocery list with five ingredients on it. Saying that I’ve got one thing (a list) is true, but saying that I’ve got five things (the foods) is also true from a certain point of view. Using $ versus @ or % expresses this difference in Raku. Thus, if I use @grocery-list with a for loop, the body of the loop will be executed five times. But if I use $grocery-list, the loop will get executed just once (with the list as its argument). " -- https://raku-advent.blog/2022/12/20/sigils/
---
" The first of these perks is interpolation. In Raku, every sigiled variable is eligible for interpolation in the right sort of string. The exact details depend on the sigil and aren’t worth getting into here (mostly based on how the characters are typically used in strings – it’s kind of nice that “daniel@codesections.com” doesn’t interpolate by default). You can selectively enable/disable interpolation for specific sigials or temporarily enable interpolation in strings that normally don’t allow it with \qq[ ] (like JavaScript’s? ${ }). Between this, its Unicode support, and rationalized regex DSL system, I’m prepared to confidently claim that Raku’s text manipulation facilities significantly outdo any language I’ve tried. " -- https://raku-advent.blog/2022/12/20/sigils/
---
" The second perk is a bit of syntax sugar that only applies to &-sigiled variables but that’s responsible for a fair bit of Raku’s distinctive look. We already said that you can invoke &-sigiled variables with syntax like &foo(). But due to sugar associated with &, you can also invoke them with by omitting both the & and the parentheses. Thus, in Raku code, you typically only see a & when someone is doing something with a function other than calling it (such as passing it to a higher order function). I’ve previously blogged about how you can write Raku with extra parens to give it a syntax and semantics surprisingly close to lisp’s, so it’s only fair to point out that, thanks to this & perk, it’s possible to write Raku with basically no parentheses at all. " -- https://raku-advent.blog/2022/12/20/sigils/
---
https://docs.raku.org/language/variables#index-entry-Twigil
---
" First, there’s the config keyword. If you write config var n=1, the compiler will automatically add a --n flag to the binary. As someone who 1) loves having configurable program, and 2) hates wrangling CLI libraries, a quick-and-dirty way to add single-variable flags seems like an obvious win.
Second, you can write the sequence 1, 2, … n-1 as 1..<n. It’s an elegant extension to the standard .. operator languages like Ruby and TLA+ use. "
---
"
kebab-case
As seen in most lisps. Instead of naming things two_words or TwoWords?, you can use the name two-words. It’s easier to write and easier to read. Of course the reason why nonlisps can’t do that is because they have infix minus, and it’s ambiguous whether x-y is the expression x minus y or the invocation of the x-y function. Seems like a bad tradeoff, though. How often do you use -, and how often do you write multiword functions? Just say that x-y is always a function, and if you want math you can use spaces like the rest of us.
(Granted this couldn’t be added to existing languages without breaking everything, but maybe worth considering if you’re making a new language?)
Symbols
Ruby has a special data type called a symbol, written like :this.1 A symbol compares equal to itself and has no other functionality. It replaces single-word strings— instead of writing dict["employee_id"], you write dict.
The advantage of having symbols is that it makes strings easier to work with. In most languages, strings are used to represent a lot of different things: tokens, text, structured data, code, etc. If you see the string “book”, it’s not clear without context whether it’s a dictionary key, or a text field with just “book” in it, or a trivial CSV, or what. With symbols, you can at least rule it out the former case, because then it’d instead be :book.
Dedicated testing syntax
As seen in D’s unit-test blocks on functions and P’s monitors. While it makes sense to keep the actual testing as library code, testing is so universal in larger software projects that it sounds nice to give it syntactic support. https://dlang.org/spec/unittest.html https://p-org.github.io/P/manual/monitors/ " -- [4]
"
7 calvin 10 hours ago
link | flag |
For examples of these not mentioned: Erlang has symbols called atoms, VB.NET has date literals (and XML literals).
~ apg 6 hours ago | link | flag |
Why stop there? In Common Lisp reader macros allow you to pretty much do whatever you want! Want a date literal? Cool. Want to mix JSON with sexpressions? No problem! Want literal regex? Yup, can do that too.
" -- [5]
"
~ roryokane edited 3 hours ago
link | flag |
Examples of symbols:
As the article mentioned, Ruby has symbols: :example As you mentioned, Erlang has atoms: example Elixir has atoms: :example Clojure has keywords, which are generally used as keys within maps: :example has symbols, which generally refer to variables when metaprogramming: 'example Many other Lisps such as Scheme and Common Lisp have symbols and use the same syntax for them.
" -- [6]
" ~ ianbicking 5 hours ago
link | flag |
Yeah… it feels like single-line strings are fixing something that’s no longer a problem: strings accidentally being unclosed and it causing confusing errors. With syntax highlighting and halfway decent error messages it’s not that important.
BUT, the one case where I’d like a multi-line string syntax is something like:
def run_query(): sql = """ SELECT * FROM x """
Where I’d like that literal to actually resolve to "SELECT * FROM x" – dropping the leading and trailing empty line and any consistent leading whitespace (with a special case for internal empty lines).
~ xiaq 4 hours ago | link | flag |
Val has this: https://github.com/val-lang/specification/blob/main/spec.md#string-literals
And I’m quite sure to have seen other languages do this, but can’t recall now. " -- [7]
" ~ agent281 5 hours ago
link | flag |
If you like the Frink date literal, you may also like Elixir’s sigils. It’s used for date, time, regex and more. You can even create your own custom sigils. " -- [8]
agent281 10 days ago
link |
If you like the Frink date literal, you may also like Elixir’s sigils. It’s used for date, time, regex and more. You can even create your own custom sigils.
4 doug-moen 9 days ago | link |
Thanks. I like sigils better than Lisp reader macros, because you can parse a sigil using a context free grammar, without executing code from the module that defines the sigil. The ability to parse a program without executing it is valuable in a lot of contexts.
" 5 ianbicking 5 hours ago
link | flag |
Here’s some other microfeatures I like:
In Python and others, f"{var=}" meaning f"var={var}" I really like JavaScript’s {key} being equivalent to {key: key} … it rewards consistent naming I like C#‘s (fairly new) slice notation of list[1..^1] which is equivalent to Python’s list[1:-1]. Using negative numbers to indicate counting from the end is cute but can lead to errors. I’m a little ambivalent about obj?.prop meaning something like obj && obj.prop … but it’s definitely a microfeature and I guess is useful. I also like C#‘s constructors like x = new X {prop1 = val, prop2 = val} which is kind of like x = new X(); x.prop1=val; x.prop2=val. There’s lots of different constructor patterns like this… I’m not sure which I like best, though I do find simplistic implementations like Python and JavaScript to be tedious." -- [9]
" .
~ ilyash 2 hours ago
link | flag |
In Next Generation Shell I’ve experimented by adding
section "arbitrary comment" { code here }
and this is staying in the language. It looks good. That’s instead of
Later, since NGS knows about sections, I can potentially add section info to stack traces (also maybe logging and debugging messages). At the moment, it’s just an aesthetic comments and (I think) easily skip-able code section when reading. Symbols
I’ve decided not to have symbols in NGS. My opinion (I assume not popular) is that all symbols together is one big enum, instead of having multiple enums which would convey which values are acceptable at each point.
" -- [10]
" ~ Corbin 9 hours ago
link | flag |
In Monte, we matured the max= syntax example; it happened to be a proposed syntactic extension for E, and we merely followed through with the proposal. We also expanded other augmented syntax. For example, this high-level syntax…
x += y
…would be equivalent to this less-sugared syntax:
x := x.add(y) " -- [11]
;)
~ icefox 18 hours ago (unread)
link | flag |
I would love symbols in more programming languages, but it’s a little tricky to implement in programming languages without a runti–
Oh, I just figured out how to implement them in something with C’s compilation and linking model. Each symbol is a global pointer to a string with its name. Each symbol also has a linker symbol with its name. Make the linker symbols exported and tell the linker to deduplicate and coalesce them, via Black Magic, and it will fix up all references to them automatically. Bingo, each symbol is represented by a unique integer that is a pointer to a unique string, and all symbols with the same string are interned.
Generating new symbols with names not previously mentioned in the program then still requires dynamic memory and some kind of global runtime, but you need to allocate memory to generate new symbols no matter what so the machinery that creates new symbols can be part of the same lib that provides your memory allocator.
ianbicking 35 hours ago
link | flag |
Yeah… it feels like single-line strings are fixing something that’s no longer a problem: strings accidentally being unclosed and it causing confusing errors. With syntax highlighting and halfway decent error messages it’s not that important.
BUT, the one case where I’d like a multi-line string syntax is something like:
def run_query(): sql = """ SELECT * FROM x """
Where I’d like that literal to actually resolve to "SELECT * FROM x" – dropping the leading and trailing empty line and any consistent leading whitespace (with a special case for internal empty lines).
~ andyc 19 hours ago (unread) | link | flag |
Oil has that!
https://www.oilshell.org/blog/2021/09/multiline.html#multi-line-string-literals-and-and
The indentation of the closing quote determines the leading whitespace that’s stripped.
And if there’s an initial newline it’s dropped. But the last newline isn’t dropped, which I think makes sense ? (You can leave it off if you want by putting it on the same line as the last line of text.)
oil$ var x = """ > hello > there > """
oil$ = x (Str) 'hello\nthere\n'
FWIW this pretty much comes from Julia, which has Python-like multi-line strings, except they strip leading whitespace
The multi-line strings are meant to supplant the 2 variants of here docs in shell.
There are unfortunately still 3 kinds multi-line strings to go with 3 kinds of shell strings. But I’m trying to reduce that even further:
https://lobste.rs/s/9ttq0x/matchertext_escape_route_from_language#c_vire9r
https://lobste.rs/s/9ttq0x/matchertext_escape_route_from_language#c_tlubcl
~ xiaq 33 hours ago
link | flag |
Val has this: https://github.com/val-lang/specification/blob/main/spec.md#string-literals
And I’m quite sure to have seen other languages do this, but can’t recall now.
~ iv 28 hours ago (unread) | link | flag |
Java’s new-ish text blocks do this too!
~ xiaq 22 hours ago (unread) | link | flag |
Aha, right. For anyone else curious it’s described in JEP 378: Text Blocks.
1 justinpombrio 7 days ago (unread)
link |
Zig does that very well. You write:
let sql =
\SELECT *
\FROM x
and you get the string “SELECT * \nFROM x”. There’s no ambiguity about leading spaces. If you wanted a leading space before SELECT or FROM, you’d just put a space there.
I’m just confused why it uses
instead of the more obvious “””.
---
5 briankung 9 days ago
link |
I haven’t actually used them, but KDL’s “slashdash” comments to knock out individual elements are pretty interesting. From The KDL Document Language:
On top of that, KDL supports /- “slashdash” comments, which can be used to comment out individual nodes, arguments, or children:
// This entire node and its children are all commented out. /-mynode "foo" key=1 { a b c }
mynode /-"commented" "not commented" /-key="value" /-{ a b }
It’s a little more clear with the syntax highlighting on the site.
1 roryokane edited 8 days ago | link |
Clojure supports something similar with its #_ reader macro, which makes the reader ignore the next form. It’s pretty handy for debugging.
Clojure also has a comment macro that ignores its body and evaluates to nil. I rarely use it.
2 hcs 8 days ago (unread) | link |
Racket (and maybe other Scheme-family?) has this as well with S-expression comments: #;
Easy to remember since ; is a line comment.
---
---
https://elizarov.medium.com/types-are-moving-to-the-right-22c0ef31dd4a
https://lobste.rs/s/yymnmm/types_are_moving_right
---
1 emiller 7 days ago (unread)
link |
Agreed, it makes sense for languages that are going to be used from the command-line. Nextflow handles these under params https://www.nextflow.io/docs/edge/config.html#scope-params in a pretty elegant way!
---
5 briankung 9 days ago
link |
I haven’t actually used them, but KDL’s “slashdash” comments to knock out individual elements are pretty interesting. From The KDL Document Language:
On top of that, KDL supports /- “slashdash” comments, which can be used to comment out individual nodes, arguments, or children:
// This entire node and its children are all commented out. /-mynode "foo" key=1 { a b c }
mynode /-"commented" "not commented" /-key="value" /-{ a b }
It’s a little more clear with the syntax highlighting on the site.
1 roryokane edited 8 days ago | link |
Clojure supports something similar with its #_ reader macro, which makes the reader ignore the next form. It’s pretty handy for debugging.
Clojure also has a comment macro that ignores its body and evaluates to nil. I rarely use it.
2 hcs 8 days ago (unread) | link |
Racket (and maybe other Scheme-family?) has this as well with S-expression comments: #;
Easy to remember since ; is a line comment.
---
msw 9 days ago
link |
I really like python’s “in between” operator. 10 <= x < 20 Rust’s include_str! macro is super useful as well. Much nicer than Java’s overcomplicated getResourceAsStream() (which is still very nice). Having the ability to include data in an “executable” in a well defined way is great.
---
]
3 Corbin 10 days ago
link |
In Monte, we matured the max= syntax example; it happened to be a proposed syntactic extension for E, and we merely followed through with the proposal. We also expanded other augmented syntax. For example, this high-level syntax…
x += y
…would be equivalent to this less-sugared syntax:
x := x.add(y)
2 kevinc 9 days ago
link |
Optional chaining and defaulting operators are my pick.
1 emiller 7 days ago (unread)
link |
Awesome read! I love the three classes of features.
I’ve got one to throw in the mix, Nextflow’s file(). It just handles remote and local files the same, so the end user can use anything from a file on an FTP server, to s3. It just works and stages the files locally.
---
.
4 msw 9 days ago
link |
I really like python’s “in between” operator. 10 <= x < 20 Rust’s include_str! macro is super useful as well. Much nicer than Java’s overcomplicated getResourceAsStream() (which is still very nice). Having the ability to include data in an “executable” in a well defined way is great.
-- [12]
---
https://tratt.net/laurie/blog/2023/why_we_need_to_know_lr_and_recursive_descent_parsing_techniques.html argues that you should design grammar as LR even if you implement it as recursive descent.
note: "Every LL(k) grammar is also an LR(k) grammar" -- [13]
---
" ’s quasi-literal syntax
The E language is a veritable cornucopia of interesting programming language ideas. But to pick one that I really like it’s the quasi-literal syntax for safely constructing values in other languages: SQL, HTML, etc. There were several iterations of this idea in E, and the documented versions are older I believe. But the basic idea is that you have a generic syntax for constructing multi-line strings with embedded expressions. On the surface this is a familiar idea from many languages, but E has a nice twist on it. For example, given a code snippet like the following
def widgetName := “flibble” def widgetCount := 42 def sql := ``` INSERT INTO widgets(name, count) VALUES($widgetName, ${widgetCount + 1}) ``` db.exec(sql)
what gets constructed here is not a simple string, but rather some kind of Template object/record. That template object has two fields:
A list of fragments of the literal string before and after each variable reference: “INSERT INTO … VALUES(”, “, ”, and “)” in this case. A list of the (evaluated) expression values. In this case, that is the value of the widgetName variable and the result of adding 1 to widgetCount.
The database exec() method then doesn’t take a String, but rather one of these Template objects and it applies appropriate processing based on the contents. In this case, by constructing a prepared statement, something like the following:
def exec(template) { def sql = template.fragments.join(“?”) def stmt = db.prepare(sql) stmt.bind(template.values) stmt.execute() }
Here the (completely made up) code replaces the originally expressions with SQL placeholders in a prepared statement: “INSERT INTO … VALUES(?, ?)”. It then binds those placeholders to the actual values of the expressions passed in the template. (The exact type of the “values” field is questionable: E was dynamically-typed. For now, let’s assume that all values get converted to strings, so “values” is a list of strings: in this example “flibble” and “43”).
This has the effect of allowing the easy use of prepared statements, while having a syntax that is as easy to use as string concatenation. Safety and convenience. The same syntax can be used to provide safe templating when outputting HTML, XML, JSON, whatever.
(E’s actual approach was somewhat different to how I’ve presented it, but I think this version is simpler to understand. E also allowed the same syntax to be used for parsing too, but I think that is less compelling and complicates the feature).
Edit: /u/kn4rf and /u/holo3146 on Reddit point out that this already exists in JavaScript? in the form of tagged template literals, and has been proposed for Java too in JEP-430. Very cool! " https://old.reddit.com/r/programming/comments/10fw1ac/a_few_programming_language_features_id_like_to_see/ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates https://openjdk.org/jeps/430
-- https://neilmadden.blog/2023/01/18/a-few-programming-language-features-id-like-to-see/
.
~ idrougge 6 hours ago
link | flag |
The first point sounds a bit like Swift’s ExpressibleByStringInterpolation?. https://davedelong.com/blog/2021/03/04/exploiting-string-interpolation-for-fun-and-for-profit/
~ lorddimwit 6 hours ago
link | flag |
The quasi-literal syntax reminds me (just coincidentally) of the hoops I jumped through in dpdb to get different types of parameter interpolation strings …interpolated.
---
https://github.com/tc39/proposal-pipeline-operator
---
---
could begin blocks with IDENTIFIER COLON and end them with END IDENTIFIER, eg:
blockname: pp Do stuff inside the block end blockname
and could begin/end anonymous blocks with {}, eg:
{ pp Do stuff inside the block }
(as a formatting choice, single line blocks could be condensed to:
{pp Do stuff inside the block}
)
---
"We propose to change for loop variables declared with := from one-instance-per-loop to one-instance-per-iteration. " -- https://github.com/golang/go/issues/60078 https://go.googlesource.com/proposal/+/master/design/60078-loopvar.md
---
about the 'dt' language and its extensibilty:
" The language is somewhat hackable already, you could define & and plenty of single-symbol “commands” to do whatever you want! Although readability can suffer of course. My only control here is that I’ve intentionally made parsing dt code evaluate in a strict left-to-right order, and avoided some forth-isms like pushing/popping a return stack, or allowing a ' (tick) operator that can semantically access values “to the right” " -- https://lobste.rs/s/b8icdy/dt_duck_tape_for_your_unix_pipes#c_4vquvy "I think avoiding push/pop as a hard & fast rule was a great design decision, and I’ve always hated forward parsing in Forth." -- https://lobste.rs/s/b8icdy/dt_duck_tape_for_your_unix_pipes#c_0cxs7u
---
~ andyc 10 hours ago (unread) | link | flag |
Yeah, the most common style is -e 'single quoted program', and also you want -v NAME=value like awk for var substitution (sed is notably missing this)
This results in safer string substitution vs. the shell doing it with something like awk -e "x == $NAME". That leads to string injection problems (meant to write a blog post about that)