#2820 Proposal: once methods on const classes

brian Fri 29 Jan 2021

I keep finding myself wanting to create lazily cached fields on const classes. This is currently disallowed by the language since its essentially a mutation which would potentially allow race conditions. However, most of the time as long as your once method only computes itself from other immutable fields then its a pure function and safe to cache.

So in the spirit of practicability, I'm proposing we enhance the compile to enhance once methods using an AtomicRef for the auto-generated cached field:

// Fantom code
once Str foo() { "bar" }

// generated psuedo-code 
Str foo() 
{
  x := foo$Once.val
  if (x === "_once_") 
    foo$Once.val = x = "bar"
  return x
}
private const AtomicRef foo$Once := AtomicRef("_once_")

It would be disallowed to use a once method on a const class if your pod did not declare a dependency on concurrent.

Note: that its possible that two different threads would actually compute your once method multiple times. So its not a 100% single entry computation (like a synchronized block in Java would be), rather the caching/memorization of what should be a pure function.

andy Fri 29 Jan 2021

Yes I run into this all the time. And this is pretty much exactly my use case.

Its icky but I'm already doing it so might as well embrace it.

SlimerDude Fri 29 Jan 2021

I was kinda happy in principle (given the once keyword already exists on non-const classes), all until "pods need to declare a dependency on concurrent".

This massively blurs the dependency hierarchy; core syntactic sugar being dependant on an external concurrent pod - it's far worse than just icky!

Yes, I occasionally do similar "field caching" myself - but I'm happy to manage that explicitly, as it serves as a reminder of the extra overhead involved (and that, if possible, maybe I should look to perform the calculation in the ctor instead?).

I understand how tempting it is to add spot bits of sugar here and there to overcome personal niggles, but then Fantom would just be de-generating down the same ugly road C# took.


A counter proposal, that I would be fully behind, is to open up these compiler hooks to introduce a new custom compiler step. Then we all could create our own Fantom pre-compilation processing steps to inject custom code as we (individually) see fit - without polluting the core Fantom syntax.

Java did similar with their JSR 269: Pluggable Annotation Processing API, as popularised by Project Lombok, and it would be fantastic if Fantom did the same.

This once proposal exists purely to reduce boilerplate code, and that is exactly what JSR 269 and Project Lombok do; see Lombok features and Reducing Boilerplate Code with Project Lombok for details.

I would love for you to instead, tinker with exposing these compiler hooks, using the once proposal as a proof of concept / end goal. It should be no different with what you're currently doing, except that the code augmentation is externalised in a different pod.

If the new compilation annotation features (such as @Once) prove useful, then it could be released as a new compiler extension pod in Fantom core.

Starting off, and following JSR 269, it may also be easier to use @facets rather than keywords, for example:

@Once Str foo() { "bar" }

vs.

once Str foo() { "bar" }

brian Fri 29 Jan 2021

A counter proposal, that I would be fully behind, is to open up these compiler hooks to introduce a new custom compiler step.

From a philosophical perspective, I do not like creating a situation where you have to have a special plugin to compile your code correctly. We sort of did that with the DSL syntax, which I think was a mistake in retrospect.

You can make the case that its not different than requiring the dependent pods be present to compile against and run. But it feels like a completely different feature. Its essentially adding macro to the language - very powerful, but also very dangerous. Plus it would create a situation where the compiler AST itself would become public API that couldn't change without breaking the macro plugins.

But I can see both sides of the macro argument for sure

This massively blurs the dependency hierarchy; core syntactic sugar being dependent on an external concurrent pod - it's far worse than just icky!

I can understand that sentiment. But from a practical perspective, concurrent should be considered part of the language itself. Pretty much every language makes their concurrency primitives part of the language/compiler - Java synchronized keyword, JavaScript await, Go channels. Any significant language enhancements such an await mechanism is going to require concurrent. So I think of it as a pretty special case.

Although in this case, maybe using AtomicRef isn't the best way to do this. It might be better in Java to just an volatile field. That would provide lower memory overhead and achieve the same outcome. So lets change the proposal to that (in Java we'll compile the cache field to a volatile). That actually makes it a lot more attractive than the hand rolled version by avoiding a boxed reference.

SlimerDude Fri 29 Jan 2021

lets change the proposal to that in Java we'll compile the cache field to a volatile

Then I concede!

The proposal is then just widening the use of once to const classes - which is fair enough... and I'll probably even use it myself!


Pretty much every language makes their concurrency primitives part of the language/compiler

And Fantom made the decisive choice (for the better) to make a clean distinction between the two. Java's synchronized is an abomination that every framework tries to bury, Javascript's await is just syntactic sugar that attempts to simplify the generic mess of callbacks and promises, to widen the appeal for newbies.

Whereas Fantom's built in immutability and Actor framework is a refreshing clean separation done right.

An external mechanism, such as (concurrent) pod dependencies and macro plugins, then enables users to bridge the gap with an explicit choice to opt-in.


the compiler AST itself would become public API that couldn't change without breaking the macro plugins.

Yes, but... the AST and macro plugins would be very rarely used compared to say, core sys classes.

I wouldn't expect many to create macro plugins, for it's quite advanced and the use-cases are limited - so the maintenance and upkeep of such pods wouldn't be such a burden should the AST API change. (And it'd be obvious it's an experimental API.)

Fantom DSLs are kinda klunky, and as Ivan(?) pointed out in his article, they need more explicit compiler integration to be useful. These macro plugins could well provide the missing layer required.

brian Mon 10 May 2021

Ticket promoted to #2820 and assigned to brian

brian Mon 10 May 2021

Ticket resolved in 1.0.76

Login or Signup to reply.