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

Reflection_Engine: SortExtensionMethods improved to return the most relevant method #2327

Merged

Conversation

pawelbaran
Copy link
Member

@pawelbaran pawelbaran commented Feb 19, 2021

Issues addressed by this PR

Closes #2326

Test files

On SharePoint - the file is a bit lame, but I could not come up with a better example 🙈. I hope the code change will be self-explanatory.

Changelog

  • SortExtensionMethods improved to return the most relevant method when the type-specific one does not exist

Additional comments

type parameter is now used to filter out the methods that are not relevant at all - possibly it could be removed, based on assumption that the methods are already prefiltered.

I was a bit concerned about the performance of GetParameters - I have tested it explicitly and there is less than 1ms difference between 1000 and 10000 calls, ergo it should not be even noticeable.

@pawelbaran pawelbaran added the type:feature New capability or enhancement label Feb 19, 2021
@pawelbaran pawelbaran self-assigned this Feb 19, 2021
//Sorted so that the method with A comes first followed by B and lastly C.
return methods.OrderBy(x => x.GetParameters().FirstOrDefault()?.ParameterType == type ? 0 : 1).ToList();
IEnumerable<MethodInfo> typeMethods = methods.Where(x => x.GetParameters()[0].ParameterType.IsAssignableFrom(type));
return methods.OrderBy(x => methods.Count(y => x.GetParameters()[0].ParameterType.IsAssignableFrom(y.GetParameters()[0].ParameterType))).ToList();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I agree with this change. The purpose of this method is to say that say Geometry(Bar bar) should be prioritised over Geometry(IElement1d element). That is, exact match before any interface level.

By using IsAssignableFrom it feels like both of those cases will be returning the same match...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, the methods are ordered by the number of target types their target type is assignable from, so for example:

MyMethod(Line)
MyMethod(ICurve)
MyMethod(IGeometry)

MyMethod(Line) will score 1 (typeof(Line) is assignable from typeof(Line) only), the others 2 and 3 respectively.

In your case, Geometry(Bar bar) should score 1, Geometry(IElement1d element) - 2. So the former will be prioritised.

Does that make sense or am I missing a point?

@pawelbaran
Copy link
Member Author

pawelbaran commented Feb 19, 2021

After a conversation with @IsakNaslundBh I felt like I will let someone hold my beer and gave it another try to build an actual inheritance hierarchy, based on which one can pick the most suitable method to call.

There is not much BCL support for that, but it looks like I have succeeded to some extent, apart from possible idiosyncrasies that may appear in case of class inheritance - this, however, seems to be safe in the context of BHoM where it barely exists (usually the only cases are types that inherit from BHoMObject, which then is rather safe because there is literally zero extension methods on BHoMObject in the entire BHoM_Engine solution).

Performance wise, it still runs in tens of ms at max, so should be fine, taken into account the method is called only once per method and type set.

For now I made all methods private due to their complex return types, happy to change that if you find it desirable @adecler.

Comment on lines +45 to +47
List<List<Type>> hierarchy = type.InheritanceHierarchy();
IEnumerable<int> levels = methods.Select(x => hierarchy.InheritanceLevel(x.GetParameters()[0].ParameterType));
return methods.Zip(levels, (m, l) => new { m, l }).Where(x => x.l != -1).OrderBy(x => x.l).Select(x => x.m).ToList();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should look into what the Weighting system in the BHoM_UI does since this is a very similar problem:
https://github.com/BHoM/BHoM_UI/blob/631e784772f456dd881492262d7f10c5c2fb7322/UI_Engine/Query/Weight.cs#L190

I am not convinced it needs to be as complex as that though. Speed is quite an important factor here and I think we should cut everything that is not that relevant. I wonder if this wouldn't be enough:

  • exact type match : comes first
  • type is assignable from: comes second
  • type not assignable from: comes last

I am not aware of the details of the situation you are trying to solve by this seems like a good compromise to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed solution works as follows:

  • exact type match : comes first
  • type is assignable from, closer to the top of inheritance tree: comes second
  • type is assignable from, further from the top of inheritance tree: comes thirs
  • type not assignable from: gets filtered out (it will throw an error at some point anyways)

The reason for such complex approach can be explained on the Transform method developed in #2321 (WIP) for BH.oM.Physical.Elements.Beam:

  • there will be an interface method ITransform taking BH.oM.Dimensional.IElement1D that will call Transform
  • there will be a general Transform method taking BH.oM.Dimensional.IElement1D that will only transform the driving curve
  • there will be a more specific Transform method taking BH.oM.Physical.Elements.IFramingElement that besides driving curve transformation will change the rotation property
  • there will be no type-specific Transfrom method for BH.oM.Physical.Elements.Beam

It is important to call the method taking BH.oM.Physical.Elements.IFramingElement not to lose the rotation part. Naturally, one could add Transform method for each type implementing BH.oM.Physical.Elements.IFramingElement instead, but this sounds like quite a bit of boilerplate, plus what if one adds another type being not aware that this method needs to be added?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When it comes to performance, I have tested this and it runs in <10ms, plus it it is ran only once per type/method name combination (then stored in a public field). So I think we should be good with that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. I am glad you are keeping those methods private though as their result is not exactly matching what a human would produce. Take for example the output of InheritanceHierarchy for a structural bar:

image

For example, it doesn't really make sense for BHoMObject to have such a big priority compared to other interfaces. Same thing for IBHoMObject being prioritised over IElement. Not the fault of you algorithm though as interfaces in the BHoM are mostly used as a tag and no so much as a strict hierarchy. On the other hand, trying to sort base types and interfaces on the same list is always going to be a bit iffy.

That's why I was suggesting a simpler 3-levels system in order to avoid a complex sorting system that has many imperfect cases. But, again, I understand why you needed something more.

I would like to have @IsakNaslundBh final opinion as well on this one before agreeing to merge.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fully agree with your comment @adecler, the system is not perfect. However, I approached it based on the assumption that an approximate solution is better than quasi-random:

  • on master, the exact type goes first, all others are random
  • on this branch, the exact type goes first, then the order is reflecting the inheritance tree, which has the faults listed by you

On top of that, from the problem-specific point of view it should be fine to have BHoMObject high in the hierarchy because it will be simply ignored due to the lack of extension methods. This does not mean the method is any more correct than it actually is, but it should not fail in the discussed application of RunExtensionMethod 🙈

Happy to hear @IsakNaslundBh's opinion too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll approve this so you're not necessarily stuck waiting for @IsakNaslundBh if this needs merging. Personally, I am ok with this being merged.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. Been struggeling a bit with time!

Just reading through the comments here, I think this looks like an improvement! Agree that the speed is not that big of an issue for the reasons @pawelbaran gave, rather have this behave as good as it can. Ideally, we would have this do the exact same thing that C# itself does when it comes to method overloading/choice, which I guess this is one step closer to doing compared to where it was before.

Have not tested this myself, but given that @adecler has, you do not have to wait for me. I approve on the concept 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we would have this do the exact same thing that C# itself does when it comes to method overloading/choice, which I guess this is one step closer to doing compared to where it was before.

I am afraid we do not want that @IsakNaslundBh, the compiler would throw an error about method ambiguity when pushed to choose between an interface method and the parent type method:

image

So there might be no perfect solution I am afraid.

@pawelbaran pawelbaran changed the title SortExtensionMethods improved to return the most relevant method Reflection_Engine: SortExtensionMethods improved to return the most relevant method Feb 22, 2021
@pawelbaran
Copy link
Member Author

@BHoMBot check compliance

@bhombot-ci
Copy link

bhombot-ci bot commented Feb 24, 2021

@pawelbaran to confirm, check-code-compliance, check-documentation-compliance, check-project-compliance, check-branch-compliance, check-dataset-compliance, and, if applicable, check-copyright-compliance tasks are now queued.

@pawelbaran
Copy link
Member Author

/azp run BHoM_Engine.CheckInstaller

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@pawelbaran
Copy link
Member Author

@BHoMBot check core

@bhombot-ci
Copy link

bhombot-ci bot commented Feb 24, 2021

@pawelbaran to confirm, check-core task is now queued.

@pawelbaran
Copy link
Member Author

@BHoMBot check installer

@bhombot-ci
Copy link

bhombot-ci bot commented Feb 24, 2021

@pawelbaran to confirm, check-installer task is now queued.

@FraserGreenroyd FraserGreenroyd merged commit 48ef7f1 into master Feb 24, 2021
@FraserGreenroyd FraserGreenroyd deleted the Reflection_Engine-#2326-ImproveSortExtensionMethods branch February 24, 2021 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type:feature New capability or enhancement
Projects
None yet
4 participants