Hi guys, I've been keeping a eye on nim development for about a year now, but only got into using the language recently (about a month ago). There is one language feature don't think I will be able to get used to though --- the post-proc pragmas. And it seems I am not the only one. They're very hard to read, especially skim so far on the right, and rather hard to type.
I would like to implement one of these four option as a PR:
[[async]]
proc foo(x: int): string =
...
+[async]
proc foo(x: int): string =
...
[.async.]
proc foo(x: int): string =
...
The last has the added advantage of already being reserved as tkBracketDotLe and tkBrackedDotRi, though I like it only slightly more than using curly braces above (which, in turn, I like far better than the current pragma position). +[async] is probably my favorite for reasons I bring up in the issue I linked to.
I feel like it would be much easier for the maintainers to deliberate over several complete implementations as opposed to musings. However, I would very much appreciate any pointers to topic-relevant resources on the nim lexer + parser. Right now, I feel like I am trying to replicate behavior by sutchering copy-pasted code.
how would you then write inline pragmas? like:
foo(callback = proc() {.closure, gcsafe.} = echo "baa")
Could be something like this, as ugly as original pragmas :>
foo(callback = [closure, gcsafe] proc() = echo "baa")
C# has prefix everything (types, annotations) because it inherits the C syntax
Nim has suffix everything (types, pragmas).
Python has it mixed because ... well, they didn't think about it at the same time, and added @ annotations as prefix and type annotations as suffix.
But there's a sort-of-precedent in nim too, which might generalize: -func is proc .... {.noSideEffecots.} ; you could just make aproc or asyncproc into a proc .... {.async.} using a macro (maybe even a template), and get your prefix without having to modify the language.
Are they implementation details? As you said in another thread, pragmas are not something the compiler can discard and still get the same thing implemented differently as they are in other languages. I can understad the drive for consistency however.
There is a way to implement this in a way that is even more consistent however. Making pragmas operate on their current scope like so:
proc foo(x: int): string =
+[exportc: "int3_tostring"]
...
That way its obvious what file level pragmas are. They are inside the file, therefore they belong to the scope of the file.
The inline syntax would be clear in meaning too:
foo(callback = proc() = +[closure, gcsafe] echo "baa")
The rule is simple: apply to current scope. As things stand, pragmas that are a part of the function signature (but not the function content) apply to a function, while pragmas that are a part of the file contents apply to the file.
And the improvement in readability is still quite significant I feel. Compare:
proc foo(x: int): string =
+[exportc: "int3_tostring"]
...
# or
proc foo(x: int): string =
+[exportc: "int3_tostring"]
...
# vs.
proc foo(x: int): string {.exportc: "int3_tostring.} =
...
# or
proc foo(x: int): string
{.exportc: "int3_tostring.} =
...
If anyone is skimming through a file, I doubt the last two options win out, especially as the number of arguments grows. Placing pragmas above is still better IMHO, and expressiveness is above elegance in the nim priority list, but ultimately you are the BDFL.
I get where you're coming from. But there is something to be said about being pragma-tic (heh). Rust (admittedly, not exactly a model citizen in terms of syntactic choices) has suffix types, and attribute and derive macros have been a part of the language basically from the beginning. It's just easier to tell what's what.
This:
proc tryInsertID*(db: TDbConn, query: TSqlQuery,
args: varargs[string, `$`]): int64
{.tags: [FWriteDb], raises: [].} =
## executes the query (typically "INSERT") and returns the
## generated ID for the row or -1 in case of an error.
...
is a far cry from this in readability terms:
+[tags: [FWriteDb], raises: []]
proc tryInsertID*(db: TDbConn, query: TSqlQuery,
args: varargs[string, `$`]): int64 =
## executes the query (typically "INSERT") and returns the
## generated ID for the row or -1 in case of an error.
...
is a far cry from this in readability terms:
it looks the same
As you said in another thread, pragmas are not something the compiler can discard and still get the same thing implemented differently as they are in other languages.
Well, they are a necessary evil.
On that we agree. And in fact, I'm glad the community doesn't shy away from them. They're definitely a useful tool. I just wish this useful tool was easier to read.
I hope you don't mind I stole some of your code for the example above. I understand we don't agree on how the problem should be solved, but can we at least agree that the second example above is more expressive than the first, even if it is less elegant in terms of implementation?
Is there a way to deal with this that you find agreeable?
The one that's a thread is the one that has a table parameter....
There are two not-entirely-orthogonal questions here:
And I offer again: If "thread" or "async" is the important character - use a macro to make a thread_proc or async_proc the same way func makes a "nosideeffect" proc; no need for language changes for that.
The only language change that might make sense that I can think of (and perhaps everyone happy) is {.next_is thread}, which is like {.push thread} wiith automatic pop at the end of the scope; But I really think that this discussion should only be carried if you've tried to get used to suffix for a while and couldn't -- not because it looks weird at first. Have you tried?
Well, if you want to highlight the "thread" aspect, then yes, putting it on its own line helps with that, no question about it. But when everything is async in your codebase anyway, the desire to stress it vanishes.
The problem is not unique to Nim or to pragmas either, newcomers to Python also complain about the fact that doc comments are under the def declarations, splitting up the code unnecessarily and they too have a point. But in the end the older and less ambiguous syntax wins.
It's not that I don't want to see pragmas. I don't want to see them when I am not interested in them. They are important annotations, but annotations nonetheless.
So when I want to zero in on the annotations I can, and when I wish to ignore them and look at the pure function signature, I can do that too.
Either way, no time is wasted disentangling on from the other. Both the function and the pragma become easier to read.
You can just as easily make the challenge above "Tell me the arguments and the return types of the functions as quickly as you can". You'll be able to do that faster in the 2nd example too.
Putting pragma above proc only makes sense when a pragma is important more than proc name or parameters but I think it is rare case. When I use a proc, proc name, parameters or return type is more important than importC, noSideEffect, raises, etc.
There are many procs with pragma os module source code. https://github.com/nim-lang/Nim/blob/devel/lib/pure/os.nim
But in the document of os module, pragmas are omitted. https://nim-lang.org/docs/os.html
If you still think pragma should be above proc, I think it is possible to create a macro that adds pragma to a proc below.
threadProc:
proc worker() =
...
withPragma(thread):
proc worker() =
...
I mainly used C++ language before I found Nim language. When I start writing Nim code, type name comes after variable/proc name syntax confused me. But I got used it now.
syntactical complaints are usually coming from people who written ~0 lines of nim code.
Usually but not always and I personally never got over C#'s type variable syntax even though I wrote much more C# code than Delphi code. Yes, really, I was a C# programmer. Nim happens to be more similar to Delphi and Python because I took syntax elements from languages with good ideas, not because "Araq was a Pascal programmer".
To expand on cumolonimbus:
Instead of having this
{.push: thread.}
proc worker(f: string, t: guarded ptr Table) =
for line in f.lines:
for w in line.split:
let h = w.hash
lock t.locks[h and (0x100-1)]:
t.buckets[h and (0x1000-1)].inc(w)
{.pop.}
Having
{.pushpop: thread.}
proc worker(f: string, t: guarded ptr Table) =
for line in f.lines:
for w in line.split:
let h = w.hash
lock t.locks[h and (0x100-1)]:
t.buckets[h and (0x1000-1)].inc(w)
It can be called once or anything else. I guess this is similar to the using pragma as well.
I've always felt that pragmas are awkwardly placed. For procedures they come after the signature. For fields, variables and objects, they come before the type. It's also possible to use some as standalone. It was a bit confusing at first.
Lots of languages have similar constructs - decorators, metadata, annotations, attributes. They are generally used as a prefix. Although ,I think Nim gets much more use out of them compared to other languages.