Code Monkey home page Code Monkey logo

Comments (13)

djc avatar djc commented on August 23, 2024

One important limitation here is that so far, I've carefully kept Askama's template grammar separate from the Rust grammar. Both if let and full match syntax would probably require relinquishing that, making the parser more complex and also require changes to stay up to date with Rust.

I do think if foo is some with that compiling to the if let as you write is a pretty good fit. It could expand to some, none, ok or err to cover your base cases. Still wondering if there should be some explicit naming of the "unpacking" in the syntax (maybe if foo is some as bar?).

from askama.

anowell avatar anowell commented on August 23, 2024

Your grammar philosophy makes sense to me. I've been reflecting on your comment for a bit and I can't think of a better alternative for explicit unpacking, though I'd slightly prefer the as bar to be optional.

The trickier case is Result...

{% if foo is ok %}
    Sucess: {{ foo }}
{% else as err %}
    Error: {{ err }}
{% end %}

or would you have to use {% else if foo is err as err %}? (note: I don't currently have any use for Result, I'm just trying to think through how this syntax might extend cleanly)

from askama.

djc avatar djc commented on August 23, 2024

Hmm, yeah, that makes it pretty ugly. On the other hand, how would it work if you access an attribute or method that returns Option? I don't think shadowing the original expression works out well in that case. I think that makes naming it a requirement?

from askama.

anowell avatar anowell commented on August 23, 2024

ah.. good catch on the shadowing problem for attributes/methods. You'd certainly have to identify names separate from more complex expressions, which is a good argument for requiring explicit names. And even though I still like the implicit shadow form, even if let Some(foo) = foo requires redundantly stating the shadow name, so I'm persuaded.

I'm curious though what you think the else unpacking for Result should look like? I feel like I've been thinking about this all day and my brain isn't coming up with any clear winner.

from askama.

despawnerer avatar despawnerer commented on August 23, 2024

Throwing in my own $.02, I'd like to say that if some sort of is construct was adopted, I'd personally greatly prefer it if it was a generic construct akin to what Jinja does โ€”ย semantically boolean checks. I've always found them to be very pleasant to use in Jinja. The actual implementation wouldn't have to return a literal bool though, could return an enum with an attached value, for example, something like Either with one value being true and the other false, to facilitate the type of unpacking you're discussing.

from askama.

djc avatar djc commented on August 23, 2024

I thought of another problem: current match (at least until this RFC lands) is pretty picky about exact typing, for example in terms of how many references. From what we've seen so far, Askama is pretty reliant on coercion to solve these problems, so a dumbed down syntax for match probably won't work so well.

As for unpacking Err, I haven't come up with anything better than else if foo is err as err either. I do think there are more design directions to explore though: specialized blocks for Option and Result, for example, as in {% option %}.

As a general principle, I think one the other principles/goals for Askama (next to the "stay away from full Rust syntax") is it should be easy to see how it translates into Rust code. I think else if foo is err as err does okay on that, at least, as it is quite similar to an if let construct. If we think about it, the main win is terms of conciseness is really something match-like such that you don't have to repeat the foo part.

from askama.

anowell avatar anowell commented on August 23, 2024

@despawnerer - I don't fully follow your suggestion. Can you provide an example of returning an "enum with attached value"?

@djc:

I think you can still achieve coersion with tricks like (&foo).as_ref(), since I believe the method call should auto-deref up to the type needed by the method, and as_ref should then just return &Option if foo was Option, &Option, or &&&&&Option.

I've chewed a lot on more generic match ideas, and finally found one that I don't hate. Here's an idea that could be generalized to a lot of enums:

{% match foo %}
{% when some val %}
    Found {{ val }}
{% when none %}
    Not found
{% end %}

Notes/Variations:

  1. match grammar is match EXPR [as TYPE] - I imagine type being optional for types where variants are already in the prelude (e.g. Result and Option), but if you have an enum Color { Red, ... }, you'd use match foo as color when red. If you just want to start with a minimal solution for Option and Result, you could omit the [as TYPE] and it'd be backward compatible to add later to support other types.
  2. when grammar is when VARIANT [ARG[, ARG]*][ or VARIANT [ARG[, ARG]*]]* where each arg unpacks a tuple variant's next arg. Then or can be used to capture multiple variants in the same arm (i.e. Rust's |). You could get extra complex and allow match guards, like when ok num if num > 32, but probably good to start simple and skip that. You could also skip the or clause.
  3. you might allow combining the first lines into {% match foo when some val %}
  4. I suggested camel_case enum and variant types because that's consistent with our conversation up to this point. If supporting just Option and Result, these keywords can be hard-coded. If supporting more enums, this would involve auto-converting the type and variant to PascalCase, though that would prevent using enums that don't follow standard naming conventions (probably fine?). The alternative is to just use the casing that is consistent with rust (e.g. match foo as Option when Some val)
  5. You could switch match grammar to match [TYPE] EXPR. That was my original idea since as feels suggestive of coersion, but in some cases the as type feels less ambiguous. (e.g. match color as color vs match color color)
  6. else could be the _ => catch all variant.
  7. this doesn't work well for the non-exhaustive if let case. You could have an empty else block, or add some keyword somewhere that implies non-exhaustive matching, or also include a syntax like if foo is some val to handle that case.

Here's a more complex example:

enum Color { 
    Rgb(u32, u32, u32),
    Rgba(u32, u32, u32, u32),
    GrayScale(u32), 
    Cmyk(u32, u32, u32, u32)
}
 
{% match item.get_color() as color %}
{% when rgb red, green, blue or rbga red, green, blue %}
    Colorful ({{red}}, {{green}}, {{blue}}) (alpha ignored)
{% when gray_scale val %}
    Gray ({{val}}, {{val}}, {{val}})
{% else %}
    Sorry, we don't yet support this color system
{% end %}

from askama.

djc avatar djc commented on August 23, 2024

@anowell

I don't think AsRef is a generic solution to the problem, it requires the user's types to actually implement AsRef. So it may solve your problem in this case, but it's not a general solution to resolving the issue we have in Askama with coercion especially for dereferencing. I believe this is a significant open problem for this issue.

Based on your proposal, I have a counter proposal, which looks like this:

{% match item.get_color() %}
  {% arm Rgb -> (red, green, blue) | Rgba -> (red, green, blue) %}
    Colorful ({{red}}, {{green}}, {{blue}}) (alpha ignored)
  {% arm GrayScale -> val %}
    Gray ({{val}}, {{val}}, {{val}})
  {% else %}
    Sorry, we don't yet support this color system
{% end %}

Non-exhaustive version:

{% if match Rgb -> (red, green, blue) = item.get_color() %}
    Colorful ({{red}}, {{green}}, {{blue}}) (alpha ignored)
{% else if match GrayScale -> val %}
    Gray ({{ val }}, {{ val }}, {{ val }})
{% else %}
    Unsupported
{% endif %}

Responding to your notes:

  1. I don't understand the need that your as on match fulfills. Rust's match keyword doesn't need an as, right? If this is in response to my remarks earlier about shadowing, as far as I understand that's only needed for the variant arguments, not the matched-against expression.
  2. when is unfamiliar, whereas arm is used in Rust vocabulary to describe the arms. It's nice to have something more familiar for this. when just reminds me of XSLT... Next, I really want some operator in there to connect the variant to the arguments instead of having them just separated by a space. I think -> works nicely for this, what do you think? In Rust it has a somewhat different meaning, but the context is different enough that I hope it's not confusing. => could work but is used differently in a very similar context in Rust, which makes it a lesser choice IMO. The challenge for me is that there's two somewhat counteracting forces going on: (1) that Rust's syntax acts as if the arguments are stored "in" the variant, (2) that the variant argument values are assigned to the local names. There's a fine line here about having a clear match to Rust's syntax, but not aping it too closely as to prevent confusion. I think I'd also want to keep Rust's | for or, similar to what I've done so far with the other operators.
  3. Do you see this as the non-exhaustive (if let) syntax, or some third way?
  4. If we special-case Option and Result we can name them differently to make it fit in with other syntax, but for the general case I really don't want to case-convert names. So maybe we should stay away from translating names in general (at least for this).
  5. Not sure what this is about, exactly.
  6. Using else for _ sounds good.
  7. I think else as non-exhaustive is exactly right, then you add if VARIANT -> ARG again if you want to do more matching.

One possibly interesting alternative is to introduce another block type, like this:

{% match item.get_color() %}
  {= Rgb -> (red, green, blue) | Rgba -> (red, green, blue) =}
    Colorful ({{red}}, {{green}}, {{blue}}) (alpha ignored)
  {= GrayScale -> val =}
    Gray ({{val}}, {{val}}, {{val}})
  {= else =}
    Sorry, we don't yet support this color system
{% end %}

It is a bit more concise, but I'm not sure it's easier to understand.

from askama.

anowell avatar anowell commented on August 23, 2024

Re: Generic solution: I think Borrow should work as it's implemented for basically everything. That said, it sounds like the match bindings mode stuff is very close to landing in nightly, so it could just wait for that (maybe get implemented behind a nightly feature flag).

Re: arm vs when vs whatever: when reads very natural to me having worked enough with ruby's case and coffeescript's switch. case from Scala's match also reads naturally to me. Rust's match arms don't have a keyword with them so I just picked the word that read most naturally as English to me. arm sounds fine too, though "when" feels grammatically more similar to "else" (when-else vs arm-else). {= ... =} feels conflicting for me: it reminds me of ERB's interpolation syntax, and makes me wonder if the = is for comparison or assignment. Other choices include {@ ... @} and {%@ ... %} since @ already implies "pattern binding". But in general, my preference isn't strong enough for me to really try and defend one of these; I could happily get used to any of these. It's your bike shed. ;-)

Jumping into the numbered points:

  1. I proposed the as keyword as a way to figure out the type being matched for enums NOT in the std::prelude. arm Rgb -> (r,g,b) needs to generate the arm: Color::Rgb(r,g,b) => (unless you already have Color::* in scope which doesn't seem to be common in Rust). So you can either prefix the type with every variant {% arm Color::Rgb -> (r,g,b) %}, or have a single as Color that specifies the type that Askama can prepend to all the variants. This doesn't apply when matching Option, Result and non-enum types like strings and numbers.

  2. At first I thought -> was a typo because I expected to see =>. Then I realized that it was actually doing something very different. IMO, it feels too unlike anything else in Askama's grammar, too unlike Rust, and reuses an existing rust symbol with a different meaning (in Rust, I read -> as "returns"). I feel like you should either go more rust-like with Rgb(red, green, blue) (but this naturally means my mental model will think that I can use full Rust syntax in the arm, like _) or find something that feels more like other Askama/Jinja constructs. I could bikeshed this all day long, so here's a table comparing some ideas:

Rgb red, blue, green          Some foo        // my previous suggestion
Rgb -> (red, blue, green)     Some -> (foo)   // your suggestion
Rgb(red, blue, green)         Some(foo)       // rust-like
Rgb with red, blue, green     Some with foo
Rgb with (red, blue, green)   Some with (foo)
Rgb yield red, blue, green    Some yield foo
Rgb yield (red, blue, green)  Some yield (foo)
(red, blue, green) in Rgb     foo in Some
(red, blue, green) @ Rgb      foo @ Some      // also guilty of changing the meaning of a rust symbol
  1. This was just a way to collapse 2 {% %} blocks into one line. Definitely not necessary. And I think match should always be exhaustive... Something like if let or if match should handle the non-exhaustive case. I think we're basically in agreement there from your examples.

  2. Either approach seems fine. My only vote here would be consistency. (GrayScale and Some) or (gray_scale and some)

  3. I can see how this wouldn't have made sense if you didn't see the need for as Type. But this was just an alternative syntax for the optional as Type.

6/7. Nothing to add.

from askama.

djc avatar djc commented on August 23, 2024

I haven't worked with Borrow, and the documentation doesn't say much about how it applies to this use case (abstracting over arbitrary number of references). We'd need some good tests.

I think you've convinced me that when is better than arm, so let's go with that for now.

  1. I understand the idea now and it makes sense to me, but I'm not sure yet about the as keyword. It feels too different from how as is used in Rust. Maybe from is a better fit? We have to connect expression to type to variant. Also, how does it work when matching a tuple? I guess we could defer this question for now to say that the variant can be a full path (like Color::Rgb).

  2. Alright, so not my suggestion. I feel that the variant should probably be named before the arguments, agree? I think Rgb with (red, blue, green) looks okay, though then I kind of want to bikeshed the keyword more, maybe from as well? The more I look at alternatives, the more I want to stay with with, though. I think parentheses should be mandatory except in the single-argument case.

  3. Alright, clear.

  4. Right.

  5. See 1.

from askama.

anowell avatar anowell commented on August 23, 2024

In trying to create a quick example of how the Borrow auto-deref it would work, I realized that the compiler won't auto-deref when resolving trait implementations, so impl Trait<T> for &T doesn't imply impl Trait<T> for &&&&T, so trying to use method-calling for arbitrary auto-deref wouldn't work for AsRef or Borrow. So I think we're stuck with a limited form of match and/or waiting for match modes to land (which sounds like it could be in nightly very soon).

with is also my current favorite inside match arms. I think your idea of mandatory parentheses except for single argument is compelling. ๐Ÿ‘

I agree that as suggests value coercion or type renaming. IMO, from suffers a similar problem of implying conversion (though less so since it's not a keyword). Other ideas include:

match foo : Color       // like type ascription
match foo use Color     // hints at bringing Color's variants in scope
match foo using Color   // similar to above, but doesn't change the meaning of use
match foo with Color    // this might conflate the meaning of 'with' in the match arms

Anyhow, I agree that deferring this decision is a good choice. It's not needed for the common cases of Option or Result, and it could be be add backward compatibly in the future if wanted.


So I think these examples basically summarize the current state of our design discussion:

// Basic Option
{% match get_item() %}
  {% when Some with val %}
    Found {{ val }}
  {% when None %}
    Not found
{% end %}

// Advanced enum
{% match item.get_color() %}
  {% when Color::Rgb with (red, green, blue) | Color::Rgba with (red, green, blue) %}
    Colorful ({{red}}, {{green}}, {{blue}}) (alpha ignored)
  {% when Color::GrayScale with val %}
    Gray ({{val}}, {{val}}, {{val}})
  {% else %}
    Sorry, we don't yet support this color system
{% end %}

from askama.

djc avatar djc commented on August 23, 2024

Nice! So, do you want to take a stab at implementing it? ๐Ÿ˜„

from askama.

djc avatar djc commented on August 23, 2024

This has long been done.

from askama.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.