Automatic scoping of nested dialogues #167

Closed
staltz opened this Issue Aug 31, 2015 · 110 comments

Projects

None yet

9 participants

@staltz
Member
staltz commented Aug 31, 2015

This idea: https://gist.github.com/axefrog/e53b915b1ddd37ce1eac

Need to let cool down, experiment with it, and evaluate whether 'benefits minus magic minus boilerplate' is positive.

@staltz staltz added the feature label Aug 31, 2015
@erykpiast
Contributor

The idea looks super-interesting, even more because yesterday I was thinking about something similar regarding DOM or D3 driver. Could you provide example usage of above snippet? I'm not sure if I fully get it.

@axefrog
axefrog commented Aug 31, 2015

So what brought this up to begin with was what I was thinking about with regards to changes I could make to the cycle-router5 driver to make it more compliant with the nested dialogue abstraction. That is, you should be able to declare routes for a given dialogue, but, if using that dialogue within a parent dialogue, those routes should be able to be automatically hung off of a route declare by the parent, with the end result being that it becomes possible to create, reuse and distribute components that have fully-portable routing capabilities. The child would never need to know (or care) about the absolute path that its own route paths are appended to, and would function correctly no matter what.

To do this, it occurred to me that I'd need some kind of scope stack so that nested dialogues could declare their local routing needs, and have those then merged back into their parents' routes as we exited the stack. I could see this looking messy and repetitive, and prone to human error if used all over the place.

It then got me to thinking about the embedding of namespaces into child dialogues for the purposes of DOM scoping, and it occurred to me that a universal pattern could probably be applied to refine that idea as well; i.e. if the DOM driver itself is scope-aware, then it would be easy for it to internally apply scope to selections, and to even filter out event matches for child scopes if you want to keep things really isolated. As you know, one potential problem with the DOM driver is that, sure, by embedding namespaces into strings throughout your component/dialogue, you can scope your event matching to that branch of the DOM, but it does also mean that you'll unwittingly match against the internals of any child dialogues, which may cause unexpected problems. This automatic scoping idea could potentially offer a solution to that problem as well, where none previously existed.

@axefrog
axefrog commented Aug 31, 2015

Two other things of note:

  1. This concept is non-invasive; even if implemented within cycle core, all existing code would continue to work, and people could continue writing code the normal way without any side effects.

  2. After having thought more about it last night, it occurred to me that this could become a clean overload of cycle's run() function. In this way, your dialogues would end up looking really idiomatic to cycle, because they themselves would be "running" their child dialogue applications, just like the main application is running the root main function.

Example usage:

let sinks = run(widgetMain, sources, { DOM: '.widget-5', router: 'widget5' });
// or
let sinks = run(widgetMain, sources, 'widget5'); // each driver could adapt that string its own way
@benoneal
benoneal commented Sep 1, 2015

+1 on this. Scoping is a major concern of mine with Cycle, and namespacing is, imo, never a solution to anything at scale. Would love to see something like this baked into cycle.

@TylorS
Member
TylorS commented Sep 1, 2015

For what it's worth, I would also love to see this in cycle as well. Whether it's within core itself or something built on top.

@erykpiast
Contributor

I like it because it seems to make applications/components more composable. So far I see it more as feature of single driver than core mechanism, but I will be glad it we find out some general pattern in that.

@axefrog
axefrog commented Sep 1, 2015

Actually it's looking like applying scope constraints may only be half the picture. Currently in my application there is a composability problem whereby a child component may be transient but, as is the norm in the dialogue pattern, able to expose multiple sinks/request observables (one per driver). If you run the child's main function and get back such an object, you then need to deal with each sink by merging them downstream until there is just one per driver which is finally returned back to cycle's run function. This is fine on the surface, and not hard to handle normally, but if there is ever a case where a component needs to be constructed in a latebound fashion, then you have to think about the lifespan of each of the observables it exposed. Currently there isn't really (as far as I can tell) a good way for cleaning up anything other than the vtree. The vtree is disposed/unsubscribed when it is dropped from the DOM, but there may be other leftover observables that the component created which are still subscribed to somewhere in an observable chain by way of combineLatest, flatMap, mergeAll or whatever. For the case of combineLatest, you should be assuming singleton with a full application lifespan of course, or you'll get memory leaks, but not so much with flatMap and mergeAll.

So what I'm seeing is perhaps this new overloaded run function not only providing a mechanism for constraining scope, but for cleanly identifying the boundaries between the observables of the parent and its children (individually speaking). I haven't quite worked out exactly what the implementation for this might look like yet, but a prototype approach I'm working on currently is as follows.

  1. The whole point of Cycle is to rethink user interface implementation in terms of reactive streams.
  2. Therefore the point of components is to separate areas of a user interface into distinct modules, some of which may be singletons, and some of which will be transient, with potentially many instances existing simultaneously. While some may be long lived (possibly even still running when not visible), some will be shortlived as well, and require proper cleanup when no longer required.
  3. We can generally assume that when a component's vtree$, having been a part of the DOM, is subsequently disposed/unsubscribed, that it has been removed from the DOM. Removal from the DOM usually indicates an end of lifecycle, which means that all of the component's resources (its other sink observables) should be cleaned up too.

When the run function is called in order to explicitly bring a child component to life, we can do all kinds of tricky things in the layer between the parent and child (i.e. inside the run function), including:

  • swapping in new driver proxies/instances constructed with child/scope context in mind (as per the example code in my gist above)
  • retaining some kind of state reference identifying this as a unique child component instance
  • mapping over the top of the request observables returned by the child in order to transform emitted data with respect to the scope context with which the component was constructed (e.g. modifying route declarations to make them children of one of the parent's routes
  • wrapping the returned request observables with a custom observable that is component aware, allowing it to internally disconnect/unsubscribe if its sibling DOM observable is disposed of, thus signalling end-of-lifecycle for the component.

The above sounds pretty doable to me, and possible to implement in a very clean way - i.e. all of this would be wrapped up in a run function, as per my examples above.

Unknowns:

  • Will we ever want to keep a component's other sinks alive even after the vtree has observable has been disposed?
  • Is it safe to assume that the existence of the vtree in the DOM is the sole arbiter of the component's lifecycle? If so, how best to identify that driver as the DOM driver? What if someone wrote their own in-house DOM management driver and wanted to use it instead? How would I know that that driver is the arbiter of the child's lifecycle?
  • Is there anything I'm neglecting to consider?
@staltz
Member
staltz commented Sep 1, 2015
  • swapping in new driver proxies/instances constructed with child/scope context in mind (as per the example code in my gist above)
  • retaining some kind of state reference identifying this as a unique child component instance
  • mapping over the top of the request observables returned by the child in order to transform emitted data with respect to the scope context with which the component was constructed (e.g. modifying route declarations to make them children of one of the parent's routes
  • wrapping the returned request observables with a custom observable that is component aware, allowing it to internally disconnect/unsubscribe if its sibling DOM observable is disposed of, thus signalling end-of-lifecycle for the component.

Good ideas.
You are a bit ahead of me with these ideas, keep it flowing and document your findings. When I have some time for this feature I'll give more meaningful inputs.

@axefrog
axefrog commented Sep 1, 2015

Will do.

@erykpiast
Contributor

I'm really excited what is happening now, compact interface for general use components are what I'm missing in Cycle. We should definitely discuss it, all issues and potential solutions.

Regarding those:

  • Will we ever want to keep a component's other sinks alive even after the vtree has observable has been disposed?
  • Is it safe to assume that the existence of the vtree in the DOM is the sole arbiter of the component's lifecycle? If so, how best to identify that driver as the DOM driver? What if someone wrote their own in-house DOM management driver and wanted to use it instead? How would I know that that driver is the arbiter of the child's lifecycle?

I'm totally against privileging some drivers to "arbiter" role. Managing component lifecycle should be responsibility of application programmer, not a framework. I see popular libraries too tightly coupled to DOM, it makes them less universal, it's almost impossible to use them with different forms of presentation. And Observables have really well-defined lifecycle - onCompleted signal means exactly that stream should be considered as "dried". That's all. I'm not sure how it could be implemented... Do we need something like "lifecycle driver"? Should the component inform world about its death? Or should parents kill their children? What fits better to reactive concept of Cycle? Those are tough and gloomy topics, I know, however questions have to be answered.

@axefrog
axefrog commented Sep 1, 2015

Do we need something like "lifecycle driver"

One step ahead of you. Not sure it needs to be a driver, but a standard pattern might be the ticket. I think I've got a solution hatching for it, but it's requiring me to put together an example "big app" application that demonstrates these patterns. Once I get a proof of concept working, I'll put it up on github for testing and discussion.

@erykpiast
Contributor

Nice!

@Frikki
Member
Frikki commented Sep 1, 2015

Namespacing dialogues quickly becomes a nightmare. This is true when you need to place several dialogues of the same type in/on the same view/page. You need to come up with namespace after namespace. Definitely not a good solution IMO.

@staltz
Member
staltz commented Sep 2, 2015

Namespacing dialogues quickly becomes a nightmare. This is true when you need to place several dialogues of the same type in/on the same view/page. You need to come up with namespace after namespace. Definitely not a good solution IMO.

Thanks for telling, I'll consider your experience when I look for alternatives/solutions.

@Frikki
Member
Frikki commented Sep 2, 2015

@staltz Well, it is not worse than the standard document.querySelector() which will also, by some means, "force" us to scope elements by some kind of unique identifier.

@staltz
Member
staltz commented Sep 3, 2015

@Frikki I wrote a blog post that addresses your namespacing concerns: http://staltz.com/random-namespacing-in-cyclejs.html

@axefrog axefrog referenced this issue Sep 5, 2015
Closed

Router Drivers #159

@benoneal
benoneal commented Sep 7, 2015

Namespacing isn't a solution, period. It's a bandaid at best, and a real pain in the ass to work with most of the time. Appending random strings isn't a solution, it just decreases the probability of collisions, but at the cost of usability because now you can't access any data from children (as you mentioned in your blog post) unless you give up the random string, and now you're back at square one: trying to name all dialogues uniquely and praying nothing ever collides.

The solution is scoping. Scoping solves all those issues and allows you to use data from nested children (which is extremely common, like with, say, every online form ever).

@staltz
Member
staltz commented Sep 7, 2015

Yes

@Frikki
Member
Frikki commented Sep 7, 2015

@benoneal "The solution is scoping"

Namespacing is scoping; whether it’s done manually or automatically. A scope is, ultimately, identified by some sort of token(s); e.g., class, function and variable scopes, etc.

If we keep the focus on the DOM in relation to a component / (nested) dialogue, the scope of the dialogue must be associated with a DOM node (whether using a virtual DOM or not). This requires a token on the DOM node, i.e., a unique DOM node attribute value, e.g., a class name or custom attribute. At least, I have yet to see a different way.

@benoneal
benoneal commented Sep 8, 2015

Sure, technically that's exactly what namespacing is. But it's the "manual" side that's a problem, since that's where all the pitfalls are, and where it increases the burden on the developer for something that really should just be a baked in feature. That's why javascript (and all other languages) have scoping baked in to how they work. Could you imagine the nightmare if every javascript variable ever defined in any block was global, and needed to be namespaced to the method or class that uses it?

But somehow that's ok when we're talking about the DOM and browser events all of a sudden?

No. No it is not.

Whether proper automatic scoping requires the insertion of tokens in the DOM or not is an implementation detail that shouldn't be exposed to developers.

@Frikki
Member
Frikki commented Sep 8, 2015

@benoneal Whether proper automatic scoping requires the insertion of tokens in the DOM or not is an implementation detail that shouldn't be exposed to developers.

I agree that is preferable. The discussion is more how this should be done and less about exposure to users of Cycle.js.

@staltz
Member
staltz commented Sep 8, 2015

@benoneal we agree with you, so apparently there isn't anything to discuss. This feature is on my list as one of the first to solve.

That said, "manual" care against pitfalls is a common practice in JS development. Take the Holy Grail of Flux for instance. In Redux, you better be sure your action reducers on the store are immutable operations, to keep all the nice properties for dev tools. It's actually quite easy to let slip an array push or an object mutation without noticing. And so are many other things in JS because it's not a typed language.

@benoneal
benoneal commented Sep 8, 2015

Yeah no worries, I just get passionate about stuff. My big three with Cycle are scoping, re-usable composable components, and lifecycle hooks. It's good to see development progress on all three fronts. Cycle is a really promising approach.

And don't get me started on the cancer that is Flux. >.<

@Frikki
Member
Frikki commented Sep 8, 2015

@staltz Do we need tokens be set on the actual DOM? (I am blind to other ways). If so, I played with custom attribute, e.g., data-cycleid, but for query selectors, this has really bad performance. Best performance is achieved by querying for element id or a single class name. However, I feel that if Cycle.js were to add these behind the scenes, it could very likely lead to unpleasant surprises for developers.

@staltz
Member
staltz commented Sep 8, 2015

@Frikki I'm intending to add them as className. Usually there are multiple classNames so I can't see how that would be a major problem. But you're right, I need to give thought to the alternative: attach event handlers on the VTree before they reach the actual DOM. Bear in mind I'm planning to do this as DOM.scope() so we are talking about DOM Driver mainly. For other drivers, such as HTTP, we need to see what HTTP.scope() would do there.

@Frikki
Member
Frikki commented Sep 8, 2015

@staltz Yes, of course, we are talking primarily DOM scope here.

Usually there are multiple classNames so I can't see how that would be a major problem.

Maybe not a major problem, but if the developers expect the class names to be only those they have explicitly set themselves, it could lead to bugs. For example, the developers could expect an empty class names string in the case they haven’t set any. However, this could be accommodated for in documentation of how the DOM driver works.

@staltz
Member
staltz commented Sep 8, 2015

Probably relevant to the VTree event handler attachment: https://www.npmjs.com/package/vtree-select

@TylorS
Member
TylorS commented Sep 9, 2015

I had an idea to which may be of interest to this topic.

Since Cycle.run(main, drivers) returns an array of requests and responses, maybe (perhaps with tweaking) it could be used as a mechanism for scoping. Every piece would have no need to know what is going on above it. A parent would communicate with the childs requests, and return the parent information by way of it's responses.

Heres a quick psuedo-sample.

//app.js
import Cycle from '@cycle/core';
import {h, makeDOMDriver} from '@cycle/dom';
import makeRouterDriver from 'cycle-router';
import myDialogue from './my-dialogue';

function main({DOM, Router}) {
  let dialogue = myDialogue(DOM.scope(), Router.scope());
  let currentRoute$ = Cycle.Rx.Observable.just('/home');
  let view$ = Cycle.Rx.Observable.just(h('div', {}, [dialogue.responses.DOM]));

  return {
    DOM: view$,
    Router: currentRoute$
  }
}
Cycle.run(main, {DOM: makeDomDriver('.app'), Router: makeRouterDriver());

// my-dialogue.js
...
function main(...){...}

export default function(DOM, Router) {
  let drivers = {
    DOM: DOM,
    Router: Router
  };
  let [requests, responses] = Cycle.run(main, drivers);
  return {requests, responses};
}

I would envision each driver's scope() taking whatever information it would find useful. Maybe DOM would take a new container to render it's vtree to remove the need to explicity place dialogue.responses.DOM in the parents vtree?

Would there be any serious drawbacks to this approach?

@Frikki
Member
Frikki commented Sep 9, 2015

@TylorS A parent would communicate with the childs requests, and return the parent information by way of it's responses.

I don’t understand what you mean by "parent information".

@TylorS
Member
TylorS commented Sep 9, 2015

Yeah that wasn't very well written. What I mean is as the (nested) dialogue sends it's responses to it's parent, the parent would send back props to the child via requests.

Does that make more sense?

@Frikki
Member
Frikki commented Sep 9, 2015

Yes, but that’s how things are working already.

@HighOnDrive

Its good then that the proposed idea does not conflict with the standard way that props are sent. The main thing I see here is the way routes would orchestrate dialogues and provide a scope for them.

@TylorS
Member
TylorS commented Sep 9, 2015

To elaborate, DOM.scope(.container) would basically be a new instance of makeDOMDriver(.container) adding scope to the DOM. DOM.select(':root') would now be from .container. All DOM.select()s would be relative to this new root container.

@TylorS
Member
TylorS commented Sep 9, 2015

Also there would be need for communication into this new world, so maybe there could be a generic driver to handle this. Maybe something like this:

//dialogue.js
export default function(DOM, Router, ...props) {
  let drivers = {
    DOM: DOM,
    Router: Router,
    Props: makePropsDriver(props)
  };
  let [requests, responses] = Cycle.run(main, drivers);
  return {requests, responses};
}

//propsDriver.js

function makePropsDriver(props) {
  return function(){
    return Rx.Observable.combineLatest(...props, (...props) => {
      return {...props}
    });
  }
}

This would provide a way for props to get into this self-centered world. And whatever is returned from your dialogues main function should be viewable from the parent.

@TylorS
Member
TylorS commented Sep 9, 2015

After some further thought you could just add to the dialogue's responses. Continuing with the example, dialogue.responses.Props = {observableOne, observableTwo}

@staltz
Member
staltz commented Sep 10, 2015

My early ideas and drafts on implementing this:

function newScope(dialogue) {
  return function scopedDialogue(sources) {
    const thisScope = ++counter;
    const scopedSources = {};
    for (let key in sources) {
      if (sources.hasOwnProperty(key)) {
        if (typeof sources[key].scope === 'function') {
          scopedSources[key] = sources[key].scope(thisScope);
        } else {
          scopedSources[key] = sources[key];
        }
      }
    }
    const sinks = dialogue(scopedSources);
    const scopedSinks = {};
    for (let key in sinks) {
      if (sinks.hasOwnProperty(key)) {
        scopedSinks[key] = sinks[key].map(ev => {
          if (ev !== null && typeof ev === 'object') {
            ev._cycleScope = thisScope;
            return ev;
          } else {
            return ev;
          }
        });
      }
    }
    return scopedSinks;
  }
}

Usage:

  let weightSlider = newScope(labeledSlider)({DOM, props$: weightProps$});
  let heightSlider = newScope(labeledSlider)({DOM, props$: heightProps$});
@TylorS
Member
TylorS commented Sep 10, 2015

How would you picture a driver handling thisScope?

@staltz
Member
staltz commented Sep 10, 2015

I think mainly of DOM Driver and HTTP Driver.

DOM Driver would return a clone of itself which tries to hook up event handlers on the VTree that has thisScope under _cycleScope. And for HTTP Driver we would need to change its response type, which is currently a higher-order Observable. Should be a collection of Observables like in DOM Driver response.

@TylorS
Member
TylorS commented Sep 10, 2015

So basically in the case of the DOM driver, it would create keys for the different sections of the VTree. The HTTP Driver would be able to make request per scope, and have it given back to the proper handlers by making use of thisScope, which I'm assuming is a number.

Do I have that correct?

@TylorS
Member
TylorS commented Sep 10, 2015

If I am correct about, thisScope, being a number, this wouldn't be as helpful for a router driver. A router driver would most likely scope routes by using a string as a prefix to the dialogue's own routes. How would you propose handling something like that?

@staltz
Member
staltz commented Sep 10, 2015

I don't know yet. That was just a draft above, so I don't forget it, and also for early feedback. Suggestions on the name are welcomed as well

@TylorS
Member
TylorS commented Sep 10, 2015

Fair enough, just want everything to work for everyone of course 👍. I'm curious how you feel about what I proposed last night (my time anyways)? Running each dialogue with run() and it's own drivers.

@staltz
Member
staltz commented Sep 10, 2015

Well it would add a lot of boilerplate because run() is made for a pure function (the main dialogue) to communicate with a group of side effecting functions. In the case of nested dialogues, both parent and child are pure, they do not need drivers just yet, and running drivers at that point would make the parent not pure. I just think run() with drivers is a different aspect than scoping.

@TylorS
Member
TylorS commented Sep 10, 2015

What do you mean by pure? I don't think I understand the term in this context

@TylorS
Member
TylorS commented Sep 10, 2015

So basically by creating new drivers for the dialogue the parent no longer has a transparent view of the dialogue, rendering it unpure?

@Frikki
Member
Frikki commented Sep 10, 2015

I think, a driver has an observable interaction with calling functions or the outside world, so it has side effects, which makes it impure. But then, I am no expert in functional programming or drivers.

@axefrog
axefrog commented Sep 10, 2015

I think whatever the solution is, it needs to take into account that different drivers have different scoping needs. What @staltz proposed above may be fine for the DOM driver. @TylorS makes a valid point about the router driver having differing requirements. My implementation currently is a simple run function that calls enterScope on each driver to create a scoped version, kind of like @staltz proposes above, and exitScope on the driver-specific result returned by the dialogue's main function. In general, the differing requirements can be encapsulated by passing an optional context object for each enterScope call, and then using that upon exit to the automatically apply any mapping wrappers to returned observables. Because it's all opt-in, i.e. a driver doesn't have to implement enter/exitScope, and properties on both the entry and exit paths are always preserved if there's not some scope-related transformation explicitly waiting to be applied, then it ends up being quite clean overall.

With respect to the run function, my suggestion was only that it be an overload, thus preserving the idiom whereby every child dialogue is activated by calling run, but there's no particular reason why it can't be its own function, like @staltz is suggesting. As I'd mentioned to @staltz, I've had to pause my own efforts while I finish off some client work, but after that's done, I'll be back and able to contribute more meaningfully.

@staltz
Member
staltz commented Sep 12, 2015

I think this will be my course of action:

  • Figure out the perfect route driver (#159). Until now I haven't tried to make a routes driver for Cycle.js because I've had a ton of other stuff in mind and no time to think about routes. But since it's blocker for automatic scoping, as @axefrog said, we need to have routes driver in place before. I know there are some route drivers already out there, but I'm not confident of their APIs (e.g. subscribing inside the main, or depend on a manifest external to main means business logic outside main, and I believe drivers should never handle business logic directly).
  • Experiment with different automatic scoping solutions.

For now, we can survive with manual scoping or random namespacing.

@TylorS
Member
TylorS commented Sep 12, 2015

@staltz Please look at the newest version of cycle-router. It just got a large API rework.

@staltz
Member
staltz commented Sep 12, 2015

I did, just a few minutes ago

@TylorS
Member
TylorS commented Sep 12, 2015

What issues do you still see facing it?

@staltz
Member
staltz commented Sep 12, 2015

Mentioned above:

depend on a manifest external to main means business logic outside main, and I believe drivers should never handle business logic directly

@Frikki
Member
Frikki commented Sep 12, 2015

Yes, the cuid approach to semi-automatic scoping works very well for DOM.

@staltz
Member
staltz commented Sep 16, 2015

Yay I'm figuring out automatic scoping for the DOM Driver. I'm starting to draft the implementation, mainly focused on the DOM Driver.

This is the main idea:

  // Preprocess
  let DOMResponseW = DOM.select('.w');
  let DOMResponseH = DOM.select('.h');
  // Call it
  let weightSlider = labeledSlider({DOM: DOMResponseW, props$: weightProps$});
  let heightSlider = labeledSlider({DOM: DOMResponseH, props$: heightProps$});
  // Postprocess
  let DOMRequestW = weightSlider.DOM.map(vtree => {
    vtree.properties.className += ' w';
    return vtree;
  });
  let DOMRequestH = heightSlider.DOM.map(vtree => {
    vtree.properties.className += ' h';
    return vtree;
  });

These pre and post processing steps will be done by, respectively, DOM.confineSource(scope) and DOM.confineSink(scope) automatically when you call

let weightSlider = confine(labeledSlider)({DOM, props$: weightProps$});

I'm still thinking about the naming, so please suggest if you have good names in mind. Alternative to confine would be newScope. Somehow it would be good to indicate through the name that it's not a referentially transparent transformation, because the weightSlider and heightSlider here are different even though we gave the same arguments:

let weightSlider = confine(labeledSlider)({DOM});
let heightSlider = confine(labeledSlider)({DOM});

Experimental implementation coming to the DOM Driver as a branch.

@TylorS
Member
TylorS commented Sep 16, 2015

It would be a missed chance not to name it unicycle(). In all seriousness though, I think newScope() is the most straightforward 👍

@Frikki
Member
Frikki commented Sep 17, 2015

I don’t see why "new" should be in the name. What does the method do? Does it "scope", as in look at, read, investigate, as in order to evaluate or appreciate? Does it confine, as in to enclose within bounds, limit, restrict?

Methods/functions are verbs. New is not a verb, but an adjective. Scope is a noun, except in slang (as above).

For scope, the method should probably be something along makeScopeFor(component).
For confine, the method should probably be confine(component).

Again, the method name should scream of intent to clearly communicate what it does.

@HighOnDrive

Scope can also be a verb, according to the Learner's Dictionary link below. I think it should be the one singular word "scope". That is what the function does, scope the component into a scope, it's an action then. The action that produces, constrains or confines the component into a status or holding pattern that is also referred to as a scope.

http://www.learnersdictionary.com/definition/scope
http://dictionary.reference.com/browse/scope

@Frikki
Member
Frikki commented Sep 17, 2015

@HighOnDrive As I mentioned, scope as a verb is slang. Meriam-Webster has it only listed as a noun. Dictionary.com as a slang verb. British Dictionary only as a noun. Oxford American-English Dictionary only as a noun. Oxford Advanced Learner’s Dictionary also as a verb in the combination with out, as in scope something out (which is slang), e.g., a project. Meriam-Webster’s Learner’s Dictionary (to which you linked) also only has scope as a verb only in combination with out, and it is American-English informal.

If we should settle on scope as a verb (though it is slang and informal), the method name would be scopeOut(component).

@staltz
Member
staltz commented Sep 17, 2015

In the gitter chat we discussed this and I'm happy with the name

var confinedDialogue = confineToNewScope(dialogue);
@staltz
Member
staltz commented Sep 20, 2015

Another possible name for confineToNewScope: a constructor which makes the use of new explicit to highlight the non-referential transparency.

  let WeightSlider = new Scope(labeledSlider);
  let HeightSlider = new Scope(labeledSlider);

  let weightSlider = WeightSlider({DOM, props$: weightProps$});
  let heightSlider = HeightSlider({DOM, props$: heightProps$});
@Frikki
Member
Frikki commented Sep 20, 2015

There’s nothing that distinguishes constructor functions from normal functions (besides capitalization convention). If a consumer forgets the new operator, any use of this inside the function will pollute the global namespace. You’d have to make checks to avoid this, e.g.:

function Scope(dialogue) {
  if (!this instanceof Scope) {
    return new Scope(dialogue);
  }
}

Also, it is a rather non-functional approach.

@TylorS
Member
TylorS commented Sep 20, 2015

Yeah I think it'd be nicer to stay away from the new keyword.

@Frikki
Member
Frikki commented Sep 20, 2015

The only reason I can think of new to be good, is when dealing with prototypes that can share memory. But then it is also prone to mutations affecting all instances. ES2015 classes are different, but again, its very OO; not FP.

@TylorS
Member
TylorS commented Sep 20, 2015

I like the previous approach more, I also personally prefer it because it could be, correct me if I'm wrong, used as a decorator.

@confineToNewScope
function myDialogue(responses) {
  ...
  return {...}
}
@staltz
Member
staltz commented Sep 20, 2015

Also, it is a rather non-functional approach.

So is confineToNewScope because it's not referentially transparent. It's not an FP approach at all.

About the decorator: they need to be attached to methods of a class, or at least to functions in an object. That's not our case, and even if it would be, I don't like decorators, they look too much like magic.

For now confineToNewScope is the strongest candidate. I'm just looking for alternatives because I have a gut feeling we can do better.

@staltz
Member
staltz commented Sep 20, 2015

Putting naming aside, I think I have most pieces of the puzzle figured out. Work will proceed like this: DOM Driver getting support first, then HTTP Driver, then Cycle Core will get confineToNewScope, then a routing solution with support for scopes.

@TylorS
Member
TylorS commented Sep 20, 2015

@staltz Not that I don't believe you, could you point me in the direction where you got that info on decorators?

@staltz
Member
staltz commented Sep 20, 2015

wycats/javascript-decorators#31

It doesn't work in Babel.js sandbox, try it: http://babeljs.io/repl/

@staltz
Member
staltz commented Sep 20, 2015

About naming, I'll just list the alternatives:

  • confineToNewScope(dialogue)
  • encapsulate(dialogue)
  • new Scope(dialogue)
@Frikki
Member
Frikki commented Sep 20, 2015

I’ll go on:

  • bindToNewScope(dialogue)
  • bindToNewPurview(dialogue)
  • attachToNewScope(dialogue)
  • attachToNewPurview(dialogue)
  • encompass(dialogue)
  • enclose(dialogue)
  • restrain(dialogue)
  • curb(dialogue)
@TylorS
Member
TylorS commented Sep 20, 2015

Ahh I guess I'm just used to Python decorators. Thanks for the link @staltz

On Sun, Sep 20, 2015, 12:34 PM Frederik Krautwald notifications@github.com
wrote:

I’ll go on:

  • bindToNewScope(dialogue)
  • bindToNewPurview(dialogue)
  • attachToNewScope(dialogue)
  • attachToNewPurview(dialogue)
  • encompass(dialogue)
  • enclose(dialogue)
  • restrain(dialogue)
  • curb(dialogue)


Reply to this email directly or view it on GitHub
#167 (comment).

@HighOnDrive

I like encapsulate(dialogue) but then why not go with wrap(dialogue). Wrap is a meaningful and well understood short word. So dialogues would then be wrapped in their own scope.

@erykpiast
Contributor

👍 for encapsulate and new Scope (the second one if we are OK with OO). confine is a nice word, but I guess not widely adapted in not-native-english programmer dictionary.

@HighOnDrive

Since so much of this naming has to do with appealing to and tipping the hat to the computer science world, so that Cycle can look sexy in a nerdy way instead of in a trend setting way, maybe it is a plus to appeal to the OOP world with new Scope.

@staltz
Member
staltz commented Sep 20, 2015

My problem with encapsulate:

Does not make it obvious that it's not referentially transparent. For instance, here is what a beginner might think: "Why call encapsulate twice here if I could reuse it?"

  let WeightSlider = encapsulate(labeledSlider);
  let HeightSlider = encapsulate(labeledSlider);

  let weightSlider = WeightSlider({DOM, props$: weightProps$});
  let heightSlider = HeightSlider({DOM, props$: heightProps$});

"This is better:"

  let WrappedSlider = encapsulate(labeledSlider);

  let weightSlider = WrappedSlider({DOM, props$: weightProps$});
  let heightSlider = WrappedSlider({DOM, props$: heightProps$});

But wrong, this above has the same problems because weightSlider and heightSlider share the same scope.


My problem with confineToNewScope: rather verbose, not sexy, and as Eryk said, confine isn't a common word for a non-native English speaker. We could consider alternatives like limitToNewScope, but that doesn't solve the verbosity.


My problem with new Scope: it looks like it returns an instance of Scope class, i.e., a "scope". Which is not true at all. It returns the given dialogue, wrapped in its own scope.

@staltz
Member
staltz commented Sep 20, 2015

Of course the problem with lack of referential transparency stems from the fact that encapsulate internally creates a new scope. We can make that explicit with an argument scope:

// encapsulate(dialogue, scope)
let safeComponent = encapsulate(component, 'foo'); 

Then we need to decide whether to always require the developer to explicitly call randomScope() or cuid() to make the scope, or we default to such so it's invisible to the developer

let safeComponent = encapsulate(component, cuid());
// equivalent to
let safeComponent = encapsulate(component);
@Frikki
Member
Frikki commented Sep 20, 2015
  • encapsulate: to place in or as if in a capsule (a small case, envelope, or covering).
  • wrap: to surround, envelop, shroud, or hide.
  • confine: to enclose within bounds; limit or restrict.
  • encompass: to enclose; envelop.
  • enclose: to surround, as with a fence or wall.
  • restrain: to strain again (to exert to the utmost).
  • curb: to control as with a curb; restrain; check.

@erykpiast confine will be understood in languages that derive mainly from Latin.

@Frikki
Member
Frikki commented Sep 20, 2015

Also, programming languages are mainly in English, so I don’t see a problem with using words understood in English. Of course, we may want to strive towards the simpler and broadly understood.

@TylorS
Member
TylorS commented Sep 20, 2015

isolate() / isolateScope()
insulate() / insulateScope()
fork() / forkScope()
branch() / branchScope()

Just some ideas

@staltz
Member
staltz commented Sep 20, 2015

Isolate! I like that more than all others :)

@Frikki
Member
Frikki commented Sep 20, 2015

@staltz I follow your reasoning (except I prefer understandable above sexy).

isolateNew(dialogue)

Reads very well. And shows that it is new.

@HighOnDrive

Some nice words but I'm still on for scope or wrap. Isolate is a new word but IMO a darker word to introduce into a forward trending framework like Cycle.

@TylorS
Member
TylorS commented Sep 20, 2015

I like isolate the most too @staltz

@Frikki
Member
Frikki commented Sep 20, 2015
  • isolate: to set or place apart; detach or separate so as to be alone.
@staltz
Member
staltz commented Sep 20, 2015

Just for the lulz:

  • "scopify"
@erykpiast
Contributor

isolate is straightforward for me.

@staltz, scopify would be nice as Browserify transform that detects when new scope is needed and adds it automagically. I like it! :D

@TylorS
Member
TylorS commented Sep 20, 2015

Insulate may have a better definition for the process.

Insulate: to cover, line, or separate with a material (scope) that prevents or reduces the passage, transfer, or leakage of heat, electricity, or sound

@TylorS
Member
TylorS commented Sep 20, 2015

Better definition from Merriam-Webster

Insulate : to prevent (someone or something) from dealing with or experiencing something : to keep (someone or something) separate from something unpleasant, dangerous, etc.

@staltz
Member
staltz commented Sep 20, 2015

Interesting. That's precisely what we are talking about.

insulate(component, scope = createRandomScope())

Would be the signature. I'm starting to like it.

@HighOnDrive

Insulate is a self descriptive word full of the meaning behind it's intent. It also sounds warmer and implies that the action is a positive thing. I can dig it 👍

@TylorS
Member
TylorS commented Sep 20, 2015

Other similar words

Restrain : to keep (something) under control
Restrict : to limit the amount or range of (something)

@Frikki
Member
Frikki commented Sep 20, 2015

segregate(component, scope = createRandomScope())

  • segregate: to part from the flock.
@teemualap

"Isolation" is what they did with Angular directives' scoping. It should feel familiar at least to those with an Angular background (a plenty). But, insulation does really sound warmer :)

@Frikki
Member
Frikki commented Sep 20, 2015

👍 insulate

Because it’s warm and fuzzy, and most importantly, @HighOnDrive dig it, too.

@TylorS
Member
TylorS commented Sep 20, 2015

👍 Insulate

@teemualap

My 👍 for insulate as well.

@HighOnDrive

Hey, lets not just do what everyone else did. But I do believe in looking before you leap √

@staltz
Member
staltz commented Sep 20, 2015

"insulate" is nice, but it's likely to be mistyped as "isolate", and also dictionaries show these two can be synonyms, so let's just go with "isolate" to be newbie-friendly. And +1 that something similar exists in Angular (thanks Teemu for showing this).

@Frikki
Member
Frikki commented Sep 20, 2015
  • insulate: to be made into an island

Isolate is the same word, just younger and mispronounced.

@Frikki
Member
Frikki commented Sep 20, 2015

Another possibility is:

sealOff(component, scope = createRandomScope())
@TylorS
Member
TylorS commented Sep 20, 2015

@staltz Will there be a way to check if the component is in an isolated scope?

function myDialogue(DOM, HTTP) {
  if ( !DOM.isIsolatedScope) {
   return isolate(myDialogue, scope = createRandomScope());
  }
}
@Frikki
Member
Frikki commented Sep 20, 2015

Or for the sake of sexy:

sealOff(component, inJar = createJar())
@HighOnDrive

Ooo, that bites me :-) I now like isolate 👍

@axefrog
axefrog commented Sep 21, 2015

Throwing my hat into the ring, I was using enterScope and exitScope in my prototyping.

@staltz
Member
staltz commented Sep 21, 2015

@TylorS yeah there will be a way to check:

function myDialogue(DOM, HTTP) {
  // DOM.namespace is an array
  if (DOM.namespace.length === 0) {
   return isolate(myDialogue, scope = createRandomScope());
  }
}

But this isn't very reliable. The parent which contains myDialogue might be scoped already, so namespace would not be empty, but that doesn't mean myDialogue is already scoped.

@Cmdv
Contributor
Cmdv commented Dec 7, 2015

Mot sure if someone would be keen to help me or already has an example of a nested routing, that would be superb? :)

@staltz
Member
staltz commented Dec 8, 2015

@Cmdv to be done for cycle-examples :)

@Cmdv
Contributor
Cmdv commented Dec 8, 2015

Thanks @staltz 😄

@staltz
Member
staltz commented Jan 9, 2016

With Cycle Nested, this issue is finally done

@staltz staltz closed this Jan 9, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment