Skip to content

Allow use of property-based row classes in ML.NET #616

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

Merged
merged 21 commits into from
Aug 2, 2018
Merged

Conversation

dsyme
Copy link
Contributor

@dsyme dsyme commented Jul 31, 2018

This is WIP to address

It builds on #600 and you can see the added diff between #600 and this PR here

Copying comment from here:

This would allow the use of ColumnNameAttribute and friends on both public fields (as today) and public properties that have getters/setters.

I've tested that allows an F# record definition to be successfully used, e.g. this test now passes in that branch. It would also allow C# classes that just use attributed public get/set properties to be used, many tests for that pattern would need to be added (e.g. covering the vector, channel and other cases)

It would be a change in spec because existing C# classes that use public fields plus some additional get/set properties may now have their get/set properties considered part of the schema, when they weren't before. I suppose this may mean the user has to add some NoColumn attributes on to these properties.

We could theoretically adjust the spec to be "if there are non-zero public fields, then use public fields. Otherwise, see if there are public get/set properties", but right now I've used the rule "combine the public fields and public get/set properties" as that seems more natural and allows gradual transition of field-based types to property-based types.

I believe this would address @terrajobst's concerns about the use of public fields, at least in the core schema model. There are other uses of GetFields() and field-reflection in the component model/catalog parameterization system which I haven't attempted to address.

@dsyme dsyme mentioned this pull request Jul 31, 2018
2 tasks

il.Emit(OpCodes.Ldarg_3); // push arg3
il.Emit(OpCodes.Ldarg_1); // push arg1
il.Emit(OpCodes.Call, propertyInfo.GetGetMethod()); // push [stack top].[propertyInfo]
Copy link
Contributor

Choose a reason for hiding this comment

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

Call [](start = 28, length = 4)

Should it be just call or callvirt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For F# records "call" was fine, but you're right that for C# get/set we may need callvirt

Copy link
Member

Choose a reason for hiding this comment

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

We could just check the property, right? If the property is virtual or abstract, then callvirt. If it isn't virtual, then just call?


In reply to: 206686593 [](ancestors = 206686593)


il.Emit(OpCodes.Ldarg_1); // push arg1
il.Emit(OpCodes.Ldarg_2); // push arg2
il.Emit(OpCodes.Call, propertyInfo.GetSetMethod()); // [stack top-1].[propertyInfo] <- [stack top]
Copy link
Contributor

Choose a reason for hiding this comment

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

Call [](start = 28, length = 4)

Same question regarding Call or Callvirt

Copy link
Contributor Author

Choose a reason for hiding this comment

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

likewise, thanks

"Can't bind the IDataView column '{0}' of type '{1}' to field '{2}' of type '{3}'.",
col.ColumnName, realColType, col.FieldInfo.Name, col.FieldInfo.FieldType.FullName);
"Can't bind the IDataView column '{0}' of type '{1}' to field or property '{2}' of type '{3}'.",
col.ColumnName, realColType, col.MemberInfo.Name, col.OutputType.FullName);
Copy link
Contributor

@Ivanidzo4ka Ivanidzo4ka Jul 31, 2018

Choose a reason for hiding this comment

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

OutputType [](start = 78, length = 10)

This look something suspicious. In case of IsComputed = true you will return something else rather than FieldType or PropertyType. #Resolved

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, will change this

@dsyme dsyme added the F# Support of F# language label Jul 31, 2018
@Ivanidzo4ka
Copy link
Contributor

Thank you for your work @dsyme!
I had pretty much same changes in my local repo, but I haven't publish them yet because I wanted to have more test coverage like this:

public void RoundTripConversionWithArrays()

Basically to make sure you can read data into IDataView properly and if you decide to map IDataView on some class it get properly mapped.

I know this is mostly to unblock F#, but I think it would be nice to have tests where you cover private getter/setter, class which inherit property from base class, class which override, readonly property, private property, and I guess lot of other cases, just to knew what allowed and what not.

But if you need to be unblocked by this PR to continue working on F# support, I think tests coverage easily can be separate issue.

userType
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(x => x.CanRead && x.CanWrite && x.GetIndexParameters().Length == 0)
.ToArray();
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't need to ToArray here, when you are just going to concat it with fieldInfos and then ToArray that as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll remove it, thanks

if (mappingAttr == null)
throw Contracts.Except($"{field.Name} is missing ColumnAttribute");
throw Contracts.Except($"field or property {memberInfo.Name} is missing ColumnAttribute");
Copy link
Contributor

@Ivanidzo4ka Ivanidzo4ka Jul 31, 2018

Choose a reason for hiding this comment

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

f [](start = 45, length = 1)

It's a totally nitpicking, but we prefer to start our sentences with Capital letter. #Resolved

break;

default:
throw Contracts.ExceptNotSupp("expected a FieldInfo or a PropInfo");
Copy link
Contributor

@Ivanidzo4ka Ivanidzo4ka Jul 31, 2018

Choose a reason for hiding this comment

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

PropInfo [](start = 77, length = 8)

is it worth to be shorten?
Also can you start sentence with capital letter? #Resolved

Copy link
Member

Choose a reason for hiding this comment

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

(nit) PropInfo => PropertyInfo


In reply to: 206681072 [](ancestors = 206681072)


if (fieldInfo == null)
if (memberInfo == null)
throw Contracts.ExceptParam(nameof(userSchemaDefinition), "No field with name '{0}' found in type '{1}'",
Copy link
Member

Choose a reason for hiding this comment

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

No field => No field or property

return (Delegate)methInfoProp.Invoke(null, new object[] { propertyInfo, assignmentOpCodeProp });

default:
throw Contracts.ExceptNotSupp("expected a FieldInfo or a PropInfo");
Copy link
Member

Choose a reason for hiding this comment

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

PropInfo => PropertyInfo

@dsyme
Copy link
Contributor Author

dsyme commented Jul 31, 2018

OK, all updated base on code review comments so far, thanks!

@Ivanidzo4ka I've added some initial testing here, is this the sort of thing you were after? https://github.com/dotnet/machinelearning/pull/616/files#diff-de10d1dc0f5785ee3a7f0053c465fbdcR632

Copy link
Member

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

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

I think this is looking in the right direction. Good work, @dsyme.

var mb = new DynamicMethod("Peek", null, args, typeof(TOwn), true);
var il = mb.GetILGenerator();
var minfo = propertyInfo.GetGetMethod();
var opcode = minfo.IsVirtual ? OpCodes.Callvirt : OpCodes.Call;
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to handle IsAbstract?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not 100% sure, I thought IsAbstract implies IsVirtual, but the MSDN docs don't say that so I'll add it

@dsyme dsyme reopened this Aug 1, 2018
for (int index = 0; index < fields.Length; index++)
var userType = typeof(TInput);

var fieldInfos = userType.GetFields();
Copy link
Member

Choose a reason for hiding this comment

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

Should we be checking for public and instance fields here? Just like we are in GetProperties.

Copy link
Member

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

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

I think this looks good, thanks for the help, @dsyme.

@TomFinley @Zruty0 - any concerns on merging this?

@dsyme
Copy link
Contributor Author

dsyme commented Aug 1, 2018

Should we be checking for public and instance fields here? Just like we are in GetProperties.

Public is the default - but yes, I think we should be checking for instance fields (I assume static fields cause failures at the moment) #Closed


default:
throw Contracts.ExceptNotSupp("Expected a FieldInfo or a PropertyInfo");
}
Copy link
Contributor

@Zruty0 Zruty0 Aug 1, 2018

Choose a reason for hiding this comment

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

As far as I understand, this clause is only present because the compiler complains that there must be a default clause, so we actually will never trigger it.
If that's the case, I suggest putting Contracts.Assert(false) before throwing, to signify that this is a perceived-never-to-execute code (as opposed to something we don't support not but may in the future). #Closed

break;

default:
throw Contracts.ExceptNotSupp("Expected a FieldInfo or a PropertyInfo");
Copy link
Contributor

@Zruty0 Zruty0 Aug 1, 2018

Choose a reason for hiding this comment

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

throw [](start = 20, length = 5)

Same here: if the code is never expected to execute, make it obvious to reader by putting Contracts.Assert(false) ahead of throw. #Closed


il.Emit(OpCodes.Ldarg_3); // push arg3
il.Emit(OpCodes.Ldarg_1); // push arg1
il.Emit(opcode, minfo); // push [stack top].[propertyInfo]
Copy link
Contributor

@Zruty0 Zruty0 Aug 1, 2018

Choose a reason for hiding this comment

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

push [stack top].[propertyInfo] [](start = 55, length = 31)

Please reconcile the comment with the code #Closed

Copy link
Contributor Author

@dsyme dsyme Aug 1, 2018

Choose a reason for hiding this comment

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

I'm not quite sure what you mean here? The comment is roughly accurate, based on the similar code for fields #Closed

Copy link
Contributor

Choose a reason for hiding this comment

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

I meant that the line 111 now emits a 'call' opcode, but the comment still says push [stack top].[propertyInfo]. Oh, but what you mean is that the result of call will be that the property value will end up on top of the stack.
I suppose it's true, but I would expect the comment to look like // call [methodInfo] or something.


In reply to: 206991828 [](ancestors = 206991828)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, done

return (Delegate)methInfoProp.Invoke(null, new object[] { propertyInfo });

default:
throw Contracts.ExceptNotSupp("Expected a FieldInfo or a PropertyInfo");
Copy link
Contributor

@Zruty0 Zruty0 Aug 1, 2018

Choose a reason for hiding this comment

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

throw [](start = 20, length = 5)

Contracts.Assert(false) before throwing #Closed

@Zruty0
Copy link
Contributor

Zruty0 commented Aug 1, 2018

I think we should be consistent with public vs instance. If we do so for properties, do so for fields as well.
We should not fail on static properties, they should be ignored I would say.


In reply to: 409618178 [](ancestors = 409618178)

@Zruty0
Copy link
Contributor

Zruty0 commented Aug 1, 2018

In general, we try to merge the completely tested code (unless we're blocking other work, in which case we can use discretion).
I would love to see all tests present. Are you planning to also test for virtual properties?


In reply to: 409402126 [](ancestors = 409402126)

@Zruty0
Copy link
Contributor

Zruty0 commented Aug 1, 2018

In my view, we should require both getter and setter to be public. Either that, or we have to split InternalSchemaDefinition into incoming and outgoing one, and have different requirements for each, but I'd like to avoid this.


In reply to: 409383628 [](ancestors = 409383628)

@dsyme
Copy link
Contributor Author

dsyme commented Aug 1, 2018

@Zruty0 All updated - the tests have now been completed #Closed

@dsyme
Copy link
Contributor Author

dsyme commented Aug 1, 2018

I will also add one more F# for another way to define F# properties #Closed

@dsyme
Copy link
Contributor Author

dsyme commented Aug 1, 2018

I will also add one more F# for another way to define F# properties

Done

Copy link
Contributor

@Zruty0 Zruty0 left a comment

Choose a reason for hiding this comment

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

:shipit:

@Zruty0
Copy link
Contributor

Zruty0 commented Aug 2, 2018

@dsyme thanks a lot for the contribution! Looks good to me now.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
F# Support of F# language
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants