#1222 Introduce "That" pseudotype for methods with passthrough arguments

qualidafial Mon 20 Sep 2010

Breaking this out from #1220.

I propose introducing a That pseudotype for use in method arguments and return types. This would allow usage such as this: (example from Mockito)

That verify(That obj)

In Java this would be equivalent to:

<T> T verify(T obj)

With this facility, API designers could write pass-through methods so that compile-time type safety is retained.

This eliminates the need for manual casting in client code, e.g.

(Mockito.verify(listeners) as Listeners).add(eventType, listener)

Could be shortened to:

Mockito.verify(listeners).add(eventType, listener)

yachris Mon 20 Sep 2010

+1

ivan Tue 21 Sep 2010

I really like the idea, also one particular case where it can be extremely helpful is sys::List#reduce function. Right now even in extremely simple cases like this:

[1,2,3].reduce(0) |Int r, Int e->Int| { r + e } 

it is necessary to write full signature for closure, because reduction value is sys::Obj. In some cases I even don't use reduction just because don't want to write a long signature.

Having signature like this should be much more friendly (and safe):

That reduce(That init, |That, V, Int ->That| c)

so the initial example turns just into:

[1,2,3].reduce(0) |a,b| { a + b }

vkuzkokov Tue 21 Sep 2010

In that respect

That[] map(|V, Int -> That| c)

is a feature I miss the most (e.g. mapping Str[] to Depend[]). This one needs either "back-and-forth" inference or explicit passing of generic parameter.

Under back-and-forth inference I mean that both closure type can be inferred from type of expected parameter and method return type will be inferred from types of parameters. Full support of this feature will unreasonably (at least before 1.0) complicate compiler.

In case of reduce (and in most others) we can infer return type from the type of first parameter. If there are more arguments of That type we can use "closest common type". IIRC that is how C++ implicit function template instantiation works.

This feature will surely add type safety (and reduce size of type-safe code).

helium Tue 21 Sep 2010

Doesn't

Mockito.verify(listeners)->add(eventType, listener)

work?

qualidafial Tue 21 Sep 2010

@helium: no, because the -> operator resolves to the sys::Obj.trap method, not the add method.

dmoebius Tue 21 Sep 2010

+1 for generic method bound parameters, as proposed by ivan and vkuzkokov.

T reduce(T init, |T, V, Int -> T| c)
T[] map(|V, Int -> T| c)

T is currently unused as generic type parameter.

vkuzkokov Tue 21 Sep 2010

Occasionally, I just looked through the code:

public final List map(Func f)
{
  Type r = f.returns();
  if (r == Sys.VoidType) r = Sys.ObjType.toNullable();
  List acc = new List(r, (int)size());
  if (f.params.sz() == 1)
  {
    for (int i=0; i<size; ++i)
      acc.add(f.call(values[i]));
  }
  else
  {
    for (int i=0; i<size; ++i)
      acc.add(f.call(values[i], Long.valueOf(i)));
  }
  return acc;
}

So it does work like T[] map(|V, Int -> T| c). Even docs read so. I definitly should learn to read the fascinating manual.

The issue here is that return type is defined at runtime. That's too dynamic for my taste.

katox Tue 21 Sep 2010

makes sense, this would be generally useful, +1

brian Tue 21 Sep 2010

I really like the idea of a That type. However my main concern is that this starts to tread on the ground of a real parameterized type system. So far we only have generics in three built-in final classes and the special This type. And issue #902 starts to open a can of worms.

My thinking is that before we create something like a That type that we really need to explore what a real generics type system would look like in Fantom. Do we really want to shut the door on that option forever? If we wanted to do it in the future how might something like a That type cause future problems?

MoOm Tue 21 Sep 2010

I like the idea of the That type, but it feels a bit like a patch to me. I think the That type would be a too small step forward to deserve the development it would require. And it wouldn't solve most of the cases where generics would actually be useful (e.g. like making your own collections, building a type-safe Event system, ...). It would indeed make nicer some methods of the sys pod (reduce is an example) but it would not fit for most of the cases where you'd need generics. I'd definitely prefer to have a real (even limited) generics type-system instead!

ivan Wed 22 Sep 2010

I think it can be a good start to explore fitness of generics to Fantom at first by introducing generic methods.

  1. Probably sys::This is already the step in that direction.
  2. Sometimes generic method params have to come into type params of built-in generic types (for example, to return parameterized function or list) (and as far as I understand, this step is already planned for sys::This - see #902)
  3. Generic method syntax can be just plain adoption of java/c#:
    <T> T reduce(T init, |T, V, Int -> T|)
  4. sys::That can be just an convenience for generic method with one parameter (as it is used to refer to 1st parameter of closure)

On the other side, I can't remember cases when I needed generics and now I can't imagine for cases where I'd need more than one generic parameter for method.

tcolar Wed 22 Sep 2010

I like the That proposal.

Like Ivan said I think one generic param is plenty enough ... it's a generic anyway so you can use composition anyway.

What I really don't like is Java generic implementation, way to verbose and just too complex / ugly looking, it would be a bit more compact and less redundant in Fantom but still

So I'll be happy enough with This & That - I'm fine without That even but I'm not against it.

helium Wed 22 Sep 2010

I can't imagine for cases where I'd need more than one generic parameter for method.

Your reduce method already has two, the result type and the type of the list elements.

ivan Wed 22 Sep 2010

helium,

I meant only generic parameters of method itself. The type of the list elements here is class generic parameter. I can easily imagine any number of generic parameters for class (for instance - Func arg types)

fansh> (|Int i, Int j, Int k, Str s, Str d, Int[] l| {}).typeof.params.size
7

helium Wed 22 Sep 2010

OK, you can see how a class could have more than one argument. Now imagine a method accepts an argument of such a type.

// class with more than one parameter      
class SortedMap(Key, Value)
{
   ... // Red-Black-Tree or whatever
}


class Foo
{
   //
   // Method that works with any SortedMap       
   //
   void bar(Key, Value)(SortedTree!(Key, Value) map)
   {
      ... // do something with "map"
   }
}

qualidafial Wed 22 Sep 2010

My wishlist for Fantom generics:

  • Runtime enforcement of type safety instead of erasure. Some possible approaches to achieve this:
    • When classes are parameterized, use a hidden constructor arg to pass the Type(s), and inject runtime type checking on all method arguments to ensure type safety at runtime.
    • Generate a new class for each parameterized type, and let the bridge methods enforce type safety, much the same way we let typed java arrays enforce type safety for sys::List for us.
  • Allow wildcards only at the class level, for restricting the parameter to a specific class or mixin: class Foo<T : Num> { ... }
  • Apply the same rules for upcasting and downcasting which currently apply to sys::List.
    • Allow e.g. Thing<Str> to be downcast to Thing<Obj>, but ensure that runtime checks are in place so that you can't put a non-Str in that object.
    • Allow upcasting e.g. from Thing<Num> to Thing<Int> to client code, and let that usage be the developers responsibility.
    • Disallow obvious wrong casts e.g. converting a Thing<Str> to a Thing<Float>.

helium Wed 22 Sep 2010

I don't like your up- and down-casting rules at all. Variance depends on where you use the types.

class Covariant(In)
{
   void method(In x) { ... }
}


class Contravariant(Out)
{
   Out method() { ... }
}


class Invariant(T)
{
   T method(T x) { ... }
}

Covariant(Str) is a subtype of Covariant(Obj). Contravariant(Obj) is a subtype of Contravariant(Str) and Invariant(Str) isn't related to Invaraint(Obj).

I think that it should either be checked like I just showed or the static checking should be very relaxed and any cast that might make sense is allowed and checked at runtime.

But the rule "make everything covariant even if it doesn't make any sense" is bad.

While I'd prefer to have variance (either inferred by the scheme shown above or with explicit annotations like in C# or Scala) I could even live completely without variance.

katox Wed 22 Sep 2010

There are other means other than generics how to achieve this

  • template metaprograming (like Haskel)
  • macros with full lang features (like LISP defmacro)
  • runtime enforcement qualidafial mentioned

A sidenote. Regardless the form I'd like an alternative syntax to that horrible TypeX<TypeB<TypeC, TypeD>, TypeY> HTML-ish garbage.

tcolar Wed 22 Sep 2010

I'm not 100% against generics as long as you can come up with something nice and simple (to understand & syntax)

Map<String, ? extends List<String>> map .. burp.

rfeldman Wed 22 Sep 2010

Runtime enforcement of type safety instead of erasure.

Lack of this, along with horrible syntax, are really the only problems I have with Java generics.

I would personally be fine if Fantom offered a relatively simple generics system for methods and continued to omit a class-level generics system outside the existing List, Map, etc. A lot of the hairiness seems to show up at the class level.

Food for thought - what if instead of...

Obj verify(Obj target) {

}

...we had:

Type.of(target) verify(Obj target) {

}

(Or maybe some special operator instead of Type.of.)

The basic idea is that instead of declaring a separate type like Java does (e.g. <TargetType>) you would base the return type off the type of a declared argument. I think it's pretty clear with this syntax that the return value of verify will have the same type as the supplied target.

Then if you need to restrict the type, instead of doing something ugly and wordy like <T extends Num> you just change the type of the argument:

Type.of(target) verify(Num target) {

}

Obviously this also works for multiple parameters, e.g.

Type.of(secondParam) foo(Num firstParam, Obj secondParam) {

}

Probably Type.of is not the ideal syntax for this (since that already has meaning in the language), I just suggested it as something that might be intuitive as a starting point for discussion.

Thoughts?

tcolar Wed 22 Sep 2010

I could be mistaken, but ins't "#" used for type literals already, so maybe var# is sugar for Type.of(var) ?

Assuming that is the case, then that could make for a nice concise syntax: target# verify(Int target)

rfeldman Wed 22 Sep 2010

+1 to that syntax

(Although in retrospect Int is a poor example since it's final...I'll change it to Num in my previous post)

yachris Wed 22 Sep 2010

macros with full lang features (like LISP defmacro)

Be careful with that axe, Eugene...

Rats, can't find the article. It was on Dr. Dobbs... but here's my quote to a friend interested in Clojure (He was asking my opinion, as I was a professional Lisp programmer during the 80s), which I wrote right after reading the article:

This is a report on a debate at the "50 years of Lisp" conference.  
The most cogent part is:

"We have experienced so many problems with side-effects in macros
that we in the Lisp world cannot do a reliable incremental build."

No one at the conference really disagreed.

NOTE: Not just some random bunch of losers but the best names in
Lisp say that *the language breaks the build*.  Wow.

So, yeah, when Brian says he wants to think about this a lot, believe me, I'm glad he will :-)

jodastephen Wed 22 Sep 2010

I'm at JavaOne, so can't reply in detail. However, my take is that most developers really don't "get" variance. If it is explained to them for a specific case, they will understand, but as it isn't important to their job, they will rapidly forget again.

So, my view is that any sensible generics system should not overly expose the reason why List<Str> can't be cast to List<Obj>.

I've been toying with a notion of language-level conversions between types as a way of bypassing the need for the ? extends form of variance (or Scala's declaration site version).

Str str := 6    // does not compile
Str str := ~6   // compiles by using a conversion function

List<Str> list = ["Hello", "World"]
List<Obj> objs = list   // does not compile
List<Obj> objs = ~list  // compiles by using a conversion function 

The generics conversion function would be a wrapper that does the necessary runtime checks.

Anyway, its just an idea, but I think the "wiggle" operator could be very neat.

qualidafial Thu 23 Sep 2010

I don't like your up- and down-casting rules at all. Variance depends on where you use the types.

Those are just the up- and down-casting rules already in place.

Downcasting example:

fansh> Str[] list := ["Hello", "World"]
[Hello, World]
fansh> Obj[] objs := list
[Hello, World]
fansh> objs.add(4)
sys::CastErr: Adding 'sys::Int' into 'sys::Str[]'
  fan.sys.List.insert (List.java:381)
  fan.sys.List.add (List.java:348)

Upcasting example:

fansh> Obj[] objs := [4, "foo"]
[4, foo]
fansh> Str[] strs := objs
[4, foo]
fansh> strs.each { echo(it) }
sys::CastErr: java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
  fan.sys.List.each (List.java:528)

Personally I find that this situation is a good compromise in exchange for having simplified generics. You still get type safety, but only at the invariant level.

My suggestion is simply to extend the same set of rules to cover all generic classes. You can guarantee type safety by only using invariance. Or if you know what you're doing, use covariance or contravariance at your own risk.

The one exception to this was that you can parameterize a class and require that the parameter type extends a particular class/mixin. This way you can create specialized generic classes which can use the methods of that class/mixin.

DanielFath Thu 23 Sep 2010

The more I see things like upcasting the more I want a ~ operator. Call it fear/lizard brain whatever but the second upcasting example really makes me scratch my head in confusion. Why does echoing the strs work, but not eachr?

go4 Thu 23 Sep 2010

*

brian Thu 23 Sep 2010

Anyway, its just an idea, but I think the "wiggle" operator could be very neat.

That actually might be a very nice to way to do "explicits" for converting types as needed.

tcolar Thu 23 Sep 2010

I do like/prefer the "explicit" way, whether using the ~ operator or something else.

Login or Signup to reply.