Microfeatures I'd like to see in more languages
buttondown.email
buttondown.email
Frink? Frink?
This Visual Basic erasure can not stand. The #-delimited date literal syntax has been found all over the VB family: Visual Basic, VBA, VBScript, and it persists to this day in Visual Basic .NET.
Of course, being a VB syntax, it's completely cursed. You can put
# 01/05/2023 #
in a VB file, and what date it represents will depend on what locale it's.. compiled? executed? evaluated? in? Maybe? Depending on which VB dialect you're in? Good luck. Some of those languages would also accept # 01/05/23 #
And they might even agree about what century it's in.> But is that December 8th or August 12?
I didn't even think YYYY-DD-MM is a possibility. Maybe I can grant 08/12/2022 is ambiguous and so is 08-12-2022 but can we please agree YYYY-MM-DD can't have variations?
Reverse-middle endian YYYY-DD-MM is used in Kazakh writing (https://en.m.wikipedia.org/wiki/Date_format_by_country )
Nowhere is it written that we must use the same field separator between the ambiguous fields. We could fix this problem by using the separators as type signifiers.
We could also stop using day <= 12 in our examples, which would also help a ton.
oh VB. you were so close to perfection. never change.
Now if you tried to convert a Date value to String at runtime, yeah, that would use the current locale. That was a constant source of bugs in VB6 apps running on non-US locales (including, famously, at least one Microsoft installer).
Still, this violates the principle of least surprise for non-american developers, at least, so seems pretty on brand for VB.
https://github.com/manifold-systems/manifold/tree/master/man...
Another one from Nim is the implicit result variable. Instead of having to do this:
func sum(nums: seq[int]): int =
var result = 0
for num in nums:
result += num
return result
you just do this: func sum(nums: seq[int]): int =
for num in nums:
result += num
It saves so much time and I'm disappointed that more languages don't have it. func sum(nums []int) (result int) {
for _, n := range nums {
result += n
}
}
No clue if it's an idiomatic usage, and named returns always felt a little too magic for me.[1] and everything in the codebase uses that style otherwise it's annoying to have to context-switch every 5 minutes.
(progn
(print "something")
0)
Is honestly pretty ugly from my point of view.I would much prefer that you must explicitly return a value (even if it's through an implicitly declared 'Result' variable) rather than just 'try to guess what happened here, in this long function with lots of expressions'.
There are a few exceptions, like Forth, where you really have to keep the current state of the stack in mind at all times anyway. Those exceptions naturally tend toward very small functions. Most languages don't, and the result is inevitably difficult to understand bugs.
func sum(nums []int) (result int) {
for _, n := range nums {
result += n
}
return
}
and it will work same as if you would do return result
As for omitting return entirely I hated it in every language where I saw it. It just feels wrong to not have return in functions that return stuffhttps://status-im.github.io/nim-style-guide/language.result....
Doesn't the reverse pollute the function namespace? If every obj.fun() can be written as fun(obj), doesn't that cause ambiguity with a previously imported global function fun()?
Right, I'll show myself out...
Nope. Rust has an extremely restrictive form of UFCS. In fact officially it's not called UFCS, but "Fully Qualified Path syntax".
Square.computeArea()
Circle.computeArea()
Clearly these should do different things. I suppose "computeArea(shape)" does dynamic dispatch based on the type of shape? But you're still putting every function defined on every type in your entire codebase in a global namespace. It's not obviously awful but I'd definitely be a bit nervous about it.
The problem really starts when you have
typeA.func()
typeB.func(x)
typeC.func(x=default_value)
Yes, although overloading can mitigate the issue.
In Nim however I've only had it happen a handful of times in a few years. Then you just need to use the module name to qualify it, or change your imports.
But since most languages have both, I don't think it's a serious concern. I don't know of any language where the only names pace support is classes, so that all functions go in a global namespace unless they are methods on a class - maybe you could argue C works like this (where "methods" are function pointer member of a struct)?
To me, these are "Tell bar's foo to do something with baz." and "Tell foo to do something with bar and baz.". So being 'able' to flipflop the syntax is at least temporarily semantic'ly confusing.
In other words, to me it's simpler and therefore less confusing.
Why is bar working with baz and not baz working with bar?
I struggle with this in Unity:
Player.collect(PickUp) // this?
PickUp.boost(Player) // ...or this?
Instead, the code should describe the interaction between the two units of state: onCollide(Player, Pickup)
If we structure our code like in the last example, it makes sense to weaken the `a.b` vs `b(a)` distinction, and instead use the dot as a kind of pipe-operator.So, in your case, depending on other modeling decisions, I could argue either for
onCollide(Player, Pickup) - if Player and Pickup are both plain data, and don't need to guarantee any invariants
Player.collect(Pickup) - if Player actually has to ensure some invariants such as health<100)
I don't see any good arguments for pickup.boost(Player), in typical games. Of course, if both the Pickup and the Player have some invariants that need to be maintained on a collision, then arguably the design has to be changed at a deeper level.
Although I rarely see Objects used this way. Often, methods are used to implement all related functionality. Unity even strongly encourages this. (...at the moment. They are working on Entity Component Systems which will work more similar to my third example)
I concede that languages shouldn't use the dot as a syntactic tool, be it through Extensions[1] or UFCS, but rather offer a pipe-operator. If they don't, I'd still prefer UFCS rather than no way of chaining at all.
[1] Extensions for interface/protocol conformance are fine of course.
What is the code driving it?
- looping over players to update them?
- looping over objects to update them?
- some other event loop?
I actually like the third least:
The first two tell me what is happening, but the third doesn’t — I could be colliding to block motion or I could be picking up.
foo.do_this(bar).do_that(baz)
instead of do_that(do_this(foo, bar), baz) do_this(foo, bar) |> do_that(baz)
Or an explicit version: do_this(foo, bar) |<> do_that(<>, baz)
(Yes, this is a bastardization of Elixir's pipe and some Lisps' arrow macros.)E.g. in D to convert between different types you can use std.conv:
import std.conv;
"123".to!int;
123.to!string;
Ints don't need to understand string building and strings don't need to understand int parsing. The conversion code just needs to declare a couple functions taking the right arguments and it just works. To me the above is much more readable than to!int("123")
to!string(123)
in any case. It's also quite nice when dealing with C APIs since it allows you to pretend they are OOP in quite a lot of cases. e.g. with SDL: SDL_CreateRenderer(window, -1, 0);
turns into window.SDL_CreateRenderer(-1, 0);
and say I'd like to have a function to initialize all the renderer stuff in one go? I can simply declare void CreateRendererAndInitialize(SDL_Window* window){<snip>}
and now you can do the following: window.CreateRendererAndInitialize();
It removes a lot of pain from extending 3rd party types you see in other languages.There, it helps if a template can say `t.foo()`, and, with UFCS, can use that template with any t for which either `foo(t)` or `t.foo()` exist. In contrast, in C++ today, a template using `t.foo()` limits its own use only to types that have a foo() method, probably unnecessarily (note that writing `foo(t)` in a C++ template is less limiting, as someone who controls neither the template nor the type of t can still define that function).
However, outside of this use case, I think conflating these two is more of a negative than a positive. It means that there are twice as many places where I may need to lookup the definition of foo(), at the very least. So I wouldn't add this to any static language that doesn't support templates or macros.
And sure, the IDE/language server/other tooling can often help, but not always (e.g. if I'm browsing some code on Github). Either way, more ambiguity for no gains is typically not a good idea, even if the downsides are minor (again, I am very much in favor of UFCS where it's directly useful, such as C++ or D).
Because an array with indexes [3, 7) has length 4, but 4 is not the index of the last element.
C/C++ doesn't have custom array indexes and as such <array[std::size(array) - 1]> is returning the last element of said array.
Delphi has custom array indexes and as such, taking your example with defining an array in the form <example_array : array[3..7] of integer>, I would not get the last element in case of <example_array[Length(example_array) - 1]. In this case I would have 2 options. Option 1 would be to use <High> function as in <example_array[High(example_array)]> to access example_array[7] element. Delphi also has <Low> function so you can iterate through a custom defined array by using <for> keyword with the help of them. Option 2 would be to actually build my own helper (this is the most wanted case when you're dealing with multi-dimensional arrays that also have custom indexes) and I would have something like <example_array.FromLastIndex(0)> to access example_array[7] element.
Hope this cleared the confusion.
for I in A'Range loop
A(I) = A(I) + A(I);
end loop;
Whatever the range is, this will work. If you really need the first and last elements or want to be explicit: Start := A'First;
End := A'Last;
And if the type of the range (since any discrete type can be used) doesn't support simple incrementing with +1 or similar, you can use 'Succ to step through: Index := A'First;
Index := Whatever_Type'Succ(Index);
Also 'Pred to work backwards. Those can be wrapped up in a simpler function if desired.Being able to give subarrays to a procedure and preventing buffer overruns everywhere, reducing screw-up scope everywhere is a superpower I didn't know I needed before starting writing proved parsers.
3 -> 0
4 -> 1
5 -> 2
6 -> 3
This works for vectors as well, so why not have a range from (0,0) to (5,5) to index into an array arr?
You could write the function that does the mapping manually: arr[(x,y)] = backing_array[x / 5 + y] //bounds checks omitted
But here it can be automated quite simply to allow for vectors of even 3 or 4 dimensions.Just know that custom indexes / ranges are not automagically broken. Personally, I like how much easier it is to read the intent with custom indices.
I'm not sure I'd want it for every list, but there are certain places it's nice.
[1] https://www.allaboutcircuits.com/technical-articles/circular...
Directly, it supports caches very well. You just increment the number of things you've ever cached and that's where your next cached value goes; you don't care when it overwrites an old value.
There are other cases where you just need some variant of a thing, but you don't actually care that much about which variant you get. You might want to vary your wording in auto-generated text, for instance, by rotating synonyms. Or rotating the tiles you use in a 2D game. In this case I'd define an interface where you pass in a "seed" integer and it gives you back some deterministic example; a circular array is the simplest implementation of this interface (but there are others).
You could also do simple load balancing by sending work to Worker[workCount++]. While usually you want to track each workers' existing workload (because the work takes unpredictable time), this simple approach could be sufficient if all your work completes in about the same time.
If you're doing fancy math or science computing, you may be working with finite groups or fields, whose elements you could stick in an N-dimensional circular array (based on the characteristics of the field).
Like:
- you can put a short chord sequence into a ring, and it now functions as a list of as many repetitions of that chord sequence as you like. You can just loop over it forever (which is kind of the essence of how sonic pi live-loop play works)
- you can put the notes that make up a scale into a ring, and use it to extract specific chords - like, take the 1st, 3rd, 5th, 7th and 9th note - from just a seven note scale.
- you can use rings of booleans to capture drum patterns and rings of notes to capture melodies, and loop them forever
- etc. etc.
Expression<Func<int, int>> lambda = n => n % 2;
Console.WriteLine(((dynamic)lambda).Body.NodeType);
Output is "Modulo".Ada has both rem and mod operatiors. I'm not sure how many other languages have operators for both.
I do think it's quite odd and frustrating that modulo can return negative numbers and I don't really get the reasoning there, but there's probably a good reason I don't know about.
For instance if you have an int array that contains the numbers 1-250 and you index with a uint8 variable i,
for (uint8 i = 247; i++;) {
// print circ_arr[i]
}
for the values of i near the overflow points of the circular array and of the uint8 it gets weird: i circ_arr[i]
247 247
248 248
249 249
250 250
251 1 # 251 % 250 = 0
252 2 # 252 % 250 = 1
253 3 # ...
254 4
255 5
0 1 # i overflows to 0
1 2
...If you're indexing with a [u]int32 you need to worry about this once every 4 billion increments, and if an incomplete cycle is a show-stopper for you, you can compute a safe modulo yourself based on the size(s) of your circular array(s), but more likely you just need something else. But really, you don't care if your cache hiccups a little once every 4 billion caches.
You make a good point, of course, I'm just allergic to people poking holes in back-of-the-napkin explanations of things with the trite "but integers can overflow!" It's one of those most common well actuallys written on this site. Of course integers can overflow. They almost never do though, do they? And if they do, a test fails and you add a single line somewhere to fix it.
I really think the vast majority of programmers are too often thinking about bits when they should be thinking about math.
You’d need a way to get that type, for example as
float a[10,20] // two-dimensional array of floats
typeof(a.dims(0)) i = 0 // modular type with values in [0,9]
typeof(a.dims(1)) j = 0 // modular type with values in [0,19]
or, slightly neater: auto i = a.indextype(0)
auto j = a.indextype(1)
Ugly syntax, but in a modern language, most code would probably do something like for (i,j,value) in a
where the types are inferred.Having those modular types means the compiler would do the arithmetic correct for the array, while the negative literals allow programmers to specify “last” and “next to last” correctly.
That's something I'd like in a bunch of languages - a real modulo operator that always returns between 0 and n, even for negative inputs, rather than a remainder operator that's advertised as a modulo operator. Grrrrr!!!!!
otherwise seems like an errors that are hard to spot.
> arr = ["a", "b", "c", "d", "e"]
> x = -2
> arr[x]
=> "d"
> Similar to the argument about signed/unsigned indices in low level languages.
Think that one has to do more with convenience where most of stuff uses int by default
This is exactly the difference between a language like PHP and a pure functional language. PHP says: usually we want to do X, but sometimes Y, so we'll make Z which does X unless Q is true in which case T1 will be set and Y will happen most of the time when you want it assuming you called it the write way and put an @ in the right spot otherwise P will happen because I hadn't had lunch when I wrote that and it seemed like P was pretty likely to be the case when T1 was set but an @ was not written but lately I've been feeling like maybe T2 should also be set sometimes so if you call Z and you want X but T1 is written and you don't want to write an @ then you can just set CONSTANT_FOO_BAR_WITHOUT_X_SET_AT to 17 because the other 16 codes are already used for other things.
Functional languages say: what if everything was just math?
TLDR: Not that weird. If it is something that is almost certainly going to fail code-review, then may as well let the compiler fail it.
Long:
Just because I want only literals allowed someplace, or only values allowed in other places is not even close to weird.
Most places, code review won't let a function call like `foo(true, false, true, false, true)` through, because the potential for errors is so high and the readability is low.
With this take I can see code review easily getting into the weeds for each `bar[x]` to determine if x will wrap around, while letting `bar[4]` through because it is clear it will not.
Right now, with most languages, we simply let `bar[x]` through because if it is out of bounds it will throw an error/panic/etc. I think it can only silently return wrong data in C and C++.
In this case everything is about intention
In general accessing index out of range (above or below) is not desirable, in almost all cases this is bug.
And now, in my opinion `array[-1]` when `-1` is hardcoded would tell, with full intention that last index is desired.
Basically it would be translated to `arr[arr.Length - 1]`. You don't write code with `array[-1]` because that's clearly wrong (when there's no going back behaviour)
Meanwhile when it is calculated, then it should result in an error.
The rules are pretty simple I'd say - if you desire to use "reverse syntax" then you can, but when you use variables with may be calculated wrongly, then you will receive an error.
But, more importantly, it means that any custom collection type can define an indexer that can handle reverse indices in the manner that is appropriate for that particular collection; it's not just for arrays.
[1] https://learn.microsoft.com/en-us/dotnet/api/system.index
OTOH a convenience feature as a lock-in is hard to believe.
And it mihjt be possible to add a static method to array to index backwards yourself (Can't remember what they are called, but look and act like methods on the object but aren't).
And no, it's not possible to do this using an extension method, unfortunately - there are no extension properties or indexers in C# (yet; it's something that keeps coming up). But then again, if and when they add extension indexers, this arrangement with a custom type is what'd allow you to write one that does backwards indexing on a collection type that doesn't support it out of the box.
lst.reverse()[x]
which the compiler could guasrantee to recognise and simply implement as a calculation.And yes, of course, you can always do the same in some other, more verbose way. But why should we tolerate that verbosity when there's a solution that makes code both shorter and more readable? I rather hope that more languages will adopt one of these techniques.
> it silently does the wrong thing
yeah, my original complaint was this
> why should we tolerate that verbosity when there's a solution that makes code both shorter and more readable?
Because it's a balance. How much it benefits how many users to what degree vs. extra cost of implementation and maintenance. If you're not careful you go down the kitchen sink road and end up with bloat. Be careful when adding stuff cos you have to support it forever.
Anyway, thoughtful answers thanks.
[1,2].at(-2) returns 1
EDIT: On the other hand, I think Matlab's array(end - number) indexing syntax is a good compromise of convenience and less error prone explicitness.
foreach my $i (0..$#list) {
say "$i: $list[$i]";
}
For getting the last element from a list you can just use -1 (and of course further negative numbers work like you would expect, -2 is second to last and so on): my @last_three = @items[-1, -2, -3];But you can also define your own sigils to create new "custom syntax" for almost any struct. Kind of a special case of reader macros, I guess. Very convenient.
Eg: ~w(foo bar bat) is a word list. `~ letter bracketed-text lettersasmodifiers` desugars as sigil_<letter>(text,modifiers). Similar to foo_str() of Julia[3], but for one-letter-names and more brackets. But not the unicode brackets of Raku.
[1] https://elixir-lang.org/getting-started/sigils.html [2] https://hexdocs.pm/elixir/main/syntax-reference.html#sigils [3] https://docs.julialang.org/en/v1/manual/metaprogramming/#met...
For example you could write an extension on Date to add initialization from a string:
extension Date: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
// parse the string here.
}
}
You can then do things like: let happyNewYear: Date = “2023-01-01 12:00:00”
There are a protocols for all literal types. For example, you could implement ExpressibleByIntegerLiteral and have have it init the Date object from a unix timestamp. There is even an ExpressibleByNilLiteral.What you just showed was a implicit conversion from String to Date. Something you would get beaten up for in Scala land.
arr = [0, 1, 2, 3, 4]
head, *body, tail = arr # head=0, body=[1, 2, 3], tail=4
head, *rest = arr
head, *_, tail = arr
*_, tail = arr
first, second, third, *rest = arr
foo = {"a": 1, "b": 2, "c": 3}
bar = {"b": 9, "x": -1}
{**foo, **bar} # -> {"a": 1, "b": 9, "c": 3, "x": -1}in javascript the … operator neatly does both
On the other hand, I'm fairly certain that having to visually disambiguate between `` and `*` and remembering which did what would have gotten a similar reaction.
[1] a misfeature, IMNSHO.
arr = [head, *body, tail]
Same with dictionaries, often very useful when you need to include **os.environ with modifications into a subprocess. Mdist = lambda (x1,y1),(x2,y2): abs(x1-x2)+abs(y1-y2)
p1,p2=(1,2),(3,4)
Mdist(p1,p2) # 4 0x[de ad be ef 00]
So much nicer than the usual condensed format. And I think it'd be valid syntax in any language that allows binary integer literal.They do e.g. Python
>>> 0x_ab_cd_01_23
2882339107
or >>> 0b_0010_0100
36TL;DR is that it's because C++ uses quote. The reason C++ uses quote (they considered underscore) is because of a very obscure feature of C++ called custom literal suffixes which I'd never even heard of, but numbers can be suffixed with a custom identifier, and since single underscore is a valid identifier you can't use that. (https://en.cppreference.com/w/cpp/language/user_literal)
[1] https://elixir-lang.org/getting-started/binaries-strings-and...
0xde_ad_be_ef_00(Given they have been listed separately rather than as a single number).
Here's how we ended up supporting byte strings (# prefix) in Ecstasy, in this case multi-line:
Byte[] bytes = #|12 34 56
|78 9a BC
|dE f0
;
console.println($|bytes=
|{bytes.toHexDump(4)}
);
Which prints: bytes=
00: 12 34 56 78 .4Vx
04: 9A BC DE F0 .¼Þð1. Lambda functions can be defined with `{}`.
2.`foo(bar, somefunc)` is the same as `foo(bar) somefunc`. In other words, if the last parameter is a function, it can be provided AFTER closing parenthesis.
3. Interfaces that require only one method can be implemented on-side with a lambda function (i.e. `{}` syntax for no-param function).
Combined those three features, the code may look like that:
routing {
static("/statics") {
files("css")
}
get("/foo") {
call.respondText("Hello world!")
}
}
So you can make a config-looking file which is just pure Kotlin, with static type checking, autocomplete, suggestions, "this" etc.It's so damned, I'm surprised author didn't mention it.
Not that I was completely innocent of this at that age.
/Rant.
Kotlin was started even as just a poor Scala clone. (Because JetBrains didn't manage to get a working Scala plugin for their IDE, so they thought it would be simpler to create their own "simpler" version of the language).
Kotlin's scope injection is one of the most terrible "features" ever invented. It's dynamic scoping on steroids!
But dynamic scoping was long ago deemed a horrible bug and never ever made it again into any new language.
Directly "stolen" form Scala.
> 2.foo(bar, somefunc) is the same as foo(bar) somefunc. In other words, if the last parameter is a function, it can be provided AFTER closing parenthesis.
Just a irregular syntax quirk that tries to get around the fact that Kotlin does not support multiple parameter lists, like the language where most Kotlin features come form, Scala.
> 3. Interfaces that require only one method can be implemented on-side with a lambda function (i.e. {} syntax for no-param function).
That doesn't have anything to do with Kotlin. That's Javas SAM (Single Abstract Method) feature.
> I'm surprised author didn't mention it.
The author seems not to know any Scala. Otherwise the lists would show mostly only Scala features… ;-)
IE
a = 1; b = 2
assert a == b
Will fail with error like: Assertion failed,
a == b
Left is 1
Right is 2
So you don’t have a bunch of assert-functions; you just assert anything and it will spit out a decent error.e.g. in pytest it won't just print out the values of "a" and "b", it will recursively document intermediate values until it's reached the toplevel expression:
assert f() == g()
assert 42 == 43
where 42 = <function TestFailing.test_simple.<locals>.f at 0xdeadbeef0002>()
and 43 = <function TestFailing.test_simple.<locals>.g at 0xdeadbeef0003>()
and it's possible to customise the report so you can report as a diff: assert "foo 1 bar" == "foo 2 bar"
- foo 2 bar
? ^
+ foo 1 bar
? ^In Ecstasy, we built the support directly into the compiler again:
val a = 1;
val b = 2;
assert a == b;
Produces: IllegalState: a == b, a=1, b=2There's a whole section in the manual [1] for string quoting operators (qq, qw, qx, ...)
In general, I feel like Perl is one of those languages that has a high amount of these "quality of life" syntactic features, and helps make it enjoyable to write, once you get over the learning curve.
Julia allows[2] defining your own non-standard string literals. foo"bar"hee and qux`...` desugar as macro calls foo_str("bar","hee") and bar_cmd("..."). But lack the bracket flexibility.
http://rigaux.org/language-study/syntax-across-languages.htm... briefly sketches other languages.
[1] https://docs.raku.org/language/quoting [2] https://docs.julialang.org/en/v1/manual/metaprogramming/#met...
But you give up something to get benefits in those areas. Making use of the expressive power of something like Perl is a wonderful sensation. The barriers between thought and making it happen are lower, and so you can be remarkably productive. It is also just more fun, I find, which has subtle and under-valued long-term benefits.
But yeah, agreed that comprehending someone else's Perl-fueled vision quest can be ... rough (:
usernames = '''
foo bar baz
hello world
'''.split()
# instead of this, which needs too many keystrokes
usernames = ["foo", "bar", "baz", "hello", "world"]
Interestingly, Python named tuples have similar interface for fields: # all of these are equivalent
EmployeeRecord = namedtuple('EmployeeRecord', ['name', 'age', 'title'])
EmployeeRecord = namedtuple('EmployeeRecord', 'name, age, title')
EmployeeRecord = namedtuple('EmployeeRecord', 'name age title')A bit like Swift's try? which converts throws to nil.[2] Less so javascript's short-circuiting optional chainging `?.`[3], which I thought of first.
[1] https://doc.rust-lang.org/std/ops/trait.Try.html [2] https://docs.swift.org/swift-book/ReferenceManual/Expression... then /Try Operator/ - section anchors don't look stable. [3] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
So while it is indeed a nifty feature if the rest of the language is also designed for it, it's not something that is easily tacked on to a language that is not.
What do you mean by those two quoted terms?
On mobile so I can’t put in a code block, but here’s how I thought Go was written: value, err := some_fn()
If err != nil { Return err }
(? Operator works here because you could do value := some_fn()? And remove the if statement boilerplate)
Do you mean that instead of “return err” Go idiomatically does something else?
A big problem, among many, with doing that is that you leak implementation details out of the abstraction. If, to stick with your example, you have a function that helps you with reading files, the caller shouldn't care where the data is stored. Today it might be the local filesystem, tomorrow S3, and when you make that change nothing about the rest of the program should break.
But you can't count on the lower level functions using the same errors. As you suggest, a "a file I’m trying to read doesn’t exist" isn't represented as a "bucket 404", even though at a higher level they are the exact same thing. If you straight returned the "a file I’m trying to read doesn’t exist" error as you got it from the file API, now the caller is going to depend on that, and when you replace it with the S3 function that returns a "bucket 404" error, everything starts to break.
What you typically want to do is return a more generalized "not found" error that can remain stable regardless of specific implementation details. There are rare cases where you can get away with simply returning the value up the stack, but in the majority of cases you need to handle the error, either by doing something with it or returning a new error that is more useful to the caller. And, so, try becomes essentially unusable without the language taking a larger macro take on supporting such a feature.
Like the sibling comment points out, Rust does "from" conversion when using try (?) to try and avoid encountering the same fate.
This would lead to the situation where a local call is treated as the same thing to a network call. Which is know to be a very bad design.
> Like the sibling comment points out, Rust does "from" conversion when using try (?) to try and avoid encountering the same fate.
Yeah, and it avoids all the hassle.
Why couldn't any language (and especially Go) just do the same?
If your abstraction leaks that the implementation is a local call, and then you try and change that later, unquestionably. Again, you need to avoid leaking implementation details, which is too why you can't just add a try operator and make it automatically useful. Any leak of any kind in your abstraction will make life miserable later. Don't let your abstraction leak.
> Yeah, and it avoids all the hassle.
All it does is move where the code is located, placing the onus on the producer "the producer is always right" instead of the caller "the customer is always right". You don't actually avoid anything, just change the perspective.
> Why couldn't any language (and especially Go) just do the same?
Perhaps it could, but it requires that the language take a more macro look at the problem. It is not a microfeature.
I'm just not sure what the syntax has to do with the semantics of string concatenation.
Using strcat() repeatedly in C for example will mean that the string is being read over and over again to find the end, making an O(n) loop actually O(n²).
At my startup, one our most beloved innovations is that you can write `resolve_config("foo", default="bar", request=request)` pretty much anywhere you'd normally hardcode a value or feature flag... and that's it.
The first time it's seen in any environment, it thread-safely inserts-if-not-present the default value into a key-value storage that's periodically replicated into in-memory dictionaries that live on each of our app servers. Any subsequent time it's accessed, it's a synchronous key-value lookup in memory, with barely any overhead. But we can also configure it in a UI without needing a code redeploy, and have feature flags and overrides set on a per-user or per-tenant basis.
Sometimes, you don't need language support if you have some clever distributed-systems thinking :)
That seems like a great way to get amazingly hard to replicate bugs or odd behaviours if different subsystems use different values for the default.
In Nim you can do compile flags which let you set constants so you avoid the problem:
const myLibraryVersion {.intdefine.} = 3Oh no they're saying that it's thread-safe, that's not an issue. Rather that depending on the order of initialisation, possibly of different systems entirely, you can have different initial states because different systems or subsystem decided of the default value.
I think you may have outwitted yourself here; I know what that looks like because I've done it so many times in the past :-)
I'm afraid your solution is not distributed-system safe, as a different bootup order of the nodes[1] in your system would result in a different config value for that key. And, at some point, your nodes are going to come up in a different order.
[1] Nodes == instances of your code that run.
They don't come up that often, but when they do they're really the best solution.
I feel like it would work better with a more explicit keyword, but I don't know which one. `nobreak`?
I've often wanted both a "then" and an "else" from both for and while loops. The "then" would be for a successful completion (no break), and the "else" would be for when the loop doesn't even run a single iteration.
But that didn't make it into our "language budget", unfortunately. It's easy to implement, but hard to argue for when it doesn't get used often
> Most languages have multiline literals, but what makes the Lua version great is that the beginning and ending marks are different characters. This solves the infuriating “unnestable quotes” problem string literals have, and you don’t have to escape all your literal \s.
That paragraph also uses “nestable marks”.
Indentation-sensitivity can also solve similar problems. (Indentation does not have to exclude requiring graphic termination. A formal language can require both. Or just a helpful tool.)
(Also agree with 'kebab-case', although the name is new to me and a bit weird.)
Scala has it too. (But OK, Scala is a kind of ML).
my $a = 45; say $a-3; # 42
let data = try? aFuncThatThrows()
Sometimes I just don't care about the reason for the exception and just want to know if it succeeded or notI like how functions in js can be `arg => result`. In F# I have to do `fun arg -> result` with the `fun` keyword. It makes sense since `MyArgType -> MyResType` is a type signature in f#, but I feel like the compiler can just check if the arguments are references to types or are argument bindings.
# Multiline Lists/Arrays ####
I like how F# doesn't require delimiters for multiline lists.
So I can do `let myList = [1; 2; 3]` or
let myList = [
1
2
3
]
# Regex Literal ####I like how in Crystal instead of doing `/my[regex]/` i can do `%r(my[regex])` where the parenthesis can be any brace type (like "(", "{", "<", "[") so I don't have to escape any characters.
# Argument Accessor Shorthand ####
In Crystal, you can use an ampersand to bind and access a property on an object, instead of writing the verbose form with a function.
So this
["a", "b"].join(",") { |s| s.upcase }
can be written as ["a", "b"].join(",", &.upcase)
If this were available in F#, for example, instead of ["a"; "b"]
|> List.map (fun s -> s.ToUpper())
|> String.concat ","
I could do ["a"; "b"]
|> List.map &.ToUpper()
|> String.concat "," waiting = sum workers #(%.in_queue + %.in_flight)
Clojure has some syntax like this though it isn’t needed for the most obvious use-case of functions to extract fields because keywords, which are usually used for map keys, are implicitly functions that look themselves up in their arg, e.g. (:bar { :foo 3, :bar 2 }) ; => 21. this is absolutely terrible because now you need feedback from the type checker to know how to parse the program
2. it is furthermore also ambiguous with function application, requiring arbitrary lookahead to disambiguate, also not a fun thing to do
JS gets away with it because the sigil was not previously used and it only requires a single lookahead to parse, as only single-parameter anonymous functions can have "bare" parameter lists.
But yes "fn" is quite nice (taking over "f" is a bit much). And a few characters can definitely degrade the experience, especially as "u" and "n" are typed with the exact same finger.
Anonymous functions were definitely one of my least favorite features in Erlang, not because they don't work well but because their leading keyword is "fun" and there's an arrow between the (parenthesised) parameters and body and they also have a closing keyword "end":
map(fun(X) -> 2 * X end, [1,2,3,4,5]).
That's a bit much.But HoFs in general are quite awkward, as referring to a named function also requires the `fun` leading keyword, and requires specifying the arity, so
map(fun double/1, [1,2,3,4,5]).
after having defined the function as double(X) -> 2 * X.
(as you can see Erlang would really rather you defined named functions). val f: Any => String =
any => any.toStringmeanwhile in C++:
[&](auto arg) { return result; }
You don't know how good you have it. List("a", "b").map(_.toUpperCase).mkString(",")
It's the shorthand for: List("a", "b").map(elem => elem.toUpperCase).mkString(",")
(Which shows the firstly proposed feature :-))Function composition operators eg
a |> b # Call `b` with `a` as an argument
b <| a # Same as above, reversed direction
Then you can do something like let x : Map = collect <|
[a, b, c]
|> map(entries)
|> flattenE.g. https://mmhaskell.com/blog/2021/7/5/function-application-usi...
paint(sand(cut(measure(wood, 12), 40, :WZ), 220), :red)
wood | measure(12) | cut(40, :WZ) | sand(220) | paint(:red)
IIRC you can do that in Haskell as well, but I forget the name of the feature. Many OOP libraries have started to adopt a chained method call style similar to this, but it is nice to be able to do with any function.
wood
& measure 12
& cut 40 WZ
& sand 220
& paint Red
See https://hackage.haskell.org/package/base-4.17.0.0/docs/Data-...It's less of an issue if your language has UFCS or other postfix function call syntax like mentioned in this thread, but if you don't this is nice to have.
It's very common already: try-with-resource (java), using (C#), bracket (haskell), unwind-protect (common-lisp), ... though it the latter two it's more of a building block.
Also building block: languages with a convenient and "unrestricted" syntax for anonymous function can just use that e.g. Smalltalk, Ruby, ... in Ruby a "with" is usually just passing a block to the corresponding object's constructor:
# python
with open(...) as f:
...
# ruby
File::open(...) do |f|
...
endFor instance Racket and Clojure have threading macros, which are more flexible as they're just macros (Clojure's `->` is equivalent to Elixir's pipe operator, but `->>` will fill in the last parameter rather than first, and `-->` lets you use a keyword to define where the parameter is inserted in each call).
Haskell let anyone who wants define their own pipe operator, historically you had to BYO, which wasn't exactly hard:
(|>) = flip ($)
or x |> f = f x
would do (modulo fixity), but today it's provided by default as "(&)".I prefer RAII.
* sub MAIN:
sub MAIN(Int $x, :$verbose) { }
generates a command line parser that expects an Integer plus an optional named switch --verbose
* It has named params (as seen above), and there are abbreviations: instead of thing => $thing you can write :$thing to avoid duplicating the name (:thing also exists, though it create a pair "thing" => True, so ruby lovers need to be careful :D )
* junctions for quick conditionals/validation: 0 <= all($x, $y, $z) <= 2 * pi
* this is a probably debatable, but: if you use a * as a term, it will create a lambda for you, so *+2 is similar to sub ($x) { $x + 2 }
They're not actually written out every time, the issue is mostly documentary (and it would be nice if Python or Sphinx ever had a good solution). And numpy actually has a bunch of generators for that e.g. https://github.com/numpy/numpy/blob/45bc13e6d922690eea43b9d8... handles filling in the common bits of documentation for the ufuncs.
default_args = ('x', 'y', 'z')
default_kwargs = {'p': 'p', 'q': 'q', 'r': 'r'}
def printer(x, y, z, /, *, p, q, r):
print(f'x={x} | y={y} | z={z} | p={p} | q={q} | r={r}')
printer(*default_args, **default_kwargs) # x=x | y=y | z=z | p=p | q=q | r=r
EDIT: Formatting.If you have multiple callables taking these parameters documenting them is awkward, by default help/pydoc and sphinx will tell you that the parameters are `default_args` and `default_kwargs`, but that's not actually true, those are just intended as shortcuts / helpers .
This is basically the premise of Project Coin, released in Java SE 7: https://openjdk.org/projects/coin/
The goal of Project Coin is to determine what set of small language changes should be added to JDK 7. That list is:
* Strings in switch
* Binary integral literals and underscores in numeric literals
* Multi-catch and more precise rethrow
* Improved type inference for generic instance creation (diamond)
* try-with-resources statement
* Simplified varargs method invocationf[x]
f@x
x // f
It matches the flow of thought more naturally when hammering out a couple of one-liners.
Its convenient when writing though.
<Some big expression here> // Column
Useful where the function in question is an "afterthought".
x |> f
as a syntax sugar for
f(x)
and is useful for the same reason as Mathematica.
AFAIK F# has these and that's about it
For instance, the `embedded_time` crate lets you do
200.microseconds()
5.Hz()
https://docs.rs/embedded-time/latest/embedded_time/[1] https://github.com/manifold-systems/manifold/tree/master/man...
with definition like:
val Double.mph: Speed
get() = Speed.mph(this)
data class Speed(val ms: Double)
companion object {
fun mph(mph: Double) = Speed(mph * 1609.344 / 3600.0)
} let x = ... in
some_function ~x
if the parameter has the same name on both sides (caller, callee) there's a syntactic shortcut. It's a small but noticeable force that pushes you towards more consistent naming. die "can't be negative" unless $i >= 0;
Perl is full of usability features, like <> for reading input, inline literate coding annotations, implicit $_.It has the concept, I think, of “statement modifiers”.
You could do things like:
S=S+A[I] IF A[I]>5 FOR I=1 TO N
It always read really well.Function Argument Labels and Parameter Name separation.
Example in gleam:
// definition
fn catalog(by list_of_qualities) {
add_to_catalog(list_of_qualities)
}
//applying the function
catalog(by: [Genre, Year])You can write functions like xyz(v) and that works well enough. Even better if you have ufcs or methods.
Therefore I would prefer something like this to be the usual array access syntax:
val chars = Array("a", "b", "c")
val secondChar = a.2 // as a shorthand for `chars.atIndex(2)`, or equivalently `chars.atOffset(1)`, maybe also with `a..1` for the offset case
(Also we should stop calling the offset "index", and get a proper "atIndex" method.)Also, I wish the unary negation operator was more visually salient. `foo * -bar` is very different from `foo * bar`, but it's only a handful of pixels on the screen. I've thought about trying to render it as an em-dash or something. Didn't NASA lose a rocket over a spurious - sign?
That said, have you considered making this outright illegal without explicit parentheses? I actually wish that more languages would require that any sequence of operators has the same precedence throughout; i.e. a+b*c would also be illegal. It's always a pain to remember the exact precedence rules, especially since they're not consistent across PLs, so I'd prefer any expression that is ambiguous to be explicitly disambiguated.
It can be very hard to spot sometimes, especially after an opening bracket - if(!something())
Of course its generally better to try and rename functions or refactor to avoid the negative if possible.
Kebab-case and snake_case may seem to read better when looked at code like it would be written text, but they read worse than camelCase when looked at it in a symbolic way.
That means you can write your number as "1 000 000" and put it in the variable "one million", and then feed that to your function called "withdraw money".
Yes, sure, makes it harder to grep. Here's a nickel, get a better grep tool (or wrapper thereof).
Job done
yield from ySee https://en.wikipedia.org/wiki/Tail_call for more. Or SICP might be a good resource. https://sarabander.github.io/sicp/html/1_002e2.xhtml
However it allows making anonymous functions recurse as well.
One thing I don't think you can do with loop/recur, though, is optimize more complicated bits of recursion than a single function that calls itself. I.e. imagine a recursive call pattern that goes like f -> g -> f -> g -> ...
(edit: I'm pretty sure this is why trampoline exists, though I've never really played with it... https://clojuredocs.org/clojure.core/trampoline)
FWIW I think in Clojure you can use “recur” inside functions too to specifically indicate tail call recursion without relying on automatic optimization
I didn't think Clojure had any automatic optimization at all, due to the JVM not supporting it.
(JVM has quite a lot of automatic optimizations that Clojure enjoys automatically, and clojure itself also has some automatic optimizations).
`unfold`, Rust's `loop`, generators, working tail recursion elimination (the lack of which loop/recur is a workaround for)
I'd regrettably add another class, quality-of-life features which you'd have hoped weren't too hard to add, but because of past choices, now are.
Examples: Adding javascript-like dots a.b.c for Julia Dict's a[:b][:c] would conflict with "wasn't intended to be public but has been" Dict implementation fields, like .count . Adding { a,b | ... } instead of a less concise { |a,b| ...} for Ruby blocks, but for a yacc grammar conflict.
The { a,b | } syntax is also still ambiguous with hash literals, unless you require that | to be there, and that looks like it gives the parser a whole lot of look-ahead work to do in order to distinguish hashes from lambdas.
${"variable-name"}=123;
Isnt it beautiful?I suspect java would work as well, not sure about golang unicode var naming.
But then the spec[1] says that only code points characterised as "Letter", an underscore, or characterised as "Number, decimal digit" are valid.
Go Playground: https://go.dev/play/p/kxgOcEWsznz
let ``foo-bar–baz—quux`` = 3 x-y : ℤ → ℤ → ℤ
x-y x y = x - y
The Agda community also heavily uses unicode characters. I've even seen a unicode colon used for a custom syntax because the ascii colon was unavailable.Wise move.
Finally a language from the 21 century.
Still sticking to ASCII is madness. Especially as most people on this planet don't use ASCII as their native char set.
You can also put a newline in a variable name if you really want. Or a 0 byte.
Here's a demo. I've used the debugger because its "X" command can print the true name of the variable:
$ perl -d -e 1
Loading DB routines from perl5db.pl version 1.60
Editor support available.
Enter h or 'h h' for help, or 'man perldebug' for more help.
main::(-e:1): 1
DB<1> ${"variable-name"} = 123;
DB<2> ${"variable\nname"} = 456;
DB<3> ${"variable\0name"} = 789;
DB<4> X ~variable
$variable^@name = 789
$variable^Jname = 456
$variable-name = 123 locals()["kebab-case"]=123 var `variable-name` = 123
`variable-name` = 456
Looks much cleaner to me.I guess it's similar to Python having a single instance of small integers. PlayStation also experimented with caching small floats which gave them some perf improvements too, but I think wasn't as performant in all cases.
All of those strings will be interned, and can thus be compared by identity. Which is an integer comparison.
1. mutable strings (ruby)
2. and / or expensive strings (erlang, also non-global)
If you have immutable "dense" strings and interning, and you automatically intern program symbols (identifiers, string literals, etc...) then symbols give you very little.
And then there's the slightly brain damaged like javascript, where symbols are basically a way to get some level of namespacing to work around the dark years of ubiquitous ad-hoc expansions so you're completely stuck unable to add new program symbols to existing types because you could break any page out there doing something stupid.
I haven't written ruby (or any lisps) for awhile, and I miss symbols.
`apple
`"cherry pie"
`one`two`three
Many languages will intern string literals implicitly, or allow a programmer to explicitly intern a string; for example Java's "String.intern()".The problem with string interning, especially for strings constructed at runtime, is that for the interning pool to be efficient it is very desirable for it to be append-only, and non-relocatable. A long-running program which generates new interned strings on the fly risks exhausting this pool or system memory.
So does a long-running program which generates new symbols on the fly.
It would be a lot easier if symbols had been just syntactic sugar for immutable frozen strings so that :foo == "foo".freeze == "foo" would be true.
And under the covers these days there is very little difference. It used to be that symbols were immutable and not garbage collected and fast. And that strings were mutable and garbage collected and slow.
These days symbols are immutable and garbage collected and fast and frozen strings are immutable and garbage collected and fast (and short mutable strings are even pretty fast).
Symbols as a totally different universe from Strings I would consider to be an antipattern in language design. They should just be syntactic sugar for frozen strings if your language doesn't already have frozen strings by default.
CL-USER 69 > :a-keyword-symbol
:A-KEYWORD-SYMBOL
Keywords with a similar name are identical: CL-USER 70 > (eq :a-keyword-symbol :a-keyword-symbol)
T
One can't set keyword symbols to another value: CL-USER 71 > (setf :a-keyword-symbol 3)
Error: Cannot setq :A-KEYWORD-SYMBOL -- it is a keyword.
They have certain features of normal symbols, like a property-list with keyword/value pairs.> Instead of writing 10000500, you can write 10_000_500, or 1_00_00_500
https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/nume...
> Balanced string literals
https://hackage.haskell.org/package/raw-strings-qq-1.1/docs/...
> Generalized update syntax
Use Lens. `fileName %~ max 2`
> you can write the sequence 1, 2, … n-1 as 1..<n.
Yup. `[1,2..n-1]` There's far more to it, you have access almost a SQL-like sublanguage.
> Symbols
In Haskell you use hash has a prefix instead of colon.
Haskell sadly does not do automatic lifting, no extended parameter blocks, and no kebab-case.
> In Haskell you use hash has a prefix instead of colon.
Can you give an example?
You can do all sorts of things with them. Use them like symbols in Scheme, say to name fields `get #name user` or to access database tables, etc.
But what's even more interesting is that the name is reflected up into the type. `get #name user` won't fail at runtime. Your database table name can be checked at compile time.
You could likely build "automatic lifting" this macros.
"Extended parameter blocks" are just normal Scala method signatures.
You can use kebab-case (though with back-ticks).
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 # blah section - start
code here
# blah section - end
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.
> const love = "love"; > { // Section > console.log(`What is ${love}`); > }
The language doesn't "understand" it's a section (no opportunities listed in the original comment).
Things like stack traces don't track them, unfortunately.
var foo string
{ // Do stuff
// ...
foo = "..."
}
{ // Other section...
}
You can also split stuff up in to sections, but for some kind of functions where you know the functions will never be re-used and are intimately related, I find this clearer.Of course, your language will need to have block scope, or at least blocks.
They are foldable in IDEs
I'm not sure this is the right approach…
But one of the things I love most is how seriously they take having their Hash be enumerable. I love being able to loop through any hash as easily as you would with an array
/**
* @param arg the string to garble
*/
fun doThing(@NotEmpty arg: String = "default)
In this example, "arg" is mandatory, or else it would have the type "String?", making it nullable. It obviously has a default value. It has an annotation that performs some validation, though admittedly that is a library and not a language feature. And it has its own documentation. I find this more concise than the Powershell example.Because C evaluates this to `((2 <= x) < 10)` which is pretty much never what you want, D just makes them illegal. You'll have to add parenthesis.
After having discovered Zig, I've been missing that feature in every other language.
Sure, comptime is great, but I've also found it hard to reason about code with it. I prefer my comptime stuff separated out into its own section/file/whatever. With that small change, it becomes so much easier.
But yeah, still powerful and nice.
As long as you're ending up with the same problems as in C/C++ it can be as great as it likes it will stay a language of the past.
Something like:
if(x in (null, 2, 3.14, foo(123)) {
//
}That's a pretty std. feature I guess.
a = {
"a": "a",
"b": {"b": 2}
}
Optional commas at the end of lines, so this is also valid a = {
“a“: "a"
"b": {"b": 2}
}
and we are able to swap or append lines without editing the commas or forgetting to do it and get a syntax error. Mandatory commas on all lines would do but it gets in the way of JSON compatibility.This must also be legal code and equivalent to the previous one
a1 = {
a: "a",
b: {"b": 2}
}
a == a1 # true
The developer decides when saving typing time is more important than JSON compatibility.PS: a big yes to kebab-case too. That's in part CSS compatibility because CSS class names are often kebab cased.
Typescript does it well. F# (completely statically typed) too…
But yeah, C++ has a ways to go on type inference.
The main case it doesn't handle is
for (auto i = 0; i < foo.size(); i++)
where obviously I want i to match the return type of foo.size(), so a size_t rather than an int. var getDate() { return „no date“; }For instance the rust developers consciously decided to remove that from the language, named functions must be fully typed.
def getDate = "no date"
That defines a method `getDate` with the type `() => String` in Scala.The type is statically know, of course.
But it's recommended to use explicit return types for public methods. This helps preventing breaking public API by refactoring the implementation of a method.
I think this concept works very well in typescript and F#.
enkebab–case
emkebab—case
These use dash, en dash and em dash, respectively. Most languages that allow you to use a decent amount of Unicode in variable names probably will accept that kind of kebab–case.
Those dashes look pretty similar to each other in monospaced fonts but not indistinguishable, so it’s readable and not super confusing. Might work. Why not?
Prolog has a weird third thing going on. Arithmetic only happens in specific contexts. i.e.
A is 4 - 2, % arithmetic, technically is(A,-(4,2)), A is 2
A #= 4 - 2, % arithmetic, A is 2
A = 4-2, % non arithmetic, A is unified with the term -(4,2) which pretty prints as 4-2
A = -2, % non arithmetic but A is the number -2 not a term -(2).
A = 4-2, B is 4 + A, % a weird one A is the term -(4,2) but when it gets called in the context is(B,+(4,A)) it gets treated as the arithmetic '-' and B is 6
you can also kebab-case predicate names so l-h-t(L,H,T) :- L = [H|T].
?- l-h-t([1,2,3,4],H,T). %works as desired
H = 1,
T = [2, 3, 4].
you can't do it with variables though. thing.process() unless thing.cancelled()
thing.doOne() until thing.queueIsEmpty()
showAdminMenu() if user.isAdmin()
etcAlso worth discussing: micro-misfeatures to be avoided when designing new languages. Maybe non-micro-misfeatures, ie the lack thereof, can be considered a microfeature. Like, for example, uniformity.
And I just have to trot out my favorite example: Java import statements do not allow keywords and numbers in package names. So we can't put our Java source code in folders named 'import', 'long', or in paths like '2023/01/'. Great. For no good-enough reason - the syntax would actually be cleaner with a separate package name syntax. (BTW, this could be fixed, I think.)
class Foo {
@Max(10) int bar;
@NonNull String name();
}
var field = @Foo::bar;
var max = @Foo::bar.Max;
var method = @Foo::name;
var foo = new Foo();
var name = method(foo);
var bar = foo.field;Example: <https://github.com/mchrisman/variables-with-units-language-p...>
Commas in multi-line lists have no use besides making trouble in refactorings and diffs.
d'2020-02-20'
I also tried to make it so that every comparison had both english and symbol representations, a range syntax, and an approximation/match comparison (e.g. "=~", "!~") which could work with both floating point numbers and strings properly.I found this useful, and wish it was in more languages.
(which is allowing nice postfix ones with currying)
Scala does it pretty well. and nowadays finally the function names don't require a PhD in ancient Egyptian hieroglyph decoding.
so requiring alphanum names for functions is pretty important imho. (sure it's okay if it's just a stern lint warning and the developer has to opt-in. but searching for iteratee is easier than searching for \∆>> or whatever.)
If I want a method called `U+1F602`¹ it's not the business of the language to judge that.
---
¹ the actual glyph, which gets filtered out here, also for no reason
But I'm also a firm believer of providing escape hatches. So it should be just a toggle in the project/file/directory to enable whatever behavior. And a very good language would require a human readable explanation for these, so when the developer says
allowEmojisInCode = true "we decided to allow emojis to make our happy DSL, see emoji reference at https://..../...."
downstream users/readers of the code are in a much better position than with just 30000 lines of emojis :)
Scala does not even let most people on the world express code in their native language, and that in the age of Unicode! Sorry, but that's a little to much of "we know better than you what's good for you" kind of thing.
Of course I know where this comes form: Scala is used broadly in education. There it's good to not allow the students to do all kind of "madness".
But Scala is also mostly used by seasoned professionals in real world settings. (Just have a look at the latest survey, found on the Scala website). For a professional it's just extremely annoying when a language tries hard to know better then they how "good code" should look like. The main thing about a expert programmer is that he knows when it's OK to break "rules". Needing to jump through arbitrary but completely useless loops just to do that — when you know exactly what you're doing(!) — makes me mad sometimes.
I considered to fork the compiler not only once because of this. I hate such kind of "but we know better" behavior.
> It's not the business of a language to judge what kind of code is "good".
Well, yes, but no. Language design is just inseparably infused with judgement calls. Making things easy leads to them being used (as the backtick illustrates, making things hard reduces their usage, even if it sounds nice that it allows for special cases).
But as you imply the language has to be flexible and thus powerful enough to provide the option of a seriously different design trade off. (Because backticks are just a bad compromise. It's not really switching to a different design choice after all.)
> I hate such kind of "but we know better" behavior.
I think that implies too much intent on the Scala core team, unfortunately the reality is - probably - that historically someone wanted something, it got done somehow, and that's it. (In the particular case of backticks maybe Martin really had a strong opinion. Dunno. Probably you have looked into this at least a few times if you considered a fork :) )
... related to this the recent discussion about Rust's GAT (generic associated trait) feature is a very interesting case study in the intersection of language design, "project governance/management" (the reality of pragmatic compromises). There a small team spent at least a year developing GAT support for the compiler, and then a bunch of people were asked to decide whether to merge it. And it's a very unenviable position, because of course the work was not perfect. So what to do? In the end, I think at least, the narrative that was comfortable for everyone involved was that "this is the best version we can have realistically, and yes it provides net positive value in this current state".
https://github.com/rust-lang/rust/pull/96709#issuecomment-12...
private def foo
“bar”
end
In this case, ‘def foo … end’ returns ‘:foo’, and ‘private’ is just another method that takes it as an argument and decorates the provided method. It’s not a special language keyword.What I always wanted is proper spaces! Designing syntax where identifier could have spaces (without backticks or something like that) might be tricky of course. But may be it's not impossible.
All those space imitations, whether they're dashes, underscores or camels - they're just imitations. Nothing compares to real spaces.
If anything, underscores are closest ones, if you ask me.
What would this look like, though?[1] How would you solve the problem of adjacent identifiers vs a single identifier which has spaces?
Maybe having identifiers, and only identifiers, starting with a uppercase letter with no uppercase in the rest of the identifier? Then lines like `if MySubRoutine()` is easily parsed as `if My Sub Routine`, and `MyVarType MyVarName;` becomes an easily parser `My var name My var type;`.
Looks harder to read than the usual camelCase, kebab-case and PascalCase identifiers though.
[1] I ask because I'd like to do something like this when writing a program that comes with it's own language for the end-user to use to enhance the program.
Of course keywords must not be allowed as part of identifiers (or there should not be no keywords at all like with Lisp).
Just an example of my head that I didn't think really much about:
function print person (p: person) {
var full name = p.first name + p.last name;
if p.middle name != "" {
full name += p.middle name
}
print line(full name)
for c : p.subordinate person list {
print person(c)
}
}
I think this syntax should be parseable with little restrictions (like your identifier can't start with keyword).I can't "parse it" by looking at it, and a computer would have even more trouble doing so.
How do I tokenise something like `full name += p.middle name`?
Maybe as `(full) (name +=) (p.middle) (name)`? Or is it `(full) (name) (+= p.middle) (name)`?
How about `print person(c)`? Is it the call of the `print` function with `person(c)` as argument?
arrayOf(1, 2, 3) - why not [1,2,3]?
emptyArray() - why not []?
mapOf("key" to "value") - why not {key => value}?
These are all solved problems. Why would they make up some harshly suboptimal syntax?
The ugly Kotlin syntax is of course just there to look different to Scala, where you would have:
Array(1, 2, 3)
Array.empty
Map("key" -> "value")8<------------
data Animal:
| elephant(name, weight)
| tiger(name, stripes)
| horse(name, races-won)
...
end
fun animal-name(a :: Animal):
a.name
endI like this article but oh man dates just trigger me. Such a missed opportunity to use an unambiguous date example like 2001-08-13
How so? Would 2021-08-12 mean month 08 or month 12? It doesn't matter if it starts from zero, it still looks ambiguous to me.
Have you actually ever watched people counting things?
In a sane world the offset based counting (zero bases) would have never surface (besides in very specific and seldom circumstances).
Also it's a shame that the C languages started to call offset "index" (and there is not even a proper index operator!).
A lot of languages provide immutable variables only for class members or statics, but not for local variables.
You might prefer Nim's visually distinct "let" for immutable, "var" for mutable. "const" is also available and means resolved to constant at compile time, similar to Zig's "comptime".
Variable sigils would work but that would be even more annoying.
In the general case of 2 or more comma-separated arguments, is "func a, b" a call with two arguments, or a tuple containing a call with one argument? Feel the same about "x = (func a, b)"? What about when this appears in list syntax like "[x, y, func a, b]", or the argument list of another function like "obj.method(x, y, func a, b)"?
They are easily solvable with a design decision about precedence, but arguably the syntax is a little confusing or worth a warning in all but simple cases.
You can also make it on no-args function, if it's a method (attached to a type). Utilizing @get(), like that: `3.mph` and have somewhere else defined
val Double.mph: Speed
get() = Speed.mph(this)to-prove-that-snake-case-is-more-readable-but-now-that-ive-written-it-i-might-be-changing-camps
I hate this so much. It means I can't grep for a constant.
You'll also be able to give these constants semantically significant names, and comment next to them providing derivations or citations. And of course, if it's a mistaken or outdated value, you can change it one place and apply it everywhere.
Consider that, if you were debugging a problem with this constant, and the problem was caused by someone having made a typo in one of it's usages (eg having typed 1000500 instead of 10000500, a mistake that's more difficult to make of you have better ways to format numbers [did you have to look back and forth to find the mistake? I did]) - your regex would fail to find it, even if there were no ambiguity about the format it was written in.
I'd say in the case of such a sprawling system, make a constants module, and it can have different files for different topics. But keep them all together. Code style is an engineering tool you can use to prevent problems.
But I do understand this is cold comfort for those working on systems where the decision around this were made 15 years ago, and there's no possibility of refactoring the constants. That's quite annoying.
I'd rather copy-paste any reused constants to different projects to avoid coupling, unless there was some kind of compelling domain/project specific reason.
But there are many ways to skin a cat.
You can also use the magic of base 16 to search for lexical subsets of a value to find where code uses things with the same mask, which are probably related. Extremely effective in reverse engineering a hardware device.
I believe the solution I proposed remains viable in that context or for that usage. If I defined a constant for the magic memory address one writes to to configure the MMU, and you and to understand how I implemented context switching, you could navigate to my constant and find usages.
If that solution doesn't work for you, no worries, it was just a suggestion/observation.
To be clear, I grep for things all the time, even though I use an IDE.
And it's also about being able to search Google for a found constant, which indexes other people's code. There have been plenty of occasions where I found newer version of proprietary driver code that the hardware manufacturer claimed it either lost or doesn't exist and won't provide to us by simple searching for constants on Google...
But more often is to just find drivers from other operating systems that already support that device, or mailing lists or forums where other people try to reverse engineer the device.
I'm sorry this language feature creates frustration for you and interrupts your workflow.
You can do almost anything in a grep
const int WAIT_TIME_MICROSECONDS = 42 * 1000 * 1000; // 42 seconds 0x2A * 10e6;
The solution is clearly to have language tooling with a find-constant tool which you give an expression and it parses all declarations in the source code looking for one which evaluates at compile time to the provided expression. // we don’t want more than 100m because …
quota = 1000_000_000
Where people assume the underscores are in the expected place. quota = 1000_000_000
than it is in: quota = 1000000000
And also when I'm typing the number, it's easier for me to be sure I got it right when I can count the zeros in groups of three. It's rare that I've needed this, but I've used it in Java a few times in my career.Sounds like a great way to make unreadable code
#define STANDARD_EXP_PARAMS ...
// I hit gotodef on func(...) and got here. What are the arguments?
void func (STANDARD_EXP_PARAMS) {
// ... long function body ...
x = y; // What is the type of x? Is it an argument or a local?
}
I'd really be irritated if someone ever used this feature, optimizing for lines written is shaky territory to begin with. When it's at an interface boundary it's not excusable. NB4 "use an IDE" - requiring an IDE to make code legible is dumb, and I'm an IDE shill!The exception is in languages that use that syntax as a way to implement keyword arguments, but I would still ask people to destructure it in the parameter list or at the top of the function so I can see what the arguments are.
The point is that when I am looking at a function definition I need to know how to call it. As described all I see is obfuscation to save some keystrokes, not clean code.