Skip to content
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

Add Resolved Values and Function Handler sections to formatting #728

Merged
merged 14 commits into from
Oct 7, 2024
55 changes: 45 additions & 10 deletions spec/formatting.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,16 @@ At a minimum, it includes:

Implementations MAY include additional fields in their _formatting context_.

## Expression and Markup Resolution
## Resolved Values

_Expressions_ are used in _declarations_, _selectors_, and _patterns_.
_Markup_ is only used in _patterns_.
This specification allows for the same value to be used for:
eemeli marked this conversation as resolved.
Show resolved Hide resolved
- formatting in a _placeholder_,
Copy link
Collaborator

Choose a reason for hiding this comment

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

That's not right, I think. The result of formatting is a string or some kind of a "formatted part" type. When talking about resolved values, I think we want to identify the intermediate type that's the result of evaluating the placeholder, before it's formatted.

Copy link
Member

Choose a reason for hiding this comment

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

I think the key thing here is the difference between the output of an expression and the evaluation of an expression. The "resolved value" is only important (or even visible) when it will be consumed by another expression later.

I think @stasm is correct that the output of formatting (or selection) is not the "resolved value". In the case of formatting, it is the string representation (or "formatted foo" parts representation), which is different from the "resolved value". Similarly, the output of selection is a filtered and stack ranked list of patterns (which is definitely not the "resolved value").

The "resolved value", to me, means "what is the value of the operand after the expression has been evaluated".

If I have a message You have {$x :integer} wildebeest, the value of $x is constrained to be an implementation-defined numeric type (it might not be an integer type, even though the formatting function formats it like one), but the output of the expression is a string.

I think that each function needs to explicitly say whether it "covers up" (changes) the operand's resolved value and what it changes it to be. Using my example:

Function: :integer

  • Operand: an implementation-defined type or a literal whose contents match the number-literal production in the ABNF
  • Format output: a string
  • Selection output: number selector (see design number-selection)
  • Resolved value: an implementation-defined numeric type

Notice that this eliminates the string representation between the operand (input) and resolved value. It also, probably, allows the implementation defined type to be changed by the function.

Copy link
Member

Choose a reason for hiding this comment

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

Operand: an implementation-defined type or a literal whose contents match the number-literal production in the ABNF

There is a third kind of entity that can be an operand.

An operand is a defined type, or literal, or the "result of a previous functional application" (call that a function-result for now). A function-result is (logically) not just an implementation-defined datatype. If so, it wouldn't have access to the information that any composing function would need.

Instead, I think the best conceptual model of a function-result is that of an object with methods to get information. Such as (of course, the names can be chosen to protect the innocent):

  • getSourceFunction() —the function used to create it
  • getSourceOperand() — the operand used to create it
  • getsourceOptions() — the options used to create it
  • getFormattedString()
  • getFormatToPartsStructure()
  • getSelector()
  • getResolvedValue()

Internally, it can lazy-evaluate. For example, it only needs to generate the formatted string if the method getFormattedString() is called. If the function-result is only used for selection, that never needs to be generated. If the function-result is passed to an :uppercase function, then the getResolvedValue() would never need to be called, etc.

In particular a function can look at its operand, and if it is a function-result, see what options it was passed and use them if the type of the function is known.

The description of a function needs to specify what its inputs are, and how its function-result behaves. Part of that involves specifying which functions it can compose with, and how it treats that function's operand and options.

- selection in a _selector_ _expression_,
- as the _operand_ of another _expression_, or
eemeli marked this conversation as resolved.
Show resolved Hide resolved
- as the _option_ value of another _expression_,
eemeli marked this conversation as resolved.
Show resolved Hide resolved
eemeli marked this conversation as resolved.
Show resolved Hide resolved

To support this, the _**resolved value**_ of each _expression_
is an implementation-dependent value that supports some or all of the above use cases.

In a _declaration_, the resolved value of the _expression_ is bound to a _variable_,
which is available for use by later _expressions_.
Expand All @@ -106,16 +112,45 @@ In a _pattern_, the resolved value of an _expression_ or _markup_ is used in its
The form that resolved values take is implementation-dependent,
and different implementations MAY choose to perform different levels of resolution.

> For example, the resolved value of the _expression_ `{|0.40| :number style=percent}`
> could be an object such as
> While this specification does not require it,
> a _resolved value_ could be implemented by requiring each function implementation to
> return a value matching the following interface:
eemeli marked this conversation as resolved.
Show resolved Hide resolved
>
> ```ts
> interface MessageValue {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Overall the direction seems OK.
But I am uneasy with specifying this interface.
Because:

  • in "intertwines" even more the formatters and selectors (which I always maintained are different concepts, with different interfaces)
  • it is just specified, without explaining how to use it, how it works.

So it feels over-specified.

> formatToString(): string
> formatToX(): X // where X is an implementation-defined type
Copy link
Collaborator

Choose a reason for hiding this comment

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

I may have missed some discussions about this, but why wouldn't we want to be more specific here about the parts?

Suggested change
> formatToX(): X // where X is an implementation-defined type
> formatToParts(): IterableIterator<MessagePart>; // MessagePart is implementation-defined.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As this is a non-normative example and we've not reached consensus on formatted parts, I'd rather not introduce them here implicitly and somewhat out of context, when the key aspect of the formatToX() inclusion is to remind the reader that a string is not the only possible formatting target.

> getValue(): unknown
> resolvedOptions(): { [key: string]: unknown }
eemeli marked this conversation as resolved.
Show resolved Hide resolved
> selectKeys(keys: string[]): string[]
> }
> ```
> { value: Number('0.40'),
> formatter: NumberFormat(locale, { style: 'percent' }) }
> ```
>
> Alternatively, it could be an instance of an ICU4J `FormattedNumber`,
> or some other locally appropriate value.
> With this approach:
> - An _expression_ could be used as a _placeholder_ if
> calling the `formatToString()` or `formatToX()` method of its _resolved value_
> did not throw an error.
eemeli marked this conversation as resolved.
Show resolved Hide resolved
> - An _expression_ could be used as a _selector_ _expression_ if
> calling the `selectKeys(keys)` method of its _resolved value_
> did not throw an error.
eemeli marked this conversation as resolved.
Show resolved Hide resolved
> - Using a _variable reference_, the resolved value of an _expression_
> could be used as an _operand_ or _option_ value if
> calling the `getValue()` method of its _resolved value_ did not throw an error.
eemeli marked this conversation as resolved.
Show resolved Hide resolved
> In this use case, the `resolvedOptions()` method could also
> provide a set of option values that could be taken into account by the called function.
>
> Extensions of the base `MessageValue` interface could be provided for different data types,
> such as numbers or strings,
> for which the `unknown` return types of `getValue()` and `resolvedOptions()`
eemeli marked this conversation as resolved.
Show resolved Hide resolved
> could be narrowed appropriately.
> An implementation could also allow `MessageValue` values to be passed in as input variables,
> or automatically wrap each variable as a `MessageValue` to provide a uniform interface
catamorphism marked this conversation as resolved.
Show resolved Hide resolved
> for custom functions.

## Expression and Markup Resolution

_Expressions_ are used in _declarations_, _selectors_, and _patterns_.
_Markup_ is only used in _patterns_.

Depending on the presence or absence of a _variable_ or _literal_ operand
and a _function_, _private-use annotation_, or _reserved annotation_,
Expand Down