Structuring a medium to large-scale Haskell project requires careful organization and modularization of the codebase. It’s crucial to split the code into smaller, manageable modules and components that can be developed, tested, and maintained independently.
Here is a typical project structure for a medium to large-scale Haskell project:
project/
├── app/
│ ├── Main.hs
├── src/
│ ├── ProjectName/
│ │ ├── ModuleA.hs
│ │ ├── ModuleB.hs
│ │ ├── SubModule1/
│ │ │ ├── ModuleC.hs
│ │ │ ├── ModuleD.hs
│ │ └── SubModule2/
│ │ ├── ModuleE.hs
│ │ └── ModuleF.hs
├── test/
│ ├── ProjectName/
│ │ ├── ModuleATest.hs
│ │ ├── ModuleBTest.hs
│ │ ├── SubModule1/
│ │ │ ├── ModuleCTest.hs
│ │ │ ├── ModuleDTest.hs
│ │ └── SubModule2/
│ │ ├── ModuleETest.hs
│ │ └── ModuleFTest.hs
├── bench/
│ ├── ProjectName/
│ │ ├── BenchmarkA.hs
│ │ ├── BenchmarkB.hs
│ │ └── ...
├── doc/
│ ├── UserManual.md
│ ├── APIReference.md
│ └── ...
├── LICENSE
├── README.md
├── .gitignore
├── .stylish-haskell.yaml
├── .hlint.yaml
├── projectname.cabal
└── stack.yaml
Let’s discuss each part of this structure:
1. **app/**: This directory contains the application’s entry point, usually ‘Main.hs‘. This file is responsible for initializing and running the main program.
2. **src/**: This directory contains the project’s source code organized into modules and submodules. Group similar functionalities under a single module, separated by subdirectories if needed. A module can expose functions and data types to be used by other modules.
3. **test/**: This directory contains unit tests and property tests for each module. A good practice is to have one test file for each module, named ‘ModuleNameTest.hs‘. This helps to maintain a clean separation between the application code and the test code.
4. **bench/**: This directory contains benchmarking code for measuring the performance of your modules and functions. Similar to tests, it’s a good idea to have one benchmark file for each module, named ‘BenchmarkA.hs‘.
5. **doc/**: This directory contains documentation related to the project, such as user manuals, API references, and usage guides.
6. **projectname.cabal**: This is the Cabal file that defines your project’s package, dependencies, and build settings.
7. **stack.yaml**: This is the Stack configuration file, which specifies the resolver, package dependencies, and other build options for your project.
8. Linters and formatters: It’s a good idea to use tools like HLint and stylish-haskell to enforce code style and conventions throughout the project, maintaining consistency and readability. These tools can be configured using ‘.hlint.yaml‘ and ‘.stylish-haskell.yaml‘.
Finally, it’s essential to have a ‘LICENSE‘ file specifying the licensing terms, ‘README.md‘ describing your project, and a ‘.gitignore‘ file to exclude unnecessary files from version control.
When there is a need to include additional libraries, like Charts or Figma for drawing charts. We can directly use Haskell chart libraries like ‘Chart‘ and ‘Chart-cairo‘ to add charts and graphs to our project:
import Graphics.Rendering.Chart.Easy
import Graphics.Rendering.Chart.Backend.Cairo
-- Example of plotting a chart
main :: IO ()
main = toFile def "example.png" $ do
layout_title .= "Example Chart"
setColors $ brewerSet Purples 9
plot (line "line" [data_points])
where
data_points = zip [0,1..10] (map sin [0,0.1..1])
The essential aspect of the project structure’s organization is to make it easy for developers to understand, build upon and maintain the codebase.