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

@:using should work for all type kinds #8188

Closed
Gama11 opened this issue Apr 18, 2019 · 19 comments
Closed

@:using should work for all type kinds #8188

Gama11 opened this issue Apr 18, 2019 · 19 comments
Assignees
Milestone

Comments

@Gama11
Copy link
Member

Gama11 commented Apr 18, 2019

Just merging these three issues that are all essentially about the same thing: @:using not behaving as expected and only working on some kinds of types.

@back2dos
Copy link
Member

I think we should not support anonymous types (structures and functions), at least initially, because of their decentralized nature and the resulting potential for conflict. Scenario:

// library A:
package libA;

@:using(libA.Vector.VectorTools)
typedef Vector = {
  var x:Float;
  var y:Float;
}

class VectorTools() {
  static public function times(a:Vector, b:Vector):Float
    return a.x * b.x + a.y * b.y;
}

// library B:
package libB;

@:using(libB.Vector.VectorTools)
typedef Vector = {
  var x:Float;
  var y:Float;
}

class VectorTools() {
  static public function times(a:Vector, b:Vector):Float
    return a.x * b.y - a.y * b.x;
}

Put both in a single project and kaboom, you have a conflict. Either one gets applied or the other. Let's assume libB's global static extension wins. In that case, all code in libA that does v1.times(v2) will break. In this particular example, it's especially bad, because it will break at runtime. But even if it broke at compile time, it would mean that you now have gazillion of compiler errors in 3rd party code. So you're left with using libA or libB.

During the original proposal, it never occurred to me that this could actually be used on typedefs, because typedef A using Main.Tools = {value:Int}; looks quite nonsensical.

In any case, unless somebody has a brilliant idea how to support typedefs while preventing libraries from mistakenly hijacking types they don't own, I propose to explicitly forbid @:using on typedefs. For the ones that alias anonymous types for the reasons above, and for the ones that resolve to named types too, because we don't want this to happen:

package libB;

@:using(libB.SomeCleverTrick)
typedef B = libA.A;

package libC;

@:using(libC.OtherCleverTrick)
typedef C = libA.A;

Any library should only be able to augment types for which it has complete ownership, e.g. an interface which either resides in this library and only in this library (regardless of whether some other lib has an interface of the same structure) or not. There's always the option to pull fun tricks with macros, but I think it's fair to say that if your lib manipulates other 3rd party code via macros, the consequences are yours to bear. In the same vein, perhaps it would make sense to back off of "the nice thing about metadata is that you can inject them from the outside", because it's a pretty explicit incitement to the type of behavior that drives the very author of this suggestions nuts ;)

@Gama11
Copy link
Member Author

Gama11 commented Apr 19, 2019

I think we should not support anonymous types

I think it's important that if not all types are supported, there's a proper compile-time error for it (rather than it just silently doing nothing). That would avoid any more of the confusion we've seen in those 3 issues.

@Gama11
Copy link
Member Author

Gama11 commented Apr 19, 2019

As for the rest of the argument: I (and apparently others, according to those issues) expected @:using to work just like a regular module-level using. What you describe there doesn't seem like a new issue at all:

using libA.Vector.VectorTools;
using libB.Vector.VectorTools;

class Main {
	static function main() {
		var v1:libA.Vector = {x: 0, y: 0};
		var v2:libB.Vector = {x: 0, y: 0};

		v1.printLibName(); // b
		v2.printLibName(); // b
	}
}

Edit: I realize that you're mostly making the argument that @:using is pretty hidden from the user compared to module-level using, but those can be pretty hidden too with import.hx.

@RealyUniqueName
Copy link
Member

I think we should not support anonymous types (structures and functions), at least initially, because of their decentralized nature and the resulting potential for conflict

In that example you put @:using on a typedef which always has a single origin. So if I access a variable typed as libA.Vector it should provide methods of libA.Vector.VectorTools.

@Gama11
Copy link
Member Author

Gama11 commented Apr 19, 2019

@RealyUniqueName But if that was doable, my example should work too? The first argument of the static extension methods is properly typed as libA.Vector and libB.Vector respectively.

@RealyUniqueName
Copy link
Member

Indeed.
Maybe I just dream about a convenient way to extend 3rd-party types in my projects )

@:using(my.pack.ArrayTools)
typedef MyArray<T> = Array<T>;

function myFun(a:MyArray<Int>) {
  if(a.contains(42)) {
    return theAnswer();
  }
  return comeBackLater();
}

@RealyUniqueName
Copy link
Member

RealyUniqueName commented Apr 19, 2019

But wait. @:using is attached to a type, while using is a module-level construct. So I think it's possible after all.

@back2dos
Copy link
Member

back2dos commented Apr 19, 2019

In that example you put @:using on a typedef which always has a single origin. So if I access a variable typed as libA.Vector it should provide methods of libA.Vector.VectorTools.

That is not how static extensions work. If you write using libA.Vector.VectorTools; then it is applied to any object matching the structure of libA.Vector, regardless of whether it is typed that way or not.

Maybe I just dream about a convenient way to extend 3rd-party types in my projects )

That is exactly the type of thing that import.hx is for. That way you can choose what is most convenient to you, without having to be careful about the effect bleeding into the class path of those 3rd-party types and messing up everything.

@RealyUniqueName
Copy link
Member

Except @:using allows to pass extended types outside of imports.hx area to the users of a library, for example.

@back2dos
Copy link
Member

back2dos commented Apr 19, 2019

I realize that you're mostly making the argument that @:using is pretty hidden from the user compared to module-level using, but those can be pretty hidden too with import.hx.

It's not about being "hidden" or not, it's about boundaries, which are the fundamental basis for composability. With using in a top-level import.hx, the effect is bounded to the particular class path, with global @:using, it's not.

If as a user of libA and libB you decide you want to use static extensions from both on all of your code, then you can go ahead and make it work for yourself. It's not a bad idea. If however both libs cannot be used together, because they clash over global static extension on structures, this a real problem. You can't really solve it, and neither can the respective authors of the libraries, because then they'd be breaking things for their users, who're already have code that depends on those static extensions being available.

I think it's important that if not all types are supported, there's a proper compile-time error for it (rather than it just silently doing nothing). That would avoid any more of the confusion we've seen in those 3 issues.

That's what I meant by "I propose to explicitly forbid @:using on typedefs", so I'm glad we're in agreement ;)

Let me be clear: I'm not against supporting typedefs on some principled stance about implicitness. I personally believe that it should be left to the user to figure out what degree of "magic" best works for them and their team. My opposition is purely on pragmatic grounds:

  1. Supporting this plainly introduces huge potential for conflict between libraries. The only thing I can think of is to constrain the effect of @:using on typedefs to the containing class path, but I suspect that that would require significant effort and also not be intuitive to the user.
  2. Another problem with this is that @:using on typedefs makes typing order so significant. Considering Alex's example, if some module that wants to rely on the static extension from MyArray.hx gets typed before MyArray.hx, it won't compile. At that point you'll have to write import MyArray; so you might as well write using MyArray; (and put it in your import.hx while you're at it)

@back2dos
Copy link
Member

Except @:using allows to pass extended types outside of imports.hx area to the users of a library, for example.

Yes and as a library author, you should not be doing that for anonymous types for the reasons I've explained quite abundantly. It may be convenient, but it won't work at the scale of an ecosystem.

@RealyUniqueName RealyUniqueName self-assigned this May 20, 2019
@RealyUniqueName
Copy link
Member

@:using also doesn't work for descendant classes if declared on a parent class.

@RealyUniqueName
Copy link
Member

The only thing left to decide and implement is @:using on typedef.

@back2dos
Copy link
Member

In any case @:using on typedef should either work or produce a warning/error. I would propose doing the latter for 4.0, which still allows supporting this later, if we really think it's worth the trouble.

@RealyUniqueName
Copy link
Member

Since @:using already emits an error on typedef let's keep it like that.

@haxiomic
Copy link
Member

haxiomic commented Apr 15, 2020

Adding to the discussion issue for future reference if this is revisited: an example where @:using would be helpful on a typedef is the case of js.lib.Iterator, where adding a @:using() to the typedef would enable native iteration (whereas currently you cannot iterate a js iterator in haxe out-of-the-box)

For example, js.lib.Iterator would become

@:using(js.lib.HaxeIterator)
typedef Iterator<T> = {
	function next():IteratorStep<T>;
}

@Gama11
Copy link
Member Author

Gama11 commented Apr 15, 2020

@haxiomic Maybe this could also be achieved by wrapping it in an abstract?

@haxiomic
Copy link
Member

@Gama11 That works but didn't semantically marry up with haxe iterators when I tried it in the original PR
7a27eed#r320362101

Although I need to think on it again because it's quite confusing

@vonagam
Copy link
Member

vonagam commented Jul 1, 2020

In that example you put @:using on a typedef which always has a single origin. So if I access a variable typed as libA.Vector it should provide methods of libA.Vector.VectorTools.

That is not how static extensions work. If you write using libA.Vector.VectorTools; then it is applied to any object matching the structure of libA.Vector, regardless of whether it is typed that way or not.

@back2dos But there is a difference between using and @:using static extensions - first (module level) works as you are describing, it is applied to any unifiable type, thus creating possible conflicts, but later (type level) is used specifically for a type, it is not applied as greedily. So your first example about vectors will work without a problem, as long as you do not use VectorTools as a module extension. (And you can even explicitly forbid using a class as a module level extension by marking it as a private one, thus setting clear boundaries.)

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

No branches or pull requests

5 participants