#1674 Inherit static make methods

Yuri Strot Sat 22 Oct 2011

I'm trying to combine mixin with static make method to have interface and factory in one place:

mixin Sample
{
  static Sample make(Str text) { SampleImpl(text) }
  ...
}

Now I can use Sample(text) syntax to create mixin which looks amazing.

However there is one disadvantage with static make compared to ctor - it can't be inherited. In my case I can't use:

mixin Sample2 : Sample
{
  static Sample make(Str text) { Sample2Impl(text) }
  ...
}

because Sample2.make conflicts with Sample.make. Is it possible to use something similar to constructor chaining or just allow to "overlap" static method name with another static method in an inheritor?

brian Sat 22 Oct 2011

Yes, this is one place where the compiler and language's distinction between constructor and static is limiting.

Just to set stage for the principles at play here:

  • each type may have exactly one slot (field/method) mapped to a given name
  • static and instance methods are inherited
  • constructors are not inherited

So the reason that we can overload constructor names is because they not are inherited. You cannot reflect nor call them directly from subclasses. But static methods are available for reflection and for un-scoped call in subclasses, which is why overloading them by name would cause a lot of problems. So I don't think we want to allow static methods in a subclass to hide the name of a static method in a superclass - it breaks Fantom's fundamental idea of types being simple maps keyed only by name.

But in this case the real feature at play here is how we allow special names like make and fromStr to be used as "constructor shortcuts". Just throwing some ideas out there, but maybe we should allow that on any named method using a facet?

qualidafial Sat 22 Oct 2011

I like the facet idea with constructor shortcuts.

This would make compiler magic more apparent when reading the code, like @Operator did.

What would this facet be called? @Cast?

qualidafial Sat 22 Oct 2011

We could also apply this facet to toX instance methods on the source type, e.g. Str.toInt could be invoked using:

Str input := 5
Int value := Int(input)

Although you could get conflicts if both the source and destination type declare conversion methods.

Yuri Strot Sat 22 Oct 2011

@brian I really like this idea! What do you think about @New facet? It's short, close to new keyword and allows to find all constructors easily.

From the other side it would be nice to have clear rule for selection of constructor shortcut by arguments.

@qualidafial I think it makes sense to keep code unambiguous both for programmer and compiler. When I see Int(input) I expect to find creation method in the source of Int type.

brian Tue 25 Oct 2011

Regarding the facet, I think the right move is to just use the @Operator facet. A constructor can then just be another category of facets prefixed with "make" or perhaps "from".

What I'm struggling with is how this ties into overloading and the existing support for "make" and "fromStr". We made a change a while back to overload operators by parameter such as Int.plus and Int.plusFloat. Should we do same for constructor shortcut?

Also should we require tagging "make" and "fromStr" with @Operator or is it somehow implied? I strongly dislike going back and requiring that facet on things like "make", but at the same time I dislike the inconsistency it would introduce to make them special.

Yuri Strot Tue 25 Oct 2011

At a glance make doesn't fit @Operator approach well:

  1. Image.make doesn't mean make(Image)
  2. Image.makePainted doesn't mean make(Pained)

However it might be good generalization for fromStr: A.from(A), A.fromStr(Str), A.fromB(B), etc. In this case fromStr is just one more @Operator.

But I'm not sure how it can fix my original issue:

mixin Sample
{
  static Sample fromStr(Str text) { SampleImpl(text) }
  ...
}

mixin Sample2 : Sample
{
  // still fail 
  static Sample fromStr(Str text) { Sample2Impl(text) }
  ...
}

May be something like ability to reassign default creation method:

@Ctor { name = #makeSample }
mixin Sample { static Sample makeSample(Str text) { SampleImpl(text) } }

@Ctor { name = #makeSample2 }
mixin Sample2 : Sample { static Sample makeSample2(Str text) { Sample2Impl(text) } }

s1 := Sample(text)  // = Sample.makeSample(text)
s2 := Sample2(text) // = Sample2.makeSample2(text)

qualidafial Tue 25 Oct 2011

At issue here is that conversion e.g. static A fromB(B) { ... } is conflated in the syntax with construction e.g. new makeB(B) { ... }.

I think we should first discuss and decide whether these two concepts are worth distinguishing from each other.

If so, then they should probably be syntactically distinct.

Just my $.0200000000001 :)

qualidafial Tue 25 Oct 2011

On one hand, there is an elegant symmetry between toStr and fromStr method names which I like.

On the other hand, explicit calls to fromStr are probably rare compared to the constructor syntax. So maybe this symmetry is only appreciated in the docs.

Another option might be to elevate from methods to constructor status. This would loosen rules on constructor names, while unifying the shorthand constructor syntax similar to how @Operator unified operator usage.

jodastephen Thu 27 Oct 2011

I would say that type conversion and construction are really two different things. That is why there are two different method names - make and from. Conflating them is less than ideal - a type conversion factory should build on a constructor, not be a constructor.

The thread on the ~ operator covered this area too. As a reminder, it proposed using ~ as the operator for from/'to'. The Type() pattern is then the "constructor operator".

Treated separately, it might then be reasonable to have to mark fromStr with @Operator, but not make (ie. saying that construction is not an operator). I'd also note that type conversion generally needs overloading, whereas construction tends not to.

The original problem here reminds me of this thread which is a pretty close description of the problem with a proposed solution. (Static in Fantom remains weak and a language anomoly IMO). Thus I'd permit:

mixin Sample {
  static Sample make(Str text)
}

which simply requires the implemening class to provide a method matching that signature.

Yuri Strot Thu 3 Nov 2011

Thanks for all comments! There are a lot of interesting discussions around type creation, conversion, constructor shortcuts, etc. But let me back to static inheritance and clarify one thing.

I see 5 different members of the type definition: methods, fields, static methods, static fields and constructors. All these members well defined with the sys::Slot class: they have parent type, signature, visibility, facets and hopefully some documentation.

I'd like to notice static slots are very different from the instance slots. I mean static is not just one more property of a method/field. For example, let's compare API of static VS instance fields as if they were two different types:

const class InstanceField : Slot
{
  Obj? get(Obj instance)
  Void set(Obj instance, Obj? value)
  Type type()
}

const class StaticField : Slot
{
  Obj? get()
  Void set(Obj? value) // Not sure this method makes sense at all
  Type type()
}

Acutally I don't think we should have two different types, current API is clear and simple enough. I'm also find very useful to identify any slot by type+name. But I don't understand why static slots are inherited as instance slots, while they look more like constructors from this perspective.

For instance, classes in JavaFX can't have static members and you should define all static stuff at the script level, outside of the class definition scope: http://devjfx.wordpress.com/2010/03/26/static-fields-and-functions-in-javafx/. I think it's an interesting idea to highlight that static members do not join usual life cycle of instance members inheritance/overloading.

To be more concrete, in the following case:

class A { static Void a() }
class B : A {}

I find B.a() incorrect the same as A().a().

brian Fri 4 Nov 2011

I would say that type conversion and construction are really two different things. That is why there are two different method names - make and from.

I sort of agree, but mostly disagree. What matters is that you are constructing a new object. There might be some conversions that aren't actually constructors, but I think in most cases conversation is just one case of construction. Maybe technically a better naming convention might have been makeFromStr or something.

I think it's an interesting idea to highlight that static members do not join usual life cycle of instance members inheritance/overloading.

To me the key issue is that namespace inheritance works the same as reflection inheritance:

class A { static Void foo() {} }
class B : A { Void main() { foo } )

In a subclass like B, we can call A.foo without explicitly saying A.foo because we inherited static methods. Note we cannot to that with constructors - they are inherited neither in namespace scope nor by reflection.

If we decided to treat static methods like constructors, then the whole namespace scoping and reflection issue would solved. However it would be a huge breaking change and I think in many cases be fairly annoying to anyone who expects Java like behavior.

I still think the original use case might be better solved with something like the @Operator facet. We already previously decided that math operators like + deserved to be overloaded by parameter type and came up with a design that let you do that, but still forced unique method names. I think same principles apply to construction.

jodastephen Sun 6 Nov 2011

> I would say that type conversion and construction are really two different things. That is why there are two different method names - make and from.

I sort of agree, but mostly disagree. What matters is that you are constructing a new object. There might be some conversions that aren't actually constructors, but I think in most cases conversation is just one case of construction. Maybe technically a better naming convention might have been makeFromStr or something.

If the code is a standalone variable declaration, then it looks like construction. But if it is passing a foo to a method that takes a bar, then it is conversion. The two are related but distinct.

Construction is about creating an object from its separate parts (and should involve little complexity). Conversion typically involves more logic, such as parsing.

As an example, you cannot construct a Decimal/Float/Int from one of the other types. The only way it can be done is to call the toXxx method on the one you have. It might make sense to add:

Float.fromDecimal
Float.fromInt

and so on, using @Operator overloading.

The problem with all this (as shown with the current number situation) is that the user has to know whether the conversion method is on type A or type B. ie:

A.fromB(b)   // or
b.toA()

(Unless you code it on both, which won't always be possible).

That is where the ~ operator comes in, to focus attention on the task of conversion, not the exact mechanism/method used to achieve it.

The more interesting case is where you have two types that don't know about each other, but you want to write conversion logic. Scala's implicits allows this - arbitrary pieces of logic to convert between two types, but it is not easy. For example, Decimal has method plusInt, but really this represents an aspect of the combination of Int and Decimal, and probably shouldn't be a method on either.

Thus, the "correct" solution is one where all aspects of the relationship between two classes is represented by a "junction" class:

junction class Int/Decimal {
  Int Decimal.toInt() { ... }
  Decimal Int.toDecimal() { ... }
  Decimal Decimal.plus(Int) { ... }
  Decimal Int.plus(Decimal) { ... }
  ...
}

Interestingly, this direction probably removes the need for @Operator.

I would point out that methods like Int#toDuration and Int#toDateTime(TimeZone) look decidedly dubious in the API right now...

Akcelisto Mon 7 Nov 2011

I want to remind about a issue "Deserialization take off short ctor". Please, fix somehow this. See #1370.

Login or Signup to reply.