message-format-wg icon indicating copy to clipboard operation
message-format-wg copied to clipboard

Spec clarification: resolving the type when chaining local variables

Open mihnita opened this issue 3 years ago • 12 comments

Here is an example:

let $foo = {$count :number currency=$cur precision=2 rounding=up}
let $pers = {$foo :person level=formal}
let $bar = {$pers :date skeleton=yMMMdE}
match {$bar} ...

This would take $count, formats it as currency, then we try to format as person, then as date, and them make a decision on it.

Is there a good use case to do such chaining? Or we are only struggling to support something that is useless, and potentially dangerous?

A more reasonable restriction would be that the "chained" variables use the same function:

let $fullDate = {$exp :date year=numeric month=full day=numeric}
let $shortDate = {$fullDate :date month=numeric}

In this case the reasonable behavior would be to merge the two maps of options and format with :date. Although even that is a bit problematic: what if I want to remove a field from the first variable?

Are we making things complicated for little benefit? (saving a copy paste once in a blue moon)

It is a bit like C macros. One level you can put up with. Go macros defined on top of macros, and things get messier and messier.

mihnita avatar Aug 17 '22 22:08 mihnita

In the meeting you raised a specialized version of the question - "Is there a value in allowing any local variable to reference any other local variable - potentially allowing for cyclical loops".

I haven't come up with a use case for the specialized question, but for a general one, I envision something like this could be useful:

let $p1_gender = {$person1 :gender}
let $p2_gender = {$person2 :gender}
let $plural = {[$p1_gender, $p_gender] :plural}

Similarly, some variants of functions around rounding, trimming, and other modifications of variables, may produce output that is useful as an input (or argument?) to another function.

Saying that - I'm comfortable not including support for it in V0, or even V1 since it is a forward-compatible extension and we can add support for it in MF2.1 if we find use cases only after 2.0 cutoff.

zbraniecki avatar Aug 17 '22 22:08 zbraniecki

[$p1_gender, $p_gender] is not valid in the current syntax.

But it does not matter, that's not the point.

The trouble is the type. I assume :gender takes the "gender" field of $person. What is the type of $plural? What would is [male female] :plural? How do we get from there to few or one?

I think we should not take this as a way to do function composition, or "save" developers from packing a few variables in a pair, or in a vector.

If we want to compose functions one can imagine this: {:functionChain func1=gender func2=plural oper1=$person1 oper2=$person2} Or {$listOfItems :fCompose fun1=fGender fun2=fPlural} Or even {$listOfItems :fCompose fun1=$f1 fun2=$f2}

The benefit of that would be that fGender and fPlural are not seen as functions in the syntax.

So they are not required to implement formatToString or formatToParts, and the parameters and output don't have to follow the fix signatures forced by an implementation (if an implementation decided that a formatter function signature is Parts func(Object toFormat, Locale locale, Map<String, Object> options)

These functions only have to have signatures expected by fCompose. So it's a lot more freedom than if we try to put them in local vars and "chain" them:

let $p1 = {$person1 :fGender}
let $p2 = {$person2 :fPlural}

mihnita avatar Aug 18 '22 02:08 mihnita

Not to mention that these are to be used by programmers. They can do the function composition in real code, not in MF2. I don't think we need to create a complete programming language.

Since MF1 was very slow to add new formatters (and that's being nice :-), it was always an option to do this: Mf1("Hello {ph}").format( ph: listFormatter(listOfItems) );

I imagine the local variables as a handy way to avoid repetition, and achieve consistency. So that you don't have to repeat the same 10 date formatting options in each plural branch, or to be sure that the thing used to do plural selection is also used for formatting (consistency). Not a way to do function composition.

mihnita avatar Aug 18 '22 03:08 mihnita

I agree with Zibi.

I'm comfortable not including support for it in V0, or even V1 since it is a forward-compatible extension and we can add support for it in MF2.1 if we find use cases only after 2.0 cutoff.

We can disallow it for now, and enable it later if we find a good use case.

BTW, I've been assuming that we also have no need to "reassign" variables: so the following would be an error.

let $fullDate = ... .. let $fullDate = ...

macchiati avatar Aug 18 '22 03:08 macchiati

Here's one real-world-ish example of a possible use case:

# $selected (number) - A Unix timestamp in milliseconds

let $time = {$selected :datetime timeStyle=short hourCycle=h23}
let $date = {$selected :datetime dateStyle=medium}
let $workday = {$date :isworkday}
let $picker = {$time :datetimepicker workday=$workday}
match {$workday}
when true
  {Your time {$time} is not available on {$date}, please reselect: {$picker}}
when false
  {Your time {$time} is not available on {$date} (not a working day), please reselect: {$picker}}
when * {}

This is making use of three custom functions:

  • datetime accepts numeric input and a basket of options matching the JS Intl.DateTimeFormat constructor. Its resolved value is a Date object along with its basket of formatting options.
  • isworkday accepts a Date and figures out if it's a working day, or e.g. a weekend or a holiday. When used as an option value, it resolves to a boolean value. When used as a selector, it will match either true or false.
  • datetimepicker accepts a Date object along with its basket of formatting options and constructs a matching picker. It also accepts a boolean option workday. With the options that $time here has, it constructs a time picker.

While this message certainly could be structured differently esp. if its input variables were extended to something more complicated than a single number, it's meant to show a couple of benefits of being able to chain local variables:

  • The isworkday function does not need to support number input, as that conversion can be handled by datetime.
  • The time formatting options don't need to be repeated for both $time and $picker, and the non-datetime options of datetimepicker are kept separate from the datetime options.
  • The potentially expensive isworkday lookup only gets done once.
  • No formatting functions or options are needed within the patterns (where they'd need to get duplicated for the true and false variants) as they're all set by local variables.

eemeli avatar Aug 18 '22 08:08 eemeli

Questions about example you provided:

datetime accepts numeric input and a basket of options matching the JS Intl.DateTimeFormat constructor. Its resolved value is a Date object along with its basket of formatting options.

Why does it return Date object rather than partially-resolved string+annotations? I'd expect it to return something more similar to [{type: "year", value: 2022}, {type: "literal", value: "/"}, ...] than Date.

let $workday = {$date :isworkday}

Why would you pass $date here instead of $selected? I think your example would be more provocative if the first local variable was lossy instead of formatting (for example if it retrieved start of the week the date is in, and then you'd like to check if that start of the date is a weekend).

let $picker = {$time :datetimepicker workday=$workday}

I think the direction of thinking about this area is correct and I'm glad to see you explore it, but I would be surprised if we wanted a function datetimepicker to produce a picker. I imagine that the picker would be created in the source code DOM model, then passed to MF2 as a variable.

If you aim for is_workday calculation to be executed once, it should be executed outside of MF2 - the value of operatin is locale dependent, but the decision whether to run it is not:

let isWorkday = calculateIsWorkday(selectedDate);
let pickerElement = document.querySelector(`#picker`);
pickerElement.dataset.isWorkday = isWorkday;

let source = `{
  let $time = {$selected :datetime timeStyle=short hourCycle=h23}
  let $date = {$selected :datetime dateStyle=medium}
  match {$isWorkday}
    when true
      {Your time {$time} is not available on {$date}, please reselect: {$picker}}
    when false
      {Your time {$time} is not available on {$date} (not a working day), please reselect: {$picker}}
    when * {}
}`;

let mf = new Intl.MessageFormat();

mf.formatToString(source, {
  selected: selectedDate,
  picker: Intl.MF2.MarkupElement("picker", "standalone", { ref: pickerElement }),
  isWorkday: isWorkday,
});

zbraniecki avatar Aug 18 '22 13:08 zbraniecki

Questions about example you provided:

datetime accepts numeric input and a basket of options matching the JS Intl.DateTimeFormat constructor. Its resolved value is a Date object along with its basket of formatting options.

Why does it return Date object rather than partially-resolved string+annotations? I'd expect it to return something more similar to [{type: "year", value: 2022}, {type: "literal", value: "/"}, ...] than Date.

To clarify, the idea would not be to return a Date, but something akin to a FluentDateTime, i.e. a partially formatted value. That's much more useful if e.g. there's a reason to alter the basket of options for the formatting before their final output. If the function returns a sequence of formatted parts, that's effectively a value that can only be formatted rather than have any further modifications done to it.

Also, if we presume that functions return something in the same shape as our partially formatted values, we can be quite certain that those will work really well in all our systems.

let $workday = {$date :isworkday}

Why would you pass $date here instead of $selected?

The idea here is that $selected is a number while $date is a "PartiallyFormattedDateTime", and this theoretical isworkday doesn't support numeric inputs.

let $picker = {$time :datetimepicker workday=$workday}

I think the direction of thinking about this area is correct and I'm glad to see you explore it, but I would be surprised if we wanted a function datetimepicker to produce a picker. I imagine that the picker would be created in the source code DOM model, then passed to MF2 as a variable.

Sure, that's a common way we would solve this using current technologies, but I'm not at all sure about what would make the most sense in an MF2 world. For instance, datetimepicker could return a "partially formatted" representation of a markup element {+input type=time min=... max=... value=$time} which would turn into a DOM element when the formatted output is processed. This way it becomes possible to take options like workday clearly and explicitly into account.

If you aim for is_workday calculation to be executed once, it should be executed outside of MF2 - the value of operatin is locale dependent, but the decision whether to run it is not:

That certainly works, but it forces the message's API to be much more complicated, as we now have three input parameters rather than one, and we hit the issue mentioned in #293, how match {$isWorkday} is opaque to localisers. With my original message, its matching behaviour is given in the function registry. That could be worked around by giving the variable's type in a semantic comment and having some defined handling for boolean values when matching.

Effectively, I would like for something like is_workday to be expressible as an implementation detail of the message, rather than as a part of its public API.

eemeli avatar Aug 19 '22 11:08 eemeli

I will not try to pick apart the example.

What is important is the type returned by various functions.

let $time = {$selected :datetime timeStyle=short hourCycle=h23}
...
let $picker = {$time :datetimepicker workday=$workday}

So :datetime returns what? The result of that is used in two places:

  let $picker = {$time :datetimepicker workday=$workday}
   ...
  {Your time {$time} is not available on {$date}, please reselect: {$picker}}

You said

  • datetime accepts numeric input and a basket of options matching the JS Intl.DateTimeFormat constructor. Its resolved value is a Date object along with its basket of formatting options.
  • datetimepicker accepts a Date object along with its basket of formatting options ...

But to render this "{Your time {$time} is not available on {$date}, please reselect: {$picker}}" the MF "rendering" code should create a string from $time and a time picker widget from $picker? What functions would it invoke to do those conversions? What is the signatures of those functions? One returns a string, one returns a widget. One can't define a unique interface for those functions. So it can't define something that can be extended by third party with new functions. Except if we make something that takes Object(s) and returns an Object, which is kind of useless as an interface.

By saying using the "resolved" concept we only kick the can of dealing with the type down the road. It means every function must take a "resolved something" and return a "resolved something" (and that "resolved something" has a component that is potentially anything, a Date, a Person, etc). So all this is similar to take an Object return an Object. Which I would argue is not a good interface for a function.

mihnita avatar Aug 19 '22 18:08 mihnita

To the example: I would not use $time in the $picker, I would use the original date. And why would a picker care about $workday?

Sorry, I said I will not pick on the example itself and focus on types, but I couldn't refrain :-) I think it is more convoluted than necessary, I am closer to Zibi's model on this.

And I also think we should not go down he slippery slope of designing a whole templating languages, where all kind of parts depend on other parts, and change dynamically, and what not, without any real code, all in MF2. One should be able to build one on top of MF2 though.

mihnita avatar Aug 19 '22 18:08 mihnita

So :datetime returns what?

To answer this fully, it's perhaps easiest for me to use the JS Intl.MessageFormat proposal and its polyfill to provide specific examples.

As we've discussed previously, it's important for the Unicode MF2 spec not to constrain implementations to behave internally in a prescribed manner, so my answer here should not be read as a request to encode this structure and behaviour within that spec, but to describe one such implementation that should be possible for MF2.

Within that context, datetime is a MessageFormatterFunction:

type MessageFormatterFunction<RV extends MessageValue> = (
  locales: string[],
  options: Record<string, unknown>,
  arg?: MessageValue
) => RV

const datetime: MessageFormatterFunction<MessageDateTime> = (
  locales: string[],
  options: Record<string, unknown>,
  arg?: MessageValue
): MessageDateTime => { ... }

Its return value is a MessageDateTime, which extends a base MessageValue interface with more specific typing and additional members:

abstract interface MessageValue {
  type: string
  value: unknown
  toString(): string
}

class MessageDateTime implements MessageValue {
  type: 'datetime'
  value: Date
  options?: Intl.DateTimeFormatOptions
  toParts(): Intl.DateTimeFormatPart[]
  toString(): string
}

As you may note, the input argument of datetime is also a MessageValue. This means that its variable or literal value may need to be wrapped within an appropriate MessageValue implementation by the corresponding resolver, unless it's already externally provided as a partially formatted value, such as a MessageDateTime instance.

But to render this "{Your time {$time} is not available on {$date}, please reselect: {$picker}}" the MF "rendering" code should create a string from $time and a time picker widget from $picker? What functions would it invoke to do those conversions? What is the signatures of those functions?

In the API proposed for the JS implementation, the final formatting is not done during the resolveMessage() call, but may be done on the result that it provides. Its output would look something like this (eliding caching and error handling):

// const date = new Date($selected);

{
  type: 'message',
  value: [
    { type: 'literal', value: 'Your time ', toString() { return this.value; } },
    {
      type: 'datetime',
      value: date,
      options: { timeStyle: 'short', hourCycle: 'h23' },
      toParts() {
        const dtf = new Intl.DateTimeFormat(['en-US'], this.options);
        return dtf.formatToParts(this.value);
      }
      toString() {
        const dtf = new Intl.DateTimeFormat(['en-US'], this.options);
        return dtf.format(this.value);
      }
    },
    { type: 'literal', value: ' is not available on ', toString() { return this.value; } },
    {
      type: 'datetime',
      value: date,
      options: { dateStyle: 'medium' },
      toParts() {
        const dtf = new Intl.DateTimeFormat(['en-US'], this.options);
        return dtf.formatToParts(this.value);
      }
      toString() {
        const dtf = new Intl.DateTimeFormat(['en-US'], this.options);
        return dtf.format(this.value);
      }
    },
    { type: 'literal', value: ', please reselect: ', toString() { return this.value; } },
    {
      type: 'markup',
      value: 'input',
      // example option values determined from date, $time options and $isworkday
      options: { type: 'time', min: '09:00', max: '16:00', value: '18:00' },
      toString() {
        let tag = this.value;
        for (const [key, opt] of Object.entries(this.options)) {
          const strOpt = '(' + String(opt).replace(/[()\\]/g, '\\$&') + ')';
          tag += ` ${key}=${strOpt}`;
        }
        return `{+${tag}}`;
      }
    }
  ],
  toString() {
    let str = '';
    for (const mv of this.value) str += mv.toString();
    return str;
  }
}

The intent with this API shape is to minimise its surface area while maximising its power and utility, in particular with an expectation that further API layers (such as DOM Localization) will be built on top of it. The individuals parts of the resolved message are not stringified, but their stringification is made very easy. This way, one consumer could just call msg.toString(), while another could walk through the message, and e.g. overlay the 'markup' options on a corresponding DOM element, while using the toParts() methods of the 'datetime' elements to style their display.

Again, to be clear, this sort of an interface makes sense in JavaScript where functions are first-class citizens, and this API shape should not be mandated in the spec. But it should also not be disallowed either.

By saying using the "resolved" concept we only kick the can of dealing with the type down the road. It means every function must take a "resolved something" and return a "resolved something" (and that "resolved something" has a component that is potentially anything, a Date, a Person, etc). So all this is similar to take an Object return an Object. Which I would argue is not a good interface for a function.

While I agree that "take an Object return an Object" is not a very useful interface, that's also not what I'm proposing. Put in similar terms, the actual API proposal is "take a MessageValue return a MessageValue", each with three required fields:

  • type: string identifies the value's type. Similar behaviour could be implemented by using actual classes and checks like instanceof; in the JS proposal that would result in adding a number of primordials to the language, which we're looking to avoid.
  • value: unknown is the actual value, the type of which may be narrowed by different MessageValue implementations. For example with type: 'literal', value is always a string.
  • toString(): string always provides for a way to get the value's string representation.

Other fields such as options or toParts() may be determined by individual MessageValue implementations.

eemeli avatar Aug 20 '22 11:08 eemeli

A bit late, but here are my thoughts.

While I agree that "take an Object return an Object" is not a very useful interface, that's also not what I'm proposing.

Got you. but for Java those are kind of redundant. The type ~ the class (or testable with instanceof, as you already noted) and toString() is available everywhere.

If these interfaces are too generic then they are not much use. The code using them would have to know the underlying type to do anything useful with it.

There is no real difference between that and "get and Object, return an Object", at least in Java and other languages that have runtime type info.

mihnita avatar Sep 01 '22 22:09 mihnita

"{Your time {$time} is not available on {$date}, please reselect: {$picker}}"

In a great many programming environments, having the display of a message produce side-effects (of setting the time and/or aborting a request) is not going to be the normal practice, and is more likely to be handled by a separate call to a timePicker after displaying the message. Are there other examples where this functionality is needed, and there isn't a simple alternative to accomplish the same effect?

On Thu, Sep 1, 2022 at 3:22 PM Mihai Nita @.***> wrote:

A bit late, but here are my thoughts.

While I agree that "take an Object return an Object" is not a very useful interface, that's also not what I'm proposing.

Got you. but for Java those are kind of redundant. The type ~ the class (or testable with instanceof, as you already noted) and toString() is available everywhere.

If these interfaces are too generic then they are not much use. The code using them would have to know the underlying type to do anything useful with it.

There is no real difference between that and "get and Object, return an Object", at least in Java and other languages that have runtime type info.

— Reply to this email directly, view it on GitHub https://github.com/unicode-org/message-format-wg/issues/292#issuecomment-1234847800, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACJLEMCG3FDOH7KH5UV6HLDV4EUD5ANCNFSM563FOP2A . You are receiving this because you commented.Message ID: @.***>

macchiati avatar Sep 01 '22 23:09 macchiati

Closing per 2023-06-19 telecon discussion

aphillips avatar Jun 26 '23 21:06 aphillips