haxe icon indicating copy to clipboard operation
haxe copied to clipboard

Infer optional fields on open structures when accessed with null coalescing operator.

Open back2dos opened this issue 3 years ago • 9 comments

Example:

import sys.net.*;
function connect(options) {
  var ret = new Socket();
  ret.connect(options.host ?? Host.localhost(), options.port);
  return ret;
}

In this case I would like options to be inferred to { ?host:sys.net.Host, port:Int }, unless there are reasons not to.

It's perhaps worth mentioning that there's a bit of a special case in combination with safe navigation:

import sys.net.*;
function connect(?options) {
  var ret = new Socket();
  ret.connect(options?.host ?? Host.localhost(), options?.port ?? 3306);
  return ret;
}

back2dos avatar Feb 26 '22 06:02 back2dos

This seems sensible.

Could you elaborate on that special case, because all I'm seeing is ??????...

Simn avatar Feb 28 '22 05:02 Simn

Hmm. Now that I'm thinking about it, I guess it comes down to treating safe navigation the same as field access inference wise, i.e. openStructure?.field ?? fallback should be working just like openStructure.field ?? fallback.

back2dos avatar Feb 28 '22 06:02 back2dos

By the way, there's something wrong with Host.localhost(). I'm getting String should be sys.net.Host on JVM because it's defined as public static function localhost():String. This code does compile on eval, but I think that's only because the definition there is extern static public function localhost();. I thought extern functions required a type-hint...

Simn avatar Feb 28 '22 11:02 Simn

Welp. The above code was really just an attempt to express some real code I have with stdlib types so there's no dependencies. When connect compiled on eval on try.haxe I thought I was done ^^

I thought extern functions required a type-hint...

Probably related to this whole _std-stuff, which does quite a few magic things.

back2dos avatar Feb 28 '22 11:02 back2dos

After thinking about this again, I'm not sure anymore if this is really a good idea. We would muddle the concept between "optionality" and nullability. The feature is called null coalescing, and I don't really see why this would imply that the field is optional.

This requires some additional discussion.

Simn avatar Feb 28 '22 12:02 Simn

Hmm, before we veer off into the conceptual, let me perhaps clarify my main motivation: I often want to pass quite a bit of stuff to a function, some of which is optional. Doing so, I face a dilemma.

I could just use optional arguments, but then this requires argument skipping to work correctly, which ... well ... you know. Skipping aside, positional arguments don't scale well with regards to readability.

So instead, I'm trying to reach for named arguments. We don't have those per se, because "we can simply pass an object". However, choosing this route, unless I spell out the whole type, there's no way to have optional arguments, which is what I wanted in the first place. I feel like I'm being punished for creating a signature that is more readable at the call site (and also with structure completion is very comfy to use).

That's really the problem I'm trying to remedy. This seems like a good fit and practically speaking, I don't see anything wrong with it. That doesn't make it right, but I'd be glad to hear any alternatives that you may have to offer ;)


As for the conceptual question. Hmpf ...

Let me posit this: type inference seeks the broadest type that can statically guarantee for the code to run correctly (to the degree that types can even make such guarantees). From a pure type perspective, we cannot really say that either of { foo:Null<Foo> } or { ?foo:Foo } is broader, because they unify both ways. But { ?foo:Foo } is broader in the sense that the space of object literals it accepts is larger. That makes it the correct choice, based on my initial postulate - which is definitely not spelled out anywhere, but I would argue that it's a fair characterization of how type inference is expected to behave and when it doesn't people file bugs.

Also, let's check the flipside: if a field can be inferred to be nullable, what's the point in still inferring as mandatory? Beyond the conceptual, I would very much like to see a concrete piece of code that benefits from the field being mandatory and thereby forces the user to explicitly pass null?

back2dos avatar Mar 08 '22 09:03 back2dos

I keep running into this. It seems like nothing more is to be said on the subject, so perhaps it's time to decide either way.

If the current behavior is truly "by design" (which I'm not really convinced of so far), then I suppose I can live with it and we can just close this.

back2dos avatar Jul 14 '22 08:07 back2dos

My problem here is that I acknowledge the problem you're describing, but I still don't think inferring fields as optional is correct when they are subject to null-coalescing. However, I don't have a concrete example where this would lead to an actual problem. I imagine it would have to be something where an optional inference causes a typo to be missed or something along those lines. I'll see if I can come up with something that might happen in the real world.

It would also be interesting to know how other languages handle this.

Simn avatar Jul 14 '22 08:07 Simn

It would also be interesting to know how other languages handle this.

Hmm. The intersection between languages that have something equivalent to implicit structures (e.g. interfaces in TypeScript and Go) and those that have strong type inference is not a very crowded place (TypeScript will just infer options as any and warn about it, while the only thing I found in Go were some GitHub issues that were closed after a workaround was suggested).

I did notice that Flow has decent structural inference, but it seems to make all fields optional anyway. Example:

/* @flow */

function foo(x):string {
  if (x) {
    return x.bar;
  }
  return "default string";
}

foo({ bar: '123' }); // works
foo({ });// also works, although maybe the playground is configured for more leniency regarding null | undefined
foo({ bar: true }); // fails as it should

back2dos avatar Jul 15 '22 06:07 back2dos