I see the value in Stas’ proposal mainly as:
1) It moves the caching problem to the consumer.
If your app has a list of 100 items to be displayed using the same Message, but different args, you solve it above FluentBundle
level.
For me, FluentBundle
is the lowest level, non-consumer-friendly, API that should allow higher level APIs such as one called currently Localization
or some bindings to do what they want.
With the current model, we basically enforce any optimizations of that type to requested out of the implementation of FluentBundle
itself.
2) It allows for separation of Message retrieval from formatting
Currently, those two actions are combined into a single operation, indivisible.
If FluentBundle
was meant to be used by consumers, I’d understand such API.
But since it is not, having ability to perform each operation separately has a value in composability and flexibility we offer to the higher level APIs.
Caching, listed above, is one, but I can imagine scenarios where one would want to retrieve a Message
and format it using multiple separate FluentBundle
instances.
I don’t think there’s any reason to disallow such operation, and Stas’ proposal seems to offer the right set of APIs to allow for such operations.
@eemeli’s suggestion to return an instance that has internal reference to the bundle
feels off to me. It’s more hidden magic that doesn’t seem to be necessary, and locks us down in multiple concepts that don’t hold.
The Message
in Fluent exists independently of any Bundle
. It’s actually part of the Resource
, not Bundle
. Bundle
on a data model level is just a list of references to Resource
objects plus a cache of intl formatters used by the bundle.
We could also just allow for Message
to be retrieved from Resource
, and then be formatted by the Bundle
.
3) It puts the error fallbacking in place
There are two classes of errors we’re discussing here: a) errors that prevent you from retrieving the Message
or value you want. b) errors that happen during Message
resolution.
The former type is a clear breaking scenario and the main result of FluentBundle.retrieveMessage(id)
should be that it either succeeded and returns what you asked for, or failed with a single error.
This is the base of for a higher level API and bindings that do error fallbacking, and maybe also last-resort option of displaying l10n-id
in place of a message if that fails.
The latter is very different, because we want to resolve as much of the message as possible, while accumulating errors on the way and in the end always return some string
and a list of errors it accumulated.
Once again, the higher level API may decide what to do in case of errors and it may decide to fallback.
In particular, it’s perfectly possible for the same Message
to fail when resolved in the context of one FluentBundle
, but succeed when resolved in the context of another.
I’d like the low-level API to reflect that.
Dislike
On the dislike side, Stas’ proposal adds abstraction allocation and logic.
I never saw the value of “pull” approach, but that’s likely because I see compound messages as tightly related to UI widgets and in such scenarios I’ve never seen a use case of retrieving some, but not all, attributes.
Stas’ also in several issues lists “performance characteristics” of different approaches as something that can change in time (his last comment in issue 364).
Since we talk about low-level generic Fluent API, I believe the common denominator should be algorithmic and cyclical complexity, not benchmark performance, especially not our API should not be designed based on snapshot performance of some JIT engine.
For that reason, I believe Rust and C++ are much better languages to consider when designing the API for all languages. If we need to allocate more, call more functions, hold more references and perform more operations, we’re moving away from a performant API, even if SpiderMonkey 69 happens to JIT away it better.