proj-oot-ootSyntaxNotes9

"It is beyond me why C thought to introduce octal and hexadecimal but not binary in their literal syntax; Rust addresses this gap with the same “0b” prefix as found in some non-standard C compiler extensions. Additionally, Rust allows for integer literals to be arbitrarily intra-delimited with an underscore character." -- [1]

---

"While Rust has no ternary operator per se, it is expression-oriented: statements have values. So the above example becomes: x = if is_foo { foo } else { bar }; That’s a bit more verbose than its C equivalent (though I personally like its explicitness), but it really starts to shine when things can marginally more complicated: nested ternary operators get gnarly in C, but they are easy to follow as simple nested if-then-else statements in Rust. " -- [2]

---

"... and I found that I often use match where I would have used a ternary operator in C, with the added benefit that I am forced to deal with every case. As a concrete example, take this code that is printing a slice of little-endian bytes as an 8-bit, 16-bit, or 32-bit quantity depending on a size parameter:

    print!("{:0width$x} ",
        match size {
            1 => line[i - offs] as u32,
            2 => u16::from_le_bytes(slice.try_into().unwrap()) as u32,
            4 => u32::from_le_bytes(slice.try_into().unwrap()) as u32,
            _ => {
                panic!("invalid size");
            }
        },
        width = size * 2
    );" -- [3]

---

regarding this, do we want the Rust behavior, or the simpler C behavior where newlines must be explicitly included a la \n?

"In C, this is a snap: string literals without intervening tokens are automatically concatenated, so the single literal can be made by multiple literals across multiple lines.

...

  But in Rust, string literals can span multiple lines (generally a feature!), so splitting the line will also embed the newline and any leading whitespace. e.g.:
    println!(
        "...government of the {p}, by the {p}, for the {p},
        shall not perish from the earth.",
        p = "people"
    );

Results in a newline and some leading whitespace that represent the structure of the program, not the desired structure of the string:

...government of the people, by the people, for the people, shall not perish from the earth.

...

Rust has support for continuation of string literals! If a line containing a string literal ends in a backslash, the literal continues on the next line, with the newline and any leading whitespace elided. This is one of those really nice things that Rust lets us have; the above example becomes:

    println!(
        "...government of the {p}, by the {p}, for the {p}, \
        shall not perish from the earth.",
        p = "people"
    );" -- [4]

---

could require capitalization on any import statement that imports a macro, even if the macro itself is in the class that doesnt need capitalization

---

mb have a punctuation character whose function is to act like whitespace, but also to place parens around the connected words.

Eg

map,add,[1 2 3]

might mean

(map add [1 2 3])

---

interesting syntax from Zig:

" You can also insert arbitrary logic into that function. Here is a function that implements the struct-of-arrays transformation:

fn StructOfArrays?(comptime T: type) type { reflect info about the type T const t_info = @typeInfo(T);

    // check that T is a struct
    if (t_info != .Struct) @compileError("StructOfArrays only works on structs!");
    // make a new set of fields where each type is an array
    var soa_fields: [t_info.Struct.fields.len]std.builtin.TypeInfo.StructField = undefined;
    for (t_info.Struct.fields) |t_field, i| {
        var soa_field = t_field;
        soa_field.field_type = []t_field.field_type;
        soa_field.default_value = null;
        soa_fields[i] = soa_field;
    }
    // make a new type with those array fields
    var soa_info = t_info;
    soa_info.Struct.fields = &soa_fields;
    const Inner = @Type(soa_info);
    // return the final type
    return struct {
        inner: Inner,
        const Self = @This();
        /// Fetch the i'th element of self
        fn get(self: *const Self, i: usize) T {
            var t: T = undefined;
            // for each field of t, get the data from the i'th element of the corresponding array
            // (this is unrolled at compile time because of the `inline` keyword)
            inline for (t_info.Struct.fields) |t_field| {
                @field(t, t_field.name) = @field(self.inner, t_field.name)[i];
            }
            return t;
        }
    };} " -- [5]

---

mb prohibit variable shadowing -- you can see them lexically after all. altho wouldn't this just be closures in our case anyways?

---

Maybe some keywords, if not followed by a '{', have an implicit one, and all such implicit ones are closed by the next '.'.

---

" I actually like c++, but I can't deny the syntax is incredibly ugly. for example, here's a declaration of a vector of pairs of strings suitable for a header file:

  std::vector<std::pair<std::string, std::string>> vec;"

---

https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/symbol-and-operator-reference/

---

an example of some nice clojure syntax/lib fns for macros, quasiquote, list expansion, 'butlast', 'case':

" (defmacro t-> [v & forms] (let [[op & args] (last forms) xf (if (#{'transduce 'into} op) (butlast forms) forms)] (case op into `(~op ~@args (comp ~@xf) ~v) transduce `(~op (comp ~@xf) ~@args ~v) `(sequence (comp ~@xf) ~v)))) " -- [6]

---

[7] makes the point that we need the efficiency of clojure transducers with the syntax of ->>

e suggests a syntax of something like

(t-> payments (filter cash?) (map :amount) (transduce +))

and provides a macro to give that (which is the example macro i gave in the previous section)

---

"The distinction is if the inner language is recursive or not.

For recursive language, an example I always look at is Kotlin. Relevant bits from Lexer and Parser. The key takeaway that no coordination between parser and lexer is required, lexer just counts curly braces. " [8]

---

"For example, Jai moves types-to-the-right. If you're going to be writing left-to-right like English, this just makes sense. It's what we should have had from the beginning. It fixes pointer syntax, so that x, y T are two pointers-to-pointer-to T, instead of T x, y which declares one T and one pointer-to-pointer-to-T."

---

"" is f"" r"" for raw string

---

" For all the words justifying brevity as his core focus, his only analysis of the nature of brevity itself is that it corresponds to the size of the parse tree rather than the number of characters. The fact that any program can be reduced down to (execute-my-program) isn’t grappled with, presumably because libraries were also deemed out of scope.

A more serious analysis of brevity might define it as “the entropy of a parse tree recursively inlined/macroexpanded down to language primitives.” This suggests our focus ought to be on two questions: how compressible are our primitives, and how can we enable users to achieve something close to optimal compression? Since the first question has a relative measure (what’s the compression ratio for our expanded parse tree?), we could productively iterate on both better primitives and better tools for abstracting over them. "

---

hardwaregeek 2 hours ago [–]

I'm a huge fan of strictNullChecks but it is remarkable how much language features contribute towards making non-nullable types ergonomic. For instance Rust has a lot of primitives on Option a lot nicer.

One that I love is `Option::ok_or` (or `Option::ok_or_else` if you like). It takes an Option and converts it to an error. With the try macro (?) it makes Option a lot easier to use. Compare:

   function parseFile(file: File | null) {
     if (file === null) {
       throw new Error("File must exist");
     }
     // TS now infers as File
   }

to:

   fn parse_file(file: Option<File>) {
      let file = file.ok_or(Error::new("File must exist"))?;
   }

Likewise if you want to apply a function to an Option if it is Some and pass along None otherwise, you can use `Option::map`:

   fn parse_file(file: Option<File>) {
      let parse_result = file.map(|f| parse(f));
   }

Indeed it's a little interesting how libraries have adopted paradigms beyond the language. React is extremely expression based, but that makes for a clunky combo with JS' primarily statement based control flow. You see this in React developers' reliance on ternary operators and short circuiting for control flow in JSX.

Of course this just JavaScript? being a classical imperative language. Not much to do about that.

reply

 rudi-c 2 hours ago [–]

> I'm a huge fan of strictNullChecks but it is remarkable how much language features contribute towards making non-nullable types ergonomic.

For sure, that was our experience as well. Without TypeScript?'s control flow analysis, it would be much less ergonomic to use and would probably lead to a lot of non-null `!` assertions everywhere. When writing correct code, you never notice that control flow analysis is there at all. A desirable feature, though as a result of operating in the background, few know how much TypeScript? innovates in this area over other mainstream languages.

reply

dlbucci 2 hours ago [–]

I was always a big fan of Optionals, but having learned Kotlin this year, I was pleasantly surprised at how much non-nullable types seem to remove the need for them. And non-nullable can be just as ergonomic as Optionals with the `?:` operator (or `??` in TypeScript?). My only issue when working with `strictNullChecks` in TS is that `null` and `undefined` both exist (thanks JS...). I also wish TS adopted a `Type?` syntax like Kotlin that would signify `Type

null undefined` and could be used anywhere, not just in function parameters and object types (like TS's current `key?: value` syntax).

reply

srcreigh 13 minutes ago [–]

The tsdef library has a Nilable<T> type that means T

undefined null. It's not so bad to type with a snippet like ?<tab> => Nilable<>.

reply ---

tabtab 45 days ago

parent [–]on: Object-oriented programming: Some history, and cha...

I believe languages should allow the programmer to define the relationship between one code block and another. This includes scope and state relationship. Think more powerful lambdas with better interface syntax and options. OOP languages typically hard-wire such relationships into a limited set of relationship conventions. This results in OOP graphs that don't fit the domain or need well.

Sure, one can do such in Lisp, but many find Lisp hard to read. With C-style languages, the "ugly" syntax gives helpful visual cues. Stuff within (...) is usually a parameter list, stuff within {...} is usually sequential code, and "[...]" indicates indexing of some kind, for example. I find such helps my mind digest code faster.

Lisp aficionados will often claim "one gets used to" the uniformity. But that's like arguing we don't need color because one can "get used to" seeing in black and white. To a degree, yes, but the color adds another dimension of info. Easily recognizable group/block types is another color.

---

http://www.oilshell.org/release/latest/doc/eggex.html

---

~ akavel 21 hours ago

link flag

Also curious, as well as why Zig uses parentheses in ifs etc. I know what I’ll say is lame, but those two things frustrate me when looking at Zig’s code. If I could learn the rationale, it might hopefully at least make those a bit easier for me to accept and get over.

    ~
    ifreund 19 hours ago | link | flag | 

One reason for this choice is to remove the need for a ternary operator without greatly harming ergonomics. Having the parentheses means that the blocks may be made optional which allows for example:

const foo = if (bar) a else b;

    ~
    Hail_Spacecake 11 hours ago | link | flag | 

There’s a blog post by Graydon Hoare that I can’t find at the moment, where he enumerates features of Rust he thinks are clear improvements over C/C++ that have nothing to do with the borrow checker. Forcing if statements to always use braces is one of the items on his list; which I completely agree with. It’s annoying that in C/C++, if you want to add an additional line to a block of a brace-less if statement, you have to remember to go back and add the braces; and there have been major security vulnerabilities caused by people forgetting to do this.

    5
    matklad 7 hours ago | link | flag | 
    That was things Rust shipped without.

~ Loup-Vaillant 8 hours ago

link flag

The following would work just as well:

const foo = if bar { a } else { b };

I’ve written an expression oriented language, where the parenthesis were optional, and the braces mandatory. I could use the exact same syntactic construct in regular code and in the ternary operator situation.

Another solution is inserting another keyword between the condition and the first branch, as many ML languages do:

const foo = if bar then a else b;

~ cmcaine 16 hours ago

link flag

For what values of a, b, c would this be ambiguous?

const x = if a b else c

I guess it looks a little ugly?

    ~
    Loup-Vaillant 8 hours ago | link | flag | 

If b is actually a parenthesised expression like (2+2), then the whole thing looks like a function call:

const x = if a (2+2) else c

Parsing is no longer enough, you need to notice that a is not a function. Lua has a similar problem with optional semicolon, and chose to interpret such situations as function calls. (Basically, a Lua instruction stops as soon as not doing so would cause a parse error).

Your syntax would make sense in a world of optional semicolons, with a parser (and programmers) ready to handle this ambiguity. With mandatory semicolons however, I would tend to have mandatory curly braces as well:

const x = if a { b } else { c };

    ~
    cmcaine 5 hours ago | link | flag | 
    Ah, Julia gets around this by banning whitespace between the function name and the opening parenthesis, but I know some people would miss that extra spacing.

~ orib 3 hours ago

link flag

abs() { x = if a < 0 - a else a }

---

https://mikeinnes.github.io/2020/06/06/modern-languages.html

" In the 2020s programmers have a shared intuition for syntax that transcends languages. Basic ideas like block structure, lexical scope and use of indentation are universal and aren’t going anywhere. More specifically, C-like syntax (via its modern incarnations in Rust and Swift) is taking over, even influencing new functional languages that use effect handlers. Still, Pascal-, ML- and Lisp-like notations aren’t dead yet. Realistically you’re going to choose one of these syntax families as a base and make minor adjustments. There’s a little room for innovation here and there, but the biggest decisions about syntax are made for you.1

1: A possible recent exception is Clojure, whose marriage of Lisp s-exprs with JSON-like data notation has been influential."

---

for the sake of autocomplete, we want ppl to type:

baseclass.attribute

rather than

attribute baseclass

(see e.g. https://news.ycombinator.com/item?id=13917206 )

---

maybe:

note that i guess this conflicts with 'an assignment returns the value of the assignment'

---

{} blocks are expressions and return the result of evaluating the last line in the {} block

---

oconnor663 3 hours ago [–]

> There is no operator precedence. A bare a * b + c is an invalid expression. You must explicitly write either (a * b) + c or a * (b + c).

Honestly I've often wished for this in mainstream languages. It seems like operator precedence should go the way of bracketless if and implicit int casts. (Though I wonder if they wind up making exceptions here for chains of method calls? I guess technically those rely on operator precedence sort of?)

Edit: Yeah I see the example code has "args.src.read_u8?()". So it looks like they figured out how to keep the good stuff.

reply

wuschel 3 hours ago [–]

LISP-like languages have enforced operator precedence due to polish notation e.g. (+ (* a b) (+ c d))

reply

aidenn0 1 hour ago [–]

In addition the variadic prefix-notation means the operators are not limited to being binary:

  3*x*y*z+w

becomes:

  (+ (* 3 x y z) w)

reply

nestorD 1 hour ago [–]

Me too! So far I have seen four actual bugs in large numerical code bases that were caused by overlooking operator precedence. I expect to see more in the years to come.

I think that precedence of '*' over '+' is acceptable (as everyone knows it instinctively) but I would love a way to require parenthesis for everything else.

reply

sleepydog 2 hours ago [–]

APL (and, its derivatives, I think) evaluate strictly right to left, so

a * b + c

is a * (b + c). It might be jarring at first but I really came to enjoy the consistency, I never had to remember operator precedence, which helps in a language like APL where most functions are infix.

reply

tragomaskhalos 2 hours ago [–]

Conversely, Smalltalk is left-to-right, so

a + b * c is (a + b) * c

which is simply a result of every operation being a message send - muddling the rules with precedence would be likewise confusing, and would ruin the simplicity of the grammar.

reply

---

"With Rust’s .. range syntax, if you want to start at the first index (zero), you can drop the value before the two periods. "

---

"In many ways Icon also shares features with most scripting languages (as well as SNOBOL and SL5, from which they were taken): variables do not have to be declared, types are cast automatically, and numbers can be converted to strings and back automatically. Another feature common to many scripting languages, but not all, is the lack of a line-ending character; in Icon, lines not ended by a semicolon get ended by an implied semicolon if it makes sense."

---

mb double smicolon for comment syntax alternatives are -- but im thinking of using those for arith anoher comment alternative is (but thats less visible than ;;) alternatively strings (not assigned to anything, eg alone on a line) alternately triple quoted strs """ or

---

maybe using macros doesn't need to be called out with explicit syntax, but defining them does

---

"

Making assignments expressions

Assignments in Gleam are now expressions that return the value being assigned. Previously in Gleam assignments were statements, meaning they did not evaluate to anything and could not be the final form in a function or a block. This had two main drawbacks.

The first is an development ergonomics issue. When writing Gleam code it is often desirable to save as often as possible so that the type checker can inform you of any mistakes or inconsistencies in the code that has just been written.

pub fn main() { let x = run(1, 2, 3) And now I've hit save... }

If I write the code above and hit save I might expect the Gleam compiler to tell me whether I’ve used the run function correctly. Instead it would complain of a syntax error, leaving me none the wiser as to whether my code is correct until I add a placeholder value on the next line and hit save again. With this latest version of Gleam this code will now be accepted and type checked.

This is a minor change but it removes friction from the development workflow for users with these editing habits, making Gleam friendlier and easier to pick up. Developer experience is a first class concern of Gleam, Gleam should be as productive and enjoyable to write as possible.

The next issue is that previously it was not possible to have an assertion as the final form in a function or block, forcing the user to add a superfluous dummy value to be returned.

/ A function that asserts `segments` is non-empty and / starts with a slash. pub fn assert_valid(segments) { assert ["/", ..] = segments }

Typically we prefer to use the Result type to safely handle invalid states, but in some scenarios it is beneficial to take advantage of the Erlang virtual machine’s offensive programming and fault tolerance capabilities by using assertions like this. For example, when prototyping or writing background batch processing script. " -- [9]

---

"Feel free to get inspired by the highly readable but very minimal syntax of V or (a bit less) Arturo." [10]

https://github.com/vlang/v/blob/master/doc/docs.md https://github.com/arturo-lang/arturo

---

re: Swift:

" Let’s take an array that we want to sort backwards:

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

The less idiomatic way of doing this would be to use Swift’s sorted method for arrays, and employ a custom function that tells it how to do pairwise order comparison on the array’s elements, like so:

func backward(_ s1: String, _ s2: String) -> Bool { return s1 > s2 } var reversedNames = names.sorted(by: backward)

The backward function compares two items at once, and returns true if they are in the desired order and false if they are not. The sorted array method expects such a function as an input in order to know how to sort the array. As a side note, here we can also see the usage of the argument label by, which is oh so beautifully terse.

If we resort to more idiomatic Swift, we find that there is a better way to do this using closures:

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

The code between { } is a closure that is being defined and passed as an argument to sorted at the same time. If you’ve never heard of them, closures are just unnamed functions which capture their context. A good way to think about them is as Python lambdas on steroids. The keyword in in the closure is used to separate the closure’s arguments and its body. More intuitive keywords such as : were already taken for signature type definitions (the closure’s argument types get automatically inferred from sorted’s signature in this case, so they can be avoided), and we all know naming things is one of the hardest things to do in programming, so we are stuck with using the not so intuitive in keyword for this.

In any case, the code is already looking more concise.

We can, however, do better:

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

We removed the return statement here because, in Swift, single line closures implicitly return.

Still, we can go even deeper:

reversedNames = names.sorted(by: { $0 > $1 } )

Swift also has implicitly named positional parameters, so in the above case $0 is the first argument, $1 the second, $2 would be the third, and so on. The code is already compact and easy to understand, but we can do better yet:

reversedNames = names.sorted(by: >)

In Swift, the > operator is just a function named >. Therefore, we can pass it to our sorted method, making our code extremely concise and readable.

"

---

" I remember the exact moment I committed to writing the book. I was stuck on a tricky language design problem: constructor syntax. I knew I wanted classes, which meant some way to construct instances. Adding a new keyword felt too special-purpose for my minimal language. I like Smalltalk and Ruby’s approach of making new be a method on the class object itself, but that requires metaclasses and a lot of other machinery.

I was struggling to find a way to add instantiation without making the language much bigger. Then I remembered JavaScript’s? thing where you can simply invoke a “class” as if it were a function to create new instances. That has all sorts of weird baggage in JavaScript? because everything does in JS, but the concept and syntax were perfect. I already had first-class classes. And I already had closures which meant a function call syntax that could be applied to arbitrary expressions. So “constructors” just became what you got when you invoked a class. " -- [11]

---

"Python's type annotation syntax is shoe-horned to minimize changes to the parser--if you're allowing yourself to rewrite the parser, why not improve the type annotations? Indeed, why not stick to a more Rust-like syntax in general?)." [12]

---

elif, elsif, elseif, else if

i guess i prefer elif, b/c it's shortest

---

my assembly language should be regex parsable

---

" hard to understand style like point free where pipes are a lot easier to follow "

"Meanwhile, you can learn SML, F# and Ocaml in a couple days, gradually enjoy the functionality they have to offer and benefit for a nice community. I really don't understand why anyone would choose Haskell.

samhh 4 days ago [–]

Function application pipelines and function composition are closely related. Here's a "pipe" in Haskell:

\x -> h $ g $ f x

At a certain point you realise that in many cases seeing the `x` is not just useless but needless visual noise. The following is identical and more readable once you've internalised composition:

h . g . f

And this extends to a cute trick in which, if you need to explicitly provide that data to begin with, you can do this:

h . g . f $ x

I'm now working with Haskell having previously come from PHP and JS/TS. Composition is more readable, it's just harder to get started with because it's different to what you're used to.

---

... Unrelated, but I also miss a binary constant notation (such as 0b10101)

OnACoffeeBreak? on April 14, 2020 [–]

I know that we're not voting, but I miss a binary literal very much.

nickysielicki on April 14, 2020 [–]

In the same vein, I really like being able to use underscores in binary and hex literals to denote subfields in hardware registers.

0xDEADB_EEF

0b1_010_110111001001

etc.

jhallenworld on April 14, 2020 [–]

Should take Verilog binary construction syntax, like { 12'd12, 16'hffee, 3'b101 } (or something similar that would fit with C's syntax).

---

wahern on April 14, 2020 [–]

GCD relies on Blocks (closures) for ergonomics, and Blocks have been proposed to WG14, for example N1451: http://www.open-std.org/jtc1/sc22/ http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1451.pdf

---

some beneficial changes Rust made:

" try!() -> ?, the addition of impl trait, and the dyn keyword ... I remember being very happy when ? came about and dyn is certainly clearer than what we had before. " [13]

"Adding `dyn` was almost completely automated, removing the need for `extern crate` was welcome and very easy to apply, likewise `try!()` to `?`." [14]

---

from nim [15]:

" Dup

dup is a macro which turns an in-place function into one that returns a result without modifying its input. The idea is that future versions of Nim will offer only in-place versions of the functions (i.e. no more sort and sorted, rotateLeft and rotatedLeft, and so on), which could be transformed to functions returning a copy via dup. "

---

" Perl was also inspired by Unix command shell languages like the Bourne shell (sh) or Bourne-again shell (bash), so it has many special variable names using punctuation. There’s @_ for the array of arguments passed to a subroutine, $$ for the process number the current program is using in the operating system, and so on. Some of these are so common in Perl programs they are written without commentary, but for the others there is always the English module, enabling you to substitute in friendly (or at least more awk-like) names.

With use English; at the top of your program, you can say:

    $LIST_SEPARATOR instead of $"
    $PROCESS_ID or $PID instead of $$
    the @{^CAPTURE} array instead of the numbered regular expression capture variables like $1, $2, and $3
    et cetera.

All of these predefined variables, punctuation and English names alike, are documented on the perlvar documentation page. "

---

re: Perl

"

Sigils seem pointless and at the wrong level of abstraction to me. If it’s a simple data structure, I’m not going to forget what it is. If it’s a complex data structure, I don’t want to constantly think of it as a hash or an array, I want to think of it as the data structure it actually represents. Ruby’s use of sigils to differentiate between instance variables, class variables, and so on makes more sense. "

---

" [*] e.g. many methods will operate without an argument, and use the default argument $_ which I pronounce “it”.

my @a = (1,2,3); say for @a;

Here “for” is looping over @a. The for loop aliases “it” to each value in turn. “say” is invoked on each iteration without an argument, so it uses “it” and prints it. "

---

https://www.jsoftware.com/help/jforc/preliminaries.htm#_Toc191734302 makes the point that C's operators also have monadic and dyadic valence, eg "for example, the unary * operator means pointer dereferencing (*p), while the binary * operator means multiplication (x*y).". Put that way, it makes C look more confusing, rather than making J look less insane. I was thinking that, like C, we should have this, but maybe we should not?

maybe a compromise would be to allow this sort of thing, but only for (built-in) single characters. That way there's no risk of user-defined things having two meanings to learn, and since there are only so many single characters, there's not too many to remember

i guess having attached vs unattached helps a lot. attached = unary operator, unattached = binary operator. Also, perhaps things can be attached on the left or attached on the right, so one symbol could have different meanings depending on whether its in the prefix or postfix -- not sure about that though. Also, what about when multiple hofs ('adverbs') are chained together (ie combined in sequence) (is that function composition? probably) -- how do you deal with adverbs that are more than one character long in that case? Maybe use . to separate 'adverbs' within the prefix or postfix.

similarly, maybe polymorphic function overloading based on number of arguments is too much. Ad-hoc overloading based on type is okay.

---

i guess '.' could be overloaded depending on whether its arguments are fns or not -- if fns, then compose, otherwise, attribute access.

---

in general, i am attracted by how haskell and J have concise syntax for point-free function definition / tacit programming. And how they have a concise syntax for hof application / adverbs. J's adverb syntax seems especially good -- some examples in https://crypto.stanford.edu/~blynn/haskell/jfh.html seem a little confusing in haskell eg " The unary version of the adverb \ is like tail . inits; observe scanl1 (*) q is equivalent to:

foldr1 (*) <$> tail (inits q) -- partial products of q " [16]

---

" Zig is WAY better in that regard, but as OP mentions: all the special characters make things more difficult to parse. Calls like these (taken from the introduction page) contain so much visual clutter:

  try expect(@call(.{}, add, .{3, 9}) == 12);

I sadly doubt any of this is possible to change at this stage anymore. Zig definitely seems the most promising C replacement with regards to tooling, especially cross compiling and C interop are a breeze. Truly inspiring work!

The only new C-like language that really nailed it for me syntax wise would be Nim. Very clean and easy to reason about, which - from a programmer perspective - makes it exceedingly safe to program in. It just seemingly lacks most of the safety guarantees that the compilers of other, newer languages provide.

reply

simplify 1 day ago [–]

Isn't the example difficult to parse only because it's unfamiliar to you? As someone who has never used zig, I feel like 5 minutes of study would make this line easy to read.

reply

"

---

should learn about/consider the following families of syntax:

(my list is pretty close to Fred Ross's, which i found (after making my own list) by googling families of syntax: algol ML apl prolog: The seven programming ur-languages (his 7 programming ur-languages are: ALGOL, Lisp, ML, Self, Forth, APL, Prolog. He thinks the best current exemplars to learn (at the time of his writing, April 2021) are: (lots of choices in ALGOL family), PLT Racket, Haskell, Self, gForth, J, Prolog; which is close to my thinking)

another related short essay by the same guy, which i think is great, is:

https://madhadron.com/posts/2009-11-13-a-perspective-on-four-languages.html

)

---

notes on smalltalk and self syntax:

from wikipedia:

"Smalltalk-80 syntax is rather minimalist, based on only a handful of declarations and reserved words. In fact, only six "keywords" are reserved in Smalltalk: true, false, nil, self, super, and thisContext. These are properly termed pseudo-variables, identifiers that follow the rules for variable identifiers but denote bindings that a programmer cannot change. The true, false, and nil pseudo-variables are singleton instances. self and super refer to the receiver of a message within a method activated in response to that message, but sends to super are looked up in the superclass of the method's defining class rather than the class of the receiver, which allows methods in subclasses to invoke methods of the same name in superclasses. thisContext refers to the current activation record. The only built-in language constructs are message sends, assignment, method return and literal syntax for some objects. From its origins as a language for children of all ages, standard Smalltalk syntax uses punctuation in a manner more like English than mainstream coding languages. The remainder of the language, including control structures for conditional evaluation and iteration, is implemented on top of the built-in constructs by the standard Smalltalk class library. (For performance reasons, implementations may recognize and treat as special some of those messages; however, this is only an optimization and is not hardwired into the language syntax.)

The adage that "Smalltalk syntax fits on a postcard" refers to a code snippet by Ralph Johnson, demonstrating all the basic standard syntactic elements of methods:[29]

exampleWithNumber: x

y
    true & false not & (nil isNil) ifFalse: [self halt].
    y := self size + super size.
    #($a #a 'a' 1 1.0)
        do: [ :each |
            Transcript show: (each class name);
                       show: ' '].
    ^x < y" -- https://en.wikipedia.org/wiki/Smalltalk#Syntax

Self: " The syntax for accessing slots is similar to that of Smalltalk. Three kinds of messages are available:

unary receiver slot_name binary receiver + argument keyword receiver keyword: arg1 With: arg2

...

valid: base bottom between: ligature bottom + height And: base top / scale factor.

...In Smalltalk-80, the same expression would look written as:

valid := self base bottom between: self ligature bottom + self height and: self base top / self scale factor. " -- [17]

---

mb: ? for quote (if you quote a variable you get a 'metavariable') $ for unquote (quasiquote) and string interpolation

in a pattern for pattern matching, either (a) use quote to bind a variable to part of the pattern, or (b) symbol names are variables by default, and use interpolate if you instead want to substitute in the current value of a variable

of course the value of a variable could itself be a subexpression containing other variables to be bound to

---

post and comments about a method to have operator precedence return 'ambiguous' for some expressions (a syntax error, in which case the user must add parens) instead of parsing every expression:

https://scattered-thoughts.net/writing/better-operator-precedence/ https://www.reddit.com/r/programming/comments/q53ch7/better_operator_precedence/ https://lobste.rs/s/jol24u/better_operator_precedence

e apparently does this by having an n^2 table where, for every pair of operators, which one binds tighter or if that pair is ambiguous. A commentator points out that you could instead group operators into 'domains' (e.g. arithmetic, comparison, bitwise), provide a precedence within each domain, and say that any cross-domain pairs are ambiguous. Swift apparently uses precedence groups: https://docs.swift.org/swift-book/LanguageGuide/AdvancedOperators.html#ID46 https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID550

and here's a detailed description of various methods for parsing operator precedence:

https://github.com/bourguet/operator_precedence_parsing

---

mb 4 levels of operator prcedence; unary, multiplication, thats that reurn a bool (inequality, and, or)

actually golang's approach isnt bad either (from https://golang.org/ref/spec#Operators subsection 'Operator precedence'):

" Unary operators have the highest precedence. ... There are five precedence levels for binary operators. Multiplication operators bind strongest, followed by addition operators, comparison operators, && (logical AND), and finally

(logical OR):

Precedence Operator 5 * / % 1 & &^ 4 + -

^
    3             ==  !=  <  <=  >  >=
    2             &&
    1             ||"

it makes sense that and binds tighter than or, by analogy with mult and add

however, if we just use that, we still need to change the operator names/symbols so that one can easily identify the precedence from the symbol. I'd like the first symbol in the operator to determine the precedence.

---

you don't want to have different numbers of parens directly nested in each other represent different things to a computer programming language; because that imply that you cannot directly macro-substitute in something surrounded by parens. This would lead to complicated quoting special cases that would discourage macro creation.

  But, don't we want to discourage macro creation, to promote communal gravity? By reserving the ability to create macros to only the more experienced members of the community, you limit the amount of code out there in the language ecosystem that uses macros unnecessarily.

No, because we want to be a welcoming community. We don't want to constrain programmers in order to protect them from themselves. We want to maximize expressivity. We want this to be a language that you can build languages and use them in.

---

https://krebsonsecurity.com/2021/11/trojan-source-bug-threatens-the-security-of-all-code/

---

" Specifically, the weakness involves Unicode’s bi-directional or “Bidi” algorithm, which handles displaying text that includes mixed scripts with different display orders, such as Arabic — which is read right to left — and English (left to right).

But computer systems need to have a deterministic way of resolving conflicting directionality in text. Enter the “Bidi override,” which can be used to make left-to-right text read right-to-left, and vice versa.

“In some scenarios, the default ordering set by the Bidi Algorithm may not be sufficient,” the Cambridge researchers wrote. “For these cases, Bidi override control characters enable switching the display ordering of groups of characters.”

Bidi overrides enable even single-script characters to be displayed in an order different from their logical encoding. As the researchers point out, this fact has previously been exploited to disguise the file extensions of malware disseminated via email.

Here’s the problem: Most programming languages let you put these Bidi overrides in comments and strings. This is bad because most programming languages allow comments within which all text — including control characters — is ignored by compilers and interpreters. Also, it’s bad because most programming languages allow string literals that may contain arbitrary characters, including control characters. ... “Therefore, by placing Bidi override characters exclusively within comments and strings, we can smuggle them into source code in a manner that most compilers will accept. Our key insight is that we can reorder source code characters in such a way that the resulting display order also represents syntactically valid source code.”

    “Bringing all this together, we arrive at a novel supply-chain attack on source code. By injecting Unicode Bidi override characters into comments and strings, an adversary can produce syntactically-valid source code in most modern languages for which the display order of characters presents logic that diverges from the real logic. In effect, we anagram program A into program B.”"

---

"let mut status = tailscale::Status::get()?;" "Plus I also get to use this to point out the little question mark at the end of the third line of this code blurb. See that question mark? It is an “if err != nil, return nil, err” statement." -- [18]

---

nim syntax:

 "
 Nim distinguishes between statements and expressions and most of the time an expression is a function application (also called a “procedure call”). Function application uses the traditional mathy syntax with the parentheses: f(), f(a), f(a, b).

And here is the sugar: Sugar Meaning Example 1 f a f(a) spawn log("some message") 2 f a, b f(a, b) echo "hello ", "world" 3 a.f() f(a) db.fetchRow() 4 a.f f(a) mystring.len 5 a.f(b) f(a, b) myarray.map(f) 6 a.f b f(a, b) db.fetchRow 1 7 f"\n" f(r"\n") re"\b[a-z*]\b" 8 f a: b f(a, b) lock x: echo "hi"

    In rules 1 and 2 you can leave out the parentheses and there are examples so that you can see why that might be useful: spawn looks like a keyword, which is good, since it does something special; echo is also famous for leaving out the parentheses because usually you write these statements for debugging, thus you are in a hurry to get things done.
    You have a dot notation available and you can leave out parentheses (rules 3-6).
    Rule 7 is about string literals: f followed by a string without whitespace is still a call but the string is turned into a raw string literal, which is very handy for regular expressions because regular expressions have their own idea of what a backslash is supposed to mean.
    Finally, in the last rule we can see that you can pass a block of code to the f with a : syntax. The code block is usually the last argument you pass to the function. This can be used to create a custom lock statement.

There is one exception to leaving out the parentheses, in the case you want to refer to f directly: f does not mean f(). In the case of myarray.map(f) you do not want to invoke f, instead you want to pass the f itself to map. "

---

see table in https://en.wikipedia.org/wiki/Set-builder_notation#Parallels_in_programming_languages

also, "The set builder notation and list comprehension notation are both instances of a more general notation known as monad comprehensions, which permits map/filter-like operations over any monad with a zero element. " -- https://en.wikipedia.org/wiki/Set-builder_notation#Parallels_in_programming_languages

---

" Values in Hash literals and keyword arguments can be omitted. [Feature #14579]

    {x:, y:} is a syntax sugar of {x: x, y: y}.
    foo(x:, y:) is a syntax sugar of foo(x: x, y: y)." [19]

---

https://craftofcoding.wordpress.com/2022/01/05/why-fortran-is-easy-to-learn/

---

issues with Python's comma syntax: https://codereviewdoctor.medium.com/5-of-666-python-repos-had-comma-typos-including-tensorflow-and-pytorch-sentry-and-v8-7bc3ad9a1bb7

---

https://wiki.haskell.org/Unary_operator

unary minus should be part of literals but otherwise part of identifiers; so -8 is allowed but -x is an identifier (0 - x is negation of x)

---

keep ~= available for 'approximately equals'

---

https://craftofcoding.wordpress.com/2022/02/11/what-fortran-does-better-than-c-like-languages/ https://lobste.rs/s/p2ewra/what_fortran_does_better_than_c_like

I must call several stateful functions, and need to abort and clean up if any of them fails. A simple goto exit; saves me from the horrendous pyramids I see so often. And since those functions return the same error codes, I could easily wrap them in a macro that does logging and error handling for me. That way the main code highlights the happy path, and error handling is kept neatly separate. " - "Error-handling with goto is the style used in unix kernels, so your day job is in good company. Typically they do it without macro magic though. error = fallible_thingy(); if (error) goto end; is how all of FreeBSD? is written. For fresh projects that only care about clang&gcc though you can just go ahead and use the cleanup attribute to basically feel like you have destructors :)"

---

https://github.com/oilshell/oil/wiki/Alternative-Regex-Syntax

---

sanxiyn 23 hours ago

link flag
    Swift elegantly disambiguates the two at the syntax level, by requiring a leading . for enum variants. Rust just hacks this at the name-resolution layer, by defaulting to a new binding unless there’s a matching constant in the scope.

I really like Swift’s solution. Can’t Rust just adopt it in a future edition?

~ quasi_qua_quasi 21 hours ago

link flag

I’ve definitely always been jealous of Swift’s enum syntax.

---

https://matklad.github.io//2022/07/10/almost-rules.html

gives examples about when the boundary between the lexer and the parser gets relaxed in Rust

---

(a comment about ocaml) " To develop further the point about "what doesn't scale" there is also the function with unnamed arguments and currying. While extremely elegant for simple programs it gets confusing for real-world applications when function needs quite a lot of arguments and there is no longer any obvious order to give them. If you stick with that and you choose an order it becomes arbitrary, difficult to remember and currying no longer makes a lot of sense. " [20]

---

"

Documentation comments

Documentation comments can describe classes, enumerated types, constants, methods and their parameters. They start with three slashes followed by a space and always immediately precede the documented thing, including a method parameter:

/ Returns the extension of the original module format. / For native modules it simply returns their extension. / For the SAP format it attempts to detect the original module format. public string GetOriginalModuleExt?( / Contents of the file. byte[] module, / Length of the file. int moduleLen) { ... }

Documentation comments should be full sentences. The first sentence, terminated with a period at the end of line, becomes the summary. Next sentences (if any) give more details.

There are limited formatting options: fixed-width font, paragraphs and bullets.

A fixed-width font text (typically code) is delimited with backquotes:

/ Returns `true` for NTSC song and `false` for PAL song.

In long comments, paragraphs are introduced with blank documentation comment lines:

/ First paragraph. / / Second paragraph.

Bullets are introduced with an asterisk followed by space:

/ Sets music creation date. / Some of the possible formats are: / * YYYY / * MM/YYYY / * DD/MM/YYYY / * YYYY-YYYY / / An empty string means the date is unknown. public void SetDate?(string value) { CheckValidText?(value); Date = value; } " -- [21]

---

https://www.r-bloggers.com/2021/05/the-new-r-pipe/

---

https://dion.systems/metadesk.html

"

The language defines a syntax for common structures, like hierarchical lists of strings with metatags. While the structures you form have a consistent pattern, the full meaning of your files is not determined by Metadesk. Take the following example of some Metadesk code: ... @struct Vec3: { x: F32, y: F32, z: F32, } ...

    Each node can also have a list of tags. In this case, Vec3 has a single tag: struct.

"

---

" the ability to restrict an open module to a very small scope

let average x y = let open Int64 in (x + y) / of_int 2

let average x y = Int64.((x + y) / of_int 2) " -- https://batsov.com/articles/2022/08/29/ocaml-at-first-glance/

---

ocaml labeled argument syntax:

    labeled arguments

let div ~num ~denom = num / denom

div ~num:3 ~denom:10

---

https://martinheinz.dev/blog/79 argues for the Python walrus operator, for example, to do stuff like regex matching and some while loops in a cleaner way:

"

m = re.match(pattern1, test) if m: print(f"Matched the 1st pattern: {m.group(1)}") else: m = re.match(pattern2, test) if m: print(f"Matched the 2nd pattern: {m.group(1)}")

  1. ---------------------
  2. Cleaner if m := (re.match(pattern1, test)): print(f"Matched 1st pattern: '{m.group(1)}'") elif m := (re.match(pattern2, test)): print(f"Matched 2nd pattern: '{m.group(1)}'")

while True: # Loop command = input("> ") if command == 'exit': # And a half break print("Your command was:", command)

  1. ---------------------
  2. Cleaner while (command := input("> ")) != "exit": print("Your command was:", command)

"

however that article also gives some other uses that i find to be too clever.

https://lobste.rs/s/0ipjdk/you_should_be_using_python_s_walrus gives more examples that i think are too clever, like walrus operator within assert statements

one comment noted that Lisp can be anaphoric macros for some cases where you would use the walrus operator in Python:

http://community.schemewiki.org/?anaphoric-if

so maybe i would prefer an extension to re.match and while syntax to handle the cases above, rather than a walrus operator?

---


Footnotes:

1.