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

Passing lambdas into child models allow them to execute arbitrary parent model code #133

Open
qiemem opened this issue Jan 26, 2019 · 4 comments

Comments

@qiemem
Copy link
Member

qiemem commented Jan 26, 2019

See https://stackoverflow.com/a/54369364/145080.

That's a pretty awesome use, but it will also break bad when running multiple children simultaneously. Child models, by default, run in parallel, which means there's no way to control what order they will run code in (even with random-seed). Normally, this is fine, since child models can't directly affect each other. But passing procedures in mean order matters, so the results of the above code will be unpredictable, even if you use random-seed. Worse, child models don't synchronize on the same object (otherwise parallelism would be pointless), and global variables are not effectively marked volatile, if two child models run

set foo foo + 1

in the parent, they may both read the un-updated foo before the other hasn't finished its update, resulting in foo being incremented once, not twice.

The other reason why this isn't supposed to work is more philosophical. All inter-level logic is supposed to live in the parent. Allowing any child to talk directly to any other child results in code that is very difficult to reason about and debug.

The safest option here is to just block anonymous procedures from being passed around (as I thought they were). Another option, which addresses the technical concern, but not the philosophical, is to disable parallelism if anonymous procedures are being passed around.

@arthurhjorth
Copy link
Member

Had missed this issue, so closing #135 and copying in my thoughts here:

As we all learned when @LaCuneta wrote his response here, https://stackoverflow.com/questions/54332081/netlogo-levelspace-how-to-pass-strings-between-two-child-models, callbacks are currently possible in LevelSpace.

My use case is LevelSpace child models acting as input to each other. I've made a child model that allows kids to use their mouse to draw a graph. This graph is then returned to the parent model who can then use it as model input (@cbradyatinquire you might like this, so tagging you :) ). I can make it work without a callback, but from a user experience perspective, a callback makes it so much nicer.

image

I know we've talked about this many times, and I know there are potential pitfalls. For modeling purposes, they also break with some Netlogo language convetions. But they are SOOOOOO nice to have for these more advanced use cases with LevelSpace. Can we keep them, @qiemem ? What, if anything, would we need to do to make sure that they don't break a model?

@arthurhjorth
Copy link
Member

I understand the problems relating to parallelism. I wonder if, as we've talked about a few times before, that we should have a way to enable and disable it, probably with it enabled by default. That way, we can let people do whatever they want. (Or more like, then I can keep doing what I want :) )

I honestly really love the ability to code NetLogo models as an interface or visualization of other models. It's just really cool, and useful.

The other reason why this isn't supposed to work is more philosophical. All inter-level logic is supposed to live in the parent. Allowing any child to talk directly to any other child results in code that is very difficult to reason about and debug.

To me, this is the best argument against it. I was using a callback today because I was using a child model as an interface (as seen above) to data in another model, and it was not easy understand what was going on and in which model what was running. Especially because I had some gui threading issues that forced me to distribute code across the child and the parent. When/if we can do LevelSpace model declarations inside the Code Tab that might change, but right now it is quite difficult to write this code.

The safest option here is to just block anonymous procedures from being passed around (as I thought they were). Another option, which addresses the technical concern, but not the philosophical, is to disable parallelism if anonymous procedures are being passed around.

They are in ls:ask but it looks like the syntax for ls:assign is more promiscuous. Just in case we decide to fix this.

@qiemem
Copy link
Member Author

qiemem commented Mar 24, 2019

I honestly really love the ability to code NetLogo models as an interface or visualization of other models. It's just really cool, and useful.

Agreed! Generally I prefer to do this the other way though; the visualization model is the parent model. It's usually easier to reason about. It also keeps the actual model agnostic to how it's visualized, which I also think is good. I confess, however, that callbacks still have use here when you need to track event-based stuff (e.g. births).

Beyond parallelism, there a few semantic issues to think about here:

  • Semantics, in general, work out consistently: everything in the lambda refers to things in the parent model. However, there's no guarantee for this, and that makes me nervous.
  • The previous breaks when the child model, for instance, passes turtles into the lambda as an argument. This leads to weird breakages:
ls:assign 0 func [ t -> ask t [ set child-model-turtle-var 0 ] ]
ls:ask 0 [ (run func turtles) ]

won't compile because the parent model doesn't know what child-model-turtle-var is

ls:assign 0 func [ t -> ask t [ set parent-model-turtle-var 0 ] ]
ls:ask 0 [ (run func turtles) ]

compiles, but breaks at runtime.

If we allowed this, we would have to only allow numbers, strings, nobody, and lists thereof to be passed in as arguments to the lambdas, just as was supposed to be the case between models.

We would also need to ensure that the semantics are, in fact, otherwise consistent, and that they are guaranteed to be so. That means that the only thing that can refer to something in the child model in a lambda are the arguments.

@arthurhjorth
Copy link
Member

arthurhjorth commented Mar 25, 2019

Agreed! Generally I prefer to do this the other way though; the visualization model is the parent model. It's usually easier to reason about. It also keeps the actual model agnostic to how it's visualized, which I also think is good. I confess, however, that callbacks still have use here when you need to track event-based stuff (e.g. births).

Yes, and it also doesn't quiet work in the case where the other model is not just a visualization but an interface can can be used to manipulate parts of the model. In my case, I want data shown in the child model, but those data can be changed, and then sent back to the parent. The only way to do that without a call back is to have a while loop in the parent that constantly checks the child model to see whether it is in a state where the user is "done" with it. Doing that locks the rest of the parent model which is really frustrating for the user because they can't do anything else with the other model in the meanwhile - and more importantly often leaves the user thinking that the model is hanging, causing them to restart NetLogo and losing their work.

Beyond parallelism, there a few semantic issues to think about here:

  • Semantics, in general, work out consistently: everything in the lambda refers to things in the parent model. However, there's no guarantee for this, and that makes me nervous.
  • The previous breaks when the child model, for instance, passes turtles into the lambda as an argument. This leads to weird breakages:

...
If we allowed this, we would have to only allow numbers, strings, nobody, and lists thereof to be passed in as arguments to the lambdas, just as was supposed to be the case between models.

I'm completely fine with doing that, but is it even possible? LevelSpace doesn't really know anything about what is passed into the lambdas during runtime, does it? Would this not require changes... to the core?!?

We would also need to ensure that the semantics are, in fact, otherwise consistent, and that they are guaranteed to be so. That means that the only thing that can refer to something in the child model in a lambda are the arguments.

I'm totally for this, but again, can we do anything to actually ensure this other than encourage users to follow this pattern?

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

2 participants