enso-org / enso

Hybrid visual and textual functional programming.
https://enso.org
Apache License 2.0
7.31k stars 318 forks source link

Unexpected block behaviour with multiline operators #9944

Open radeusgd opened 1 month ago

radeusgd commented 1 month ago

I ran into a problem when constructing a big JS Object - I wanted to split the construction into several lines and ran into a problem that some of the fields I was adding were missing.

Below I show a simplified repro showing the reconstruction of the issue:

from Standard.Base import all

main =
    # Approximation how I found it
    json = JS_Object.from_pairs <|
        [["type", "Postgres_Connection"], ["libraryName", "Standard.Database"]] +
            [["host", "H"], ["port", 1234], ["database_name", "DB"]] +
            (if False then [["schema", "det.sch"]] else []) +
            (if True then [["credentials", "cred"]] else [])
    IO.println json

    # But it seems it can be simplified
    v1 = [1] +
            [2] +
            [3] +
            [4]
    IO.println v1

    v2 = [1] +
            [2]
    IO.println v2

    v3 = [1] +
            [2] +
            [3]
    IO.println v3

This yields:

{"type":"Postgres_Connection","libraryName":"Standard.Database","credentials":"cred"}
[1, 4]
[1, 2]
[1, 3]

We can see that only the first and last line of the addition is considered and everything in between is ignored.

Intuitively, we'd expect it to print:

{"type":"Postgres_Connection","libraryName":"Standard.Database","host":"H","port":1234,"database_name":"DB","credentials":"cred"}
[1, 2, 3, 4]
[1, 2]
[1, 2, 3]

After a while, I realized that this is correct according to Enso spec. The thing is that the few indented lines were creating a new block. The block was being executed and only returned the last expression, which was then passed to the top-most +.

So for

v1 = [1] +
        [2] +
        [3] +
        [4]

The block

        [2] +
        [3] +
        [4]

computed [2].+ (which is an unevaluated function) and [3].+ and then returned [4] that was passed as second argument to [1] + _, yielding [1, 4].

My confusion stemmed from the fact that I forgot that to use multiline operators in Enso, the operator must be at the beginning of each line, not at the end like I did in the example above. Indeed, once I rewrote the example above to:

from Standard.Base import all

main =
    # Approximation how I found it
    json = JS_Object.from_pairs <|
        [["type", "Postgres_Connection"], ["libraryName", "Standard.Database"]]
            + [["host", "H"], ["port", 1234], ["database_name", "DB"]]
            + (if False then [["schema", "det.sch"]] else [])
            + (if True then [["credentials", "cred"]] else [])
    IO.println json

    # But it seems it can be simplified
    v1 = [1]
            + [2]
            + [3]
            + [4]
    IO.println v1

    v2 = [1]
            + [2]
    IO.println v2

    v3 = [1]
            + [2]
            + [3]
    IO.println v3

That indeed prints the result as expected above.

We could say that this is by-design and ignore it, but I thought that since it caused my quite a big confusion, it means it will be even more confusing for any new users. Fortunately, the multiline syntax is not yet being used much in the GUI yet. But still I think that we should figure out some way to deal with such problems.

The problem here is that the block is created, discarding intermediate values, in a way that a user may not exactly expect to have a block. Reading the code above, intuitively looks like a valid + concatenation, not a block with side-effecting values.

radeusgd commented 1 month ago

I guess one way to deal with such situations is by implementing #5430 or #5431