nim-lang / RFCs

A repository for your Nim proposals.
135 stars 26 forks source link

Remove the do notation for anon procs #264

Open dom96 opened 3 years ago

dom96 commented 3 years ago

The do notation has rightly been kept "experimental" (https://nim-lang.org/docs/manual_experimental.html#do-notation) for the 1.0 releases. Every time I see it used it feels innately un-Nim to me, when I mentioned this to other Nim users they have a very similar reaction.

Objectively speaking these are the problems with it:

Let's simplify the language and deprecate this feature to discourage its use, then eventually remove it.

Note: I am proposing this for the do notation which applies to anon procs specifically. Not the variant which allows passing multiple code blocks to a template.

timotheecour commented 3 years ago

eg usage:

fn(3) do: echo 1 do: echo 2



* ditto with `quote do`
ghost commented 3 years ago

@timotheecour please read the RFC, it explicitly talks about do notation for anonymous procs, not about do notation in quote do or for templates/macros

dom96 commented 3 years ago

Added a clarification at the bottom of my post since the title is the only one that mentions this.

timotheecour commented 3 years ago

@timotheecour please read the RFC, it explicitly talks about do notation for anonymous procs, not about do notation in quote do or for templates/macros

still, how do you express this without do?

proc fn[T1, T2](a: T1, b: T2) =
  a(1)
  b(2)

fn do(x: int):
  echo "before" # example block, can span multiple lines
  echo x
do(y: int):
  echo "before"
  echo y

I'm not sure how to use proc(x: int) in this context, the only thing that I found was adding a dependency on sugar and using:

import sugar
fn((x: int) => (block:
  echo "before"
  echo x),
  (y: int) => (block:
  echo "before"
  echo y))

not 100% clear which is better here

Araq commented 3 years ago

Easy, like so:


proc fn[T1, T2](a: T1, b: T2) =
  a(1)
  b(2)

fn( (proc(x: int) =
      echo "before" # example block, can span multiple lines
      echo x),
    (proc (y: int) =
      echo "before"
      echo y)
)
timotheecour commented 3 years ago

good enough, thanks!

mratsim commented 3 years ago

The do() anonymous notation provides key ergonomics to our RPC protocols: https://github.com/status-im/nimbus-eth2/blob/dba39c5/beacon_chain/beacon_node.nim#L692-L768.

Using proc with parenthesis would be too lispy for my tastes. While I felt do was always forein/magic/alien, anonymous proc are really annoying otherwise.

On that topic the recurrent need of {.nimcall.} or {.closure.} explicit annotations is another ergonomic meh.

I would like to point out that this success story and appealing use of RPC on embedded is thanks to the do notation: https://forum.nim-lang.org/t/6916

import nesper/servers/rpc/rpcsocket_json
# import nesper/servers/rpc/rpcsocket_mpack

# Setup RPC Server #
proc run_rpc_server*() =

    var rt = createRpcRouter(MaxRpcReceiveBuffer)

    rpc(rt, "hello") do(input: string) -> string:
        result = "Hello " & input

    rpc(rt, "add") do(a: int, b: int) -> int:
        result = a + b

    rpc(rt, "addAll") do(vals: seq[int]) -> int:
        result = 0
        for x in vals:
            result += x

    echo "starting rpc server on port 5555"
    logi(TAG,"starting rpc server buffer size: %s", $(rt.buffer))
    startRpcSocketServer(Port(5555), router=rt)

Hence, I would welcome the change but only if an alternative syntax less clunky than proc with parenthesis is available.

zah commented 3 years ago

I appreciate the fact that Nim allows me write generic code that works fine both when something is implemented as a template or a proc. Adding pointless restrictions to the do notation will break this property and it's not going to make the language any simpler. After all, the proposed implementation will just add more rules that the compiler must enforce and user must cater to.

Araq commented 3 years ago

@mratsim I think your example is poorly chosen as this works just as well (I think):


 rpc rt, "hello", proc input: string): string =
        result = "Hello " & input
disruptek commented 3 years ago

Well, "easy" or not, it's clearly not as ergonomic and thus, I'd argue, less idiomatic.

If you must remove it, I think a substitute that preserves legibility needs to be introduced.

dom96 commented 3 years ago

I would like to point out that this success story and appealing use of RPC on embedded is thanks to the do notation: https://forum.nim-lang.org/t/6916

This is actually what inspired me to create this RFC. My take on this is that it would read far better with proc, I haven't tested this but I think this would work:

import nesper/servers/rpc/rpcsocket_json
# import nesper/servers/rpc/rpcsocket_mpack

# Setup RPC Server #
proc run_rpc_server*() =

    var rt = createRpcRouter(MaxRpcReceiveBuffer)

    rpc(rt, "hello", 
      proc (input: string): string =
        result = "Hello " & input
    )

    rpc(rt, "add",
      proc (a: int, b: int): int =
        result = a + b
    )

    rpc(rt, "addAll",
      proc (vals: seq[int]): int =
        result = 0
        for x in vals:
            result += x
    )

    echo "starting rpc server on port 5555"
    logi(TAG,"starting rpc server buffer size: %s", $(rt.buffer))
    startRpcSocketServer(Port(5555), router=rt)

As for the examples where multiple procs are passed in... once you're passing in more than one proc into a function call then you should be starting to consider refactoring that out. I would argue that we shouldn't be encouraging it via syntax sugar implemented in the compiler.

timotheecour commented 3 years ago

this is the best notation I can think of, and it's better than do objectively (less foreign, easier to grok etc):

block:
  proc rpc[T](a: int, b: T) = echo (a, $b.type)

  rpc 1,
    proc(c:int) = echo 1.2 # single line

  rpc 2,
    func(c:int): int =
      # can use `func`, unlike `do` (and other proc kinds pending alias PR #11992)
      # multiline
      c*c

As for the examples where multiple procs are passed in... once you're passing in more than one proc into a function call then you should be starting to consider refactoring that out. I would argue that we shouldn't be encouraging it via syntax sugar implemented in the compiler.

it also works out nicely, see below:

  # with multiple procs:
  proc rpc2(a, b: int, b1: auto, b2: auto) = echo (a, $b1.type, $b2.type)

  rpc2 1, 2,
    func(c: int): float = 1.2,
    proc(x, y: int) = echo (x,y)

  rpc2 1, 2,
    func(c: int): float =
      discard,
    proc(x, y: int) =
      echo (x,y)

  rpc2 1, 2,
    func(c: int): float = 1.2,
    proc(x, y: int) =
      echo (x,y)

note

there is just 1 gotcha but it's acceptable and it's still better than using do which does look foreign:

  fn 123,
    proc(x: int) =
      #[
      this doesn' work:
      echo x,

      this works:
      echo(x),
      (echo x),
      discard,
      ]#
      echo(x),
    proc (y: int) =
      echo y

When there is no argument before the first block:

block:
  proc fn[T1, T2](a: T1, b: T2) = discard

  # use this (preferable IMO)
  fn proc(x: int): auto =
       echo(1),
     proc (y: int) =
       echo y

  # or this:
  fn(
    proc(x: int) =
      echo x
    , proc (y: int) =
      echo y)

  # but not this:
  # fn proc(x: int): auto =
  #      echo(1)
  #    , proc (y: int) =
  #      echo y
disruptek commented 3 years ago

I'd say that the user should be incentivized to remove code that is hard to read, but I can't think of a technical reason to deny the code, so not supporting better syntax just seems like petty laziness.

liquidev commented 3 years ago

An alternative to do notation for anon procs could be implemented with macros:

proc example(callback: proc (i: int)) = callback(1)

# this is parsable:
example <- (i: int):
  echo i

# i personally prefer words over operators
pass example, (i: int):
  echo i

One thing I'm not sure about is the syntax for return types. Obviously : cannot be used, -> also is problematic because of precedence, but that could be solved by making it into another macro which adds a return type to the closure added by <-/pass. Another option would be Go-style example <- (i: int) int, but I think it's a bit ugly.

HugoP707 commented 3 years ago

how about changing do: to something more intuitive like lambda?

alaviss commented 3 years ago

how about changing do: to something more intuitive like lambda?

The issue raised here is that do is a special syntax. I doubt renaming it would help, and we can't just randomly take the name lambda and turn it into a keyword.

metagn commented 3 years ago

Many points have been made already but downvoting without saying something feels wrong, do notation creates the most expressive way of using a macro in my library https://github.com/hlaaftana/applicates, it is the best way to mirror routine declaration syntax in macros IMO. Yeah we have to do -> instead of : but it's not like that changes anything. Type sections are ugly enough to pass to macros at least keep the syntax that supports routine argument syntax for macros.

liquidev commented 3 years ago

@hlaaftana Looking at the example in your readme, this:

doAssert @[1, 2, 3, 4, 5].map(applicate do (x): x - 1) == @[0, 1, 2, 3, 4]

could easily be replaced with sugar.=>:

doAssert @[1, 2, 3, 4, 5].map(applicate((x) => x - 1)) == @[0, 1, 2, 3, 4]

and for multiline procs, Araq's idea works well:

doAssert @[1, 2, 3, 4, 5].map(applicate proc (x) =
  let a = x + 2
  a - 3
) == @[0, 1, 2, 3, 4]
metagn commented 3 years ago

=> is worse. You can't use semicolons between arguments and return types are going to be extremely ugly, if you were going to support them you would need parentheses or something, and to handle the parentheses you have to distinguish them from (a) vs a, and (a, b) counts as parentheses but (a, b: int) and (a: int, b: int) are both named tuple constructors somehow. Not just that, I'm not trying to work with anonymous procs here (I believe you are trying to tell me to use the output of sugar.=>?), I'm trying to generate anonymous templates, and the most expressive argument syntax for them is with do.