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

Action rule syntax #54

Closed
jgosmann opened this issue Jun 26, 2017 · 23 comments
Closed

Action rule syntax #54

jgosmann opened this issue Jun 26, 2017 · 23 comments
Assignees
Milestone

Comments

@jgosmann
Copy link
Collaborator

It was noted during the last lab meeting that the current action rule syntax might be misleading with its use of the equals sign. Currently, action rules use this syntax: utility --> sink = source. Please use this issue to suggest alternatives to the --> and = symbols.

@jgosmann
Copy link
Collaborator Author

Give #63 I feel that the usage of the equals sign in more of a problem because the rest of the action rule language ends up being much closer to normal Python. To me a syntax like

  • sink <- source
  • sink <-- source
  • source -> sink
  • source --> sink

seems much better (potentially even allowing both directions of the arrow) as it highlights more the graph structure and direction of information transmition. However, that requires to change the syntax of the basal ganglia utility value (currently using -->) which is very similar (or identical) to this. So far I don't have very many ideas. Potentially a colon : could work, but is more easily overlooked: utility: source -> sink.

Tagging @astoeckel (who initially brought this issue up, I believe) and tagging @tcstewar who's opinion is always welcome in SPA questions.

@tcstewar
Copy link
Collaborator

I really have no idea on this one... :( But I do like the idea of shaking up the syntax a bit. The current --> syntax was mostly stolen from ACT-R, so there's no great reason to keep it....

I wonder if we could also bring in some indenting to help with this? And also to help with complex action rules starting to become unreadable? I'm thinking something like this:

Current approach:

    actions = spa.Actions([
        'dot(vision, WRITE+SAY) --> verb=vision',
        'dot(vision, YES+NO+HI+BYE+OK) --> noun=vision',
        'dot(phrase, VERB*WRITE) - 2*dot(vision, WRITE+SAY+YES+NO+HI+BYE+OK)'
            '--> hand=phrase*~NOUN',
        'dot(phrase, VERB*SAY) - 2*dot(vision, WRITE+SAY+YES+NO+HI+BYE+OK)'
            '--> speech=phrase*~NOUN',
        'phrase = verb*VERB + noun*NOUN',
        ])

Alternate approach:

    actions = spa.Actions('''
        dot(vision, WRITE + SAY):
            verb <- vision
        dot(vision, YES+NO+HI+BYE+OK):
            noun <- vision
        dot(phrase, VERB*WRITE) - 2*dot(vision, WRITE+SAY+YES+NO+HI+BYE+OK):
            hand <- phrase*~NOUN
        dot(phrase, VERB*SAY) - 2*dot(vision, WRITE+SAY+YES+NO+HI+BYE+OK):
            speech <- phrase*~NOUN
        phrase <- verb*VERB + noun*NOUN
        ''')

Hmm, now that I write that out, I kinda like it... I very much like having the : and indentation thing be the same as normal python... and I like having it all as one multi-line string....

@jgosmann
Copy link
Collaborator Author

Interesting idea! What do you think should happen if someone nests the utility statements?

dot(vision, WRITE):
    verb <- vision
    dot(audition, YES + NO + BYE + OK):
         noun <- audition
    dot(audition, STOP):
        state <- audition

@tcstewar
Copy link
Collaborator

What do you think should happen if someone nests the utility statements?

Hmm.... too many options for what that could mean, I think, so I'm tempted to just say that no, it's not allowed....

@jgosmann
Copy link
Collaborator Author

I think I would add a “keyword” to make it a bit more pythonic:

    actions = spa.Actions('''
        utility dot(vision, WRITE + SAY):
            verb <- vision
        utility dot(vision, YES+NO+HI+BYE+OK):
            noun <- vision
        utility dot(phrase, VERB*WRITE) - 2*dot(vision, WRITE+SAY+YES+NO+HI+BYE+OK):
            hand <- phrase*~NOUN
        utility dot(phrase, VERB*SAY) - 2*dot(vision, WRITE+SAY+YES+NO+HI+BYE+OK):
            speech <- phrase*~NOUN
        phrase <- verb*VERB + noun*NOUN
        ''')

Maybe even be more explicit about cortical rules (this could be optional):

    actions = spa.Actions('''
        utility dot(vision, WRITE + SAY):
            verb <- vision
        always:
            phrase <- verb*VERB + noun*NOUN
        ''')

I think I would also like to include the naming of rules into that syntax. Maybe something like:

    actions = spa.Actions('''
        utility dot(vision, WRITE + SAY) named 'vision-rule':
            verb <- vision
        always 'cortical-connection':
            phrase <- verb*VERB + noun*NOUN
        ''')

@tcstewar
Copy link
Collaborator

I think I would add a “keyword” to make it a bit more pythonic:

Hmm... I'm not sure that ends up being more pythonic... it just ends up looking like visual clutter to me... maybe something shorter like act or action? Although even that's not all that pythonic as those are verbs/nouns, rather than closed-class function words.... I'm not sure that a keyword is needed here... hmm....

Maybe even be more explicit about cortical rules (this could be optional):

I was also tempted to put in exactly that always: for the cortical rules, and I like that it's a closed-class word that's pretty unlikely to cause naming conflicts.

I think I would also like to include the naming of rules into that syntax. Maybe something like:

I'd stick with the Python as for this, rather than named. :)

@jgosmann
Copy link
Collaborator Author

I'm not sure that ends up being more pythonic...

Everyline in Python that ends with a colon will start with a keyword (only exception dictionary definitions). Without a keyword it look to me like an evaluation that isn't used or assigned to anything, it might make parsing harder (the parser would have to look for colons at the end of the line, but colons might appear at the end of the line for other reasons). action is only one letter shorter than utility, act might be a bit cryptic. Also, cortical rules are also actions.

I'd stick with the Python as for this, rather than named. :)

I could see that ... but as produces a variable, not a string that can be used as a reference in Python. However, with the inspective parsing it might be possible to assign the action rule object to an actual variable ...

@jgosmann
Copy link
Collaborator Author

A keyword could also allow us to more easily add other types of actions in the future.

@tcstewar
Copy link
Collaborator

Everyline in Python that ends with a colon will start with a keyword (only exception dictionary definitions).

True. I think what I meant to say is that I don't find it more usefully pythonic to have that keyword there. Or at least I don't feel like anything is missing there. After all, the <- isn't pythonic at all. I guess what it comes down to is the "Without a keyword it look to me like an evaluation that isn't used or assigned to anything" part. I don't get that feeling. So this might be something where we have to see what other people think...

it might make parsing harder (the parser would have to look for colons at the end of the line, but colons might appear at the end of the line for other reasons).

What other reasons are you thinking of? I can't think of any way that there could be a : in this code other than these end-of-line markers.

action is only one letter shorter than utility, act might be a bit cryptic.

Looking at all of those options, I think I'm most worried about the naming conflict possibilities. All of those are reasonable names for spa.State objects, and that would just get confusing. If we really had to have a keyword, I think then I'd maybe argue for if, but I'm not sure we do....

I could see that ... but as produces a variable, not a string that can be used as a reference in Python. However, with the inspective parsing it might be possible to assign the action rule object to an actual variable ...

I think that's going to far down the black-magic route, and trying to tie it too closely to Python. I was just saying that as is a perfectly good thing to use rather than named, as it has pretty similar meaning, and is guaranteed to not cause variable name problems.

@jgosmann
Copy link
Collaborator Author

After all, the <- isn't pythonic at all.

Why not? With pythonic I doesn't mean that it is something that currently exists in Python (because obviously we are extending the language), but something that follows common prinicples in Python. <- would just be a new, special operator.

What other reasons are you thinking of?

We're using Python eval to parse the rules, so someone could do something like sink <- translate(**{'source': source, 'vocab': vocab}). Probably no one will ever want to do that, but I'd prefer to not break this part of parsing without a very good reason.

Actually, there might be good reasons to use : as end of line marker, but not in a utility value definiton:

spa.Action('''
    if model_variant_a:
        a <- b
    else:
        a <- c
    for selector, source in zip([A, B, C], [a, b, c]):
        utility dot(stimulus, selector):
            state <- source
    ''')

Not sure how easy it would be to make this work and whether it is a good idea ...

@jgosmann
Copy link
Collaborator Author

I think I'm most worried about the naming conflict possibilities

We have the same problem with the “built-ins” (dot, translate, reinterpret). If we didn't have those, I would be more inclined to avoid adding one or two more reserved words.

@tcstewar
Copy link
Collaborator

Why not? With pythonic I doesn't mean that it is something that currently exists in Python (because obviously we are extending the language), but something that follows common prinicples in Python. <- would just be a new, special operator.

Possibly. I just feel that "not having a keyword in front of a statement ending in a colon" is better than "adding a keyword that must be typed at the beginning of every action line and might conflict with existing objects". But, that's starting to get into very subjective territory, and I don't particularly trust my own judgement on these things and would want to see how other people feel. :)

We have the same problem with the “built-ins” (dot, translate, reinterpret). If we didn't have those, I would be more inclined to avoid adding one or two more reserved words.

All the more reason to keep these to a minimum. And it does help a lot that translate and reinterpret are verbs (and relatively long verbs at that) so they have a pretty low chance of having naming conflicts. And dot is at least a relatively common programming operator and also unlikely to have naming conflicts other than with np.dot. But I'm pretty sure I've built models with a utility ensemble, an act ensemble, and an action ensemble.....

@jgosmann
Copy link
Collaborator Author

I had dot ensembles before ...

@astoeckel
Copy link

Just to add my two cents: I agree that having <- or <-- for sink <-- source is a good idea. I also like the idea of structuring the action rules in a more Phytonic way. What I'm still not comfortable with is the fact that it isn't apparent to the average programmer what is going on here: it isn't made explicit that we're actually selecting between a set of utilities and picking the one with the maximum utility.

So I'd propose something more along these lines, though I'm not 100% sure whether this is practical:

ismax dot(vision, WRITE + SAY):
    verb <- vision
ismax dot(vision, YES+NO+HI+BYE+OK):
    noun <- vision
ismax dot(phrase, VERB*WRITE) - 2*dot(vision, WRITE+SAY+YES+NO+HI+BYE+OK):
    hand <- phrase*~NOUN
ismax dot(phrase, VERB*SAY) - 2*dot(vision, WRITE+SAY+YES+NO+HI+BYE+OK):
    speech <- phrase*~NOUN
otherwise:
    phrase <- verb*VERB + noun*NOUN

@tcstewar
Copy link
Collaborator

Interesting... I like the idea of indicating the operation here... (one minor error: that last one should be always, not otherwise). Other options for ismax might be ifmax or oneof?

@tcstewar
Copy link
Collaborator

Here's another option, which might be getting a bit crazy:

ifmax dot(vision, WRITE + SAY):
    verb <- vision
elifmax dot(vision, YES+NO+HI+BYE+OK):
    noun <- vision
elifmax dot(phrase, VERB*WRITE) - 2*dot(vision, WRITE+SAY+YES+NO+HI+BYE+OK):
    hand <- phrase*~NOUN
elifmax dot(phrase, VERB*SAY) - 2*dot(vision, WRITE+SAY+YES+NO+HI+BYE+OK):
    speech <- phrase*~NOUN
always:
    phrase <- verb*VERB + noun*NOUN

(the reason for doing the elifmax would be that you could also start another block of ifmax/elifmax statements, which would be implemented with a separate BasalGanglia (which, biologically, would just be one of the other tracts through the basal ganglia). Then again, you can already do that just by having a separate spa.Actions, which may be a lot clearer than doing it this way....

@astoeckel
Copy link

I actually like the ifmax, elifmax idea. To me the above code seems very clear while allowing for future extensions.

@tcstewar
Copy link
Collaborator

We're using Python eval to parse the rules, so someone could do something like sink <- translate(**{'source': source, 'vocab': vocab}). Probably no one will ever want to do that, but I'd prefer to not break this part of parsing without a very good reason.

Okay, that is disturbingly beautiful... I hadn't thought of that at all.... :) Hmm... so I guess it ends up being one of those "find the colon at the end of a line that is outside of any nested parentheses, brackets, or braces" algorithms.... which I think would work.... The only non-line-ending colon that's not nested like that that I can think of is the colon for a lambda, but since you'd have to call the function it'll need to be nested inside parentheses.... I'm thinking of something pathological like this:

ifmax dot(state, DOG)*(lambda x:
                   x**2)(0.5)):
    state <- CAT

@xchoo
Copy link
Member

xchoo commented Aug 16, 2017

I'm not going to get too involved in this thread but here are some things to consider:

The ifmax syntax is neat, but it reminds me very much of the old def ... syntax from the spa module in Nengo 1.4. That was dropped in favour of the current syntax for a reason (can't remember why. @tcstewar might have a better idea), but that reason should be taken into account when designing the new system.

I'm not entirely a fan of sink <- source. It read's backwards to me. I think it's more intuitive to do source -> sink?.

@jgosmann
Copy link
Collaborator Author

I'm not entirely a fan of sink <- source. It read's backwards to me. I think it's more intuitive to do source -> sink?.

I agree. I would also be fine with supporting both variants.

@tcstewar
Copy link
Collaborator

tcstewar commented Aug 16, 2017

The ifmax syntax is neat, but it reminds me very much of the old def ... syntax from the spa module in Nengo 1.4. That was dropped in favour of the current syntax for a reason (can't remember why. @tcstewar might have a better idea), but that reason should be taken into account when designing the new system.

The old def system was an attempt to do it all as normal Python, rather than as a separate string. So that put very strong restrictions on the syntax, in that it had to by valid Python code. In other words, I was doing this:

def action_a(state='A'):
    state='B'

rather than:

ifmax dot(state, A):
    state <- B

(and then doing a bunch of Python black magic to make all that work). The def syntax forced the use of the = sign, both for comparison (now dot) and for the routing information (maybe now <-).

Anyway, that was the main reason for me to step away from the old def syntax. So I think with this version, where it's done as a separate string but with as-Python-like-as-possible syntax (and using eval in the local context to process it so you have access to your local variables), I think we're avoiding the syntax restrictions nicely.

For me, the two biggest goals for this new system would be:

  1. Clarity about what is going on. This is why I really like the explicit dot (and translate), and the <- instead of the equal sign. I'm also liking that for the ifmax (and maybe elifmax) and always, as it's at least a hint about how to interpret it behaviourally (in a way that something like utility or act doesn't).

  2. Simplifying the syntax, especially for multi-line actions. For example, last month I wrote this set of Actions:

    speed = 0.3
    separation = 0.3
    strength = 0.4
    actions = spa.Actions(
        'dot(rule, (BELOW-ABOVE-LEFT-RIGHT)*V)*{strength} +'
        '(subjy - objy)+{separation} --> '
                 'status=BAD, '
                 'objs=-{speed}*Y*objs*~S + {speed}*Y*objs*~O'.format(**locals()),
        'dot(rule, (BELOW-ABOVE-LEFT-RIGHT)*V)*{strength} +'
        '(subjy - objy)-{separation} --> '
                  'status=GOOD'.format(**locals()),
    )

My hope is that could be made a bit nicer, turning into something like:

    speed = 0.3
    separation = 0.3
    strength = 0.4
    actions = spa.Actions('''
        ifmax dot(rule, (BELOW-ABOVE-LEFT-RIGHT)*V)*strength +
              (subjy - objy)+separation:
                 status <- BAD
                 objs <- -speed*Y*objs*~S + speed*Y*objs*~O
        ifmax dot(rule, (BELOW-ABOVE-LEFT-RIGHT)*V)*strength +
              (subjy - objy)-separation:
                 status <- GOOD
       ''')

I didn't think that it'd be at all possible to have those local variables accessible in the parsing until @jgosmann came up with this "inspective parsing" approach.... To me that lets us adjust the syntax in some interesting ways, while still keeping access to normal python variables and whatnot.

Anyway, not sure if all that helps, but it's what I'm thinking about this at the moment. :)

@jgosmann jgosmann added this to the 0.3 release milestone Aug 20, 2017
@jgosmann jgosmann self-assigned this Aug 20, 2017
@jgosmann
Copy link
Collaborator Author

I spent some time on writing a tokenizer/lexer for the new action rule syntax, rewriting my partial solution once to improve it, and still ending up dissatisfied. Now I discovered the standard library module tokenize that gives access to a complete tokenizer for Python. 😃 Because it's only lexing it doesn't care about new keywords etc.

While playing around, I noticed that -> is already a valid operator in Python 3 for function annotations. This leads to the question whether we should better come up with a different operator to avoid that double meaning? (Though function definitions won't be possible in the action rules for now.)

@jgosmann
Copy link
Collaborator Author

Actually most arrow-like constructs are already Python operators in at least one direction: ->, <=, <<, >>

@jgosmann jgosmann mentioned this issue Sep 15, 2017
13 tasks
@jgosmann jgosmann closed this as completed Oct 3, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

4 participants