Menu

#4333 makelist evals 2nd arg and doesn't report some errors

None
open
nobody
5
2024-07-18
2024-07-12
No

I expect the 4-argument version of makelist to evaluate its 3rd and 4th arguments exactly once each, its 1st argument once per iteration, and its 2nd argument not at all. This is the expected behavior:

makelist(print(a,i),print(b,i),print(c,2),print(d,4));
c 2 
d 4 
a 2 
a 3 
a 4 
=> [2, 3, 4]  

But that is not what it does. In fact, makelist evaluates its second argument on each iteration:

makelist(print(a,i),print(b,i),print(c,2),print(d,4));
c 2 
d 4 
b i 
a 2 
b i 
a 3 
b i 
a 4 
=> [2, 3, 4]    << This part is OK

Why?
You might think this is perversely useful if you want the iteration variable to be able to be variable (really bad idea, the iteration variable should be local), but that doesn't work:

var: 'i$ makelist([i,var],var,2,4);
 => [[i, 2], [i, 3], [i, 4]]

It's true that makelist needs to evaluate its second argument in the 2-argument case:

makelist(print(a,i),print(b,3))
b 3 
a i 
a i 
a i 
=> [i, i, i]

But it should only do that if it has exactly two arguments.

Then we have this weird case:

makelist(print(a,i),print(b,3),5,6);
b 3 
a i 
b 3 
a i 
=> [i, i, i]

where it sees to be OK with a numerical second argument, but is using the third and fourth for the count, and this weird case (which you'd think would work like the previous one):

 makelist(print(a,i),3,2,3);
Only symbols can be bound; found: 3

What a mess!

Discussion

  • Robert Dodier

    Robert Dodier - 2024-07-12
    • labels: --> makelist, evaluation
     
  • Robert Dodier

    Robert Dodier - 2024-07-12

    I'm generally in favor of fixing up stuff like that, in the interest of making it easier to predict what's going to happen. makelist being pretty commonly used, it seems like it merits special attention -- if you have any bug fixes, I would be interested to try it.

     
  • Viktor Toth

    Viktor Toth - 2024-07-13

    I must say I actually like the way makelist behaves. I know, I know, "perversely useful" though Maxima certainly has features far more quirky than this one. Here's a variation that I was just playing with:

    (%i1) l:1;makelist(print(a,[i,j,k][(l:l+1)-1]),
                       print(b,[i,j,k][l]),print(c,2),print(d,4));
    (%o1)                                  1
    c 2 
    d 4 
    b i 
    a 2 
    b j 
    a 3 
    b k 
    a 4 
    (%o2)                              [2, 3, 4]
    

    What this example tells me even without looking at the implementation of makelist is that it is not actually using the second argument as an iteration variable; rather, it simply assigns the (internally maintained) iteration variable to its second argument in each iteration.

    Is this a Bad Thing? Does it need fixing? The behavior actually makes sense to me. But my biggest worry would be that by fixing this, we might break existing code that relies (maybe it shouldn't but who am I to judge?) on side effects of this behavior.

     
  • Robert Dodier

    Robert Dodier - 2024-07-15

    Here's another bit reported by @willisbl a few days ago.

    "To find the number of terms, makelist calls float on the difference of the fourth and third arguments. The function float looks up numerval data. So

    (%i1)numerval(a,7)$
    (%i2)makelist(a,k,a,8);
    (%o2)[a,a]
    

    This is OK, I guess, but is it predictable from the user documentation? No, I don't think so."

     
  • Robert Dodier

    Robert Dodier - 2024-07-15

    @vttoth I'm inclined to look at whether a concise description of the behavior is possible; if so, then fix up the documentation, otherwise formulate a concise description and make the code match it.

    I would be interested to consider requiring that the second argument be a mapatom, and to assign it but not evaluate it. I'm not yet ready to claim that must be the case, only that I'm willing to consider it.

    If we change the behavior, we should ideally make some previous behavior an error, as opposed to just getting a different result.

     
    • Viktor Toth

      Viktor Toth - 2024-07-16

      Maybe through an example?

      (%i1) counter():=(print("Counter was called"),i);
      (%o1)            counter() := (print("Counter was called"), i)
      (%i2) init():=(print("Init was called"), 0);
      (%o2)               init() := (print("Init was called"), 0)
      (%i3) stop():=(print("Stop was called"), 3);
      (%o3)               stop() := (print("Stop was called"), 3)
      (%i4) result(i):=(print("Result(",i,") was called"),f(i));
      (%o4)      result(i) := (print("Result(", i, ") was called"), f(i))
      (%i5) makelist(result(i),counter(),init(),stop());
      Init was called 
      Stop was called 
      Counter was called 
      Result( 0 ) was called 
      Counter was called 
      Result( 1 ) was called 
      Counter was called 
      Result( 2 ) was called 
      Counter was called 
      Result( 3 ) was called 
      (%o5)                      [f(0), f(1), f(2), f(3)]
      

      Then there's also this:

      (%i1) numerval(a,6);
      (%o1)                                 [a]
      (%i2) makelist([a,k],k,a,8);
      (%o2)                  [[a, a], [a, a + 1], [a, a + 2]]
      

      Again, this seems quirky but not any more quirky than many other Maxima features. Not a bug that needs fixing I think, and fixing it may break thing elsewhere. Documenting it may indeed be the right thing to do.

       
  • Stavros Macrakis

    I agree with Dodier. The current behavior really is perverse, is not part of the documented behavior of makelist, and has no obvious benefit.

    In fact, the second argument should be required not just to be a mapatom, but an assignable atom/symbol. This would make it consistent with other cases like sum and product. Allowing it to be (for example) a subscripted variable allows side-effects in the subscript calculation, which isn't necessary when you're talking about a formal variable.

     
  • Barton Willis

    Barton Willis - 2024-07-17

    I suggest that we consider removing the calls to float from makelist.

    I did this experiment and the testsuite, including share, gave no unexpected failures.

    But sum also consults numerval facts, at least for the limits of the summation. So for consistency, we should retain the calls to float.

    (%i2) sum(f(x),x,1,a);
    
    (%o2) 'sum(f(x),x,1,a)
    (%i3) numerval(a,3)$
    
    (%i4) sum(f(x),x,1,a);
    
    (%o4) f(3)+f(2)+f(1)
    
     
    • Stavros Macrakis

      float and $bfloat are called by is, so that do can support things like this:

      for i:%e thru 5/3*%pi do print(i)$
      for i:2.0b0 thru 8/3 do print(i)$
      
       
      • Raymond Toy

        Raymond Toy - 2024-07-17

        Isn't that kind of confusing? How does is now whether to use float for bfloat? This would be important for something like for i:%e thru %e-<tiny rat> do print(i), where <tiny rat> is some tiny rational such that float(%e-<tiny rat>) would be the same as float(%e), but if bfloat were used, a value just smaller than bfloat(%e) would have been returned?

         
        • Stavros Macrakis

          Actually, for i:%e thru %e+<tiny rat> step <tinier rat> works just fine since is doesn't need to use (b)float in that case -- it can do exact comparisons.

          It's certainly possible to find bad cases of various kinds, although I'm pretty sure that it's extremely rare for users to want to do things like

          assume(a>2);
          for i:a next i^2 thru 4*a^6 do print(i);
          

          but in some sense it's a side-effect of handling rats and bfloats -- Maxima doesn't have imperative arithmetic primitives; it just uses simplification and is.

          So this isn't really a do issue but an is issue.

          And even with pure floats, you can get problems, e.g., for i step 1e-30 ... will always be an infinite loop since 1+1e-30 = 1.0.

           

          Last edit: Stavros Macrakis 2024-07-18

Log in to post a comment.