Skip to content
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

Open
wants to merge 3 commits into
base: master
from

Conversation

Projects
None yet
6 participants
@benmerckx
Copy link

benmerckx commented Aug 2, 2018

Follow up of HaxeFoundation/haxe#7304

Optionally declare a default type for generic type parameters.

class Test<T = String> {}
$type((null: Test)); // Test<String>

Rendered version

benmerckx added some commits Aug 2, 2018

@RealyUniqueName

This comment has been minimized.

Copy link
Member

RealyUniqueName commented Aug 2, 2018

That introduces ambiguity for expressions like this:

var t = new Test();

Currently, it's being typed as Type<Unknown> which is later get inferred to a specific type parameter.

@benmerckx

This comment has been minimized.

Copy link
Author

benmerckx commented Aug 2, 2018

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 Unknown there.

@skial skial referenced this pull request Aug 8, 2018

Closed

Haxe Roundup 443 #532

1 of 1 task complete
@back2dos

This comment has been minimized.

Copy link
Member

back2dos commented Aug 9, 2018

This would be super awesome. One could add multiple parameters without making the API more cumbersome for users.

@DavisDevelopment

This comment has been minimized.

Copy link

DavisDevelopment commented Aug 15, 2018

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..

@benmerckx

This comment has been minimized.

Copy link
Author

benmerckx commented Aug 16, 2018

@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.

@DavisDevelopment

This comment has been minimized.

Copy link

DavisDevelopment commented Aug 17, 2018

@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 ?convert is marked as optional, which tells haxe that it's acceptable to simply omit it from the arguments, and its return-type is the only thing from which Out's concrete type could be inferred in this instance. But, it seems to me that the first if statement could be used for inference in the given example. Could one not somehow look to the if (convert == null) return ... for the return-type, since the return expression is an invokation of 'magic', but with the second argument provided, thus providing the return type. Since you can check if an invokation expression for 'magic' has the second argument provided, then it stands to reason that you could also resolve the return-type of an invokation expression that didn't provide the second argument by checking for the return-type of code that will always be executed when the second argument is unspecified.

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 Out to be the return-type of default_f.
That's what I think the solution to oddities like http://try-haxe.mrcdk.com/#9Ff3e are, but it might be easier or otherwise better to instead solve the problem this way:

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 null

@benmerckx

This comment has been minimized.

Copy link
Author

benmerckx commented Sep 2, 2018

I didn't see the edit to your post before and it took me quite some time to realize it was <In, Out = In> you were after :) I guess that makes sense but it also feels like it ought to be possible without defining the default type, however that's another discussion. It would require access to the other type params to get there. I'll make a mention of it in the proposal as it was still an open question.

I tried implementing it last week. Got as far as parsing and storing the fields in type_param. I got stuck trying to access that data in load_instance' though so I gave up. I now think supporting defaults on methods is not that much harder than on types, if I interpret it right, so I'll add that too.

@Simn

This comment has been minimized.

Copy link
Member

Simn commented Sep 3, 2018

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.

@RealyUniqueName

This comment has been minimized.

Copy link
Member

RealyUniqueName commented Sep 21, 2018

@benmerckx
The example from the proposal:

class Component<Props: {} = {}, State: {} = {}> {}
class MyComponent extends Component {}
class MyComponentWithProps extends Component<MyProps> {}
class MyComponentWithPropsAndState extends Component<MyProps, MyState> {}

How would I declare Component<MyState>?
If it's not possible, then it's just a partial solution to the problem, which is described in the motivation part.

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?

@back2dos

This comment has been minimized.

Copy link
Member

back2dos commented Sep 22, 2018

How would I declare Component<MyState>?
If it's not possible, then it's just a partial solution to the problem, which is described in the motivation part.

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 Props before State, since stateless components with props are far more common than property-less statefull components (which kind of miss the point of composability). See github code search: 391 results vs 40 results.

Now we could use the default keyword for explicit skipping, both in function calls and when specifying type parameters, in which case you'd do Component<default, MyState>.

where you'd want this feature for the reasons other than completely ignoring a type parameter?

Uhm, I would start in tink_core with this:

enum Outcome<Data, Failure = Error> {
  Success(data:Data);
  Failure(failure:Failure);
}

Or simple examples like typedef Node<Attributes = haxe.DynamicAccess<String>> = ..., in which case the default behavior is probably good enough for many cases, but can be specialized to something typed.

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.

@kLabz

This comment has been minimized.

Copy link

kLabz commented Sep 24, 2018

How would I declare Component<MyState>?

Maybe kinda like bind, with Component<_, MyState>?

Compared to manually adding the default parameter for Props with Component<{}, MyState>, this has the benefit of staying up to date if Component's default type parameter changes.

@benmerckx

This comment has been minimized.

Copy link
Author

benmerckx commented Sep 24, 2018

How would I declare Component<MyState>?
If it's not possible, then it's just a partial solution to the problem, which is described in the motivation part.

In that case you would have a Component<{}, MyState>. Skipping default type parameters does not seem like something that is possible without being specific and naming them or using a keyword for ignoring one. That's however not something I'd like to add to this proposal. @back2dos suggested using Component<default, MyState> which would work if this is a requirement for having this proposal succeed.

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.

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?

I added a few more. If these are not convincing I'm hoping @back2dos could help me out.

@back2dos

This comment has been minimized.

Copy link
Member

back2dos commented Sep 24, 2018

Maybe kinda like bind, with Component<_, MyState>?

Totally forgot about _ 🤦‍♂️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.