Skip to content

[DESIGN] Effect of selectors on placeholders #755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
May 6, 2024
360 changes: 360 additions & 0 deletions exploration/selection-declaration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
# Effect of Selectors on Subsequent Placeholders

Status: **Proposed**

<details>
<summary>Metadata</summary>
<dl>
<dt>Contributors</dt>
<dd>@aphillips</dd>
<dt>First proposed</dt>
<dd>2024-03-27</dd>
<dt>Pull Requests</dt>
<dd>#000</dd>
</dl>
</details>

## Objective

_What is this proposal trying to achieve?_

Define what effect (if any) the _annotation_ of a _selector_ has on subsequent _placeholders_
that access the same _variable_.

## Background

_What context is helpful to understand this proposal?_

In MF2, we require that all _selectors_ have an _annotation_.
The purpose of this requirement is to help ensure that a _selector_ on a given _operand_
is working with the same value as the _formatter_ eventually used for presentation
of that _operand_.
This is needed because the format of a value can have an effect on the grammar used
in the localized _message_.

For example, in English:

> You have 1 mile to go.
> You have 1.0 miles to go.

These messages might be written as:

```
.input {$togo :integer}
.match {$togo}
0 {{You have arrived.}}
one {{You have {$togo} mile to go.}}
* {{You have {$togo} miles to go.}}

.input {$togo :number minimumFractionDigits=1}
.match {$togo}
0 {{You have arrived.}}
one {{Unreachable in an English locale.}}
* {{You have {$togo} miles to go.}}
```

It is tempting to want to write these as a shorthand, with the _annotation_ in the _selector_:

```
.match {$togo :integer}
0 {{You have arrived.}}
one {{You have {$togo} mile to go.}}
* {{You have {$togo} miles to go.}}
```

## Use-Cases

_What use-cases do we see? Ideally, quote concrete examples._

1. As a user, I want my formatting to match my selector.
This is one of the reasons why MF2 requires that selectors be annotated.
When I write a selector, the point is to choose the pattern to use as a template
for formatting the value being selected on.
Mismatches between these are undesirable.

```
.match {$num :number minimumFractionDigits=1}
one {{This case can never happen in an English locale}}
* {{I expect this formats num with one decimal place: {$num}}}
```

2. As a user, I want to use the least amount of MF special syntax possible.
3. As a user, I don't want to repeat formatting, particularly in large selection matrices.
```
.match {$num1 :integer} {$num2 :number minimumFractionDigits=1}
0 0 {{You have {$num1 :integer} ({$num2 :number minimumFractionDigits=1}) wildebeest.}}
0 one {{You have {$num1 :integer} ({$num2 :number minimumFractionDigits=1}) wildebeest.}}
0 * {{You have {$num1 :integer} ({$num2 :number minimumFractionDigits=1}) wildebeest.}}
one 0 {{ }}
one one {{ }}
one * {{ }}
// more cases for other locales that use two/few/many
* 0 {{ }}
* one {{ }}
* * {{ }}
```

4. As a user (especially as a translator), I don't want to have to modify
declarations and selectors to keep them in sync.
```
.input {$num :number minimumFractionDigits=1}
.match {$num}
* {{Shouldn't have to modify the selector}}
```
> Note that this is currently provided for by the spec.

5. As a user, I want to write multiple selectors using the same variable with different annotations.
How do I know which one will format the placeholder later?
```
.match {$num :integer} {$num :number minimumFractionDigits=2}
* * {{Which selector formats {$num}?}}

.match {$num :number minimumFractionDigits=2} {$num :integer}
* * {{Which selector formats {$num}?}}
```

If both formats are needed in the message (presumably they are or why the selector),
how does one reference one or the other?


6. As a user I want to use the same operand for both formatting and selection,
but use different functions or options for each.
I don't want the options used for selection to mess up the formatting.

For example, while LDML45 doesn't support selection on dates,
it's easy to conceptualize a date selector at odds with the formatter:
```
.input {$d :datetime skeleton=yMMMdjm}
.match {$d :datetime month=numeric}
1 {{Today is {$d} in cold cold {$d :datetime month=long} (trying to select on month)}}
* {{Today is {$d}}}
```

Note that users can achieve this effect using a `.local`:
```
.input {$d :datetime skeleton=yMMMdjm}
.local $monthSelect = {$d :datetime month=numeric}
.match {$monthSelect}
1 {{No problem getting January and formatting {$d}}}
* {{...}}
```

## Requirements

_What properties does the solution have to manifest to enable the use-cases above?_

## Constraints

_What prior decisions and existing conditions limit the possible design?_

## Proposed Design

_Describe the proposed solution. Consider syntax, formatting, errors, registry, tooling, interchange._

## Alternatives Considered

_What other solutions are available?_
_How do they compare against the requirements?_
_What other properties they have?_

### Do nothing

In this alternative, selectors are independent of declarations.
Selectors also do not affect the resolved value.

Examples:
```
.input {$n :integer}
.match {$n :number minimumFractionDigits=2}
* {{Formats '$n' as an integer: {$n}}}

.match {$n :integer}
* {{If $n==1.2 formats {$n} as 1.2 in en-US}}
```

**Pros**
- No changes required.
- `.local` can be used to solve problems with variations in selection and formatting
- Supports multiple selectors on the same operand

**Cons**
- May require the user to annotate the operand for both formatting and selection.
- Can produce a mismatch between formatting and selection, since the operand's formatting
isn't visible to the selector.

### Allow both local and input declarative selectors with immutability

In this alternative, we modify the syntax to allow selectors to
annotate an input variable (as with `.input`)
or bind a local variable (as with `.local`).
Either variable binding is immutable and results in a Duplicate Declaration error
if it attempts to annotate a variable previously annotated.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if it attempts to annotate a variable previously annotated.
if it attempts to re-declare a previously declared variable.


Example:
```
.match {$input :function} $local = {$input :function}
* * {{This annotates {$input} and assigns {$local} a value.}}

.match $local1 = {$input :function} $local2 = {$input :function2}
* * {{This assigns two locals}}

.input {$input :function}
.local $local = {$input :function}
.match {$input :function} {$local :function}
* * {{This produces two duplicate declaration errors.}}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? I only see one declaration for each of the two variables.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.input {$input} and .match {$input} both "declare" $input in this design. Ditto the .local and later selector.

```

The ABNF change looks like:
```abnf
selector = expression / declaration
declaration = s variable [s] "=" [s] expression
```

**Pros**
- Shorthand is consistent with the rest of the syntax
- Shorthand version works intuitively with minimal input
- Preserves immutability
- Produces an error when users inappropriately annotate some items

**Cons**
- Selectors can't provide additional selection-specific options
if the variable name is already in scope
- Doesn't allow multiple selection on the same operand, e.g.
```
.input {$date :datetime skeleton=yMdjm}
.match {$date :datetime field=month} {$date :datetime field=dayOfWeek}
* * {{This message produces a Duplicate Declaration error
even though selection is separate from formatting.}}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with the previous example, I'm confused. I thought that in this alternative, you need the '=' sign for a declaration, and the .match here wouldn't introduce any declarations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-titled this option. In this option, we do not have the .local-like feature. These selectors behave exactly like .input declarations. A .input is a declaration too and doesn't use an = sign.

```
However, this design does allow for a local variable to be easily created
for the purpose of selection.

### Allow _immutable_ input declarative selectors

In this alternative, selectors are treated as declaration-selectors.
That is, an annotation in a selector works like a `.input`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't seem right to me. .input can only take an argument (free variable), but I think what you're proposing is for selectors to also shadow local variables (bound variables).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. Only the first (well, now second after "do nothing") option provides a local variable binding (which alters the syntax!)

This permits `.match` selectors to be a shorthand when no declarations exist.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what this means.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A selector works like .input in that it annotates the operand with a function and its options.
This later affects the formatting.

When you use this feature, you don't have to write a .input. That is, these are equivalent, with the first being a shorthand of the second:

.match {$n :integer}
* {{Your answer is {$n}}}

.input {$n :integer}
.match {$n}
* {{Your answer is {$n}}}

The option does not permit local variable declaration.

It is not an error to re-declare a variable that is in scope.
Instead the selector's annotation replaces what came before.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Instead the selector's annotation replaces what came before.
Instead the selector implicitly shadows the name of its variable operand.

(This is another place where the distinction between literal and variable operands is important. You can shadow variables, but changing the value of a literal doesn't make sense.)


```
.input {$num :integer}
.match {$num :number minimumFractionDigits=1}
* {{Formats {$num} like 1.0}}
```

**Pros**
- Shorthand version works intuitively with minimal typing.

**Cons**
- Violates immutability that we've established everywhere else

### Allow _mutable_ input declarative selectors

In this alternative, selectors are treated as declaration-selectors.
That is, an annotation in a selector works like a `.input`.
However, it is an error for the selector to try to modify a previous declaration
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
However, it is an error for the selector to try to modify a previous declaration
However, it is an error for the selector to shadow a previous declaration

(just as it is an error for a declaration to try to modify a previous declaration).
This permits `.match` selectors to be a shorthand when no declarations exist.

It is also an error for a selector to modify a previous selector.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
It is also an error for a selector to modify a previous selector.
It is also an error for a selector to shadow a previous selector.

(Again, this needs some qualifiers... writing .match {-1 :integer} {-1 :number} shouldn't be an error.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example you gave doesn't modify a previous selector: the literal -1 in the first is a separate literal from that in the second. "Shadow" doesn't really say what is going on?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a selector implicitly introduces a name, then what you're referring to as "modifying" is actually "shadowing". If you write, in this scenario:

.match {$x :integer} {$x :number} 

then the second $x doesn't modify the first one. Rather, it introduces a new name $x, so that in the patterns in the variants, $x now has a different meaning. That's what I mean by name shadowing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$x (a variable by the name of x in the formatting context) is different from the literal -1 (which is just a string--in your example, the second one is a different string that contains the same characters as the first).

I'm steering clear of "shadowing" because the thing that is or is not affected is the (ill-defined) "resolved value" in the formatting context (which I tend to think of as a Map<String,ResolvedValue>). I think it is clearer to say that the second $x in your example is "trying to modify" the resolved value of $x in the formatting context.

This implies that multiple selecton on the same operand is pointless.

```
.match {$num :integer}
* {{Formats {$num} as integer}}

.input {$num :integer}
.match {$num :number maximumFractionDigits=0}
* {{This message produces a Duplicate Declaration error}}

.input {$num :integer} {$num :number}
* * {{This message produces a Duplicate Declaration error}}
```

**Pros**
- Shorthand version works intuitively with minimal typing
- Preserves immutability
- Produces an error when users inappropriately annotate some items

**Cons**
- Selectors can't provide additional selection-specific options
if the value has already been annotated
- Doesn't allow multiple selection on the same operand, e.g.
```
.input {$date :datetime skeleton=yMdjm}
.match {$date :datetime field=month} {$date :datetime field=dayOfWeek}
* * {{This message produces a Duplicate Declaration error
even though selection is separate from formatting.}}
```

### Match on variables instead of expressions

In this alternative, the `.match` syntax is simplified
to work on variable references rather than expressions.
This requires users to declare any selector using a `.input` or `.local` declaration
before writing the `.match`:

```
.input {$count :number}
.match $count
one {{You have {$count} apple}}
* {{You have {$count} apples}}

.local $empty = {$theList :isEmpty}
.match $empty
true {{You bought nothing}}
* {{You bought {$theList}!}}
```

The ABNF change would look like:
```diff
match-statement = match 1*([s] selector)
-selector = expression
+selector = variable
```

**Pros**
- Overall the syntax is simplified.
- Preserves immutability.

**Cons**
- A separate declaration is required for each selector.

### Provide a `#`-like Feature

(Copy-pasta adapted from @eemeli's proposal in #736)

Make the `.match` expression also act as implicit declarations accessed by index position:

```
.match {$count :number}
one {{You have {$0} apple}}
* {{You have {$0} apples}}
```

Assigning values to `$0`, `$1`, ... would not conflict with any input values,
as numbers are invalid `name-start` characters.
That's by design so that we encourage at least _some_ name for each variable;
here that's effectively provided by the `.match` expressions.

ABNF would be modified:
```diff
-variable = "$" name
+variable = "$" (name / %x30-39)
```

...with accompanying spec language making numeric variables resolve to the `.match` selectors in placeholders,
and a data model error otherwise.

**Pros**
- More ergonomic for most `.input` cases
- Enables representation of many messages without any declarations

**Cons**
- Confusing that the operand name can't be used in the pattern?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the operand name have to be disallowed from appearing in the pattern?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps not worded correctly. You can use the operand name in the pattern, but it's annotation won't/might not be applied.

.match {$count :integer}
* {{You have {$count} apples might print 2 or 2.0 with input count=|2.0|}}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so maybe something like: "The meaning of the operand name in the context of the pattern might not be what the user expects"?

Removes some self-documentation from the pattern.
- Requires the pattern to change if the selectors are modified.
- Limits number of referenceable selectors to 10 (in the current form)