New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Default type parameters #50
Conversation
That introduces ambiguity for expressions like this: var t = new Test(); Currently, it's being typed as |
I'm not proposing to change the old behaviour for types without defaults. But yes, if a default was declared it should be used instead of |
This would be super awesome. One could add multiple parameters without making the API more cumbersome for users. |
Oh My Goodness, the number of times I've wished for this! Moreso in methods that accept type parameters than generic types, but both would be super nice. I REALLY need to devote some time to familiarizing myself with OCaml so I can fiddle with the compiler.. |
@DavisDevelopment could you share an example of such method? I've not run into enough situations there myself, type inference often suffices, so it's not mentioned in this proposal. It would probably make sense to include it though. |
@benmerckx Yeah, if I'm understanding correctly, then just this should demonstrate: static function magic<In, Out>(input:In, ?convert:In->Out):Out {
if (convert == null)
return magic(input, x->x);
else
// actually perform the magic
}
// using the magic
static function wizard(mw: String) {
return magic(mw).split('');
} this example may not be exact, but as far as I can tell, this pattern (and its ambiguity) should be resolvable by Haxe's type-inference. I say that because So, to further simplify, the compiler might handle patterns like: static function default_f(x: String):String {
return x;
}
static function example<Out>(value:String, ?f:String->Out):Out {
if (f == null)
return example(value, default_f);
// rest of method body which only executes when [f] has been provided
} and use the logic laid out above to infer the concrete type for class Test {
static function main() {
trace(magic('Hello').length);
}
static function magic<In, Out = In>(input:In, ?f:In->Out):Out {
if (f == null)
return magic(input, (v -> v));
return f(input);
}
} I think that, either way, it'd be super nice to be able to check specifically for an argument being "unspecified" (not provided), that won't give a false-positive when the argument is explicity provided as |
I didn't see the edit to your post before and it took me quite some time to realize it was I tried implementing it last week. Got as far as parsing and storing the fields in |
I'm a bit wary of implicit types like that. It can cause some confusion because it's not easy to tell where a type came from. Still, I'm not against this feature from a design point of view. |
@benmerckx class Component<Props: {} = {}, State: {} = {}> {}
class MyComponent extends Component {}
class MyComponentWithProps extends Component<MyProps> {}
class MyComponentWithPropsAndState extends Component<MyProps, MyState> {} How would I declare Also, could you please add some examples for use cases, where you'd want this feature for the reasons other than completely ignoring a type parameter? |
It's an absolutely adequate solution, that can be expanded upon. Or not, which would be fine. In languages that don't allow argument skipping for functions, people do the following (as most do in Haxe anyway, because our implicit argument skipping causes quite a lot of headaches): arrange parameters by decreasing likelihood of needing anything other than the default. In this case it's clearly Now we could use the
Uhm, I would start in tink_core with this: enum Outcome<Data, Failure = Error> {
Success(data:Data);
Failure(failure:Failure);
} Or simple examples like I could go on all night, but I don't want to waste your time and mine, so if you could please give a number for how many examples you want, I'd be happy to satisfy your curiosity. |
Maybe kinda like bind, with Compared to manually adding the default parameter for |
In that case you would have a I think the current state of the proposal has enough use without having a mechanism of skipping over some defaults. What I'm proposing is pretty much the same feature as in flow or typescript where it is heavily used.
I added a few more. If these are not convincing I'm hoping @back2dos could help me out. |
Totally forgot about |
Could we have a vote on the proposal as is, excluding skipping of parameters that was mentioned? Skipping parameters doesn't seem like a requirement to me but can definitely be revisited if the need arises. |
We are fine with the proposal, but have to check how to implement this in our type system. My intuition is that this is similar to constraints, but happens earlier. Not quite sure how much earlier though... do we unify constructor arguments first and then apply defaults, or the other way around? |
Sorry, I confused proposals. The one I was thinking of was #36. I don't think we came to a conclusion on this one. |
One more case where this is useful is to help the compiler determine a @:generic class Loader<K, V, C = String> {
final load: K -> V;
final cacheKeyFn: K -> C;
public function new(load: K -> V, ?cacheKeyFn: K -> C) {
this.load = load;
this.cacheKeyFn = switch cacheKeyFn {
case null: Std.string;
case v: v;
}
}
// ...
}
// Currently: could not determine type for parameter C
var cache = new Loader((i: Int) -> i + 1); |
@benmerckx but that looks that it shouldn't compile anyway, because if |
@nadako If It all boils down to being able to tell the compiler what type a generic type parameter is in case you are manually assigning a default value to an optional (null) argument. The example might not be super illustrative because it keeps a lot of the details out but it is a use case I ran into. |
You could say I'm still totally in favor, but this is probably not the best use case ;) |
Right, I also just realized how a cast would be needed there for it to compile with any other |
We didn't reach a consensus on this one in our haxe-evolution meeting yesterday. We agreed that this works well for normal type-hints and there shouldn't be any obstacles supporting this feature there. However, the constructor situation is unclear: class C<T = String> {
public function new(t:T) {}
}
function main() {
new C(12); // C<Int> or compiler error?
} The question becomes at which point the default type should be applied. On a related note, the situation for default type parameters on methods is unclear. In this case, we don't even have a syntax for explicit type parameters. It might make sense to support default type parameters only at type-level, at least initially. |
I'm leaning toward letting The problem that this proposal sets out to solve is that often you have a pattern like this: class FooOf<T:DefaultType> {}
typedef Foo = FooOf<DefaultType>; Where >90% of the time the user will be using Consider this: import js.html.*;
class Test {
static function main() {}
}
class Component<T> {
public function new(t:T) {}
}
interface Factory {
function getComponent():Component<Element>;
}
class MyFactory implements Factory {
var someImage:ImageElement;
public function getComponent()
return new Component(someImage);
} This produces an impressive error:
The point of this proposal is that users who don't care to narrow the type don't have to pay for it being narrowable. With |
I don't think default type-parameters should be used to constrain types (any more than default values constrain values) @back2dos I think your example can be solved by improving existing type-constraint behavior: The error is a type-parameter invariance issue class Component<T: Element> {
public function new(t: T);
}
var someImage: ImageElement
var c: Component<Element> = new Component(someImage);
// Internally the compiler would see this as `new Component((someImage: Element))` because of the constraint
// (currently it doesn't which causes the error) When you have class Component<T> {
public function new(x:T)
} And you do var anything: SomeType
new Component(anything) // -> Component<SomeType> I argue the default type parameter shouldn't come into play because you've specified the type parameter by passing the argument The default type parameter should only be used when no type information has been supplied For example class Vector<T = Float> {
public function new(length: Int);
public function get(index: Int): T;
}
new Vector() // -> Vector<Float>
new Vector<Int>() // -> Vector<Int> To implement a component that takes any html element as input, but if none is supplied it generates its own element might look like this class Component<T: Element = Element> {
var element: T
public function new(?el: T)
} So on |
Let's go back to Simn's statement:
There are two options, how to treat omitted optional type parameters:
You seem to be suggesting the second option, where the ability to infer the type parameter from constructor usage is simply a fortunate edge case. This however is the actual behavior: class Test {
static function main() {
var v = new haxe.ds.Vector(100);
$type(v);// haxe.ds.Vector<Unknown<0>>
v[0] = 1;
$type(v);// haxe.ds.Vector<Int>
}
} Thus the assertion I'm equally confused by var c:Component = new Component(someImage);
$type(c);//Component<Element> - ideally this would just print Component
var c = new Component(someImage);
$type(c);//Component<ImageElement> I don't find that particularly intuitive, let alone practical. Furthermore, you're free to verify that constraining A default type parameter should be used when said default is useful in the vast majority of cases. It is therefore impractical to then make the user explicitly specify it anyway, to avoid being outsmarted by the compiler. |
I think I didn't fully clarify my examples – none will work in current haxe, they're examples for how things could work
I'm aware constraining The example I gave var c: Component<Element> = new Component(someImage); Was meant to be a reduced example of your case in |
I think the thing to do is go over a different use case examples and see if one approach is obviously better than the other So far I can’t find a strong example for one over another but the explicit ( (Apologies for the close; I probably shouldn’t try to use GitHub on a phone) |
This proposal has been accepted, see https://haxe.org/blog/evolution-meeting-2021/ for details. |
Follow up of HaxeFoundation/haxe#7304
Optionally declare a default type for generic type parameters.
Rendered version