Ah I was also talking to @ners about this subject at NixCon. There’s also this neat little GHC option that simplifies the syntax behind these qualified import rules you listed called require. It’s not up to date with latest GHCs and it doesn’t quite line up with your rules but maybe this can be some inspiration to standardizing the pattern.
Great article, thanks a lot!
Thank you for this, I’m switching to ImportQualifiedPost.
This brings up the fact that whenever you bring in a new library you’re not sure how the imports will clash. Even a name like Span.ID could clash with something else. So I always import qualified by default with and maybe refactor later if I know the names well enough. Sometimes with a new library it helps to just convert the unqualified import tutorial code to qualified imports so that you know what is where.
The other thing is that if you only work on a codebase occaisionally you might forget if you import it as Span.ID or ID when you come back to it and then you start a new file with a different import name.
At first I thought that a solution might be to allow the module to define a default alias name but now I feel like maybe it’s best if there could be a file in the project that keeps track of import names.
So if I can put import Effectful.OpenTelemetry.Tracing.Span.ID qualified as Span.ID in one file and then in the rest of the code use import Span.ID qualified. Then at least if some other library uses Span.ID I can add import OtherLibrary.Span.ID qualified as OtherSpan.Id in the imports file.
And I believe @taylorfausak is a huge fan of qualified imports. I was amazed when I read the source code of cabal-gild.
Indeed I am! Qualified imports are the best. I even built Imp, a GHC plugin for automatically importing modules based on qualified usage.
I’m also a fan.
This is basically the style I like to use. Thanks for writing it up!
So colloquially call this Java style Haskell and I follow this very heavy in Hazy. I’m pretty happy with the result, I rarely ever have name conflicts and I think the end result is pretty readable.
Let me talk about my major issue I’ve run into following philosophy and that is .hs-boot hell. Anytime I need types or functions there are mutually recursive, I need to painfully create new .hs-boot modules. Sometimes I even need to reorganize my code because the resulting hs-boot files would be mutually recursive (see There are mutually recursive programs that GHC can't compile). For reference I currently I have 445 normal Haskell files and 95 .hs-boot modules, so 20% of my modules need .hs-boot files (this could probably be reduced).
Still, even with this major problem, I think the end result is much better then alternative.
I’ve always thought of it is as Python style because many python styleguides say you have to write from x.y import z where z is a module. I add an additional convention, which is to not rename modules. If the module name is too long, rename the file. If it’s not unique, try make it unique. If first conflicts with second, then try to clarify boundaries so it’s usually only imported locally. This conflicts with “no re-exports” though so it’s all a balance and none are absolute.
I don’t see what it has to do with import cycles though, cycles are cycles, regardless of the syntax used to import them. I don’t use hs-boot but it’s true removing cycles can be hard. I think it’s worth it though, even if some modules get large. I have 948 .hs and 17 .hsc files and no hs-boot.
I’ve used fix-imports: Program to manage the imports of a haskell module for the last 15 years or so, qualified imports make automatic importing into a simple source transformation, it predates the existence of LSP let alone haskell support. It also makes finding callers trivial, I have a key bound which is just a grep for “CurrentModel.” and goes into a quickfix menu.
It also encourages (but does not require) a “no re-exports” convention, which I generally like anyway.
iirc Java does allow circular imports but it’s an exception, not the rule (e.g: Golang - which is qualified by default - cannot handle circular imports.
That’s true, I recall it does. I think there are some sharp corners with order of initialization. From my single sample size with such a project, for me the greatest problem was not the technical sharp edges, but such loops made it hard for me to grasp the structure. It was not clear who was lower level and who was a client of who because they all wrapped around, and it was difficult to study them in isolation. My conclusion was that forbidding loops is annoying but it’s ultimately good for you.