Open PMunch opened 2 years ago
I’d kinda prefer it if it didn’t also remove “proc”, so maybe “safe proc …” and “async proc …” or so, might look nicer for those cases
The problem with that is that if you define e.g. a pure function the proc
(for procedure) keyword is now lying about what you create. You can easily run into similar issues with other pragmas that rewrite the body so it's not strictly speaking a procedure any longer (but still a callable, which is what the syntax actually implicates, take e.g. template
/macro
/iterator
which are all callable like a proc, but isn't a proc).
My original design of func
was made in this direction, in my proposal you could define per-module what func
means, proc .noSideEffect
being the default but it was overridable. Nobody liked it back then. Your proposal does not overload an existing keyword so that is better, however your proposal is really hard to implement with today's parser (and grammar formalism!).
Yes I was afraid that the parser could pose some issues. ElegantBeef over in the chat proposed a declaration syntax similar to for-loop with a proc that has as its only argument a ProcDef type. This doesn't help at all with parsing the procedure definition part though of course. I'm guessing the parser would actually have to be changed to have a kind of "Callable" type with an extra identifier, and then later on it would resolve whether or not the Callable is a proc/macro/template/custom etc. Not sure if this is feasible though, I guess that might change the entire AST representation..
@Clonkk, pragmas are kind of like decorators already. But yes, this would be similar to UFCS. And @Varriount, putting them before is one thing, but replacing the keyword entirely is another. It just feels a bit weird that the Nim syntax is so flexible, but if you want to write something which looks like a procedure it's nigh impossible because that part of the Nim syntax is completely locked off.
The problem with that is that if you define e.g. a pure function the
proc
(for procedure) keyword is now lying about what you create. You can easily run into similar issues with other pragmas that rewrite the body so it's not strictly speaking a procedure any longer (but still a callable, which is what the syntax actually implicates, take e.g.template
/macro
/iterator
which are all callable like a proc, but isn't a proc).
To be fair they always become some kind of "proc" just only in a way the compiler knows how to work with. Nim macros are supposed to be called procedural macros when also talking about other languages's macros. The template
and iterator
keywords are like macros builtin to the compiler that generate some kind of procedure while macro
, func
, method
are more similar to an extra property of a proc type.
The routine declaration and expression syntaxes are very easy to unify so async proc ...
wouldn't be hard to implement. If the keyword is bad, async do foo(...) -> T:
could also be made to work, but people have expressed the ugliness of do
notation before. These wouldn't be pretty but they are one possible roundabout part of a solution.
Fwiw I personally don't like async proc
at all or other prefixes for that matter, the "async" property is not the most important aspect of a function so it should come later in the declaration, not earlier. Having to read and write the irrelevant first is bad design, no matter how many other languages do it.
I think we have enought Bugs with the current syntax to grow it even bigger, and that was the whole point of macros as pragmas with metaprogramming, otherwise we will have 1430980943 ways to declare a function...
@Araq
Fwiw I personally don't like async proc at all or other prefixes for that matter, the "async" property is not the most important aspect of a function so it should come later in the declaration, not earlier. Having to read and write the irrelevant first is bad design, no matter how many other languages do it.
But it is significant! async
(and similar pragmas) impact the entire semantics of a procedure, including its arguments. Pragmas have the capability to completely rewrite a procedure or type. They can add or remove arguments, modify the procedure or type's name, or even change the semantics of various constructs (including such as whether a type is distinct or a what a procedure's name or parameters are).
If I'm reading a procedure definition, and suddenly come across a pragma in between the procedure's arguments and body, I have to throw away my current interpretation of the procedure, and then reconsider (and often re-read!) the procedure's name and arguments in the context of what that pragma does. If it's a cimport
pragma, I have to reconsider how the arguments are going to be passed (by copy or reference). If it's a deprecated
pragma, I have to reconsider whether I'm reading the correct version of the procedure (with the correct set of arguments) or whether I need to look for an alternative with the same name, but a different set of arguments.
This kind of cognitive significance is why all those languages put pragmas (or their respective equivalents) before type and procedure definitions. It places the reader's mind in the context required to accurately interpret the entire definition. Having to switch from "this is a plain old procedure", to "this is an async procedure" after one has already started reading the procedure's definition is jarring. The alternative, blindly scanning for the pragma section, reading it, then jumping back to the beginning of the definition, is not much better.
If we want to make pragmas more visible, I think this proposal here https://github.com/nim-lang/RFCs/issues/15#issuecomment-558160311 was the best that worked with current parser. Note: the whole thread was closed due to the need of stability before 1.0
How about
+[async] proc foo(x: int): string = ...
Visually familiar to rustaceans, removes the signature clutter, and feels like adding a "property" to a proc. Kinda reminiscent of org-mode options too. Would this cause any syntactic clashes?
That one specifically doesn't work with the current parser since it's valid syntax for a standalone statement. The special casing to implement it would be insane.
import macros
dumpTree:
+[async]
proc foo(x: int): string
#StmtList
# Prefix
# Ident "+"
# Bracket
# Ident "async"
# ProcDef
# Ident "foo"
# ...
On the other hand, [. .] (. .)
are still unused reserved tokens.
[.async.]
proc foo(x: int): string =
...
I'm not advocating for or against this, just thinking this makes more sense.
This kind of cognitive significance is why all those languages put pragmas (or their respective equivalents) before type and procedure definitions.
Well, no. They don't merely put it "before" the declaration because it's so important, they put it in the line above in order to not clutter the real/important parts of the definition. Both Nim's and C#'s syntax "agree" that these annotations are "clutter", they simply deal with it in a different way.
This kind of cognitive significance is why all those languages put pragmas (or their respective equivalents) before type and procedure definitions.
Well, no. They don't merely put it "before" the declaration because it's so important, they put it in the line above in order to not clutter the real/important parts of the definition.
So "being able to impact the entire meaning of the definition" isn't important? If that's the case, we should probably discuss moving the position of the template
, proc
, iterator
, etc. keywords elsewhere, since they certainly have the same amount of impact pragmas have.
I'd argue that placing pragmas above the definition is less about the fact that they're "clutter" and more about the fact that:
Both Nim's and C#'s syntax "agree" that these annotations are "clutter", they simply deal with it in a different way.
But sure, let us consider the case where pragmas are clutter (completely ignoring the arguments I have made to the contrary). Is Nim's way of dealing with them actually better? What benefits does placing pragmas in the middle of a definition have over placing them before the definition? I'm honestly curious, because other than "looking nice when there's no definition body" and "looking nice when there's only a couple of argument-less pragmas", I can't think of anything. I can think of how it's worse though.
As an addition to my previous argument, let's consider the case where pragmas have relatively minor effects that a reader wants to ignore. Nim places the "cluttering" pragmas in the middle of a definition, where a reader must frequently skip over them when moving between either "sides" of the definition split by the pragma section. This is a minor annoyance when there's only one pragma, but it becomes frustrating when you have a procedure with multiple pragmas (and downright infuriating when they take arguments!).
As an aside, I put more than just links to C#. Python and Java also have similar mechanisms. Especially in Python's case, I doubt one can claim they are mere "clutter".
I don't "ignore" your arguments but for me there is a big difference between async def foo
and @async\n def foo
(note the newline please).
This is a minor annoyance when there's only one pragma, but it becomes frustrating when you have a procedure with multiple pragmas (and downright infuriating when they take arguments!).
The hyperbole weakens your otherwise good arguments.
As an aside, I put more than just links to C#. Python and Java also have similar mechanisms. Especially in Python's case, I doubt one can claim they are mere "clutter".
Well but they are positioned in a way that suggests that they are clutter and should not disturb the def name(args)
part.
But it doesn't matter if "super important" or "clutter", we both seem to be fine with the syntax:: annotations \n declaration
If you think for a moment, this does not make too much sense...,
because in Nim a function can be async and sync at the same time, like {.multisync.}
,
async def foo
can be sync blocking code. 🤷
But it doesn't matter if "super important" or "clutter", we both seem to be fine with the syntax:: annotations \n declaration
So if I understand correctly, you propose to move pragmas before the declaration like (or keep the {. .}
but before the declaration; whatever works best) :
[. async, gcsafe.]
def fooBar() = discard
I'm not proposing it but I don't mind it. And I do mind async noSideEffect proc name(args) = body
enough to veto against it.
So this RFC has drifted way of target for what I proposed.. async noSideEffect proc name(args) = body
is terrible, and I agree that it shouldn't be allowed. What I was proposing was some way of doing mycallable name(args) = body
and be able to modify that procedure body. Whether that is with something similar to for-loop macros with a macro with a single ProcStmt
type or through simply converting it to proc name(args) {.mycallable.} = body
is irrelevant. The pragma approach just hit me as the simplest way of implementing this.
This is something that I've felt would tidy up many different kinds of projects and make Nim syntax even more readable. Essentially I propose that Nim add in some way of creating aliases like
func
but in user code.func
for those not in the know is just an alias forproc {.noSideEffect.}
. I propose that we'd be allowed to define aliases like these. Originally I imagined a syntax like:But that would obviously be confused with just creating an alias for a procedure so it might not be suitable. I've chosen to use this syntax in the following examples however.
The ability to create aliases for different procedure types which automatically attaches pragmas would allow libraries like
async
to behave like this:Or allowing things like:
It could of course also be extended to any library which requires the definition of procedure-like constructs and wishes to avoid using pragmas. In my opinion it is much easier to miss a pragma attached to a
proc
than it would be to miss that it wasn't defined as aproc
at all. The existingfunc
could of course then be migrated into a simple type definition in the system module.