Open tyranron opened 2 years ago
Regarding "Further investigation" section...
Polymorphism over
ScalarValue
.
Well, this is simple. Once we decouple the orphan rules omitting responsibility from ScalarValue
type param, it worths nothing to act with the ScalarValue
the same way as with Context
/TypeInfo
: keep it as generic everywhere and require Into<MyCustomScalarValue>
in concrete impl
blocks. This way the requirement will bubble-up naturally up to the place where root node is execute
ed, without messing up any intermediate types.
Separate type parameter for omitting orphan rules.
The main problem with polymorphism in both questions is that we cannot express
forall
quantifier for types in Rust:trait Foo<T> {} impl<A, B, T> Foo<A> for Box<T> where T: Foo<B> {} // error[E0207]: the type parameter `B` is not constrained by the impl trait, self type, or predicates // --> src/lib.rs:2:9 // | // 2 | impl<A, B, T> Foo<A> for Box<T> where T: Foo<B> {} // | ^ unconstrained type parameter
This one is trickier as hits something quite fundamental regarding the Rust type system:
impl
above, we should have B
as a associated type parameter, so it be a concrete type, not a "Rust, please, guess it out of nowhere" type parameter.We cannot have 1 and 2 at the same time, as the requirements are clear contradiction.
To have them both, we need to introduce some transparent wrapper type holding our type parameter B
:
struct Owned<X, By>(X, PhatomData<By>);
Now our B
is constrained and we have the desired "polymorphism":
impl<A, B, T> Foo<A> for Owned<Box<T>, B> where T: Foo<B> {}
Hooray? Not yet! The problem has shifted to the place where we should or should not wrap the type. Unfortunately, we cannot do that seamlessly.
trait IntoOwned {
type Owned;
}
struct Mine;
impl juniper::IntoOwned for u8 { // oops! orphan rules again
type Owned = juniper::Owned<Self, Mine>;
}
Without seamless conversion, we cannot use this implementation just like that:
#[derive(graphql::Object)]
struct Human {
id: u8,
}
This shows very well our fundamental limitation of unconstrained type parameter, described above. Who should guess which implementation should be used here? Different crates may provide different impl
s for u8
. Well, so we may conclude that the user code must specify the desired implementation to use.
Ok, then:
#[derive(graphql::Object)]
struct Human {
id: Owned<u8, Mine>,
}
Hmm, not quite ergonomic. We don't want to mess with wrappers in the user code.
#[derive(graphql::Object)]
struct Human {
#[graphql(owner = Mine)]
id: u8, // and we do wrapping under-the-hood in the macro expansion only
}
Naming still requires bikeshedding to be better, though.
Conclusions?
With this small ergonomic hit (in a form of additional attribute argument in places where foreign types and our types are connected) we solve the problem of "polymorphism" for a type parameter for omitting orphan rules. This way a custom type won't pollute all the schema types above it, just a field which refers it directly.
And now we're able to use ScalarValue
only where it's really needed.
@ilslv thoughts? ☝️
@tyranron this sounds a lot like #[serde(with = "module")]
attribute and looks like a big ergonomic improvement 👍
From what i can tell, there is no way we can exactly replicate #[serde(with = "module")]
by providing mod
with a couple of functions (maybe until we can use fn
pointers inside const
generics?), but I think we can require users also add a type alias for a local struct:
#[derive(graphql::Object)]
struct Human {
#[graphql(with = "custom")]
id: u8, // and we do wrapping under-the-hood in the macro expansion only
}
mod custom {
pub(super) enum Custom {}
// functions ...
}
@ilslv they look similar, but the logic seems different there. with
is more like "use intermediate transformation", while owner
(renamed to behavior
in sources) is like "use the concrete implementation for this type".
(maybe until we can use
fn
pointers insideconst
generics?)
Nope, as const
values are values, not types, so don't interfere with orphan rules.
Nota bene
At the moment we have a problem:
error[E0637]: `&` without an explicit lifetime name cannot be used here
--> juniper/src/schema/schema.rs:140:37
|
140 | fn description(&self) -> Option<&str> {
| ^ explicit lifetime name needed here
Meaning that Option<&str>: ::juniper::resolve::Resolve
bound at impl
level is problematic.
Seems to be resolvable by quantifying all the type lifetimes into HTRB: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=1f8b36c78e61a771540b7a3324f9dd8b
Do GATs change any of the design thinking here?
@LegNeato at the moment I don't see any need of them here, but it may appear eventually to solve some problems.
Are we going to complete this rework eventually? This is a lot of work indeed, we should not leave them around.
Requires #1028, #1047, #1052
Required for #975
Related to #1091
Eliminates #1097
This PR represents an attempt to make a fundamental rework of Juniper resolving machinery (
GraphQLType
,GraphQLValue
, etc).Motivation
At the moment, the core traits like
GraphQLType
andGraphQLValue
impose the following problems to codebase:Context
andTypeInfo
types, as they're defined as associative types. So, once some leaf type in schema uses a different type forContext
, we're fucked, and should either make the whole schema work with a singleContext
type, or doesn't compile at all. The same applies toTypeInfo
, which, in result, makes impossible to combine static and dynamic GraphQL schemas at all.Solution
This PR tries to remodel the core traits machinery in the following ways:
impl
blocks prevents redudant bounds pollution of the code base.impl
blocks. Require only the minimum to makeimpl
work. For thetransparent
behavior the bounds should bubble-up structurally (structure implements trait if all the required fields implement trait).TypeInfo
andContext
as generic type parameters for all possible implementations, and requireBorrow<ConcreteContextType>
for the implementation needs. Not a single implemetation should put a concrete type into these type parameters, otherwise the whole polymorphism will be broken (as we have now with associative types). This allows us to fully remove thecontext =
arguments in macros, and to use different context types in different methods of a single type. As for type info, it allows to combine dynamic and static schemas naturally.FromInputValue
trait, likeserde::Deserialize<'de>
does. This allows to pass references as arguments (and so parse unsized types) to fields in the similar degree as supported byserde
deserialization.ScalarValue
type parameter into a separate type parameter. RemoveScalarValue
type parameter in places where it's not really required.Example
Meet a shiny new Juniper:
Further investigation
The solution for the following additional capabilities is not figured out yet and requires further investigation:
ScalarValue
. At the moment the concreteScalarValue
type in a leaf type implementation polutes all the schema the same way theContext
/TypeInfo
does. But we cannot justBorrow<CustomScalar>
from generic like we're doing forContext
. Still it would be nice to figure out the way for combining types using differentScalarValue
s in their implementations.ScalarValue
type in implementations for this purpose. But it's not the responsibility of theScalarValue
notion, and usingScalarValue
for that makes to put it into places where it's not required at all (likereflect::BaseType
trait). Having a separate mechanic for omitting orphan rules would be much more intuitive. However, to work this polymorphically, we need to resolve the same implications imposed by previous question.The main problem with polymorphism in both questions is that we cannot express
forall
quantifier for types in Rust:Solved: See https://github.com/graphql-rust/juniper/pull/1072#issuecomment-1147604603
Progress
This is a huge work, as requires an accurate re-implementation of the whole Juniper resolving and type registration machinery.
&T
(ref)&mut T
(mut ref)Box<T>
Rc<T>
Arc<T>
Cow<'_, T>
Option<T>
Nullable<T>
Vec<T>
[T]
(slice)[T; N]
(array)HashSet
(requires #1070)BTreeSet
str
#[graphql::Scalar]
macro#[graphql::Enum]
macro (requires #1047)#[graphql::InputObject]
macro (requires #1052)#[graphql::Object]
macro#[graphql::Union]
macro#[graphql::Interface]
macro (requires #1028)#[graphql::Subscription]
macro