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

generated functional interface instances with Type parameters result are sometimes impure #73

Closed
chisui opened this issue Nov 15, 2019 · 7 comments

Comments

@chisui
Copy link

chisui commented Nov 15, 2019

If you generate instances of functional interfaces whose return type is a type variable of the property function then that instance is not pure.

Example:

@Property <A> void aInArgsPos(
        @ForAll Function<A, Integer> f,
        @ForAll A a) {
    assertEquals(f.apply(a), f.apply(a)); // <- passes
}

@Property <A> void aInValuePos(
        @ForAll Function<Integer, A> f,
        @ForAll Integer i) {
    assertEquals(f.apply(i), f.apply(i)); // <- fails
}
@jlink
Copy link
Collaborator

jlink commented Nov 15, 2019

Looks like a bug I’ll check it.

@jlink
Copy link
Collaborator

jlink commented Nov 15, 2019

I did some research. The problem you're observing here is not related to the type paramater but to the fact that 'A' also fits type Object (or List<Object>). Instances of Object are only equal if they are the exact same instance. So you can also observe this behaviour with:

@Property
void aInValuePos(
	@ForAll Function<Integer, Object> f,
	@ForAll Integer i
) {
	Object first = f.apply(i);
	Object  second = f.apply(i);
	Assertions.assertThat(first).isEqualTo(second);
}

A generated function, however, does not store all results, but generates a hash from the parameters which is than used as a random seed to generate the actual value. This will generate an equal (but not same) object when called with the same parameters.

You can mitigate the problem by making the type variable more specific, e.g.
<A extends Number>

Storing all generated values would use up additional memory. I could implemented this as an optional feature. Would that be an important improvement for you?

@jlink
Copy link
Collaborator

jlink commented Nov 16, 2019

Having had one more night to think it over, I'd even say that always returning the exact same object would be wrong, ie. would leave out behaviour that can definitely happen. In a language like Java where you can have objects with identity and changing state it's common behaviour that functions or functional interfaces create those kind of objects - and create them freshly whenever they are being invoked. Consider for example all those factory methods we use in the context of Spring et al.

If you want to constrain the type of objects to be created for A you could use a provider method like this:

@Property
<A> void aInValuePos(
	@ForAll Function<Integer, @From("valuesWithEquality") A> f,
	@ForAll Integer i
) {
	Assertions.assertThat(f.apply(i)).isEqualTo(f.apply(i));
}

@Provide
Arbitrary valuesWithEquality() {
	return Arbitraries.oneOf(
		Arbitraries.strings(),
		(Arbitrary) Arbitraries.strings().list(),
		(Arbitrary) Arbitraries.integers(),
		... // Add whatever you think could be the target type
	);
}

If you need those range of types regularly you might consider to register a default arbitrary provider.

So I close this issue as "works as designed". Feel free to reopen it if you still see a bug or want to make a feature request out of it.

@jlink jlink closed this as completed Nov 16, 2019
@chisui
Copy link
Author

chisui commented Nov 18, 2019

I understand that this is the way Java objects behave, but it would nice be nice to be able to restrict or filter which types A may take in this circumstance, without requiring A to be of a concrete subtype. equals and hashcode are part of the contract of Object so they should be implemented correctly. This may not be possible or meaningful all the time. Unfortunately Java doesn't have something like Haskells Eq or Ord (or Hashable) typeclasses. It would be nice to be able to annotate type parameters so that it's clear that only classes that implement equals and hashcode correctly should be chosen for A. Something like:

@Property
<@Eq A, @Eq B> void aInValuePos(
	@ForAll Function<A, B> f,
	@ForAll A a
) {
	Assertions.assertThat(f.apply(a)).isEqualTo(f.apply(a));
}

But I guess that would be essentially two different features. First the ability to register arbitrary providers on typeparameters and secondly the providers for @Eq and @Hashable. This would be pretty easy using Object.class.equals(targetClass.getMethod("equals", Object.class).getDeclaringClass()).

Those are some pretty specific requirements though. I can understand if you don't think that this is something that you want to support. Although being able to annotate typeparameters would be nice regardless.

@jlink
Copy link
Collaborator

jlink commented Nov 18, 2019

The first required feature is rather simple since annotations are already being considered for the usage part of the type variable, thus @Eq in

@Property
<A, B> void aInValuePos(
	@ForAll Function<@Eq A, @Eq B> f,
	@ForAll A a
)...

can be used for choosing an arbitrary.

The providers for @Eq and @Hashable are the bigger problem since with the current design of arbitrary provision the providers themselves would have to know about @Eq and @Hashable which they obviously should not. Let me think about it for a while...

@chisui
Copy link
Author

chisui commented Nov 18, 2019

The advantage of declaring the Annotations at the variable declaration site would be that you could constrain all usages of that variable.

It would also be kind of inconsistent. In this example:

@Property
<A> void p(
	@ForAll @Eq A a,
	@ForAll A b
)...

would a and b resolve to different types essentially? a resolves to only types that implement equals correctly, but b would resolve to any Object?


Regarding the problem that the provider shouldn't know about @Eq etc. Some kind of filter mechanism would be useful. You could register filters for annotations that would filter the arbitraries returned by the providers. It would also be nice if there was a way for an arbitrary provider to communicate that it can handle some of these filter annotations directly in a more efficient manner.

@jlink
Copy link
Collaborator

jlink commented Nov 20, 2019

I extracted part 1 into an issue of its own: #74

As for filtering arbitraries I haven't (yet) come up with an idea how to do that without breaking existing arbitrary providers or force them to know about a filter annotations. The problem is that neither ArbitraryProvider instances nor Arbitrary instances tell anything about themselves or the kind of objects they are going to create at runtime. So the question is: How would a filter function determine if it wants to let an arbitrary - or arbitrary provider - get through or not?

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

No branches or pull requests

2 participants