donkirkby / live-py-plugin

Live coding in Python with PyCharm, Emacs, Sublime Text, or even a browser
https://donkirkby.github.io/live-py-plugin
MIT License
292 stars 57 forks source link

Show result of loop-condition or if-condition in output #15

Open Mqrius opened 11 years ago

Mqrius commented 11 years ago

enhancement: (I believe I can't set labels)

I'd like to see the result of a loop condition in the output.

def p(msg):
    print msg

a = 4
b = 5

if a < b:
    p("yes")
else:
    p("no")

Desired output:

msg = 'yes'
print 'yes'

a = 4
b = 5

True

Actual output:

msg = 'yes'
print 'yes'

a = 4
b = 5

Although it's usually not impossible to trace the flow of the program through its subsequent output, it would be easier to see it immediately. Perhaps it would even be possible to output some information for every line. For example, the code

p("yes")

could just output

p("yes")

to signify that that is what was done on that line, even though no variable assignment too place. However, the conditional statements are of a higher priority.

donkirkby commented 11 years ago

I have thought about this kind of thing, but I'm worried that the display will get too cluttered if I keep adding stuff to it. I will keep thinking about what to display, but I'm going to decline this request for now.

For the specific example you included, I would modify the code to make it easier to follow:

def p(msg):
    print msg

a = 4
b = 5

if a < b:
    msg = "yes"
else:
    msg = "no"
p(msg)

Of course, you could also introduce a local variable anywhere in your code that you want to see more detail.

def p(msg):
    print msg

a = 4
b = 5

is_alphabetical = a < b
if is_alphabetical:
    p("yes")
else:
    p("no")

I use this pattern a bit like a print statement in debugging, but you don't have to disable it when you're finished debugging.

donkirkby commented 11 years ago

Coaldust argued pretty persuasively for this feature, so I'm reconsidering it. Here's my current plan:

x = 10              | x = 10
if x < 20:          | 10 < 20
    x += 5          | x = 15
                    |
while x > 10:       | 15 > 10 | 12 > 10 | 9 > 10
    x -= 3          | x = 12  | x = 9

I'm not sure whether I want to add a question mark like this:

10 < 20?

Maybe I should put the word if or while in there, but I think I will try the minimal approach first. The thing I like least about this is that it adds an extra column to the while loop.

Do either of you have any thoughts on how I should display it?

ghost commented 11 years ago

The original request was to simply show "True" or "False" beside the tests of branch and loop statements.

After thinking about it, I really like seeing the values filled into the expression.

Benefits of Filled-In Expressions:

It would be preferable to also show the boolean value. You could append something like " => True" or " => False" to the end of the filled in expression. This would save effort in mental arithmetic. It would appear that "=>" is a symbol commonly used to indicate return value in what few instant feedback debuggers exist. This seems to be the "implies" symbol from formal logic (http://en.wikipedia.org/wiki/List_of_logic_symbols). You could use the UTF-8 U+21D2 ⇒ symbol to make it look less like something someone would type (like a screwed up greater-than symbol).

Example: while x > 10: | 15 > 10 ⇒ True | 12 > 10 ⇒ True | 9 > 10 ⇒ False

Alternatives I came up with include: "=T=" and "=F=" " -> T" and " -> F" (or the U+2192 → symbol)

These are okay I guess. They're shorter. They won't conflict with real Python syntax either. I haven't seen them used elsewhere, though. Maybe it's best to not invent more confusing notation.

donkirkby commented 11 years ago

I'm really hesitating with this feature, because it feels like I'm adding a new kind of information. I'm worried that it will add more confusion than clarity, so I'm not going to include it in the next release. I'll do some more thinking, and maybe tackle it later. I'll leave the issue open for now.

In the meantime, try the current version, and think about when this feature would be helpful. If you come up with an example where it would make the display easier to read, please post it here.

donkirkby commented 11 years ago

Here's an example where I think it would be confusing:

class bar(object):
    def __init__(self, n):
        self.n = n

    def __repr__(self):
        return 'bar(%d)' % self.n

    def inc(self, n):
        self.n += n
        return self.n

b = bar(3)
b.inc(2)
b.inc(2)

if b.inc(1) > 5:
    print 5

Paste that into your editor to see how it currently displays. It shows the change in b as you make each increment, so adding this feature would make the if statement display both the True/False value and the change to b.

donkirkby commented 11 years ago

Here's another possibility to consider: use pipe symbols to show which branch of a conditional is executing. That's consistent with the way control flow is displayed in function calls and loops.

Here's an example:

def foo():              #           |
    print 'f'           # print 'f' | print 'f' 
                        #
def bar():              #
    print 'b'           # print 'b' 
                        #
for x in range(3):      # x = 0 | x = 1 | x = 2 
    if x % 2 == 0:      #       |       |
        foo()           # |     |       | |
    else:               #       |       |
        bar()           #       | |     |
ghost commented 11 years ago

The Discomfort of Expressing Expressions

I don't really understand why you find the proposed display so unnatural or confusing.

If you're willing to implement it, there's always the option of having it off by default. At least then it'll never confuse you.

If I had to guess at the problem, it may be because you're primarily/exclusively used to imperative programming.

Imperative programming languages often have some unnecessary separation of concepts. They have procedures, statements, and expressions. Declarative languages often just have expressions and ways of, optionally, naming things. Procedures are just expressions that have been given a name, and always return the value of the last expression in their block. Things like branches return the value computed by the last expression in each of their blocks, and are thus themselves expressions, because they return a value. All loops may be expressed as tail-recursive functions.

After exposure to declarative languages, I don't see any real difference between:

foo != bar

and

notEqual(foo, bar)

It's just cosmetic, yet you seem to feel that expressions somehow involve a different kind of information, and are very confusing. You wouldn't be at all surprised at people wanting to know the values of "foo" and "bar" when getting undesired results from the "notEqual" function, or that "notEqual" would return a value. Why is it so hard to imagine the same for the "foo != bar" expression, which is literally, the same thing?

Also, there's the repeated concern of clutter.

x = 0          # x = 0
while x < 3:   #       |       |
    x = x + 1  # x = 1 | x = 2 | x = 3

The above is output from live-py as it is currently. I haven't modified it in any way. I draw your attention to something:

x = 0          # x = 0
while x < 3:   #       |       |       <-- empty row of wasted space
    x = x + 1  # x = 1 | x = 2 | x = 3

There are already rows, complete with dividers, 'cluttering' our space. Filling the empty space with useful information won't waste any horizontal scroll space. It won't even spoil the airy feng shui we currently have if you leave a option to turn it off. Then people that want empty rows with no information between the dividers can have them.

A version with your workaround:

x = 0              # x = 0
x_lt_3 = x < 3     # x_lt_3 = True                                  <-- first extra line
while x_lt_3:      #               |               |                <-- empty row of wasted space
    x = x + 1      # x = 1         | x = 2         | x = 3
    x_lt_3 = x < 3 # x_lt_3 = True | x_lt_3 = True | x_lt_3 = False <-- duplicated code, second extra line

This actually looks more cluttered to me. It's 2 lines longer. It has duplicated code, which is known to be error-prone in the face of modifications. There's still that row of wasted space. Anyone not using this tool would definitely not write the code this way.

A better (no duplicated code) workaround:

def lt_3(x):
    return x < 3

x = 0
while lt_3(x):
    x = x + 1

This is now 3 lines longer than the natural way of writing it, and nobody would define a function for such a simple expression.

You want examples... I suppose that's fair. If you're convinced this is a bad idea without some mindblowing demonstration from me, I'm probably not going to be able to give it to you. I'm not a mathematical genius. I sometimes write ugly code.

A expression is a function without a name. Your tracer only displays functions with names. It's possible to write complicated expressions just like it's possible to write complicated functions. Therefore the fact you can have bugs in expressions should not be surprising. Since you can have bugs in expressions it would be nice if the tracer worked with them. I'm just asking for our "one line nameless functions" (expressions) to show the value of their arguments and return values (and I'm only asking for one line, that's already taking up space, to show it in), just like named functions do.

I suppose the 'killer app' for debuggable expressions would be some sort of approximation function, where the bug involved 'oscillating' so the loop never terminated. Such things are usually written as a while loop with a test for 'near-enough-ness' in an expression, and some fancy math in their body. I seem to remember hearing of Java having a security hole (denial of service) like this in its square root implementation, but I can't find the link any more. If you could see the 'filled in' values in the loop test expression bobbing up and down, along with the fact it's always returning True, you could pretty quickly spot and understand the bug.

The somewhat less convincing version that has to do with using this on branches instead of loops is probably the lexer example. If you want to write a direct coded lexer in a language without goto, you'll probably use switch-case. If you don't have switch-case, you'll use chained if-then-else's. To recognize the keywords: carrot cartoon dog you'd have a if-then-else chain that tested if the first character was a "c" or "d" and otherwise would error. If it was a "c", it would go into a loop that made sure the next two characters were "ar" and otherwise error. Then there would be another chain of branches to see if the next character was a "r" or "t" and otherwise error... You get the idea, hopefully. All this tends to be inside one big loop that passes over the string, usually chucking whitespace, and jumping into this tree-like part when it encounters something non-whitespace. These lexers are usually one very long procedure with deep indentation. It's real easy to make a typo. It can be hard figuring out why the control flow isn't what you expected. You might actually want to write this sort of thing by hand if you want good error reporting. Let's say I screw up the first test, and ask if it's a "c" or a "c" (instead of "c" or "d"), and then can't figure out why dog isn't recognized. Being able to see the if latest_character != 'c' code and "latest_character" is "d" in the trace, might make my brain start working.

After picking at all this tonight I notice the tracer doesn't show arguments of functions 'filled in' at the point where it's called, or return values, ever. That means if the content of branch or loops only contains calls that do some side effect elsewhere, it may be impossible to even tell which branch was taken.

Personally I try to write code with as few side-effects as possible. That means I'm mostly interested in seeing the value of arguments and of return values. I'm realistic. Without side-effects the computer just gets warm. I use side-effects when I have to, but try to keep them out of most of my code. I do want to see the values that are assigned. I just think the tracer needs improvement for people that have adopted a more functional style.

Maybe we just want different things from this kind of tool. It's your project. I respect your decision whatever it is.

The Confusing Display

I see what you're getting at with the:

if b.inc(1) > 5: | b = bar(8) > 5 ⇒ True

display. I don't think the problem is with the idea of displaying test expressions, however. I think it's with the confusing use of "b = bar(?)" for displaying objects. I know part of that is your definition for "repr".

I would display it as:

if b.inc(1) > 5: | b.inc(1) > 5 ⇒ True

because that shows the values 'filled in'. It would be more useful if the argument to "inc" was a variable (e.g. "b.inc(x)") instead. If the result ("True") was confusing, I could look at the trace of "inc". This does nicely illustrate the value in showing the result though.

The only other reasonable alternative I can think of is:

if b.inc(1) > 5: | 8 > 5 ⇒ True

because 8 (a integer) is what is returned (not a instance of "bar").

What if "b" had more than one field? I would be very uncomfortable with saying "b = bar(8)", which looks like b being the same as calling "bar"'s constuctor with a argument of 8, when I had, after construction, set:

b.meowmeow = "Hello Kitty!"

Not only does "b" not really equal bar(8) (it sort of equals bar(8).meowmeow = "Hello Kitty!"), but I wouldn't get a equivalent object if I called "bar(8)" (because "meowmeow" would be wrong).

I find the fact that:

b.inc(2)
b.inc(2)

is displayed as:

b = bar(5)
b = bar(7)

rather than:

b.inc(2)
b.inc(2)

(i.e. just the arguments filled in, again, more useful when they're variables) or:

5
7

disturbing for the same reasons. It doesn't look like what the source code actually does. It calls a method that returns a value. It doesn't call a constructor and assign it to a variable. It also has the "I wouldn't necessarily get the same thing by calling the constructor that way" issue I just mentioned.

Pipes for Separating Passes and Control Flow Indication

I think the pipes display of the execution path is a especially bad idea, since pipes are the dividers between sections. I don't see how it's consistent with anything. When I tried a simple tail recursive function it didn't look like that.

def tail_call(x):
    if x < 3:
        tail_call(x + 1)
    else:
        print("sing a tune")

tail_call(0)

the result looked like this:

x = 0 | x = 1 | x = 2 | x = 3 
      |       |       | 
      |       |       | 
      |       |       | 
      |       |       | print('sing a tune') 

which is not surprising or undesirable.

I would like it if it looked more like:

tail_call(0)     | tail_call(1)     | tail_call(2)     | tail_call(3)
0 < 3 ⇒ True     | 1 < 3 ⇒ True     | 2 < 3  ⇒ True    | 3 < 3 ⇒ False
tail_call(0 + 1) | tail_call(1 + 1) | tail_call(2 + 1) | 
                 |                  |                  | 
                 |                  |                  | print('sing a tune') 
donkirkby commented 11 years ago

Thank you for your intense feedback. I will think more about whether to display program state or data flow, but I don't think I will add any enhancements for the next couple of months.

ghost commented 11 years ago

Okay. Thanks for at least considering it, and thanks again for making this tool.

Mqrius commented 11 years ago

So the main problem here is the need to get information vs the need to present that information in an uncluttered way.

Perhaps it's possible to allow the user to turn on certain outputs after the execution is done. This would likely require major restructuring of the inner workings and interface though.

On the other end of deviation from the current situation, there's full reverse debugging. Being able to go back in execution steps and seeing all variable assignments is pretty useful. Even with reverse debugging you have to make sure that you keep the interface simple though. For example: the Java omniscient debugger looks very chaotic on first sight.

Neither of these options is easily implementable, but they would both improve things. On 15 Jun 2013 07:14, "Coaldust" notifications@github.com wrote:

Okay. Thanks for at least considering it, and thanks again for making this tool.

— Reply to this email directly or view it on GitHubhttps://github.com/donkirkby/live-py-plugin/issues/15#issuecomment-19491468 .

donkirkby commented 11 years ago

Exactly, @Mqrius. I'll continue to think about it and watch for confusing examples that come up as I'm using the tool.

donkirkby commented 9 years ago

I'm still thinking about whether to display the expressions evaluated for conditionals and loops. After rereading this discussion, my favourite option is to display function calls, conditionals, and loops with their input parameters or expressions.

Here's a rough example:

def foo(x):                # foo(3):
    while x > 0:           # while 3 > 0: | while 1 > 0: | while -1 > 0:
        x = x - 3          # x = 0        | x = -2       |
        if x == 1:         # if 0 == 1:   | if -2 == 1:  |
            return 'Happy' #              |              |
        else:              #              |              |
            x += 1         # x = 1        | x = -1       |
    return x + 5           # return 4
                           #
y = foo(3)                 # y = 4

I haven't decided whether to include True or False after the expression, a check mark or cross, nothing, or use colour/underline to indicate true or false.