Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

defaulted type parameters #3469

Closed
CeylonMigrationBot opened this issue Jul 12, 2012 · 23 comments
Closed

defaulted type parameters #3469

CeylonMigrationBot opened this issue Jul 12, 2012 · 23 comments

Comments

@CeylonMigrationBot
Copy link

[@gavinking] In #4995, @ikasiuk proposes to introduce defaulted type parameters as a great way of eliminating ContainerWithFirstElement. In the past this is something I've suggested in conversations with @RossTate as a kind of alternative to virtual types. I think we should do it.

The syntax is clear:

shared Interface Collection<Element=Void> { ... }
shared Interface Entry<Key=Object,Item=Object> { ... }
shared Interface Iterable<Element=Void,Null=Nothing> { ... }

There is a little redundancy here, in that the default arg is likely to almost always be the upper bound on the type parameter. But we don't want to get into the stuff Java has with implicitly inferring the upper bound from the constraint (that has decidability problems).

An open question is: are the default type arguments allowed to refer to previous type parameters in the list, for example:

shared interface Foo<Bar, Baz=Iterable<Bar>> { ... }

To me that seems reasonable, but we probably don't need it for now.

[Migrated from ceylon/ceylon-spec#363]
[Closed at 2013-01-10 18:30:26]

@CeylonMigrationBot
Copy link
Author

[@gavinking] Note that the absolutely best thing about this feature is that it can be completely implemented by me fucking with the typechecker. It does not require anything new in the backend at all. :-)

@CeylonMigrationBot
Copy link
Author

[@RossTate] I can't see any problems with this provided we don't require constraints on type parameters to hold for a type to be valid (only required when one allocates or extends a type), otherwise you'll need the defaults to fit with the variance to prevent confusing situations.

@CeylonMigrationBot
Copy link
Author

[@gavinking] > otherwise you'll need the defaults to fit with the variance to prevent confusing situations.

I don't understand this. Example?

@CeylonMigrationBot
Copy link
Author

[@RossTate] Actually, I thought of an example of a slightly different problem. It's off the type of my head, so it's by no means a practical example, but hopefully it'll illustrate the concern.

class Foo<out X, Y = Invariant<X>>

Now Foo<String> is not a subtype of Foo<Object> despite Foo being covariant with respect to its first parameter. Nothing unsound here; it's just weird.

As for the original problem, say I had this weird class:

class Bar<out X, Y = Contra<X>> given Y satisfies Contra<Number>

There are no explicit constraints on X. However, Bar<Integer> satisfies the constraints while its subtype Bar<Object> does not.

@CeylonMigrationBot
Copy link
Author

[@gavinking] > Nothing unsound here; it's just weird.

Yeah, a little strange, but to be honest I'm not very bothered by it.

There are no explicit constraints on X. However, Bar<Integer> satisfies the constraints while its subtype Bar<Object> does not.

Apparent subtype, you mean?

Well, again I guess I'm not bothered by this. Bar<Object> is just an abbreviated way to write Bar<Object,Invariant<Object>>, which also doesn't satisfy the constraints. I'm not bothered by it because I think it's unlikely that types like this will often occur in practice.

@CeylonMigrationBot
Copy link
Author

[@RossTate] Cool with me. Just figured I should let you know, heheh.

@CeylonMigrationBot
Copy link
Author

[@gavinking] > Cool with me. Just figured I should let you know, heheh.

Yes, nice to not be caught by surprise by these things later on ;-)

@CeylonMigrationBot
Copy link
Author

[@gavinking] So I just realized that this problem can pretty much already be solved using type aliases, without introducing new syntax. We could define:

interface IterableWithFirstElement<Element,Null> given Null satisfies Nothing { ... }
interface Iterable<Element> = IterableWithFirstElement<Element,Nothing>;

The question is: is this the only place where we're really going to want to do stuff like this, or are default type arguments sufficiently generally-useful that we should have 'em anyway (they're easy enough to add).

P.S. This exposes an irregularity in the language. Type aliases let me partially apply a type constructor to a type argument. But I can't do the same with value-level functions. I can't write:

function rangeFromOne(Integer max) = Range(1,max);

Of course I can write:

function rangeFromOne(Integer max) { return Range(1,max); }

But that's somehow not quite the same thing...

(i.e. we have ML's syntax at the type level, and C's at the value level.)

@CeylonMigrationBot
Copy link
Author

[@gavinking] So if we wanted to resolve this irregularity in a truly consistent way, I would take the fashionable route of languages like coffeescript and use a fat arrow here:

function rangeFromOne(Integer max) => Range(1,max);

And at the type level:

interface Iterable<Element> => IterableWithFirstElement<Element,Nothing>;

i.e. => implies lazy evaluation. You could think of => f(x) is an abbreviation of { returns f(x); }. Different to = and := which mean immediate evaluation and assignment of the result.

Not sure how you guys are going to react to the idea.

@CeylonMigrationBot
Copy link
Author

[@RossTate] I'm not sure what irregularity you're talking about here. Partial application surely isn't the difference, since an alias can do anything. For example, alias RaggedMatrix<T> = List<List<T>>.

@CeylonMigrationBot
Copy link
Author

[@gavinking] @RossTate the irregularity is that a type expression IterableWithFirstElement<Element,Nothing>, List<List<T>>, whatever, that appears on the RHS of = has the parameters of the LHS in scope. The expression is re-evaluated each time a list of arguments is supplied, so RaggedMatrix<Float> evaluates to List<List<Float>>. That's not the case with value expressions that appear on the RHS of =. They are evaluated when the statement they appear in is executed&mdashi.e. once per declaration. So the parameters of the LHS are not even in scope on the RHS.

function rangeFromOne(Integer max) = Range(1,max);

is a type error. To be consistent with the syntax for type aliases, it should be acceptable and rangeFromOne(5) should evaluate to Range(1,5), like it would in ML. By introducing => we would resolve this irregularity, and get a shortcut way to write single-line functions.

FTR, this irregularity in the semantics of = actually causes problems in the typechecker, where the rules for determining what scope the RHS of an = occurs in depends upon the kind of declaration. It's a PITA, and there is at least one open issue against the type checker relating to this that is very difficult to fix.

@CeylonMigrationBot
Copy link
Author

[@RossTate] Got it. I wouldn't use lazy evaluation to explain it then.

@CeylonMigrationBot
Copy link
Author

[@gavinking] Of course, the other path to resolve the irregularity is to disallow the following, which I personally like:

function range(Integer min, Integer max) = Range;

Instead giving you the choice of:

value range = Range;
function range(Integer min, Integer max) = Range(min, max);

This is the path that languages like ML go down, and is also the traditional syntax in mathematics. I don't think it's quite as nice in Ceylon where you have named arguments, defaulted parameters, and sequenced parameters, but it's worth reconsidering in light of the discussion about the syntax for type aliases.

@CeylonMigrationBot
Copy link
Author

[@gavinking] Oh and an especial problem with the ML-style syntax is forward declaration/named arguments. The following doesn't look remotely right:

function range(Integer min, Integer max);
//lots of other stuff
...
range = Range(min, max); //wtf are min and max??

Nor does this:

function generate(Integer next(Integer previous)) { .... }

generate { next = sqr(previous); }; //wtf, previous, what is that?!

Going down that path, you would have to write:

function range(Integer min, Integer max);
//lots of other stuff
...
range = (Integer min, Integer max) Range(min, max);

and:

generate { next = (Integer previous) sqr(previous); };

It's not nice that you wind up declaring the parameters twice here. I think that's the real reason I originally decided to go down the path we went down.

With our syntax we have the convenience of being able to write:

function range(Integer min, Integer max);
//lots of other stuff
...
range = Range;

And:

function generate(Integer next(Integer previous)) { .... }

generate { next = sqr; };

OTOH, we can't write this:

function rangeFromOne(Integer max) = Range(1,max);

We have to write either:

function rangeFromOne(Integer max) { return Range(1,max); }

or:

function rangeFromOne(Integer max) = curry(Range)(1);

A => would give us total flexibility.

@CeylonMigrationBot
Copy link
Author

[@RossTate] I wouldn't call that ML-style syntax. I'm also not sure why the declarations are separated from the definitions.

@CeylonMigrationBot
Copy link
Author

[@gavinking] > I wouldn't call that ML-style syntax.

What is ML-style is that the parameters of the function are in scope on the RHS of =. In SML I can write:

fun rangeFromOne n = range 1 n

I'm also not sure why the declarations are separated from the definitions.

That's something you quite commonly do in Java-like languages.

  • the definition might be inside one branch of an if or switch, or, in Ceylon
  • the declaration might be in a superclass, and the definition in a subclass, or
  • the declaration might be in a parameter list, and the definition in a named argument list.

@CeylonMigrationBot
Copy link
Author

[@gavinking] FTR, I just remembered that Dart went down the path of fat arrow => for single-expression method bodies (coffeescript uses ->.).

@CeylonMigrationBot
Copy link
Author

[@gavinking] Hrm, this runs into the same problem that prevents us from using inferred type arguments in extends or type aliases.

The typechecker needs to assign all static types that occur in a "LHS-like" location in a single phase. (Or arbitrary phases would be required.)

So if I'm not mistaken, we can't have defaulted type parameters...

@CeylonMigrationBot
Copy link
Author

[@gavinking]

So if I'm not mistaken, we can't have defaulted type parameters...

No, that's nonsense (it was late). It can surely be implemented by substituting default arguments "lazily".

@CeylonMigrationBot
Copy link
Author

[@gavinking] Wow, this one is a lot harder than it looks. I got quite far in implementing it here:

ceylon/ceylon-spec@sequential...defaulttypeargs

Unfortunately to complete this implementation, I would have to completely rework how union/intersection type canonicalization works (it would have to happen lazily). I'll take a look at this when I get a chance, but if it's not "easy", I'll leave this issue for 1.1.

@CeylonMigrationBot
Copy link
Author

[@gavinking] OK, so now that #3566 is resolved, this one is back in play!

Indeed, it seems that I've now got it working, in just a few minutes work.

@CeylonMigrationBot
Copy link
Author

[@gavinking] So in order to cleanly fix #3638, I need to clean up the mess of ContainerWithFirstElement and friends. What I would like to do is remove that interface completely, and just add a new defaulted type parameter to Iterable. I think that's a simplification we're really going to appreciate in the future.

@CeylonMigrationBot
Copy link
Author

[@gavinking] This is implemented in the branch and seems to be working. (I guess it could still use some further testing though.)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants