Giter Site home page Giter Site logo

Cannot define recursive schema about graphql-api HOT 12 OPEN

jml avatar jml commented on September 13, 2024
Cannot define recursive schema

from graphql-api.

Comments (12)

teh avatar teh commented on September 13, 2024 2

Here's a minimal example that doesn't work:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DeriveGeneric #-}

module Examples.Rec where
import Protolude hiding (Enum)

import GraphQL
import GraphQL.API
import GraphQL.Resolver (Handler)

type Directory = Object "Directory" '[] '[Field "subdirs" Directory]

directory :: Handler IO Directory
directory = pure directory

from graphql-api.

jml avatar jml commented on September 13, 2024 1

OK. I'm convinced. Removing this from the milestone, but think it should be top priority for next release—writing a file server should be obvious to any newbie.

from graphql-api.

jml avatar jml commented on September 13, 2024

FWIW, if we can figure out a plausible solution and have something hacky that demonstrates it, I'm OK with releasing without this being fixed.

However, if we can't prove that this is possible, then I'm reluctant to release. It's kind of a make-or-break feature, IMO.

from graphql-api.

teh avatar teh commented on September 13, 2024

For recursive types I don't think we'll get around newtyping. The following code doesn't work because it doesn't implement resolve correctly (it needs to look through the selection set ss and dispatch accordingly), but it shows how recursion can be implemented in principle:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ScopedTypeVariables, FlexibleInstances, MultiParamTypeClasses #-}

module Examples.Rec where
import Protolude hiding (Enum)

import GraphQL
import GraphQL.API
import GraphQL.Value.ToValue
import GraphQL.Resolver (Handler, HasResolver(..))

newtype Directory = R (Object "Directory" '[] '[Field "subdirs" Directory, Field "x" Text])

instance forall m. Monad m => HasResolver m Directory where
  resolve _ ss = (pure . pure . toValue) ("broken" :: Text)

directory :: Handler IO Directory
directory = directory

example :: Text -> IO Response
example = interpretAnonymousQuery @Directory directory

from graphql-api.

teh avatar teh commented on September 13, 2024

This turns out to be more tricky than expected. Sticking to the directory example I can't write the following instance because the type family computation is recursive, and therefore will never terminate:

instance forall m. Monad m => HasResolver m Directory where
  type Handler m Directory = Handler m Directory :<> Handler m Text

from graphql-api.

teh avatar teh commented on September 13, 2024

Just to add more context: We also care about mutual recursion as pointed out in the opening comment of this issue. For pure self-recursion we could add a Self combinator that gets transformed into recursive calls at the value level.

from graphql-api.

teh avatar teh commented on September 13, 2024

Here's a working example though I don't fully understand yet why this specific combination introduce recursion, as I am still calling resolve recursively (but that's probably it). I will try to extract the core of it into a nice interface (we don't need everything in that example).

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators, ScopedTypeVariables, RankNTypes #-}
{-# LANGUAGE DeriveGeneric, UndecidableInstances, StandaloneDeriving #-}
{-# LANGUAGE GADTs, KindSignatures, FlexibleInstances, MultiParamTypeClasses, TypeFamilies #-}

module Examples.Rec where
import Protolude hiding (Enum)

import GraphQL
import GraphQL.API
import GraphQL.Resolver
import Data.Aeson (encode)

newtype RD m = RD (Handler m Directory)
data R = R

type Directory = Object "Directory" '[] '[Field "subdirs" R, Field "name" Text]

instance forall m. (Monad m) => HasResolver m R where
  type Handler m R = RD m
  resolve (RD rd) = resolve @m @Directory rd

instance HasAnnotatedType R where
  getAnnotatedType = getAnnotatedType @Int

directory :: Handler IO Directory
directory = pure (RD directory :<> pure "tom")

run :: Text -> IO Response
run = interpretAnonymousQuery @Directory directory

example :: IO LByteString
example = encode <$> run "{ subdirs { name subdirs { name subdirs { name } }} }"
-- "{\"data\":{\"subdirs\":{\"subdirs\":{\"subdirs\":{\"name\":\"tom\"},\"name\":\"tom\"},\"name\":\"tom\"}}}"

from graphql-api.

teh avatar teh commented on September 13, 2024

And an example with mutual recursion - the trick is to include a judiciously placed loop-breaker that's an actual data type with a constructor, which is how mutual recursion works in Haskell of course.

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators, ScopedTypeVariables, RankNTypes #-}
{-# LANGUAGE DeriveGeneric, UndecidableInstances, StandaloneDeriving #-}
{-# LANGUAGE GADTs, KindSignatures, FlexibleInstances, MultiParamTypeClasses, TypeFamilies #-}

module Examples.Rec where
import Protolude hiding (Enum)

import GraphQL
import GraphQL.API
import GraphQL.Resolver
import Data.Aeson (encode)

newtype RecIssue m = RecIssue (Handler m (Issue m))

type User m = Object "User" '[] '[Field "issues" (List (RecIssue m)), Field "name" Text]
type Issue m = Object "Issue" '[] '[Field "owner" (User m), Field "title" Text]

instance forall m. (Monad m) => HasResolver m (RecIssue m) where
  type Handler m (RecIssue m) = RecIssue m
  resolve (RecIssue rd) = resolve @m @(Issue m) rd

instance forall m. HasAnnotatedType (RecIssue m) where
  getAnnotatedType = getAnnotatedType @Int

issues :: Handler IO (Issue IO)
issues = pure (user :<> pure "issue-title")

user :: Handler IO (User IO)
user = pure (pure [RecIssue issues] :<> pure "tom")

run :: Text -> IO Response
run = interpretAnonymousQuery @(User IO) user

example :: IO LByteString
example = encode <$> run "{ issues { owner { name } } }"
-- "{\"data\":{\"issues\":[{\"owner\":{\"name\":\"tom\"}}]}}"

from graphql-api.

jml avatar jml commented on September 13, 2024

Interesting!

from graphql-api.

teh avatar teh commented on September 13, 2024

Note that HasAnnotatedType returns garbage, but that doesn't matter because we aren't really using that information yet.

If HasAnnotatedType used getAnnotatedType @Directory it'd lead to infinite recursion. In order to break that we need to change the code to recognise cycles by some identifier (e.g. the object name) and then store a pointer. This would imply globally unique object names which I think is something that facebook do.

from graphql-api.

jml avatar jml commented on September 13, 2024

You mean globally unique type names, right? That's a requirement for a valid schema.

We'll need HasAnnotatedType for introspection.

from graphql-api.

teh avatar teh commented on September 13, 2024

"Type" is overloaded :). I specifically mean GraphQL types, i.e.:

type X = Object "X" '[] '[...]
                ^^^ this

from graphql-api.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.