#1309 Conversion operator ~

jodastephen Fri 12 Nov 2010

This is a proposal to add a conversion operator ~ to Fantom.

The aim of the feature is to use the standard toXxx and fromXxx methods to allow conversion between types at lots of points in the code.

In statically typed languages, one of the biggest hassles is specifying and converting types. Fantom minimizes this with type inference on fields/locals and auto-casting. However, there is another category where the two types are not linked by type hierarchy - conversions.

date := Date~"2010-11-11"
// compiles to
date := Date.fromStr("2010-11-11")

Void process(Int val) { ... }
s := "76"
process(~s)
// compiles to
process(s.toInt())

The rules would be as follows:

  • the ~ operator binds strongly to the following code as per !
  • the ~ operator may specify the type to convert to (LHS) or be inferred (no type)
  • the type must not be nullable (but see below)
  • the conversion will first try to match a toXxx method, then a fromXxx method, then try a regular cast
  • the inference works in all locations that an auto-cast works now, plus field/local declaration

The effect of these rules are to eliminate the need for the cast operator:

s := (Foo) t     // today
s := Foo~t       // with change
s := ((Foo)a).t  // today
s := (Foo~a).t   // with change

I have a basic implementation working in the compiler. Parsing is easy, so is finding a method to call and field/local inference. Other inference is tricky as auto-cast is currently in CheckErrors when it would be easier in ResolveExpr or a separate step.

The above represents the minimum feature. However an extension to support nullable LHS is recommended:

  • if the LHS is nullable, then the method called must have a boolean parameter that can be set to false as per Date.fromStr
  • if a method is not found, the behaviour would be like the as operator today

eg.

s := t as Foo     // today
s := Foo?~t       // with change

date := Date?~"20109999"   // bad date parses to null

I haven't examined this properly ye, but I also believe that this operator provides the basis for safer list conversions. `http://fantom.org/sidewalk/topic/563` explored this a little. At the point that a Str[] is changed to a Obj[] it can be wrapped to ensure safety with the ~ indicating the safety. This would also help to manage variance problems in any future generics.

Anyway, the core feature of type conversion (explicit implicits) is very worthy of implementing as it stands.

dmoebius Fri 12 Nov 2010

Why not simply handle the "as" keyword the same as an operator:

t as Foo
// compiles to
t.toFoo()

It's for the implementor to decide whether the result is nullable.

With the new @Operator facet one could mark any method:

@Operator
Foo toFoo() { ... }

@Operator
Bar toBar() { ... }

The advantage is that future IDEs could help with suggestion popups: If the user types:

s := t as // <-- cursor is here, user presses spaces

the IDE suggests a list of all toXXX methods.

I'm not sure how the IDE would to that with the ~ proposal.

helium Fri 12 Nov 2010

The advantage of the ~ operator is that you don't have to specify a type if it can be inferred.

someFunction(~foo)

vs

someFunction(foo as TypeExcpectedBySomeFunction)

go4 Fri 12 Nov 2010

foo.toBar and foo as Bar are different.

Maybe there can be implicitly converted for foo as Bar:

a.doSomething(foo as Bar) => a.doSomething(foo);

But I prefer a explicit invoke for foo.toBar:

a.doAnthor(foo.toBar);

jodastephen Fri 12 Nov 2010

I agree with the @Operator part, and meant to mention it.

As to whether a cast/'as' are the same or different to ~#/?~' its a fine line. Basically, you want in the code to convert from type A to type B. Currently, the only mechanism to do this is cast/'as'. I see ~ as a superset of those operations, however there are edge cases where a cast would be converted to a method call, which might be undesirable. Personally, I think the simplicity of one conversion mechanism is preferable to two related ones.

brian Sat 13 Nov 2010

First off, my plan was to avoid adding any features for 1.0, so we might want to hold off tackling this for 1.1.

I think the basic idea is pretty slick. I definitely think conversion should be explicit, I strongly dislike the Scala implicits design because I think it is seriously hurts code readability. But it seems heavily used, so perhaps there is a real need for conversion.

Although I am a little concerned about taking conciseness all the way down to a single symbol. It seems like a toFoo or fromFoo method ensures clean readability (especially if/when we do extension methods).

tcolar Sat 13 Nov 2010

I agree with Brian I like the idea but I think that syntax is not readable to a newcomer

I think fantom is instantly readable and that is very good and important IMO.

DanielFath Sat 13 Nov 2010

Well syntax is cheap.

s:= Date~"2009-09-09"; foo(~Date) //current proposition
s:= Date~>"2009-09-09"; foo(Date~>) //my proposition 
s:= "2009-09-09"~>Date; foo(Date~>) //somewhat more intuitive approach since Str is cast into Date and Date is casted into foo
s:= "2009-09-09" to Date; foo(Date to) //use a keyword like as or to. You could also modify as to be able to automatically cast into method parameter.

rfeldman Sat 13 Nov 2010

s:= "2009-09-09" to Date

+1 to this syntax. Extremely readable and intuitive.

One could argue that this isn't any better than just following a convention and implementing .toDate, but there's something to be said for enforcing common invariants around conversion methods, such as " to Foo always returns an instance of type Foo".

There's no such guarantee if you're just going by convention; to be really certain of the return type of toFoo, you'll have to look it up.

Also, among these options:

foo(~dateStr)
foo(dateStr~>)
foo(dateStr~>Date)
foo(dateStr to)
foo(dateStr to Date)

The last is the most verbose, but also the most readable and intuitive. +1 to keeping it simple like that.

DanielFath Sat 13 Nov 2010

Glad you like the example syntax. Just a note'foo(dateStr to Date)' and foo(dateStr to) aren't mutually exclusive (likewise goes for ~>Date and ~>). You could treat foo(dateStr to) as a shortcut for let compiler figure it out, while using foo(dateStr to Date) when you want to add some verbosity to your commands.

rfeldman Sat 13 Nov 2010

True, although foo(dateStr to) is not especially intuitive to a newcomer.

Food for thought: introducing a conversion operator would probably imply that foo.toStr would become deprecated in favor of foo to Str (or whatever syntax), which would become the preferred usage.

If that's the case, and if this feature is likely to ever to make it into the language, it might be worth reconsidering for 1.0; deprecating toStr would be a pretty far-reaching change.

helium Sun 14 Nov 2010

I think fantom is instantly readable and that is very good and important IMO.

Instantly readable by whom? Please show this simple line to a programmer who is unfamiliar with Fantom and let him guess what it means:

foo := Str:Int[:] { def = 0 }

Henri Sun 14 Nov 2010

No programming language that has some expression power is instantly readable by programmers unfamiliar with it. Although readability for newcomers is important, I don't think that has to be the primary criterion for syntax decisions. A language should be readable (and intuitive) first of all for programmers who actually develop programs with it.

qualidafial Sun 14 Nov 2010

-1 to the ~ operator.

+1 to the to operator, although if to becomes a new keyword I'll have to modify my data binding API to use some other method name. :-/

katox Sun 14 Nov 2010

I don't have strong opinion on proposed operators but I would like to get rid of messy and unreadable c-ish ((Different)object).method syntax. If there was an operator for that it'd deserve a better order of precedence.

Not having to specify the destination type and just relying on a conversion function is a win - the same as local type inference.

jodastephen Mon 15 Nov 2010

Firstly, the concept of using to doesn't work with method names. The definition I gave was that the conversion uses either toXxx() or fromXxx(). And in fact we should allow make prefixes too. This is essential to allow evolution of large codebases...

If libraries A and B are released, and library B depends on library A, then there will be no method toBbb() in library A referring to library B, but there can be a method fromAaa() in library B. Thats why both are required.

Moreover, this is such a common operation in statically typed languages, that a simple operator is the best way to go. A word as an operator is poor style generally, and too verbose in this case (I'd like to remove all word operators from Fantom). Besides, as pointed out, to is a useful method name!

I think the main argument against ~ is that it doesn't exist in any other language. But the concept exists elsewhere - implicits in Scala, and this is a lot more readable.

This is also an enabler for future change. To add generics to Fantom, there needs to be a solution to the variance issue. One solution is requiring a ~ operator to convert from List<Str> to List<Obj> or vice versa, where the conversion involves adding a suitable checking wrapper. This approach is simple to use, and explicit, but would be far less usable with a to operator.

I'd also note that this last point (generics) may be impossible to change compatibly after 1.0, as Str[] currently assigns to Obj[] directly, which has poor side effects as discussed in previous threads.

Finally, the key to this change is realising just how much type inference Fantom can do that isn't being used ATM. With no method overloading, extensions like this are easy and reliable, as new semi-literal forms like dates become easy. Few other languages have had the no overloads before, and so this can seem radical.

The to simply doesn't work right in the inference case process(dateStr to) simply looks weird compared to process(~dateStr). The first focusses on the conversion, the second with ~ focusses on the method call. Conversion is just a detail, which is the real aim here, because it fits 100% with Fantom's dynamic/relaxed approach to types (I'm calling this method with some form of date, and the operator is used to massage it into the right form. The conversion itself is NOT the goal, calling the method IS).

Fantom isn't yet making proper use of the inference possibilities, and ~ is one incredibly powerful and useful feature that does.

DanielFath Mon 15 Nov 2010

Yeah it is a slick feature. I just think most people dislike the squiggly ~ operator. Maybe something that symbolically says conversion would be preferred.

BTW why is word as operator poor style?

helium Mon 15 Nov 2010

How powerful do you want this operator to be? You could allow something like this:

class Foo {
}

class Bar {
   Void method() {}

   @Operator
   static Bar fromFoo(Foo foo) {
      return new Bar()
   }
}

...

foo := Foo()

(~foo).method()  // converts to Bar using fromFoo and calls method on that

So you would have the ability to extend the interface of existing classes.

But I think a postfix-operator might safe you some parenthesis:

foo~.method()

Better looking

foo~>method()

Scala goes as far as

foo.method()

jodastephen Mon 15 Nov 2010

If we went this far, then I would choose foo~.method(). Its most in tune with the other language parts.

I have to admit that I hadn't thought of this extension to the syntax, but at first glance, I do like it.

rfeldman Mon 15 Nov 2010

In a real-world setting, just looking at

(~foo).method()

I'd have no way of knowing which class it was converting to. As it happens, the answer is Bar, but the only way to find that out is to go through all the included pods in this file and hunt for a .method() declaration.

No thanks. -1 to mystery meat.

DanielFath Mon 15 Nov 2010

Given:

Foo {}

Bar {
 Void method() {}

 static Bar fromFoo(Foo foo) {
    return new Bar()
 }
}

Baz {
 Void method() {}

 static Baz fromFoo(Foo foo) {
    return new Baz()
 }
}

What happens when I do foo~.method()? Obviously there is going to be an error. But how will I choose which to cast as?

jodastephen Mon 15 Nov 2010

An ambiguous foo~.method() would be an error I would suspect.

However, this talk of foo~.method() is an additional feature that I'm not 100% certain is worthwhile, whereas process(~foo) for standard conversions is very useful, and the focus of this thread.

brian Mon 15 Nov 2010

I am in agreement that using foo~.method to bind some arbitrary method where a conversion is allowed might be too much power. I also think that extension methods are a better solution for this fit. In fact my thoughts were how to make extension methods more explicit in source code (maybe explicit declarations in using statements for each one).

As an overall general principle I do have concerns about the level of "magic" that Fantom already uses which decreases readability. As it stands today reading Fantom code often requires digging up a few levels to figure what a local variable is really typed as. With conciseness comes some loss of readability. It is a balance of finding the right amount of "noise" in the source code. So I am against adding any more magic to the 1.0 release until we get solid experience under our belt with the current features.

For 1.1, I really want to explore extension methods, I am beginning to think that conversion is really just an aspect of extension methods (with or without operator sugar right?). I think a key issue extension methods will solve is the bi-directionality that sort of complicates Stephen's original proposal with both to/from methods. We could keep it one way with extension methods (using C# syntax):

// convert Bar -> Foo as method Bar
@Operator static Foo toFoo(this Bar bar)

jodastephen Mon 15 Nov 2010

Actually, I'd consider removing auto-casting if ~ were added. I suspect that would make the code more readable in totality, yet more powerful.

qualidafial Mon 15 Nov 2010

The one thing I'm opposed to in this proposal is implicit conversion. I don't like how the ~ operator can mean one of several things, forcing you to hunt down what the compiler actually does:

process(~bar)

Let's see:

  • Bar is being converted to something.
  • Look at process method signature to find out it expects a Foo. So bar is being converted to a Foo.
  • Look through Bar class for a toFoo method
  • Look through Foo class for a fromBar method (if we didn't find toFoo in previous step)

Contrast with:

process(bar.toFoo)

From a maintainability standpoint, I would much rather add a few keystrokes if it means my code will be explicit and easy to understand.

I do like the extension methods proposal. This would alleviate the pain of object conversion but without any loss of clarity in the code.

jodastephen Mon 15 Nov 2010

qualidafial, thats what IDEs are for ;-)

And in Scala you wouldn't get any operator, and the conversion could be an import. Thats a true nightmare.

katox Mon 15 Nov 2010

thats what IDEs are for ;-)

That's the kind of argument that got Java where it is.

cheeser Mon 15 Nov 2010

By that I'm guessing you mean the #1 development language in the world. ;)

brian Mon 15 Nov 2010

From a maintainability standpoint, I would much rather add a few keystrokes if it means my code will be explicit and easy to understand.

Think I am in agreement with this line of thinking, if you have extension methods, then foo.toBar might not be as concise - but a lot more readable.

tcolar Mon 15 Nov 2010

+1 to readability (again)

andy Tue 16 Nov 2010

I'm still catching up from my week out here, but I think while this ~ proposal sounds interesting and there maybe something to it - the from/toFoo pattern seems simpler, and worth the tradeoff of readability vs keystrokes. Tho worth some more simmering in my almost-back-to-normal brain.

yachris Tue 16 Nov 2010

+1 to readability

helium Tue 16 Nov 2010

-1 to readability

qualidafial Tue 16 Nov 2010

-1 to readability

Readable code is for wimps

helium Tue 16 Nov 2010

COBOL:

ADD 1 TO x

Very readable. C:

x++;

unreadable nonsense.

qualidafial Tue 16 Nov 2010

COBOL:

Haha, ok I see your point.

I guess I'm not arguing so much for readability in the sense of plain english vs code. What I want is for the code to remain self-explanatory. I think that with expressions like process(~foo) you lose some of that.

yachris Tue 16 Nov 2010

-1 to cryptography

Henri Wed 24 Nov 2010

+1 to from/toFoo.

This may need some API changes in order not to confuse the reader, though. For example, gfx::Font.toSize(Int) doesn't return a Size, but a (new) Font. A similar thing applies to Font.toBold(), Font.toItalic() etc.

Maybe these could be renamed to Font.withSize(Int), Font.withBold() etc. Admittedly, Font.withPlain() does not sound so good.

Login or Signup to reply.