Jump to page: 1 2
Thread overview
`with` across function calls
Jan 18, 2019
Nicholas Wilson
Jan 18, 2019
12345swordy
Jan 18, 2019
Nicholas Wilson
Jan 18, 2019
JN
Jan 18, 2019
Nicholas Wilson
Jan 18, 2019
Simen Kjærås
Jan 18, 2019
Nicholas Wilson
Jan 18, 2019
Nicholas Wilson
Jan 18, 2019
luckoverthere
Jan 19, 2019
Nicholas Wilson
Jan 19, 2019
luckoverthere
Jan 19, 2019
Nicholas Wilson
Jan 19, 2019
luckoverthere
Jan 20, 2019
Nicholas Wilson
January 18, 2019
So now that I finished moving LDC from my LLVM backend to an externally maintained "backend" I was thinking about how I could improve the design of the API. I was not very happy with the use of globals which basically follow the pattern:

struct Global { void* handle; }
Global g;

void usercode()
{
     g = ...;
     Foo foo; foo.foo();
     bar();
     Bar.baz();
}

Here foo bar and baz call functions that somewhere down the call need to use `g` at some point. The value of `g.handle` is not going to be change by the library code, but it can't be immutable or const because handle is passes to other functions and it needs to be assignable by the user when they need to initialise it.

I was hoping to be able to change that to something like
// note no global
void usercode()
{
     auto g = ...;
     with (g)
     {
         Foo foo; foo.foo();
         bar();
         Baz.baz();
     }
}

but then I realised that I can't pass that implicitly down the call stack even if I change foo, bar and baz. I was reminded of Martin Odersky's DConf Keynote and wondered if implicit parameters could be used to do something like:

void bar(with Global g)
{

}

or

struct Bar
{
    void baz(Global g = with)
    {

    }
}

such that

void usercode()
{
     {
         auto g = ...;
         with (g) // or with g:
         {
             bar(); // calls: bar(g);
         }
     }
     // or
     {
         with g = ...;
         Baz.baz(); // calls: Baz.baz(g);
     }
}

Obviously this requires a DIP, but what you do think of it?

The example above is a bit simplified, the call that I'm trying to not pass `g`directly to looks like

q.enqueue!(f!(args))(arg_set1)(arg_set2);

and the function within that needs `g` also needs `f`. The expected usage is a whole lot of those calls all in one spot with different `f`, `args`, `arg_set1`, `arg_set2` I really don't want the user to have to repeat themselves anymore than absolutely necessary.
January 18, 2019
On Friday, 18 January 2019 at 12:35:33 UTC, Nicholas Wilson wrote:
> So now that I finished moving LDC from my LLVM backend to an externally maintained "backend" I was thinking about how I could improve the design of the API. I was not very happy with the use of globals which basically follow the pattern:
>
> struct Global { void* handle; }
> Global g;
>
> void usercode()
> {
>      g = ...;
>      Foo foo; foo.foo();
>      bar();
>      Bar.baz();
> }
>
> Here foo bar and baz call functions that somewhere down the call need to use `g` at some point. The value of `g.handle` is not going to be change by the library code, but it can't be immutable or const because handle is passes to other functions and it needs to be assignable by the user when they need to initialise it.
>
> I was hoping to be able to change that to something like
> // note no global
> void usercode()
> {
>      auto g = ...;
>      with (g)
>      {
>          Foo foo; foo.foo();
>          bar();
>          Baz.baz();
>      }
> }
>
> but then I realised that I can't pass that implicitly down the call stack even if I change foo, bar and baz. I was reminded of Martin Odersky's DConf Keynote and wondered if implicit parameters could be used to do something like:
>
> void bar(with Global g)
> {
>
> }
>
> or
>
> struct Bar
> {
>     void baz(Global g = with)
>     {
>
>     }
> }
>
> such that
>
> void usercode()
> {
>      {
>          auto g = ...;
>          with (g) // or with g:
>          {
>              bar(); // calls: bar(g);
>          }
>      }
>      // or
>      {
>          with g = ...;
>          Baz.baz(); // calls: Baz.baz(g);
>      }
> }
>
> Obviously this requires a DIP, but what you do think of it?
>
> The example above is a bit simplified, the call that I'm trying to not pass `g`directly to looks like
>
> q.enqueue!(f!(args))(arg_set1)(arg_set2);
>
> and the function within that needs `g` also needs `f`. The expected usage is a whole lot of those calls all in one spot with different `f`, `args`, `arg_set1`, `arg_set2` I really don't want the user to have to repeat themselves anymore than absolutely necessary.

The main issue I see with this is unintentional function calls.
with (g)
{
  bar(); //Meant to call bar() but end up calling bar(g)
}
January 18, 2019
On Friday, 18 January 2019 at 13:51:53 UTC, 12345swordy wrote:
> The main issue I see with this is unintentional function calls.
> with (g)
> {
>   bar(); //Meant to call bar() but end up calling bar(g)
> }

Well it'd do what is currently done for

import std.stdio;
void bar() { writeln("()");}
void bar(int i = 0) { writeln("(int)");}

void main()
{
    bar(); // ()
}


January 18, 2019
On Friday, 18 January 2019 at 12:35:33 UTC, Nicholas Wilson wrote:
> void usercode()
> {
>      {
>          auto g = ...;
>          with (g) // or with g:
>          {
>              bar(); // calls: bar(g);
>          }
>      }
>      // or
>      {
>          with g = ...;
>          Baz.baz(); // calls: Baz.baz(g);
>      }
> }
>
> Obviously this requires a DIP, but what you do think of it?

Not a big fan of such implicit stuff. I think it'd be very hard to debug if something goes wrong.

Your usecase reminds me a bit of Dependency Injection in OOP world, or even Service Locator pattern. With DI frameworks, you'd declare a function/class to require a "g", but you don't have to pass it to every call. Instead, you obtain your "g" from a global provider class.

Is 'with' commonly used in D? I don't think I've ever seen it used in any sourcebase. I think it's main usecase is to initialize structs. I kind of like the .. pattern that Dart has:

Foo f = new Foo()
..x = 10     // equivalent to f.x = 10
..y = 20    // equivalent to f.y = 20
January 18, 2019
On Friday, 18 January 2019 at 12:35:33 UTC, Nicholas Wilson wrote:
> void bar(with Global g)
> {
>
> }

How does this work with nested calls? e.g.

struct Global {
    int n;
}
void foo() {
    auto g = Global(0);
    with (g) {
        bar();
    }
}
void bar(with Global g) {
    assert(g.n == 0);
    baz();
    assert(g.n == 0); // g is not overwritten by baz's new g
}
void baz() {
    auto g = Global(1);
    with (g) {
        qux();
    }
}
void qux(with Global g) {
    assert(g.n == 1); // use the nested g
}


If my intuition is correct, the above should compile with no asserts triggered.
I further expect that attempting to call bar() or qux() outside a with(g) will fail:

unittest {
    bar(); // Error: calling bar requires Global g in surrounding with() scope.
    Global g;
    bar(); // Error: Global g found in surrounding scope, but not used in with().
}

--
  Simen
January 18, 2019
On Friday, 18 January 2019 at 14:23:48 UTC, JN wrote:
> Not a big fan of such implicit stuff. I think it'd be very hard to debug if something goes wrong.

Thats what a debugger is for, it would behave just like any other parameter. The debug workflow wouldn't really change, not anymore than of using default parameters.

Think of it like a context aware default parameter.

> Your usecase reminds me a bit of Dependency Injection in OOP world, or even Service Locator pattern. With DI frameworks, you'd declare a function/class to require a "g", but you don't have to pass it to every call. Instead, you obtain your "g" from a global provider class.

I'm trying to avoid globals and this is a structs wrapping opaque handles to OOP objects code, I don't deal in classes at all.

> Is 'with' commonly used in D? I don't think I've ever seen it used in any sourcebase. I think it's main usecase is to initialize structs. I kind of like the .. pattern that Dart has:
>
> Foo f = new Foo()
> ..x = 10     // equivalent to f.x = 10
> ..y = 20    // equivalent to f.y = 20

It used pretty frequently (for some value of frequently) for switching on enums to avoid retyping the prefix.

mir-glas uses it for register-blocking
https://github.com/libmir/mir-glas/blob/fd9adb0750c23db4c3948f79b63384b8082f3601/source/glas/internal/symm.d#L173
and more general for transparent access to config-type structs that need to be short lived (i.e. have a dtor that needs running).
January 18, 2019
On Friday, 18 January 2019 at 14:29:59 UTC, Simen Kjærås wrote:

> If my intuition is correct, the above should compile with no asserts triggered.

Yes.

> I further expect that attempting to call bar() or qux() outside a with(g) will fail:
>
> unittest {
>     bar(); // Error: calling bar requires Global g in surrounding with() scope.
>     Global g;
>     bar(); // Error: Global g found in surrounding scope, but not used in with().
> }

Correct.

January 18, 2019
On 1/18/19 7:35 AM, Nicholas Wilson wrote:
> So now that I finished moving LDC from my LLVM backend to an externally maintained "backend" I was thinking about how I could improve the design of the API. I was not very happy with the use of globals which basically follow the pattern:
> 
> struct Global { void* handle; }
> Global g;
> 
> void usercode()
> {
>       g = ...;
>       Foo foo; foo.foo();
>       bar();
>       Bar.baz();
> }
> 
> Here foo bar and baz call functions that somewhere down the call need to use `g` at some point. The value of `g.handle` is not going to be change by the library code, but it can't be immutable or const because handle is passes to other functions and it needs to be assignable by the user when they need to initialise it.
> 
> I was hoping to be able to change that to something like
> // note no global
> void usercode()
> {
>       auto g = ...;
>       with (g)
>       {
>           Foo foo; foo.foo();
>           bar();
>           Baz.baz();
>       }
> }
> 
> but then I realised that I can't pass that implicitly down the call stack even if I change foo, bar and baz. I was reminded of Martin Odersky's DConf Keynote and wondered if implicit parameters could be used to do something like:
> 
> void bar(with Global g)
> {
> 
> }
> 
> or
> 
> struct Bar
> {
>      void baz(Global g = with)
>      {
> 
>      }
> }
> 
> such that
> 
> void usercode()
> {
>       {
>           auto g = ...;
>           with (g) // or with g:
>           {
>               bar(); // calls: bar(g);
>           }
>       }
>       // or
>       {
>           with g = ...;
>           Baz.baz(); // calls: Baz.baz(g);
>       }
> }
> 
> Obviously this requires a DIP, but what you do think of it?
> 
> The example above is a bit simplified, the call that I'm trying to not pass `g`directly to looks like
> 
> q.enqueue!(f!(args))(arg_set1)(arg_set2);
> 
> and the function within that needs `g` also needs `f`. The expected usage is a whole lot of those calls all in one spot with different `f`, `args`, `arg_set1`, `arg_set2` I really don't want the user to have to repeat themselves anymore than absolutely necessary.

All you seem to be looking for is a context with specified default parameters. Why not make a struct?

auto context = With!g;

context.bar();

With opDispatch and introspection, this should be doable.

-Steve
January 18, 2019
On Friday, 18 January 2019 at 12:35:33 UTC, Nicholas Wilson wrote:
> q.enqueue!(f!(args))(arg_set1)(arg_set2);
>
> and the function within that needs `g` also needs `f`. The expected usage is a whole lot of those calls all in one spot with different `f`, `args`, `arg_set1`, `arg_set2` I really don't want the user to have to repeat themselves anymore than absolutely necessary.

Isn't that the usual argument between using globals and not using them. It's a lot cleaner when using them, especially when it is some sort of data that needs to be passed to basically everything. But then you deal with the joys of globals.

The use case for this is very narrow and the implementation is error prone. Having to go through and check functions to see which one's have altered behavior in a with statement isn't going to be fun. In the general case this doesn't make any sense, eg I don't see this being used anywhere in phobos at all. It'd be the very specific case of an API that needs to pass around some sort of state-like object a bunch of functions.

The rationale is also pretty weak, you want reduce the number of arguments you have to pass in this one specific use case. Honestly from the looks of that function call, it might be better just finding a better way of implementing whatever it is you are trying to implement.
January 18, 2019
On Friday, 18 January 2019 at 14:51:35 UTC, Steven Schveighoffer wrote:
> On 1/18/19 7:35 AM, Nicholas Wilson wrote:
>> The example above is a bit simplified, the call that I'm trying to not pass `g`directly to looks like
>> 
>> q.enqueue!(f!(args))(arg_set1)(arg_set2);
>> 
>> and the function within that needs `g` also needs `f`. The expected usage is a whole lot of those calls all in one spot with different `f`, `args`, `arg_set1`, `arg_set2` I really don't want the user to have to repeat themselves anymore than absolutely necessary.
>
> All you seem to be looking for is a context with specified default parameters. Why not make a struct?
>
> auto context = With!g;
>
> context.bar();
>
> With opDispatch and introspection, this should be doable.
>
> -Steve

So `q` is a struct with a number of methods, calls to its other methods (which won't need `g`) will be interspersed between calls that need `g`. I can't add `g` to `q`, because that breaks the logical objects, they are wrappers of opaque classes, and `g` has nothing to do with `q` except that the call `q.enqueue!(f!(args))(arg_set1)(arg_set2);` needs a `g` to create a object based on f and arg_set2.

I'm not sure that would improve the legibility of the code.

« First   ‹ Prev
1 2