Haskell Modules

Haskell Modules

Haskell modules are closely tied to source files. Each source file can only contain 1 module. The module name must match the name of the source file it is contained within (without .hs).

Module Example

For example let’s say our project’s folder structure looks as follows:

project
|--app
|  | Main.hs
|  | Foo.hs
|  project.cabal
|  .
|  .
|  .

Main.hs looks as follows:

module Main where

import Foo

main :: IO ()
main = putStrLn "Hello, Haskell!"

Foo.hs looks as follows:

module Foo where

you :: String
you = "you"

bar :: Int
bar = 3

project.cabal looks as follows:

cabal-version:      3.0
-- The cabal-version field refers to the version of the .cabal specification,
-- and can be different from the cabal-install (the tool) version and the
-- Cabal (the library) version you are using. As such, the Cabal (the library)
-- version used must be equal or greater than the version stated in this field.
-- Starting from the specification version 2.2, the cabal-version field must be
-- the first thing in the cabal file.

-- Initial package description 'testing' generated by
-- 'cabal init'. For further documentation, see:
--   http://haskell.org/cabal/users-guide/
--
-- The name of the package.
name:               testing

-- The package version.
-- See the Haskell package versioning policy (PVP) for standards
-- guiding when and how versions should be incremented.
-- https://pvp.haskell.org
-- PVP summary:     +-+------- breaking API changes
--                  | | +----- non-breaking API additions
--                  | | | +--- code changes with no API change
version:            0.1.0.0

-- A short (one-line) description of the package.
-- synopsis:

-- A longer description of the package.
-- description:

-- The license under which the package is released.
license:            BSD-2-Clause

-- The file containing the license text.
license-file:       LICENSE

-- The package author(s).
author:             Joseph Haugh

-- An email address to which users can send suggestions, 
-- bug reports, and patches.
maintainer:         glue500@unm.edu

-- A copyright notice.
-- copyright:
build-type:         Simple

-- Extra doc files to be distributed with the package, 
-- such as a CHANGELOG or a README.
extra-doc-files:    CHANGELOG.md

-- Extra source files to be distributed with the package, 
-- such as examples, or a tutorial module.
-- extra-source-files:

common warnings
    ghc-options: -Wall

executable testing
    -- Import common warning flags.
    import:           warnings

    -- .hs or .lhs file containing the Main module.
    main-is:          Main.hs

    -- Modules included in this executable, other than Main.
    other-modules:    Foo

    -- LANGUAGE extensions used by modules in this package.
    -- other-extensions:

    -- Other library packages from which modules are imported.
    build-depends:    base ^>=4.15.1.0

    -- Directories containing source files.
    hs-source-dirs:   app

    -- Base language which the package is written in.
    default-language: Haskell2010

For now everything is fairly simple. We have two Haskell source files that both reside in a flat folder structure. When we added Foo.hs we also made sure to add it to the other-modules field in our project.cabal. All is well in the world. However, what if we want to start structuring our source code hierarchy with some folders? Well let’s see how this would play out. Let’s add a folder called “Utils” and put a Haskell file called “Baz.hs” in there. Thus, our folder structure would look as follows:

project
|--app
|  | Main.hs
|  | Foo.hs
|  |--Utils
|     | Baz.hs
|  project.cabal
|  .
|  .
|  .

Baz.hs looks as follows:

module Utils.Baz where

baz :: Float
baz = 3.14

bah :: String
bah = "bahhaha"

Notice that the module name is not just “Baz”. This is because the module name matches to folder structure of your source file directory. The root folder is determined by the hs-source-dirs field in your .cabal file. Whatever folder you set this too all source files directly in that folder only need the name of the file as the module name. However, any source files in sub-folders underneath the root folder must include their folder path in their module name. Note it is best practice to user capitalized names for all sub-folders. Thus, we get the “Utils.Baz” module name. Now let’s say we want to import this file in Main, then our Main.hs would look as follows:

module Main where

import Foo
import Utils.Baz

main :: IO ()
main = putStrLn "Hello, Haskell!"

But wait we also need to add it to our project.cabal file! Thus, our other-modules would now look as follows:

other-modules:    Foo
                , Utils.Baz

Now we can use the function from Utils.Baz in Main.

Controlling Exports

By default a module exposes all of the functions and datatypes contained within them. This is usually not desirable as we often want to only expose some of the function. This is easily achievable with Haskell modules. For example let’s say we only wanted to export the baz function from Baz.hs, we would then have to change the file to be the following:

module Utils.Baz (baz) where

baz :: Float
baz = 3.14

bah :: String
bah = "bahhaha"

Whatever functions we put in the parenthesis next to the module name get exported. Note the export list is a comma separated. Now if we tried to use bah in Main we would get an error. Now let’s say that we want to add a custom datatype to Foo:

module Foo where

data FancyType = FancyConstructor
  { getFancy :: String 
  }

you :: String
you = "you"

bar :: Int
bar = 3

Now we are exporting an additional 3 things:

  1. The type FancyType
  2. The constructor FancyConstructor
  3. The getter function getFancy

Haskell let’s you control which of these three things you export. For example if we only want to export the type then our file would look as follows:

module Foo (FancyType) where

data FancyType = FancyConstructor
  { getFancy :: String 
  }

you :: String
you = "you"

bar :: Int
bar = 3

This does not export FancyConstructor or getFancy only the type. Thus, anyone importing our module could only see the FancyType but could not create one or access into using the getter function. What if we do want to export everything about our datatype? Well then our file would need to look as follows:

module Foo 
  ( FancyType(FancyConstructor, getFancy)
  , you
  ) where

data FancyType = FancyConstructor
  { getFancy :: String 
  }

you :: String
you = "you"

bar :: Int
bar = 3

Note how the exports are now formatted a bit differently this usually how they are formatted to be able to easily add more exports in the future, just add a new line with a comma and a new export. We are now exporting everything associated with our datatype. However, it seems a bit annoying having to list it all out which is why there is a shorthand for exporting everything:

module Foo 
  ( FancyType(..)
  , you
  ) where

data FancyType = FancyConstructor
  { getFancy :: String 
  }

you :: String
you = "you"

bar :: Int
bar = 3

Voila! You may be asking yourself well why wouldn’t we just export everything anyway? The answer is an idiom called smart constructors.

Smart Constructors

A smart constructor is the idea of having a function to produce your type rather than the automatically provided data constructor. The reason for this is because then you can do input checking before actually constructing your datatype. For example say that we wanted a smart constructor for FancyType to make sure that the input string was non-empty. We also want to only export the type and our smart constructor, our file would look as follows:

module Foo 
  ( FancyType
  , makeFancyType
  , you
  ) where

data FancyType = FancyConstructor
  { getFancy :: String 
  }

makeFancyType :: String -> Maybe FancyType
makeFancyType [] = Nothing
makeFancyType s  = Just $ FancyConstructor s

you :: String
you = "you"

bar :: Int
bar = 3

Note this code uses Maybe which we will cover in another post. Now users of our module will have to use makeFancyType in order to construct FancyType!

Controlling Imports

There are also language mechanisms to control what you import from another module. For example if in Main we only cared about the you function from the Foo module then we could do the following:

module Main where

import Foo (you)
import Utils.Baz

main :: IO ()
main = putStrLn "Hello, Haskell!"

Now only the you function will be imported from the Foo module. This of course presupposes that Foo actually exports the you function. This can be useful when we don’t want to muddy up our namespace with a bunch of functions from another module. Note this is also a comma separated list just like the exports list. When importing many large modules it can become confusing to determine from which module a given function is coming from. Luckily Haskell allows us to do qualified imports. These let us name an imported module which in turn let’s us be explicit which module a given function is coming from. For example:

module Main where

import qualified Foo as F
import Utils.Baz

main :: IO ()
main = do
    print F.you
    putStrLn "Hello, Haskell!"

Now we know exactly where the you function is coming from.