Hackfoofery

Alson Kemp

Sad about Import Cycles …

Turbinado has the beginnings of a database ORM built in.  I’m hoping to make it super simple to interact with a relational DB, but I’m bumping up again import cycles. Ticket about import cycles here.

The Problem

import qualified App.Models.PageModel as Page
import qualified App.Models.CommentModel as Comment
 
someFn = do p  <- Page.find "tutorial"  -- find using a primary key of "tutorial"
            cs <- Page.findAllChildComment p
            let c = head cs
            p2 <- Comment.getParentPage c

Yeah, could be prettier (hmm… maybe Rails’ pluralization is good after all…), but that’s a pretty nice way to access related Pages and Comments in a SQL database. What doesn’t work, though are the imports. Since Page.findAllChildComment returns Comments, PageModel needs to import CommentModel. But, since CommentModel also uses the Page type, CommentModel needs to import PageModel. And thus the cycle begins…

Fix #1

The custom seems to be to push the data types out to a Types.hs file.

Assuming that Types.hs is imported and exported by all Models, then the following doesn’t work:

import qualified App.Models.PageModel as PageModel
import qualified App.Models.CommentModel as CommentModel
 
someFn = do let p = Page {title = "blah", content = "ack", _id = "page1"}
            PageModel.insert p False

Is the Page type the one from PageModel or from CommentModel? Both models export the Page type.

Update: As Twan points out in the Comments, the above would work if I were happy to do either of:

  let p = PageModel.Page {title = "blah", content = "ack", _id = "page1"}
  let p = CommentModel.Page {title = "blah", content = "ack", _id = "page1"}

Perhaps it’s unreasonable of me, but I want to be able to refer to the type without being forced to refer to the module. Besides, this post is really a gripe about import cycles, so I can’t have Fix #1 work and still gripe…

Fix #2

Push the data types out to a Types.hs file, but don’t export Types from the Models. Now you have to explicitly import Types whenever you’re using a Model:

import App.Models.Types   -- I'm not pretty...
import qualified App.Models.PageModel as PageModel
import qualified App.Models.CommentModel as CommentModel
 
someFn = do let p = Page {title = "blah", content = "ack", _id = "page1"}
            PageModel.insert p False

Now Page is clearly taken from App.Models.Types. But my delicate aesthetic sensibilities are offended.

But wait! We’re still broken! Since PageModel and CommentModel may use functions from each other, import cycles can still be produced. For example, Page.findAllChildComment uses a function from CommentModel:

instance HasChildComment (Page) where
    findAllChildComment p = CommentModel.findAllWhere "page_id = ?" [HDBC.toSql $ _id p]

Now if CommentModel needs a function from PageModel, then an import cycle is formed. So for the ORM to work relations in the DB can only form an acyclic graph. That’s bad.

Fix #3

Even though PageModel depends on CommentModel, don’t import it. Just duplicate the CommentModel code into PageModel. Ugly:

import App.Models.Types
 
instance HasChildComment (Page) where
    findAllChildComment p = do
        conn <- getEnvironment >>= (return . fromJust . getDatabase )
        res <- liftIO $ HDBC.handleSqlError $ HDBC.quickQuery' conn ("SELECT _id , commenter , page_id , post FROM comment WHERE (page_id = ?) ")  [_id p]
        return $ map (\r -> Comment (HDBC.fromSql (r !! 0)) (HDBC.fromSql (r !! 1)) (HDBC.fromSql (r !! 2)) (HDBC.fromSql (r !! 3))) res

I’m Sad

Haskell is such a lovely language, but my workaround for my module import cycles is pretty hideous. Am I missing something?

Written by alson

January 3rd, 2009 at 8:39 pm

Posted in Haskell

with 15 comments

15 Responses to 'Sad about Import Cycles …'

Subscribe to comments with RSS or TrackBack to 'Sad about Import Cycles …'.

  1. Why doesn’t fix #1 work? I think it should, aside from the fact that you imported both modules qualified, so you need PageModel.Page{…}.

  2. Twan,

    Fix #1 would work if I were happy about prepending the types with PageModel or CommentModel. It looks strange to me to have CommentModel.Page. I’ve amended the post with a note to that effect. Thanks for catching that.

    A

    alson

    3 Jan 09 at 11:28 pm

  3. Well one thing is that Haskell 98 requires mutually recursive modules to be supported. It’s the implementations that are broken, not the language (not that that helps anyone.)

    Next, the usual way to avoid such situations (in general) is to use abstraction of one sort or another. e.g. programming to interfaces. The modules then never directly depend on each other. I’m too lazy to actually give you any kind of advice in that direction.

    Derek Elkins

    4 Jan 09 at 2:23 am

  4. I discussed how to do the Types.hs thing properly in my Monad Reader article, issue 12, Hoogle Overview. I have had similar problems in the past, but using the techniques described in there they have all gone away. http://www.haskell.org/haskellwiki/The_Monad.Reader

    Neil Mitchell

    4 Jan 09 at 8:52 am

  5. Hi, I had similar problem a few days ago, finally I fixed it via #2 route but as You say it isn’t pretty way.

    I think that problem can be solved by more carefully analysis what can cause cycles, i.e. if we have some stuff f, which are defined in module A and module A imported module B, but f doesn’t depend on module B, then module B which imports module C, which imports f from module A, should be compiled without cycles (if there aren’t other cycles)

    After I wrote this, I realized how nasty it is, but could work ;]

    MaK

    4 Jan 09 at 10:37 am

  6. If you still have import cycles after pushing types into Types.hs, then start moving functions into new modules.

    Either pull them up into something higher-level that knows about both Page and Comment models, or push them down into something primitive that doesn’t know about either.

    Mutually recursive modules are stinky in any language!

    Christopher Lane Hinson

    4 Jan 09 at 12:13 pm

  7. Why should you export Types.hs from other models? What about not doing it and requireing user to manually export Types.hs in order to refer any model?

    Victor

    4 Jan 09 at 6:42 pm

  8. I don’t get why Fix #1 shouldn’t work as it is. If Page is defined in Types it doesn’t matter if both PageModel and CommentModel re-export it, the compiler knows that it’s the same Page type.

    Saizan

    4 Jan 09 at 10:07 pm

  9. Derek,

    Yeah, Haskell98 does require that. I know it’s a low priority, but it’d be great if GHC supported that requirement, too…

    Neil,

    I just bumped into a nasty problem with using a single Types file. My data types are generated from the database tables. A lot of the tables have overlapping field names (e.g. _id). So Types.hs has something like:

    data Page = Page {_id :: Int, …} data Comment = Comment {_id :: Int, …}

    Since the field labels are just functions, GHC complains about _id being defined twice… Doh.

    Christopher,

    That would separate like/related bits of code from each other. I’m kinda sad that a compiler issue is forcing me to organize my code into non-intuitive blocks…

    Victor,

    Doh! As mentioned above, Types.hs won’t work (because of multiply defined “_id”)…

    Argh… This is getting ugly.

    Alson

    alson

    4 Jan 09 at 10:15 pm

  10. Saizan,

    Funny. I’d had it in my mind that importing Types twice would be a problem, but it makes sense that it wouldn’t be…

    … in which case, I’m left with the problem that CommentModel requires functions from PageModel and PageModel requires functions from CommentModel …

    … also, there’s the problem with overlapping “_id” fields in Types.hs…

    A

    alson

    4 Jan 09 at 10:20 pm

  11. But, but… GHC supports recursive modules! Take a look at the documentation on .hs-boot files: http://www.haskell.org/ghc/docs/latest/html/users_guide/separate-compilation.html#mutual-recursion

    dmwit

    5 Jan 09 at 1:09 am

  12. dmwit,

    I think that the .hs-boot thing has been supported for a while (also, I referenced that link at the top of my post). I’ve never been able to get the .hs-boot thing to work too well and I’m really afraid to make it part of building Turbinado… But maybe I should relax and see if I can get the code generator to generate the .hs-boot files. It still feels awfully hack-ish, though.

    A

    alson

    5 Jan 09 at 2:57 am

  13. hs-boot files are just a way of doing some work that should actually be done by the compiler. It’s awfully tedious, but it isn’t hackish.

    maltem

    5 Jan 09 at 2:53 pm

  14. maltem,

    “hack-ish” is just a grumpy way to say “work-around-ish” and .hs-boot is definitely “work-around-ish”. I’d love GHC to support Haskell98 in this area.

    A

    alson

    5 Jan 09 at 3:13 pm

  15. The haskell-src-exts package can now parse and produce the {-# SOURCE #-} pragmas and I am using it to generate Haskell code for a different project.

    chris k

    6 Jan 09 at 12:39 pm

Leave a Reply