nim-lang / RFCs

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

Passing routine parameters via `using` by parameter name #305

Closed metagn closed 3 years ago

metagn commented 3 years ago

Outside of just looking ugly or being confusing, this could be very hard or cumbersome or problematic to technically pull off, and it could have some fundamental flaws interacting with the rest of the language that I am missing, but maybe indulge it for a little bit.

Proposal: An expansion on the using statement that allows syntax like using a = b, where if any routines called in the following code have a named argument named a, it will be set to b for each call by default (I believe this is slightly similar to implicit parameters in Scala). Example:

let window = newWindow("Window Title", 800, 600)
let renderer = window.createRenderer()

using
  window = window
  renderer = renderer

# render logic

setDrawColor(0, 255, 0)
drawText("Hello world", rect(10, 10, 80, 20))

A possible further expansion is specifying a type to work with routines that take arguments of the same name but need a completely different value.

using
  context: GlContext = renderer.createGlContext()
  context: SslContext = newSslContext()

This has a fair bit of nuance to deal with.

Too implicit

You could think this is way too much hidden control flow, but what is a clear alternative? Just putting context in the proc arguments to mean context = context would make no sense. If you wanted to restrict it inside a block which is specifically annotated with a {.cast().}-like construct though prettier like Rust unsafe, you could almost do it with a macro.

Maybe you could force annotating specific proc parameters that this would replace with {.implicitable.} or something at the proc definition somewhat like in Scala (Scala goes by type, not by name). I don't think restricting this feature in whatever way would be a hard task, so I'm not going to focus on it very much.

Confusion with default argument values in procs

Someone might think these are the same:

using a: int = 5
proc foo(a) = discard
# and
proc foo(a: int = 5) = discard

This is not allowed in current Nim (you have to do using a: int; proc foo(a = 5) = discard), and I can't think of many cases where it would be needed either. If this wasn't supported but the proposal was, I don't think the confusion here would be too much of a problem if someone were to fall for it. The compiler wouldn't give an error that's too cryptic, it would just be a little unexpected.

Small sidenote: using a {.pragma.}: int is allowed though I think it's ignored.

If this syntax were to be supported for proc default arguments in definitions, it wouldn't necessarily have to clash with proc passing, it could do both, though doing 2 different things at once could be undesirable. Maybe a pragma could be used to only apply it to 1 kind of either default argument definitions or default argument passes, maybe this would be ugly.

Beyond that, there is another problem with supporting default arguments in definitions, proc argument default values can depend on other arguments, like so (not sure if this is documented):

proc foo(a: int, b = a + 1) = echo a + b

using wouldn't have to support this, but it's not exactly clear what it should be doing instead. Should it store an untyped expression or type the expression once and pass it around or evaluate it as a constant? Should the programmer be able to choose for themselves?

Typed or untyped

In the example I gave above with window and renderer and context, they all depend on a strictly runtime value that might be used in a lot of code but can only be created under strict conditions (well, window at least). It would obviously be asking for trouble to do something like using window = newWindow(), but one might still want to be careful. In this example:

var b = whatever
using a: SomeT = b
callSomethingThatUsesA()

It would make sense that b is type checked at the using statement first and then again every time it's injected. But you would have to ensure that you can use the b that was typed in the using statement can be used at the injection site (not that this would be too much of a problem, as using statements are local to a module. exported templates would break with it but those should not be supported at all).

You could want b to be re-typed at every callsite (it's important to not emulate let or const behavior here), or you might want to support this syntax for templates (no matter how broken that would be). If you set this to be the default behavior, you lose some safety that you might want otherwise. Perhaps type it by default, and allow leaving it untyped as an option:

using a: untyped = b

This notation could be a problem as untyped and auto are the same type in Nim internals IIRC and we want using a = b to be typed but by inferring the type of b, which auto is meant for. So maybe yet another pragma like using a {.untyped.} = b or something weird like using a = untyped(b).

We're still not done with this kind of "type safety".

using is top level only

What if we wanted "type safety", but didn't want to deal with expressions that can only be typed at top level (e.g. global variables)? Then we can change:

var window: Window
using window: Window = window

proc foo =
  usesWindow()

proc bar =
  usesWindow()

To:

proc foo(window: Window) =
  using window: Window = window
  usesWindow()

proc bar(window: Window) =
  using window: Window = window
  usesWindow()

There are obviously more use cases for scope-local variants of these but I just wanted to get that one out of the way to keep up with the topic of type safety. The point is Nim does not currently support local using at all, though it would greatly be benefited from here. If you think it's too cumbersome you can do

template implicitWindow(): untyped =
  using window: Window = window

proc foo(window: Window) =
  implicitWindow()
  usesWindow()

proc bar(window: Window) =
  implicitWindow()
  usesWindow()

Yes, this means macros and templates could take advantage of this feature for DSLs, though currently you can just fill in overloads for routines like I'm sure many people do already.

I think that's enough points in the main post for now, I don't expect this issue to be necessarily supported very much as there was an RFC to remove the existing using statement, the most support I've seen for something like it is stuff like https://github.com/nim-lang/Nim/issues/12873 which would be very nice and I would 100% use but seems a little disconnected from the normal using statement IMO.

ghost commented 3 years ago

There was actually a somewhat similar feature in the language called "Automatic self insertions" https://nim-lang.org/0.19.0/manual.html#overloading-resolution-automatic-self-insertions - it got removed.

juancarlospaco commented 3 years ago

with does this https://nim-lang.github.io/Nim/with.html#with.m%2Ctyped%2Cvarargs%5Buntyped%5D

with window:
  setDrawColor(0, 255, 0)
metagn commented 3 years ago

A couple more downvotes and closing

Araq commented 3 years ago

We had that feature, one problem:


echo "a"

# rewritten to:

echo this, "a"

Now what are the rules? Why is echo special? I never found a good rule.

metagn commented 3 years ago

Is that a response to the RFC or a tangent on {.this.}? The RFC is that it would only replace parameters with a specific name.

Araq commented 3 years ago

It would equally apply to your variant of the feature:


let window = newWindow("Window Title", 800, 600)
let renderer = window.createRenderer()

using
  window = window
  renderer = renderer

# render logic

setDrawColor(0, 255, 0)
drawText("Hello world", rect(10, 10, 80, 20))
echo "hello"

If not, why not? Because echo doesn't have a parameter named window nor renderer?

metagn commented 3 years ago

Yes