NetLogo / LevelSpace

This is the LevelSpace extension repository. LevelSpace allows you to run NetLogo models |: from inside NetLogo models :|
Other
19 stars 8 forks source link

Decision about LevelSpace CodeBlock syntax #56

Closed arthurhjorth closed 8 years ago

arthurhjorth commented 9 years ago

We need to make some decisions about LevelSpace CodeBlock syntax. Basically the question is how to resolve/evaluate reporters that are ambiguous. Let's make it concrete and say that we want to set a global variable in a child model to a list of xcors from its parent model. The way we would do that now is:

(ls:ask child "set global-var ?" count turtles)

In a CodeBlock I'm less sure how that would work. I talked with @frankduncan about this, and we came up with three different approaches:

ls:ask child [set global-var (ls:eval-in-parent count turtles)] (ls:ask child [set global-var ?] count turtles) ls:ask child [set global-var [count turtles] ls:of-myself]

My preference is the last because it's closest to current NetLogo. My 2nd preference is the middle one, because it works like current tasks do, which makes it NetLogo-like too. My least favorite is the first one, just because the eval-in-parent reporter seems not very NetLogo-like. But it might be easier to implement.

What do you guys think?

frankduncan commented 9 years ago

I want to note that none of these are particularly challenging to implement if the rules are set up rather rigorously. The final is just a matter of bracket counting.

qiemem commented 9 years ago

Middle is easiest to implement, since it's already implemented! We just convert the code blocks to strings and... done!

Another possibility would be local variable injection:

let foo count turtles
ls:ask child [ set global-var foo ]

I think I might prefer ls:of-myself. It has fairly clean semantics, though I was hoping to avoid having to parse and transform the code block itself.

cbradyatnu commented 9 years ago

I like the middle one - that is, (ls:ask child [set global-var ?] count turtles)

My reasoning against [3] is that myself is notoriously a challenge in NetLogo, and it requires an additional construction of the present model as an agent. It would be good to encourage that way of thinking, but this syntactic moment doesn't feel like the place to demand that.

Along a parallel but different line of thinking, [1] seems to pose an even worse problem to me because it not only involves a myself-like construction but it ALSO does so while invoking parent-child in the syntactic place of myself-self.

So, I think that my order of preference would be [2], [3], [1].

Question to @qiemem : Does the local variable injection come "for free"? It seems like a good option to offer, if the cost is low.

frankduncan commented 9 years ago

Now that I've thought about it more, I actually dislike the second one. The way I came to this conclusion was by thinking about more intermediate netlogo code with larger blocks.

(ls:ask child [
  crt ? [
    fd ?
    rt ?
    set shape ?
  ]
]  get-val1 get-val2 get-val3 get-val4)

Then if you wanted to insert something in the middle, you have to line up the args with the question marks, and may get confused. The bug will also be really subtle if you do something out of order, and you also have to do ? matching with the number of arguments. These are all things that get resolved by doing it inline:

(ls:ask child [
  crt [get-val1] ls:of-myself [
    fd [get-val2] ls:of-myself
    rt [get-val3] ls:of-myself
    set shape [get-val4] ls:of-myself
  ]
]
cbradyatnu commented 9 years ago

well... (this is to Frank about his last post) --> here is where I would want the local variable injection idea that @qiemem mentioned. So

let v1 get-val1 let v2 get-val2 let v3 get-val3 let v4 get-val4 (ls:ask child [ crt v2 [ fd v2 rt v3 set shape v4 ] ])

frankduncan commented 9 years ago

Ironically, you have a bug where you accidentally used v2 twice. That's the kind of subtle bug that is annoying to track down, especially with the limited debugging available when executing remote code.

cbradyatnu commented 9 years ago

blah. well, i wouldn't have used such bad variable names except that i had to follow your code example :)

arthurhjorth commented 9 years ago

/\/\/\ Ha!

I can see how it's a neat idea, but I really don't like variable injection. It totally changes how lets work, and I don't want for LevelSpace. At least the ?-semantics are well-established in NetLogo already.

cbradyatnu commented 9 years ago

? @arthurhjorth ?

do you disagree that...

let share-ideas  .1 * ideas 
ask neighbors [ set ideas ideas + share-ideas ]

could be reasonably normative netlogo? i would consider it superior for code early in an intro course, compared to

ask neighbors [ set ideas ideas + [ .1 * ideas ] of myself ]

or, maybe i don't understand the claim about totally changing how lets work

arthurhjorth commented 9 years ago

Maybe I am not either - maybe I had a wrong intuition about it. I think I felt that it would be a frivolous use of let. But maybe not, because I can't see how what you're suggesting isn't the best way to do it.

qiemem commented 9 years ago

One point about the let stuff: The semantics are guaranteed to be a little weird and inconsistent.

I'm still not sure what the right answer is. The problems with arguments are quite as bad as Frank made out since he didn't use ?1 arguments. So his example is actually:

(ls:ask child [
  crt ?1 [
    fd ?2
    rt ?3
    set shape ?4
  ]
]  get-val1 get-val2 get-val3 get-val4)

Now if you want to inject something in the middle, you can just do it. If you want to name your arguments, you can just use lets in the code block:

(ls:ask child [
  let num-turtles ?1
  let dist ?2
  let angle ?3
  let appearance ?4
  crt num-turtles [
    fd dist
    rt angle
    set shape appearance
  ]
]  get-val1 get-val2 get-val3 get-val4)

This is pretty much the same verbosity as with let-injecting:

let num-turtles get-val1
let dist get-val2
let angle get-val3
let appearance get-val4
ls:ask child [
  crt num-turtles [
    fd dist
    rt angle
    set shape appearance
  ]
]

Again, I'm still not sure what's right, but I do like the explicitness of arguments.

qiemem commented 9 years ago

Alright, I have a new proposal. I dislike ls:of-myself because myself is confusing, and you can get into some really weird situations with the proposed implementation of ls:of-myself. (I'll post examples if people are curious)

So, what about ls:of-parent? It always goes to the child's model's parent (myself does not necessarily do this), so doesn't have any of the weird ambiguity of myself. Here's the example we've been using. It's identical to myself in this case:

ls:ask child [
  crt [ get-val1 ] ls:of-parent [
    fd [ get-val1 ] ls:of-parent
    rt [ get-val1 ] ls:of-parent
    set shape [ get-val1 ] ls:of-parent
  ]
]

Things I like:

Things I don't like:

Implementation:

I dislike the proposed AST-preprocessing implementation of the ls:of-myself. The fact that the "myself"-block has to be prior-to the rest of the code block creates some super weird situations (again, I can provide examples). So, instead, I propose adding ls:of-parent to LevelSpace as a normal primitive. This goes against a long held opinion of mine, but I think I was wrong. @arthurhjorth is probably going to ask "what about the class loader?" at this point. That's actually pretty easy to get around I believe with a touch of reflection and a callback.

We can inject LevelSpace into models that don't use it normally so that they can use this procedure. Regarding whether or not this is possible, I quote @mrerrormessage from gitter:

I don't know the exact procedure, but it can probably fool around with the extension manager if it can't, we should consider making it possible to do that

qiemem commented 9 years ago

Hmm, just realized a tricky part of the ls:of-parent. In the simplest implementation, it loses context, so you can't reference local variables. You can only use reporters/globals.

However, I think we can get around this without too much trouble. The parent can give the child its context to use when the child runs ls:of-parent. So, pseudo code for ls:ask:

function askChild(child, command, context) {
  oldContext = child.getParentContext() // There may be a parent context already in deeply nested things
  child.setParentContext(context)
  child.runCommand(command)
  child.setParentContext(oldContext)
}

and ls:of-parent:

function ofParent(reporter) {
  parent.runCommand(reporter, getParentContext())
}

I guess context is an optional argument of runCommand. Obviously, this is very pseudo-y code and doesn't deal with the reflection shenanigans you have to do because classloader.

arthurhjorth commented 9 years ago

If we can run a reporter in the parent, could we run commands in the parent too?

qiemem commented 9 years ago

Um, I guess. Anyway, opinions on the reporter syntax?

arthurhjorth commented 9 years ago

I'm not sure I understand this part: "Hmm, just realized a tricky part of the ls:of-parent. In the simplest implementation, it loses context, so you can't reference local variables. You can only use reporters/globals."

Can you give an example of what you mean? Can we not reference local variables in the child model that runs ls:of-parent, or can we not reference local variables in the parent? The former seems like a problem, but the latter seems like the way it should be. At least the way I'm thinking about it right now.

qiemem commented 9 years ago

I figured out a pretty obvious way to get the benefits let-injection without possible collisions. If we add symbol-support to the NetLogo compiler (which I just did...), we can add ls:let, which just creates bindings to inject into ls:ask and ls:of. So:

ls:let num-turtles get-val1
ls:let dist get-val2
ls:let angle get-val3
ls:let appearance get-val4
ls:ask child [
  crt num-turtles [
    fd dist
    rt angle
    set shape appearance
  ]
]

I've just implemented this and it seems to work pretty well. ls:let actually stores the bindings in the Context, just like let, so it's lexically scoped, just like let. Thus, your ls:let bindings don't leak. They inject by just sticking more arguments onto the task. Super simple. I've implemented it for ls:ask so far.

SethTisue commented 9 years ago

I like it

arthurhjorth commented 9 years ago

I really, really like it too! It's (arguably) slightly more verbose than the ls:of-myself alternative, but it reads so much better! Awesome job! A few questions:

Do you have to have declared the num-turtles or angle, etc. first? or can you give it the name of a new variable that has never been initialized or mentioned before?

Maybe you answered this in CSy terms when you talked about leaking (which I didn't quite understand), but what if people name their lets the same as a variable in the other model?

Arthur Hjorth PhD Student, Learning Sciences Center for Connected Learning http://ccl.northwestern.edu/ Northwestern University

e: arthur.hjorth@u.northwestern.edu s: arthurhjorth m: +1 (773) 943-0013

On Sun, Oct 11, 2015 at 6:35 AM Seth Tisue notifications@github.com wrote:

I like it

— Reply to this email directly or view it on GitHub https://github.com/NetLogo/LevelSpace/issues/56#issuecomment-147184702.

cbradyatnu commented 9 years ago

Bryan - This is awesome What variable types are allowed? Did you just also enable agents to be local ls:ask variables? That is, does this work?

repeat 10 [

 let /*childmodel*/ 0
 ls:let *aturtle* one-of turtles

 ls:ask /*childmodel*/ [
    crt 10 [
        set shape [shape] of *aturtle*
     ]
 ]

 ls:ask /*childmodel*/ [
     crt 5 [
         setxy [xcor] of *aturtle* [ycor] of *aturtle*
     ]
 ]

]

(i do the 2 ls:asks in case that's related to arthur's question about the scope the ls:let has the same scope (in terms of the surrounding netlogo model) as any other let, if i undertand you right.)

-c

On 10/11/15 12:33 AM, Bryan Head wrote:

I figured out a pretty obvious way to get the benefits let-injection without possible collisions. If we add symbol-support to the NetLogo compiler (which I just did...), we can add |ls:let|, which just creates bindings to inject into |ls:ask| and |ls:of|. So:

|ls:let num-turtles get-val1 ls:let dist get-val2 ls:let angle get-val3 ls:let appearance get-val4 ls:ask child [ crt num-turtles [ fd dist rt angle set shape appearance ] |

I've just implemented this and it seems to work pretty well. |ls:let| actually stores the bindings in the |Context|, just like |let|, so it's lexically scoped, just like |let|. Thus, your |ls:let| bindings don't leak. They inject by just sticking more arguments onto the task. Super simple. I've implemented it for |ls:ask| so far.

— Reply to this email directly or view it on GitHub https://github.com/NetLogo/LevelSpace/issues/56#issuecomment-147160060.

cbradyatnu commented 9 years ago

Scratch my last email - I didn't read carefully. Bryan, maybe you could simulate the binding that happens from this (i.e., the task that gets sent to the child model) If I understand right, Arthur's concern about collision between ls:let vars and globals in the child doesn't happen because the ls:let variables (num-turtles etc) are replaced by positional arguments before they get to the child model's context for interpretation. Is that right? --c

On 10/11/15 12:33 AM, Bryan Head wrote:

I figured out a pretty obvious way to get the benefits let-injection without possible collisions. If we add symbol-support to the NetLogo compiler (which I just did...), we can add |ls:let|, which just creates bindings to inject into |ls:ask| and |ls:of|. So:

|ls:let num-turtles get-val1 ls:let dist get-val2 ls:let angle get-val3 ls:let appearance get-val4 ls:ask child [ crt num-turtles [ fd dist rt angle set shape appearance ] |

I've just implemented this and it seems to work pretty well. |ls:let| actually stores the bindings in the |Context|, just like |let|, so it's lexically scoped, just like |let|. Thus, your |ls:let| bindings don't leak. They inject by just sticking more arguments onto the task. Super simple. I've implemented it for |ls:ask| so far.

— Reply to this email directly or view it on GitHub https://github.com/NetLogo/LevelSpace/issues/56#issuecomment-147160060.

qiemem commented 9 years ago

Do you have to have declared the num-turtles or angle, etc. first? or can you give it the name of a new variable that has never been initialized or mentioned before?

It must be a new variable that's never been mentioned before in that scope.

So this breaks:

and this breaks:

but this works fine since the first foo's scope ends before the second:

This is exactly like let because ls:let pretty much is let under the hood.

Maybe you answered this in CSy terms when you talked about leaking (which I didn't quite understand), but what if people name their lets the same as a variable in the other model?

It will error with, for instance: There is already a global variable called FOO.

However, Corey's suggestion:

collision between ls:let vars and globals in the child doesn't happen because the ls:let variables (num-turtles etc) are replaced by positional arguments before they get to the child model's context for interpretation.

would work. Right now, this:

gets translated into:

but it would be easy enough to just swap out foos for ?1s instead (though it might make error messages harder to read).

The advantage of ls:let over let injection is that avoids many unintentional collisions. However, I think we should still error on collisions. I can definitely see people sending a variable foo to a model with a global foo and trying to access both in the same block without thinking about it (remember, blocks can be arbitrarily long and complex). With Corey's suggestion, the ls:let foo would mask the global one. If we don't error, they won't necessarily realize anything is wrong. Cool idea though Corey, I hadn't thought of that.

qiemem commented 9 years ago

Should we still allow the argument syntax? e.g. (ls:ask 0 [ crt ? ] 50)

qiemem commented 9 years ago

I realized the prefix with let foo ?1s to inject lets doesn't work with reporters. So I'm just going to substitute foo tokens with ?1 tokens for now. This means that ls:let locals will actually shadow the child model variables with the same name. I don't think this is good for the long term, but it's good enough for now.

qiemem commented 8 years ago

We now do let foo ?1 prefixing as of 0ea7978021574609deacd665a7f7d90a2bd51e56