On Mar 11, 2016, at 07:29, Michał Żukowski <thektulu.pp@gmail.com> wrote:


But in Haskell, the `where` keyword also considers scoping. That is,
outside the statement/expression with the `where`, you can't access the
variables introduced by the where.

 
Yes, but I wanted it to be simple and powerful, not necessarily right in every context.

Even though the `where` looks kind-of-nice, it (at least to me) is also
a bit confusing with respect to evaluation order. Consider

    [ stripped for idx, line in enumerate(lines) if idx >= 5 or stripped where stripped=line.strip() ]

(intended semantics: give me all lines (stripped), but ignore
any lines that are whitespace-only in the first 5 lines)

    retval = []
    for idx, line in enumerate(lines):
        stripped = line.strip()
        if idx >= 5 or stripped:
            retval.append(stripped)

now I'm not very sure, but I expect what actually happens is:

    retval = []
    for idx, line in enumerate(lines):
        if idx < 5:
            stripped = line.strip()
        if idx >= 5 or stripped:
            retval.append(stripped)

that is, should I read it as
    (if idx >= 5 or stripped) where stripped=line.strip()
or
    if idx >= 5 or (stripped where stripped=line.strip())

I've implemented it as "or_test [where_expr]" so the default order is the same as in:
    (if idx >= 5 or stripped) where stripped=line.strip()

This makes where look like another top-level comprehension clause like for and if, but it doesn't follow the same nesting rules as the other clauses. Which means that whenever something isn't trivial enough to just read holistically, trying to apply the dead-simple rule of "write each clause as a nested statement, in the same order" will just lead to confusion, instead of immediately telling you the interpretation.

Where that line is depends on how experienced you are with Python, how much time you've spent recently in a language with similar but not identical comprehension syntax, how tired you are, etc., but that doesn't matter--any comprehension with a where clause has at least three clauses, and is likely to be complicated enough to be over that line for some people. So, if you're going to recommend that this is only used when it's simple enough that it doesn't matter that the translation is confusing, that would mean recommending never using it, which implies that we shouldn't have it.

A new clause that nests in order, like some of your other variations, doesn't have this problem.


I wanted it to always "scope" left as much as possible - in precedence order between "lambda" and "... if ... else ..." - but left recursion forced me to place it after  "... if ... else ..." which can't be used without brackets in list comprehension "if" filtering.
But one can always control order with brackets, and make it work like in second example.

For comprehensions, I'd think the 'let' statement might make more sense.
Abusing Haskell's notation:

    [ stripped | (idx, line) <- zip [0..] lines, let stripped = strip line, idx >= 5 || length stripped > 0 ]

Porting this to something Python-ish, it'd be

    [ stripped for idx, line in enumerate(lines) let stripped = line.strip() if idx >= 5 or stripped ]

where `let` is a keyword (possibly only applicable in a compexpr). In
Haskell it's a keyword everywhere, but it has somewhat different
semantics.

I was thinking about "let", but as I said, I think that new keyword should be more powerful than just filtering in list comprehensions, and in regular expresions it would look like this:
    if let x=foo() in x:
        print(x)

Which does not look great, and there is problem with "in" keyword that make this expresion ambiguous.

I think you're overcomplicating this. You want let to be a statement, but also have a value--but you only need one or the other.

As a statement, there's no need for a value; you're just creating or rebinding a variable for use in the controlled suite:

    let x = foo():
        if x:
            print(x)

And in a comprehension, it's just another clause that converts to the identical compound statement and nests the same as other clauses:

    [stripped for line in file let stripped=line.strip() if stripped]

Hopefully you don't want scoping, but if you do, it's pretty obvious that the scope is the controlled suite; still no need for an in clause.

If you really want a let expression instead of a statement, you still don't need an in clause unless you want scoping. Just make it an assignment expression whose value is the assigned value, just like in C and friends:


    if let x = foo():
        print(x)

Which extends easily to:

    if (let x = foo()) > 5:
        print(x)

    [stripped for line in file if let stripped=line.strip()]

The only reason you'd need an in clause is if you wanted scoped expressions. But those would be almost useless in Python because, by design, you can't do too much inside an expression. But if you really want that, the design is again obvious: just as in functional languages, it's equivalent to a lambda call:

    let x=foo() in (print(x) if x > 5 else None)

    (lambda x: print(x) if x > 5 else None)(foo())

... which is obviously unpythonic enough and useless enough that I think scoped let expressions are immediately dismissible as an idea.