brendanhay / gogol

A comprehensive Google Services SDK for Haskell.
Other
281 stars 105 forks source link

How are you meant to "re-scope" an env? #75

Open MichaelXavier opened 7 years ago

MichaelXavier commented 7 years ago

I've got a module in my project that's the single interface to all of my usage of google cloud services. Currently, each individual function is rebuilding the env each time, which seems pretty wasteful. I would rather the user be able to pass in an env to each function.

The problem comes with the scopes needed for each function:

  1. Require the user to initialize an env on their own that happens to have the full set of scopes that the application needs. This seems like a good bit of documentation I guess as an env would "accumulate" scopes sort of like how usage of multiple typeclass-constrained functions accumulates constraints.
  2. Let them pass in any old env and append the scopes I need.

The problem with 1 is if one of my google functions uses multiple google functions internally, the required type signature does not seem to concatenate lists but forces me to specify them individually, duplicates and all. So if I use foo which requires '[a, b, c] and bar which requires '[b, c, d], then fooBar's constraints won't require (HasScope s '[a, b, c, d] ~ True) => Env s -> ... but rather (HasScope s '[a, b, c] ~ True, HasScope s '[b, c, d]) => Env s -> ..., which can get really repetitive the more you use. I am not willing to forgo writing type signatures for toplevel functions.

The problem with 2 is envScopes :: Lens' a (Proxy s) uses a Lens' which does not allow type-changing, so I cannot do something like let env' = env & envScopes .~ neededScopes because that changes s and that lens will not allow it.

I tried to look at the examples for guidance but because they set up the env and set scopes in one shot, they can't really model the scenario of the user passing in an env to something that demands a certain set of scopes. What should I do?

MichaelXavier commented 7 years ago

A bit of an update after bashing on this for a while: from what I can tell, scopes are treated like a set, but they are order dependent. What I did was write a mkEnv function in my Google Cloud module that will make an env with the set of all scopes used by all the API calls the module supports. To test it, I then wrote a function that used this env and called every one of those functions. Initially it would not typecheck and presented me with 2 equivalent sets of scopes, one required and one given. I reordered the construction of the scope set to align with the set it demanded and it typechecked.

So going forward:

  1. Is it intentional that the scope sets are order-dependent? I don't really know the practical purpose of that, unless it is really hard to implement order-invariant sets in the type level (or makes compilation prohibitively slow or something).
  2. I'd recommend documenting this behavior if you intend to stick with it, perhaps even in the examples. The examples only make 1 API call so they just don't deal with this issue and thus can't signal to the user what the "right" thing to do is.
ocramz commented 3 years ago

I ran into this problem as well. For example, when downloading and uploading a bytestring in the same function, the constraints become :

                            HasScope'
                             s1
                             '["https://www.googleapis.com/auth/cloud-platform",
                               "https://www.googleapis.com/auth/devstorage.full_control",
                               "https://www.googleapis.com/auth/devstorage.read_write"]
                           ~ 'True,
                           HasScope'
                             s1
                             '["https://www.googleapis.com/auth/cloud-platform",
                               "https://www.googleapis.com/auth/cloud-platform.read-only",
                               "https://www.googleapis.com/auth/devstorage.full_control",
                               "https://www.googleapis.com/auth/devstorage.read_only",
                               "https://www.googleapis.com/auth/devstorage.read_write"]
                           ~ 'True) =>

I believe getting rid of type-level lists and declaring distinct constraints for each API scope (HasScope s CloudPlatform, HasScope s DevStorageRW etc.) would make this interface more ergonomical.