Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scope Questions #16

Open
rwaldron opened this issue Nov 9, 2017 · 14 comments
Open

Scope Questions #16

rwaldron opened this issue Nov 9, 2017 · 14 comments

Comments

@rwaldron
Copy link

rwaldron commented Nov 9, 2017

EDIT:

The readme has been updated since this was first posted, however the changes made do not sufficiently address all of the scoping problems. Ref: 3280e50

Original follows the break


Re: the example from the readme:

// this ...
a {
  ...
  b {
    ...
  }
  ...
}

// ... is desugared to ...
a(function() {
  ...
  this.b(function() {
    ...
  })
  ...
});
  1. What is this inside the callback function? In strict mode code, that desugaring has no this unless explicitly bound:

    "use strict";
    function a(m, callback) {  
      console.log(m);
      console.log(`inside a(): typeof this === ${typeof this}`);
      callback();
    }
    
    function b(callback) { callback(); }
    
    a("hello", function() { 
      this.b(function() {
        console.log(`(inside b(): typeof this === ${typeof this})`); 
      });
    });
    
    // hello
    // (inside a(): typeof this === undefined)
    // Uncaught TypeError: Cannot read property 'b' of undefined
  2. The example does work with non-strict mode code, but also assumes that b was created via VariableStatement or FunctionDeclaration in the top level scope:

    function a(m, callback) {  
      console.log(m);
      console.log(`(inside a(): typeof this === ${typeof this})`);
      callback.call(this);
    }
    
    function b(callback) { callback(); }
    
    a("hello", function() { 
      this.b(function() {
        console.log(`(inside b(): typeof this === ${typeof this})`); 
      });
    });
    
    // hello
    // (inside a(): typeof this === object)
    // (inside b(): typeof this === object)
    • If b was created as a LexicalDeclaration, it won't have a binding on global this object:
      let a = function(m, callback) {  
        console.log(m);
        console.log(`(inside a(): typeof this === ${typeof this})`);
        callback.call(this);
      };
      
      let b = function(callback) { callback(); };
      
      a("hello", function() { 
        this.b(function() {
          console.log(`(inside b(): typeof this === ${typeof this})`); 
        });
      });
      
      // hello
      // (inside a(): typeof this === object)
      // Uncaught TypeError: this.b is not a function 
    • That also means that this won't work in Module Code—which is strict mode code by default.
  3. (2) falls down when the user defined functions have an explicit this object set:

    var unbounda = function(m, callback) {  
      console.log(m);
      console.log(`(inside a(): typeof this === ${typeof this})`);
      callback.call(this);
    };
    
    var unboundb = function(callback) { callback(); };
    
    var thisObject = {};
    
    var a = unbounda.bind(thisObject);
    var b = unbounda.bind(thisObject);
    
    /*
    a("hello") {
      b {
        console.log(`(inside b(): typeof this === ${typeof this})`); 
      }
    }
    */
    
    a("hello", function() { 
      this.b(function() {
        console.log(`(inside b(): typeof this === ${typeof this})`); 
      });
    });
    
    
    // hello
    // (inside a(): typeof this === object)
    // Uncaught TypeError: this.b is not a function 
@samuelgoto
Copy link
Owner

I was originally thinking that the caller would set this and pass the right methods that are supported. For example:

function a(block) {
  block.call({
    b: function(block) {
      // ...
    }
  });
}

Enabling

a {
  b {
  }
}

To be equivalent to:

a(function() {
  this.b(function() {
  })
})

However, in this discussion I think I'm going back to my original formulation, which was to desugar things as:

a {
 b {
  }
}

// equivalent to

a(function() {
  b.call(this, function() {
  })
})

In this formulation, a would still be possible to pass a reference to b such that a use case like the following to work:

select (foo) {
  case (bar) { ... } 
}

WDYT?

@dead-claudia
Copy link

Just thought I'd drop in and note that there is also some relevant discussion in #13, primarily starting here.

@rwaldron
Copy link
Author

a {
  b {
  }
}

// equivalent to

a(function() {
 b.call(this, function() {
 })
})

I addressed this in 1 and 2 of my first comment: this, as in b.call(this, function() {, is undefined in strict mode code and global in non-strict mode code.

@dead-claudia
Copy link

@rwaldron Not if a calls its callback like func.call(inst, ...). a sets this, and it's only undefined or global if a calls it like func(). this isn't bound, even though it looks like it should be.

It still falls into the trap of making nested DSL calls ambiguous.

@rwaldron
Copy link
Author

Not if a calls its callback like func.call(inst, ...). a sets this

I explicitly addressed this in number 3 of my first comment.

@samuelgoto
Copy link
Owner

I addressed this in 1 and 2 of my first comment: this, as in b.call(this, function() {, is undefined in
strict mode code and global in non-strict mode code.

Can you help me understand what you mean here? Specifically:

b.call(this, function() {, is undefined in strict mode

Can you help me understand what is undefined? For example:

(function() { "use strict"; function b() { console.log(this) } b.call({c: 1}) })()
// > {c: 1}

Allows b.call({c:1}) to pass a this reference in strict mode.

@samuelgoto
Copy link
Owner

WRT

I explicitly addressed this in number 3 of my first comment.

and

(2) falls down when the user defined functions have an explicit this object set:

Yes, you are correct that if a user-defined function was bound per var b = unbounda.bind(thisObject); the this reference would be changed as a would call b.call(notherthis).

I think that's working as intended, in that's part of the contract for the functions that take block params in that they cannot assume that the bindings would be kept (and would rather point to the parent).

@rwaldron
Copy link
Author

Can you help me understand what is undefined? For example:

(function() { "use strict"; function b() { console.log(this) } b.call({c: 1}) })()
// > {c: 1}

Allows b.call({c:1}) to pass a this reference in strict mode.

Of course it does, because you used call({c:1}), but this feature cannot assume that such a thing will always work: if b is an arrow function or the result of fn.bind(...), then b.call({...}) will have no effect:

(function() { "use strict"; let b = () => { console.log(this) }; b.call({c: 1}) })()
// undefined

(function() { "use strict"; function f() { console.log(this) }; let b = f.bind({ d: 1}); b.call({c: 1}) })()
// { d: 1 }

t if a user-defined function was bound per var b = unbounda.bind(thisObject); the this reference would be changed as a would call b.call(notherthis).

I don't understand what you're saying here. Once var b = unbounda.bind(thisObject); occurs, the bound this of b can never be changed again, it cannot be overridden by a b.call(...)

in that's part of the contract for the functions that take block params in that they cannot assume that the bindings would be kept

If you're telling me that block params can change the bound this of a function object, then I believe there is an object-capability security violation. @erights can you check my assessment?

@erights
Copy link

erights commented Nov 27, 2017

If you're telling me that block params can change the bound this of a function object, then I believe there is an object-capability security violation. @erights can you check my assessment?

Yes. While I am supportive of the overall direction, @samuelgoto knows that I am against the specific this binding semantics he's proposing. Once that's fixed, the blocks should expand to arrow functions rather than function functions, as arrow functions are already much closer to TCP. (The remaining TCP violations still must be statically prohibited or fixed, but that's another matter.)

@samuelgoto
Copy link
Owner

samuelgoto commented Nov 27, 2017

Of course it does, because you used call({c:1}), but this feature cannot assume that such a thing
will always work: if b is an arrow function or the result of fn.bind(...), then b.call({...}) will have no
effect:

I understand that if b is an arrow function or the result of fn.bind, then b.call() will have no effect. I think that, perhaps, what I am genuinely confused about, is that the feature is meant to be used primarily from the newly introduced syntax:

a() { // <- this is a block param. neither an arrow function or a previously bound function
  //
}

Is your point that the function a here cannot assume / observe that / whether the parameter that was passed was passed via the newly introduced syntax? Example:

a(() => { "hello" });
function a(block) {
  // I cannot assume that block.call() will have any effect because block may
  // have been passed as an arrow function or as a previously bound function.
}

Did I understand that correctly? Is that the point that you are trying to make?

@samuelgoto
Copy link
Owner

(oops, sorry for closing/reopening, pressed the wrong button)

@samuelgoto
Copy link
Owner

Just reporting back on this thread here with what I think was forward progress made in this thread.

I'm generally in agreement with the desire to move away from the this nesting mechanism as well as the with-like scoping mechanism to find the variable names.

Just to give context, the use case that I think represents why we need a nesting mechanism is select and when: they are attached to one another in some way and need to pass information back and fourth. Here is an example:

select (expr) {
  when (cond) {
    // execute this block if cond == expr.
  }
}

This was initially proposed as a series of nested function() {}s and passing this around, which, like it was pointed out earlier, creates all sorts of challenges.

Looking a bit into what this could look like with arrow functions, here is what we explored in the other thread:

  • using arrow functions as opposed to functions, to avoiding messing with this
  • using a "special" argument (maybe protected with a Symbol) as opposed to "this" to nest
  • using a sigil (for consistency, borrowing :: from the bind operator) to connect with the "special" argument

For example, this is what you write instead:

select (expr) {
  ::when(cond) {
    // ... this gets executed if expr == cond ...
  }
}

Which gets transpiled as:

select (expr, (__parent__) => {
  __parent__.when(cond, (__parent__) => {
    // .. gets executed if expr == cond ...
  });
})

This gives select the ability to choose which implementation of when to be used, because :: looks at methods in an object that gets passed to the block param from select. For example:

function select(expr, block) {
  block({
    // this is the "when" implementation that gets accessed when called like ::when() {}
    when(cond, inner) {
      if (expr == cond) {
        inner();
      } 
    }
  });
}

Does that address some of the concerns raised here with regards to scoping and this?

@samuelgoto
Copy link
Owner

Similar discussion here too:

#21 (comment)

@ljharb
Copy link

ljharb commented Nov 30, 2017

Related to #24

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants