#2263 The Const Mixin Paradox

SlimerDude Tue 15 Apr 2014

Some thoughts on const mixins...

As rule of thumb I try to keep all my classes as const classes. For services it means they can be reused between threads / web requests and for entities / DTOs it means they can be passed through Actors and stored in a cache. (It can also reduce complexity knowing an object is never in a dirty state.) Code is simple and it gives me a warm fuzzy feeling.

Then there are mixins; interfaces with optional method implementations. Mixins are great for abstracting out behaviour. They can do most things except be instantiated or hold state. As a contrived example, my entities will often implement a Displayable interface:

const mixin Displayable {
  abstract Str displayName()
}

The Paradox

Now what strikes me as odd with the mixin above is the word const. For a construct that doesn't care about state, because it can't hold any, why should it need, or care about, being const or not?

This was further highlighted when I converted one of my entities to be normal, or non-const. This entity couldn't use Displayable anymore, because it was no longer const, and Displayable, well, is!

Could I use 2 mixins?

const mixin ConstDisplayable {
  abstract Str displayName()
}

mixin Displayable {
  abstract Str displayName()
}

Well, not really, because I still can't cast to a common parent. It's frustrating to think that a such simple interface forces all my entities to be const, or non-const; which is quite a fundamental design choice.

Why?

I understand why the const key word is needed; for static typing and defining const classes.

const class Example {
  const Displayable displayable
}

The Example class can only be const if Displayable (be it a mixin or class) is defined as const. So for a class to be const it needs to know at compile time that all its fields are also const classes.

As an interesting aside, if a mixin is never used as a field, but only passed around in method signatures:

Void display(Displayable displayable) { ... }

Then nobody really ever cares if Displayable is const or not.

Maps and Lists

The glaring exceptions to all this are Maps and Lists, which bring in the concept of immutability. Maps and Lists are not const, but can be immutable. This is somewhat an abstract concept because currently, user defined classes can not be immutable.

Resolution

I'm just thinking out loud here... (and fishing for an answer to my directionless diatribe!)

It'd be nice if the const mixin paradox could be resolved, and preferably without changing Fantom too much.

To that end, I have 2 ideas:

Runtime Const Field Checking

If in the case of a const class definition, when a field declaration is found to be a mixin, defer the const check until runtime.

My initial reaction to this was, "Woah, that'll bring in heaps of bugs!" But then, after scrutinising my code, I found very few instances where I declare fields with a mixin type.

User Defined Immutable Classes

Maybe by whatever means (a keyword, a class level facet or a system mixin) we could declare a class or mixin as immutable, that is, they can become immutable by implementing toImmutable().

Then we wouldn't have const mixins anymore, but immutable mixins. Still a bit strange but being immutable is much less of a constraint than being const.

Those are my thoughts, I'd be interested to hear other people's take on it.

brian Tue 15 Apr 2014

If in the case of a const class definition, when a field declaration is found to be a mixin, defer the const check until runtime.

I've been back on forth for this. But I always come back around to that the immutability of a type really is one of the most important aspects of its signature. I've found having const mixins all typed checked at compile is actually what I want virtually all the time.

BTW there is a simple (if bit ugly work around too):

mixin Foo {}  // non-const mixin

const class Bar
{
  new make(Foo f) { fooRef = f }
  Foo foo() { fooRef }
  private const Obj fooRef
}

that is, they can become immutable by implementing toImmutable().

I definitely have wanted this too, and was tempted to make it virtual earlier this year. But the problem is how to you actually make it work with any sort of sane compile time and/or runtime checking? I'd love to hear some design proposals

SlimerDude Tue 15 Apr 2014

BTW there is a simple (if bit ugly work around too)

Cheers Brian, I didn't think of hiding a non-const declaration in an immutable Obj.

It still doesn't let me const class Bar : Foo { ... } though... : (

toImmutable() ... I'd love to hear some design proposals

I need to go wake up my other brain cell for this... it may take some time!

SlimerDude Fri 13 Mar 2015

toImmutable() ... I'd love to hear some design proposals

Some belated thoughts...

Run Time

Run Time behaviour is easy - just assume that all objects could become immutable. The standard toImmutable() behaviour would then also:

  • attempt to perform a deep clone by calling toImmutable() on all storage fields

Any class that can not be immutable (usually classes with native storage, like Buf) would override toImmutable() to throw an Err.

Compile Time

But as mentioned, it would be nice to know at compile time if a class is immutable or not. (This is the difficult part!)

A problem with mixins is that they're unaware of their implementation. Given that most Fantom classes just contain Lists, Maps, Strs and Ints and other immutable types, how about assuming all types could be immutable until proved otherwise.

Then we could have some sort of Mutable keyword / interface / facet that marks a Type as mutable. Similar to how const works, if a class contains a field that is a mutable type, then it too must be marked as mutable.

The toImmutable() behaviour would then also contain:

  • if any field is a mutable type, throw NotImmutableErr.

Unless explicitly marked mutable, mixins and super classes are assumed to be toImmutable()'able. Implementations and subclasses could have mutable fields, but that doesn't affect the parent types. (*)

Rationale

Fantom's unique compile time Type inference puts faith in the developer and says if a Type could be coerced then it can. This catches 99.9% of errors at compile time and leaves the other 0.1% to be caught at run time.

I see the above (*) idea being similar. For compile time checks, we assume the developer is right, and if a class could be immutable we allow it. All other checks are then deferred to run time.

Addendum

Maybe mutable could be held (mainly) as an internal flag - to prevent the any proliferation of mutable keywords?

Login or Signup to reply.