Allow to suppress the search for Generators for Generic Parameter Types
DanielGronau opened this issue · 10 comments
I would like to write a Generator like this (please excuse the use of __ as class name)
public class __Gen extends Generator<__> {
public __Gen() {
super(__.class);
}
@Override
@SuppressWarnings("unchecked")
public __<?, ?> generate(SourceOfRandomness random, GenerationStatus status) {
Generator<__<?, ?>> gen = (Generator<__<?, ?>>) (gen(random).type(
types().get(0).getEnclosingClass(),
types().get(1)
));
return gen.generate(random, status);
}
}
As you can see, the provided type information is sufficient to delegate to another Generator, which does the job. The problem is that __Gen is discarded from the matching Generators list because GeneratorRegistry insists on finding Generators for both type parameters (in composeWeighted), which are not needed and do not even exist in the case of the first one (which is a kind of "placeholder"). I spent quite some time to solve this problem, but couldn't find a solution. I don't think my example is pathologic, e.g. Generators for generic classes using Phantom Types should have exactly the same problem.
Please consider giving Generators a choice to opt out of the automatic search for component Generators. If there is already a way to do this now, please add related documentation instead.
@DanielGronau Thanks for this -- I will investigate.
@DanielGronau Would you be willing to give an example of a property test that wants instances of __
? It might help clarify a couple of issues in my mind. Thanks!
@DanielGronau From looking at the above, it appears that ___
is a type with two type parameters. I'm thinking you may want to extend ComponentizedGenerator
here instead.
class __<T, U> {
// assuming one constructs a __ with an instance of T and of U
__(T first, U second) {
// ...
}
}
public class __Gen extends ComponentizedGenerator<__> {
public __Gen() {
super(__.class);
}
@Override public int numberOfNeededComponents() {
return 2;
}
@Override
public __<?, ?> generate(SourceOfRandomness r, GenerationStatus status) {
return new __<>(
componentGenerators().get(0).generate(r, status),
componentGenerators().get(1).generate(r, status)
);
}
}
There are a few examples in both source and tests about componentized generators...also a section in the docs with the heading "Generators for types with component types: ComponentizedGenerator
". Let me know if you see a way to make this capability clearer. Also let me know if this proposed solution helps or not. Appreciate the feedback!
Hello Paul, I already tried using ComponentizedGenerator, and it didn't work. The root issue is that regardless what I try GeneratorRegistry wants to instantiate generators for the type parameters, which is impossible in my case.
Here is the simplest scenario I can think of to explain it: I have my own List type, which is in fact implementing the infamous __
interface:
public interface __<F, T> { }
public abstract class List<A> implements __<List.µ, A> {
public interface µ {
}
...
}
You can see why I don't need any List.µ
instances to instantiate my __<List.µ, A>
object, it's just simply List.of(1, 2, 3)
or so, as one would expect, but in the interface, the first parameter (representing that this is a list, and not something else) is still there.
The whole idea of this construct is to be able to define abstractions like Functors and Monads over base types sharing some common behavior. Here is a functor, the abstraction over all types allowing a "mapping" behaviour:
public interface Functor<F> {
<A, B> __<F, B> map(Function<A, B> fn, __<F, A> nestedA);
}
Of course, the F stands not for a normal parameter type, but for a base type (or to be precise, it's marker interface µ ). Now I can finally write a ListFunctor implementation, a SetFunctor implementation etc with a common interface. However, all functors have obey some laws, e.g. mapping with the identity function shouldn't change the content:
public interface FunctorContract<F> {
Functor<F> subject();
@Property
default void mapIdentity(__<F, String> a) {
Functor<F> functor = subject();
__<F, String> mappedA = functor.map(Function.identity(), a);
assertThat(mappedA).isEqualTo(a);
}
}
This would allow me to check if all my functor implementations like ListFunctor behave correctly. I have already a generator for List<A>
, but to use it in this context, I have to write a generator for __<List.µ, A>
, which somehow "delegates" to the correct one. This is what I tried to achieve in my example code on the top. And as you can see, with reflection it's not even that hard. But the problem is that when picking the generator for __
, GeneratorRegistry tries to find generators for its type parameters, and it can't find one for List.µ
, and discards my generator - at least that's what it looks like from debugging. Further, the kind of implementation doesn't seem to matter, the algorithm always tries to find the type parameter generators, and I found no way to prevent that.
Thank you for looking in this issue, I know that this is a weird use case. However, as I wrote above, there are other situations where type parameters are used just as "markers".
@DanielGronau Thanks, this is extremely helpful. Would you be willing to share a sample implementation of FunctorContract
?
@DanielGronau OK, I believe I've reproduced your context. It seems like what's holding this up is that the machinery wants to find a generator so that it can instantiate List.mu
, when there's no such generator, and List.mu
is more like a placeholder than anything else. Plus, what would it mean to instantiate a marker interface anyway, unless it were instantiating concrete implementers of it?
Here's an idea. One thing that junit-quickcheck does for property types that are single-abstract-method (functional) interfaces, is if based on that type there are no generators to satisfy it, the library will synthesize and register a "lambda generator" which, when called, makes new instances of a dynamic proxy for the interface. What if we broadened such functionality to say, if your property type or one of its type parameters is a marker interface, it'll synthesize and register a generator for that interface which, when called, produces a dummy instance (maybe even the same instance every time)?
That sounds like a good solution. Of course, you can estimate best which change has the least impact to the code base.
@DanielGronau If you have a spare moment, have a look at #276 ... I believe I've got a solution for this issue therein. Thanks!
Yes, this looks good to me. Thanks again!
@DanielGronau No worries, thanks! This should be resolved in 0.9.5.