target / theta-idl

Define communication protocols between applications using algebraic data types.
Other
45 stars 9 forks source link

Question on functionality of generated Haskell code and `import` in theta files #63

Closed Cmdv closed 2 years ago

Cmdv commented 2 years ago

Unsure if this is a bug or the expected behaviour but here is the scenario:

Two theta files and one imports from the other types/carriers/backend2config.theta:

import config

type MessageConfig = {
    message : config.ConfigCard,
}

types/config.theta

type ConfigScorecard = String

Now two haskell files again doing a similar thing

haskell/Types/Carriers/Backend2Config.hs:

module Types.Carriers.Backend2Config where

import Theta.Target.Haskell (loadModule)
import Types.Config (fakeConfigCard)

loadModule "types" "carriers.backend2config"

fakeMessageConfig :: MessageConfig
fakeMessageConfig =
    MessageConfig
        { message = fakeConfigCard
        }

haskell/Types/Config.hs

module Types.Config where

import Theta.Target.Haskell (loadModule)

loadModule "types" "config"

fakeConfigCard :: ConfigCard
fakeConfigCard = ConfigCard "null"

inside of Backend2Config.hs I'm getting the following type error:

Couldn't match expected type 'ConfigCard'
                  with actual type 'Types.Config.ConfigCard'

Looks like maybe the parsing is unable to recognise something was imported from another location so give is a type of ConfigCard but that type isn't even generated when running loadModule so it becomes a bit of a no op type that exists but doesn't 😓

I could have this totally the wrong way around and if there is a preferred method to doing what I'm trying to achieve that would be great.

TikhonJelvis commented 2 years ago

Yeah, importing is a bit of a hack right now.

You can fix this particular example by importing everything from Types.Config in Backend2Config:

```Haskell
module Types.Carriers.Backend2Config where

import Theta.Target.Haskell (loadModule)
import Types.Config

loadModule "types" "carriers.backend2config"

fakeMessageConfig :: MessageConfig
fakeMessageConfig =
    MessageConfig
        { message = fakeConfigCard
        }

By default, loadModule will generate all the types that it needs, including types from imported modules. In your example, the ConfigCard type gets generated twice: once in Config.hs and once in Backend2Config.hs. This is where the error message is coming from—fakeConfigCard uses the ConfigCard type generated in Config.hs, but MessageConfig used the one generated in Backend2Config.hs.

The way Template Haskell works is fundamentally on a per-module basis—as far as I know, it has no way to know about types defined in other modules unless they are imported and visible where the TH function (loadModule) was called. This means that if you want to reuse the ConfigCard type generated in Config.hs, you fundamentally have to import it. I also couldn't figure out any way for TH to deal with qualified imports, so it'll have to be imported unqualified.

loadModule has another limitation. As part of generating types for a Theta module (say com.example), it generates a top-level identifier (theta'com'example :: Theta.Module). Right now, it uses this identifier to avoid duplicates: if theta'com'example is in scope, it'll skip generating anything for that module and assume that the Haskell types for com.example are all in scope. So in your example, you would have to import not only ConfigCard but also theta'config.

It's a bit fiddly, but I couldn't figure out a better approach given Template Haskell's limitations. Thinking about it now, I could try checking whether a type name is in scope for every type I try to generate (rather than doing it on a per-module basis), but I'm not sure exactly how that would work, and it might make the generated HasTheta instances inconsistent with each other.

Cmdv commented 2 years ago

ah lovely thanks you, yeah it's certainly a tricky one and I had a feeling it would be TH related 😓

Think what you've said to do is totally acceptable, we're just having to be a little more explicit where types are coming from. The confusion comes from not knowing exactly what TH is generating 😂

Didn't at all think about importing everything like you suggested! Doing so then bought Haskell language server into action and it recommended the following:

import Types.Config
    ( fakeConfigCard,
      ConfigCard,
      theta'config )

Purely out of interest could I ask what is theta'config is ? My guess is it's some sort of a TH pointer to the types inside config.theta as without it the loadModule from within Backend2Config errors with a lots of Ambiguous occurrence ‘ConfigCard'.... My guess is that makes the types concrete/parameterized as then TH knows that ConfigCard does indeed come from config.theta due to its use of loadModule inside of Congig.hs.... That's a massive guess though 😂

I'd say this isn't really a dramatic bug and the work around again isn't bad at all, I might make a little PR to explain that somewhere if you like? (any preference as to where?)

TikhonJelvis commented 2 years ago

Purely out of interest could I ask what is theta'config is ?

theta'config is the Theta.Module object for the config module. It contains the Theta type definitions, imports and metadata for each module. Theta types can reference each other, so we need to keep track of the module a type was defined in when we do anything with it. Each of the types generated in Haskell has a HasTheta instance which maps the Haskell type to its Theta type, and having a single top-level theta'config definition lets each of those Theta types point to exactly the same object describing the config module.

So it mostly isn't a TH-specific definition—I originally needed it to connect generated Haskell types with the Theta types they were generated from. It then happened to be a convenient way to avoid generating duplicate types from loadModule and that, without my intending it, made the imports work too, albeit in a slightly hacky way.

(As an aside, wish I could link to generated Haddock docs rather than the code itself, but I don't have that set up and I probably need to specify some version bounds before the package is ready to be uploaded to Hackage.)

I might make a little PR to explain that somewhere if you like? (any preference as to where?)

That would be great! There are a few places that could use some love in that respect:

I'm honestly not sure which places make the most sense. I could see the same explanation working well in both the guide and one of the Haddock docs. In general, I've mostly kept the Haddock docs up-to-date—although there seem to be some small updates needed for the ones I linked—but the user guide is a bit less fresh, and updating it has been lingering on my mental to-do list for a while :P

dmvianna commented 2 years ago

This is magic! 🚀

Cmdv commented 2 years ago

thanks @TikhonJelvis will add something in the following few days. Bit snowed under at the moment 😅

dmvianna commented 2 years ago

@TikhonJelvis @Cmdv I believe we can close it now?

Cmdv commented 2 years ago

yeah can do, I didn't get the time to update docs sorry!!