Do you have a giant `Types` module in your projects?

So, there is a monolithic back end application I and some friends are working on. It has a module Types with 116 data definitions (and counting) that everything else depends upon. A smallest change, like say adding a Show instance, entails recompilation that takes several minutes, with all the generic instance FromJSON definitions and such.

I am starting to question if this is a best practice.

In my recent additions to the code base, I prefer to write the data definitions at the same module where I need them so that they do not burden the compilation process. However, this is also not optimal: often a client module only needs the types, not the logic, and it is comfortable to import all the types at once.

So, if we decide to always keep types separate from logic, then there are several ways to organize the types:

  1. A single module Types that everything else depends upon.

    Graphically. Types ← Logicᵢ

  2. To every module Logicᵢ, a module Logicᵢ.Types.

    Graphically, Logicᵢ.Types ← Logic.

  3. Split the module Types into Types.Logic₁, Types.Logic₂, then import everything into Types.

    Graphically, Types.Logicᵢ ← Types and Types ← Logicᵢ.

  4. More radically, have Types split a module for each type, like Types.Type₁, Types.Type₂ and so on.

Another consideration is that, in the future, we might start to generate some types from the data base schema and the HTTP API specification automatically, and some combination of approaches 3 and 4 seems to be the easiest to automate.

What is the advisable way forward?

4 Likes

I remember this blog post: Haskell for all: Module organization guidelines for Haskell projects. It doesn’t really mention compile times (it does actually), but it does discourage the use of one big Types module.

3 Likes

I think everyone starts with a “vertical” organisation, (a Syntax module, a Parsing module, etc.).

Eventually as your program grows bigger you can encounter circular dependencies. Now you have the option to:

  • use .hs-boot files; or
  • use Syntax.Primitive / Syntax.Type modules; or
  • use one big Types module.

With the first you have two files with overlapping information; with the second you are splitting “related functionality” among different files; the third one a type salad.

1 Like

Organise your application around the business concepts that it handles, and keep the types close to the functions that use them.

Do avoid a monolithic Types module, even if it only re-exports. This will mess the compilation graph of your project and create a bottleneck to parallel compilation.

7 Likes

Having a huge Types.hs module is definitely a bit of an antipattern, but it’s not something I worry about when I just want to get something working. So I do usually end up with something like that in the initial phases, but Haskell is easy enough to refactor once I want to clean it up a bit later.

4 Likes

I know that the conventional wisdom is to organize your project by what things do rather than what they are. However I tend to organize my projects with each type in its own module, all under the MyProject.Type.* namespace. That has worked well for me in a variety of situations. I haven’t run into many circular dependencies, and when I do they are often trivially solved by introducing a type variable. A plus side of organizing things this way is that the build is very parallel and recompiles after changes are usually very quick.

3 Likes

I acquired the same habit at some time.
I tend to lay out modules around one central type and its related instances, functions etc.
Mutually-recursive types can be a problem, but may be they’re just a subpart of some other domain and really should be placed together as supporting types for that domain.

But, please, please, for the love of whatever gods may be looking upon us, don’t name all your types T and constructors C :pray:
Haskell is not ML, we don’t have necessary support for this pattern.

2 Likes