layout: post
title: “Organizing modules in a project”
description: “A recipe for a functional app, Part 3”
seriesId: “A recipe for a functional app”
seriesOrder: 3

categories: [Modules]

Before we move on to any coding in the recipe, let’s look at the overall structure of a F# project. In particular: (a) what code should be in which modules and (b) how the modules should be organized within a project.

How not to do it

A newcomer to F# might be tempted to organize code in classes just like in C#. One class per file, in alphabetical order. After all, F# supports the same object-oriented features that C# does, right? So surely the F# code can be organized the same way as C# code?

After a while, this is often followed by the discovery that F# requires files (and code within a file) to be in dependency order. That is, you cannot use forward references to code that hasn’t been seen by the compiler yet**.

This is followed by general annoyance and swearing. How can F# be so stupid? Surely it impossible to write any kind of large project!

In this post, we’ll look at one simple way to organize your code so that this doesn’t happen.

** The and keyword can be used in some cases to allow mutual recursion, but is discouraged.

The functional approach to layered design

A standard way of thinking about code is to group it into layers: a domain layer, a presentation layer, and so on, like this:

Design layers

Each layer contains only the code that is relevant to that layer.

But in practice, it is not that simple, as there are dependencies between each layer. The domain layer depends on the infrastructure, and the presentation layer depends on the domain.

And most importantly, the domain layer should not depend on the persistence layer. That is, it should be “persistence agnostic”.

We therefore need to tweak the layer diagram to look more like this (where each arrow represents a dependency):

Design layers

And ideally this reorganization would be made even more fine grained, with a separate “Service Layer”, containing application services, domain services, etc. And when we are finished, the core domain classes are “pure” and have no dependencies on anything else outside the domain. This is often called a “hexagonal architecture” or “onion architecture”. But this post is not about the subtleties of OO design, so for now, let’s just work with the simpler model.

Separating behavior from types

“It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures” — Alan Perlis

In a functional design, it is very important to separate behavior from data. The data types are simple and “dumb”. And then separately, you have a number of functions that act on those data types.

This is the exact opposite of an object-oriented design, where behavior and data are meant to be combined. After all, that’s exactly what a class is. In a truly object-oriented design in fact, you should have nothing but behavior — the data is private and can only be accessed via methods.

In fact, in OOD, not having enough behavior around a data type is considered a Bad Thing, and even has a name: the “anemic domain model”.

In functional design though, having “dumb data” with transparency is preferred. It is normally fine for the data to be exposed without being encapsulated. The data is immutable, so it can’t get “damaged” by a misbehaving function. And it turns out that the focus on transparent data allows for more code that is more flexible and generic.

If you haven’t seen it, I highly recommend Rich Hickey’s excellent talk on “The Value of Values”, which explains the benefits of this approach.

Type layers and behavior layers

So how does this apply to our layered design from above?

First, we must separate each layer into two distinct parts:

  • Data Types. Data structures that are used by that layer.
  • Logic. Functions that are implemented in that layer.

Once we have separated these two elements, our diagram will look like this:

Design layers

Notice though, that we might have some backwards references (shown by the red arrow). For example, a function in the domain layer might depend on a persistence-related type, such as IRepository.

In an OO design, we would add more layers (e.g. application services) to handle this. But in a functional design, we don’t need to — we can just move the persistence-related types to a different place in the hierarchy, underneath the domain functions, like this:

Design layers

In this design, we have now eliminated all cyclic references between layers. All the arrows point down.

And this without having to create any extra layers or overhead.

Finally, we can translate this layered design into F# files by turning it upside down.

  • The first file in the project should contain code which has no dependencies. This represents the functionality at the bottom of the layer diagram. It is generally a set of types, such the infrastructure or domain types.
  • The next file depends only on the first file. It would represents the functionality at the next-to-bottom layer.
  • And so on. Each file depends only on the previous ones.

So, if we refer back to the use case example discussed in Part 1:

Recipe Happy Path

then the corresponding code in an F# project might look something like this:

Design layers

At the very bottom of the list is the main file, called “main” or “program”, which contains the entry point for the program.

And just above it is the code for the use cases in the application. The code in this file is where all the functions from all the other modules are “glued together” into a single function that represents a particular use case or service request. (The nearest equivalent of this in an OO design are the “application services”, which serve roughly the same purpose.)

And then just above that is the “UI layer” and then the “DB layer” and so on, until you get to the top.

What’s nice about this approach is that, if you are a newcomer to a code base, you always know where to start. The first few files will always be the “bottom layer” of an application and the last few files will always be the “top layer”. No folders needed!

Putting code in modules, not classes

A common question from newcomers to F# is “how should I organize my code if I don’t use classes?”

The answer is: modules. As you know, in an object oriented program, a data structure and the functions that act on it would be combined in a class. However in functional-style F#, a data structure and the functions that act on it are contained in modules instead.

There are three common patterns for mixing types and functions together:

  • having the type declared in the same module as the functions.
  • having the type declared separately from the functions but in the same file.
  • having the type declared separately from the functions and in a different file, typically containing type definitions only.

In the first approach, types are defined inside the module along with their related functions. If there is only one primary type, it is often given a simple name such as “T” or the name of the module.

Here’s an example:

  1. namespace Example
  2. // declare a module
  3. module Person =
  4. type T = {First:string; Last:string}
  5. // constructor
  6. let create first last =
  7. {First=first; Last=last}
  8. // method that works on the type
  9. let fullName {First=first; Last=last} =
  10. first + " " + last

So the functions are accessed with names like Person.create and Person.fullName while the type itself is accessed with the name Person.T.

In the second approach, types are declared in the same file, but outside any module:

  1. namespace Example
  2. // declare the type outside the module
  3. type PersonType = {First:string; Last:string}
  4. // declare a module for functions that work on the type
  5. module Person =
  6. // constructor
  7. let create first last =
  8. {First=first; Last=last}
  9. // method that works on the type
  10. let fullName {First=first; Last=last} =
  11. first + " " + last

In this case, the functions are accessed with the same names (Person.create and Person.fullName) while the type itself is accessed with the name such as PersonType.

And finally, here’s the third approach. The type is declared in a special “types-only” module (typically in a different file):

  1. // =========================
  2. // File: DomainTypes.fs
  3. // =========================
  4. namespace Example
  5. // "types-only" module
  6. [<AutoOpen>]
  7. module DomainTypes =
  8. type Person = {First:string; Last:string}
  9. type OtherDomainType = ...
  10. type ThirdDomainType = ...

In this particular case, the AutoOpen attribute has been used to make the types in this module automatically visible to all the other modules in the project — making them “global”.

And then a different module contains all the functions that work on, say, the Person type.

  1. // =========================
  2. // File: Person.fs
  3. // =========================
  4. namespace Example
  5. // declare a module for functions that work on the type
  6. module Person =
  7. // constructor
  8. let create first last =
  9. {First=first; Last=last}
  10. // method that works on the type
  11. let fullName {First=first; Last=last} =
  12. first + " " + last

Note that in this example, both the type and the module are called Person. This is not normally a problem in practice, as the compiler can normally figure out what you want.

So, if you write this:

  1. let f (p:Person) = p.First

Then the compiler will understand that you are referring to the Person type.

On the other hand, if you write this:

  1. let g () = Person.create "Alice" "Smith"

Then the compiler will understand that you are referring to the Person module.

For more on modules, see the post on organizing functions.

The organization of the modules

For our recipe we will use a mixture of approaches, with the following guidelines:

Module Guidelines

If a type is shared among multiple modules, then put it in a special types-only module.

  • For example, if a type is used globally (or to be precise, within a “bounded domain” in DDD-speak), I would put it in a module called DomainTypes or DomainModel, which comes early in the compilation order.
  • If a type is used only in a subsystem, such as a type shared by a number of UI modules, then I would put it in a module called UITypes, which would come just before the other UI modules in the compilation order.

If a type is private to a module (or two) then put it in the same module as its related functions.

  • For example, a type that was used only for validation would be put in the Validation module. A type used only for database access would be put in the Database module, and so on.

Of course, there are many ways to organize types, but these guidelines act as a good default starting point.

Dude, where are my folders?

A common complaint is that F# projects do not support a folder structure, which supposedly makes it hard to organize large projects.

If you are doing a pure object-oriented design, then this is a legitimate complaint. But as you can see from the discussion above, having a linear list of modules is very helpful (if not strictly necessary) to ensure that the dependencies are maintained correctly. Yes, in theory, the files could be scattered about and the compiler might be able to figure out the correct compilation order, but in practice, it’s not easy for the compiler to determine this order.

Even more importantly, it’s not easy for a human to determine the correct order either, and so it would make maintenance more painful than it needed to be.

In reality, even for large projects, not having folders is not as much of a problem as you might think. There are a number of large F# projects which successfully work within this limitation, such as the F# compiler itself. See the post on cycles and modularity in the wild for more.

Help, I have mutual dependencies between my types

If you are coming from an OO design, you might run into mutual dependencies between types, such as this example, which won’t compile:

  1. type Location = {name: string; workers: Employee list}
  2. type Employee = {name: string; worksAt: Location}

How can you fix this to make the F# compiler happy?

It’s not that hard, but it does requires some more explanation, so I have devoted another whole post to dealing with cyclic dependencies.

Example code

Let’s revisit at the code we have so far, but this time organized into modules.

Each module below would typically become a separate file.

Be aware that this is still a skeleton. Some of the modules are missing, and some of the modules are almost empty.

This kind of organization would be overkill for a small project, but there will be lots more code to come!

  1. /// ===========================================
  2. /// Common types and functions shared across multiple projects
  3. /// ===========================================
  4. module CommonLibrary =
  5. // the two-track type
  6. type Result<'TSuccess,'TFailure> =
  7. | Success of 'TSuccess
  8. | Failure of 'TFailure
  9. // convert a single value into a two-track result
  10. let succeed x =
  11. Success x
  12. // convert a single value into a two-track result
  13. let fail x =
  14. Failure x
  15. // appy either a success function or failure function
  16. let either successFunc failureFunc twoTrackInput =
  17. match twoTrackInput with
  18. | Success s -> successFunc s
  19. | Failure f -> failureFunc f
  20. // convert a switch function into a two-track function
  21. let bind f =
  22. either f fail
  23. // pipe a two-track value into a switch function
  24. let (>>=) x f =
  25. bind f x
  26. // compose two switches into another switch
  27. let (>=>) s1 s2 =
  28. s1 >> bind s2
  29. // convert a one-track function into a switch
  30. let switch f =
  31. f >> succeed
  32. // convert a one-track function into a two-track function
  33. let map f =
  34. either (f >> succeed) fail
  35. // convert a dead-end function into a one-track function
  36. let tee f x =
  37. f x; x
  38. // convert a one-track function into a switch with exception handling
  39. let tryCatch f exnHandler x =
  40. try
  41. f x |> succeed
  42. with
  43. | ex -> exnHandler ex |> fail
  44. // convert two one-track functions into a two-track function
  45. let doubleMap successFunc failureFunc =
  46. either (successFunc >> succeed) (failureFunc >> fail)
  47. // add two switches in parallel
  48. let plus addSuccess addFailure switch1 switch2 x =
  49. match (switch1 x),(switch2 x) with
  50. | Success s1,Success s2 -> Success (addSuccess s1 s2)
  51. | Failure f1,Success _ -> Failure f1
  52. | Success _ ,Failure f2 -> Failure f2
  53. | Failure f1,Failure f2 -> Failure (addFailure f1 f2)
  54. /// ===========================================
  55. /// Global types for this project
  56. /// ===========================================
  57. module DomainTypes =
  58. open CommonLibrary
  59. /// The DTO for the request
  60. type Request = {name:string; email:string}
  61. // Many more types coming soon!
  62. /// ===========================================
  63. /// Logging functions
  64. /// ===========================================
  65. module Logger =
  66. open CommonLibrary
  67. open DomainTypes
  68. let log twoTrackInput =
  69. let success x = printfn "DEBUG. Success so far: %A" x; x
  70. let failure x = printfn "ERROR. %A" x; x
  71. doubleMap success failure twoTrackInput
  72. /// ===========================================
  73. /// Validation functions
  74. /// ===========================================
  75. module Validation =
  76. open CommonLibrary
  77. open DomainTypes
  78. let validate1 input =
  79. if input.name = "" then Failure "Name must not be blank"
  80. else Success input
  81. let validate2 input =
  82. if input.name.Length > 50 then Failure "Name must not be longer than 50 chars"
  83. else Success input
  84. let validate3 input =
  85. if input.email = "" then Failure "Email must not be blank"
  86. else Success input
  87. // create a "plus" function for validation functions
  88. let (&&&) v1 v2 =
  89. let addSuccess r1 r2 = r1 // return first
  90. let addFailure s1 s2 = s1 + "; " + s2 // concat
  91. plus addSuccess addFailure v1 v2
  92. let combinedValidation =
  93. validate1
  94. &&& validate2
  95. &&& validate3
  96. let canonicalizeEmail input =
  97. { input with email = input.email.Trim().ToLower() }
  98. /// ===========================================
  99. /// Database functions
  100. /// ===========================================
  101. module CustomerRepository =
  102. open CommonLibrary
  103. open DomainTypes
  104. let updateDatabase input =
  105. () // dummy dead-end function for now
  106. // new function to handle exceptions
  107. let updateDatebaseStep =
  108. tryCatch (tee updateDatabase) (fun ex -> ex.Message)
  109. /// ===========================================
  110. /// All the use cases or services in one place
  111. /// ===========================================
  112. module UseCases =
  113. open CommonLibrary
  114. open DomainTypes
  115. let handleUpdateRequest =
  116. Validation.combinedValidation
  117. >> map Validation.canonicalizeEmail
  118. >> bind CustomerRepository.updateDatebaseStep
  119. >> Logger.log

Summary

In this post, we looked at organizing code into modules. In the next post in this series, we’ll finally start doing some real coding!

Meanwhile, you can read more on cyclic dependencies in the follow up posts: