Remix.run Logo
pansa2 9 days ago

Putting aside template strings themselves for the moment, I'm stunned by some of the code in this PEP. It's so verbose! For example, "Implementing f-strings with t-strings":

    def f(template: Template) -> str:
        parts = []
        for item in template:
            match item:
                case str() as s:
                    parts.append(s)
                case Interpolation(value, _, conversion, format_spec):
                    value = convert(value, conversion)
                    value = format(value, format_spec)
                    parts.append(value)
        return "".join(parts)
Is this what idiomatic Python has become? 11 lines to express a loop, a conditional and a couple of function calls? I use Python because I want to write executable pseudocode, not excessive superfluousness.

By contrast, here's the equivalent Ruby:

    def f(template) = template.map { |item|
            item.is_a?(Interpolation) ? item.value.convert(item.conversion).format(item.format_spec) : item
        }.join
davepeck 9 days ago | parent | next [-]

We wanted at least a couple examples that showed use of Python's newer pattern matching capabilities with the new Interpolation type. From this outsider's perspective, I'd say that developer instincts and aesthetic preferences are decidedly mixed here -- even amongst the core team! You can certainly write this as:

    def f(template: Template) -> str:
        return "".join(
            item if isinstance(item, str) else
            format(convert(item.value, item.conversion), item.format_spec)
            for item in template
        )
Or, y'know, several other ways that might feel more idiomatic depending on where you're coming from.
pansa2 9 days ago | parent [-]

Your example is interesting - `join`, a generator expression and a ternary, and Python requires us to write all of them inside-out. It's a shame we can't write that in a more natural order, something like:

    def f(template):
        return (for item in template:
            isinstance(item, str) then item else
            format(convert(item.value, item.conversion), item.format_spec)
        ).join('')
davepeck 9 days ago | parent [-]

Yeah. `join` has forever been backwards to me in Python and I still sometimes get it wrong the first time out.

Comprehensions, though -- they are perfection. :-)

pansa2 9 days ago | parent [-]

IMO comprehensions like `[x**2 for x in range(4)]` would be better written as `[for x in range(4): x**2]`.

That would make the mapping between a comprehension and the equivalent loop much clearer, especially once you use nested loops and/or conditionals.

For example, to flatten a list of lists `l = [[1, 2], [3], [4, 5, 6]]`:

    [item for sublist in l for item in sublist]
vs

    [for sublist in l: for item in sublist: item]
zahlman 8 days ago | parent | next [-]

I think it's very deliberate that these sorts of expressions are inside-out from the corresponding statements. It mixes and matches modes of thinking in my mind - the same kind of mistake as command-query separation violations. When I read a list comprehension, I can read it left to right: "A list (`[`) of each `item` that I get, if I look `for` each `sublist` that is `in l`, then `for` each `item` which is `in` that `sublist`." And the entire description is - well - descriptive, not imperative. With the other order, I have to think in terms of explicitly following the steps, and then mentally translating the last `item` into an imperative "... and put it into the result list".

Hackbraten 8 days ago | parent | prev [-]

That would make dict comprehensions confusing due to two occurrences of colons with different semantics.

pansa2 8 days ago | parent [-]

Ah, yes - good point!

the-grump 9 days ago | parent | prev | next [-]

This is how Python has always been. It's more verbose and IMO easier to grok, but it still lets you create expressive DSLs like Ruby does.

Python has always been my preference, and a couple of my coworkers have always preferred Ruby. Different strokes for different folks.

pansa2 9 days ago | parent [-]

> This is how Python has always been.

Nah, idiomatic Python always used to prefer comprehensions over explicit loops. This is just the `match` statement making code 3x longer than it needs to be.

the-grump 8 days ago | parent [-]

You can express the loop as a list comprehension, and I would too.

As for the logic, I would still use pattern matching for branching and destructuring, but I’d put it in a helper. More lines is not a negative in my book, though I admit the thing with convert and format is weird.

pansa2 8 days ago | parent | next [-]

> I would still use pattern matching for branching and destructing, but I’d put it in a helper

Yeah, using a helper function makes things much clearer. To be honest though, I'm not a huge fan of using either `isinstance` (which is generally a sign of a bad design) nor `match/case` (which is essentially a "modern" way to write `isinstance`).

I can't help but think that a better design could avoid the need for either of those (e.g. via polymorphism).

the-grump 8 days ago | parent | next [-]

What I would use is an ADT and I make a wish for that every day in every language that’s not Haskell :D

Haskell also has operator overloading on steroids so you could use the (|>) operator from Flow and write transformations the same as you would shell pipes. I’d love to whip up an example but it’s difficult on this tiny screen. Will try to remember when I’m on my computer.

Before someone chimes in with ML propaganda, I warn you that I’m going to exercise my rights under the Castle Doctorine the moment you say “of”.

davepeck 8 days ago | parent | prev [-]

You can access .strings and .interpolations directly and avoid type checks. There is always one more string than interpolation, of which there may be zero.

zahlman 8 days ago | parent | prev [-]

> As for the logic, I would still use pattern matching for branching and destructuring, but I’d put it in a helper.

I wrote it up (https://news.ycombinator.com/item?id=43650001) before reading your comment :)

zahlman 8 days ago | parent | prev | next [-]

A compromise version:

    def _f_part(item) -> str:
        match item:
            case str() as s:
                return s
            case Interpolation(value, _, conversion, format_spec):
                return format(convert(value, conversion), format_spec)

    def f(template: Template) -> str:
        return ''.join(map(_f_part, template))
The `match` part could still be written using Python's if-expression syntax, too. But this way avoids having very long lines like in the Ruby example, and also destructures `item` to avoid repeatedly writing `item.`.

I very frequently use this helper-function (or sometimes a generator) idiom in order to avoid building a temporary list to `.join` (or subject to other processing). It separates per-item processing from the overall algorithm, which suits my interpretation of the "functions should do one thing" maxim.

Mawr 8 days ago | parent | prev | next [-]

The Python version is straightforward to read and understand to a programmer of any language. The Ruby version is an idiosyncratic symbol soup.

If I were tasked to modify the Python version to say, handle the case where `item` is an int, it would be immediately obvious to me that all I need to do is modify the `match` statement with `case int() as i:`, I don't even need to know Python to figure that out. On the other hand, modifying the Ruby version seems to require intimate knowledge of its syntax.

pansa2 8 days ago | parent [-]

I think for someone with a basic knowledge of both languages, the Ruby version is more understandable than the Python. It's a combination of basic Ruby features, whereas Python's `match` statement is much more obscure - it isn't really Python at all, it's "a DSL contrived to look like Python [...] but with very different semantics" [0].

I don't particularly love the Ruby code either, though - I think the ideal implementation would be something like:

    fn stringify(item) =>
        item.is_a(Interpolation) then
            item.value.convert(item.conversion).format(item.format_spec)
        else item.to_string()

    fn f(template) => template.map(stringify).join()
[0] https://discuss.python.org/t/gauging-sentiment-on-pattern-ma...
slightwinder 8 days ago | parent | prev | next [-]

> Is this what idiomatic Python has become?

What do you mean? Python has always been that way. "Explicit is better than implicit. [..] Readability counts." from the Zen of python.

> By contrast, here's the equivalent Ruby:

Which is awful to read. And of course you could write it similar short in python. But it is not the purpose of a documentation to write short, cryptic code.

pansa2 8 days ago | parent [-]

> Readability counts

Almost all Python programmers should be familiar with list comprehensions - this should be easy to understand:

    parts = [... if isinstance(item, Interpolation) else ... for item in template]
Instead the example uses an explicit loop, coupled with the quirks of the `match` statement. This is much less readable IMO:

    parts = []
    for item in template:
        match item:
            case str() as s:
                parts.append(...)
            case Interpolation(value, _, conversion, format_spec):
                parts.append(...)
> [Ruby] is awful to read

I think for someone with a basic knowledge of Ruby, it's more understandable than the Python. It's a combination of basic Ruby features, nothing advanced.

I don't particularly love Ruby's syntax either, though - I think the ideal implementation would be something like:

    fn stringify(item) =>
        item.is_a(Interpolation) then
            item.value.convert(item.conversion).format(item.format_spec)
        else item.to_string()

    fn f(template) => template.map(stringify).join()
slightwinder 7 days ago | parent [-]

> Almost all Python programmers should be familiar with list comprehensions

Being familiar doesn't mean it's readable. They can be useful, but readability is usually not on that list.

> I think for someone with a basic knowledge of Ruby, it's more understandable than the Python.

I know both, and still consider it awful. Readability is not about making it short or being able to decipher it.

pphysch 8 days ago | parent | prev [-]

That Ruby code is clever and concise, and terrible to read and extend

pansa2 8 days ago | parent [-]

IMO it's much closer to the ideal way to write the function, which would be something like:

    fn stringify(item) =>
        item.is_a(String) then item else
        item.value.convert(item.conversion).format(item.format_spec)

    fn f(template) => template.map(stringify).join()
pphysch 8 days ago | parent [-]

By what definition of "ideal"? You just hid all the complexity in those undefined `convert` and `format` methods.

pansa2 8 days ago | parent [-]

The original code does the same! All the real work is inside `convert` and `format`, it just adds another layer of complexity on top.

The ideal version has the same behaviour and shows that the extra complexity is unnecessary.