Coding Guidelines for FPSE

Be explicit. Don’t make the reader solve a puzzle; tell them what is happening by being explicit. Implicit behavior requires lots of hard thinking to understand, whereas explicit behavior can be read straight off the page. This guide here is all about how to be explicit in your code so that you are clear in your intent and so that others can review, understand, and extend your code easily and correctly.

General guidelines

Functional first

  • Functional code makes effects and data flow explicit because meaningful outcomes are expressed in the return type (referential transparency).
    • Imperative code, on the other hand, has side effects and state that you must know about and keep in mind; the returned value, if any, is not the only outcome of calling the function.
  • Keep the scope small and prefer functional solutions, but remember: this is “functional first”, not “functional only”. Use imperative style where it is the least cumbersome.

    For example, let’s revisit the parenthesis matching/(zipfile) example from the previous lecture on side effects – at the end of this file is a purely functional version of paren matching.

    For a standard example of when state is likely better,

    let counter = ref 0
    let fresh_name () : string =
      counter := !counter + 1;
      "name" ^ Int.to_string !counter
    

    is a function which fully encapsulates the property of “give me a fresh string every time I am called”. Without state,

    let fresh_name (previous : int) : string * int =
      ("name" ^ Int.to_string previous, previous + 1)
    let name1, count1 = fresh_name 0
    let name1, count2 = fresh_name count1
    

    it exposes unnecessary details. (Note a state monad is another option, which we will cover in a future lecture.)

Encapsulate with modules

  • Your code should be grouped into meaningful, coherent modules. Those modules should have a single purpose, and that purpose is often to contain a single type and several functions that work mainly on that type.
  • Writing code in this way separates responsibilities and creates appropriate abstraction boundaries.
  • Then, document with interface files. Interfaces are a great place to comment and to think critically about the purpose of your module.

    Assignment 3, for example, reflects this with separate tree and dictionary modules.

Hide representations

  • Modules should expose operations but not representations. Hide implementation details behind interfaces whenever practical so that users depend on the semantics of the module, not its implementation. This prevents misuse and enhances maintainability.

    The Histogram module you implemented in Assignment 4, for example, hides the internal keyword map since users of the Histogram don’t need to see it.

  • However, hiding is not always the best choice. For example, if you are writing getters for every field of a record type, you should instead just expose that type.

Know your standard library

  • It is not too big (and that is a good thing), so you should know what it provides. It will solve many simple problems for you.
  • Never reimplement anything that is in the standard library, and always reach for the standard library when it expresses your intent.
  • If you find that your code is just a library function, then use the library function. If you find that your code is essentially a composition of two library functions, then use those two libraries functions. That said…

Think structurally

  • Do use combinators when they help make readable code, but
  • Don’t make combinators an end in and of themselves
    • Using too many combinators clouds the structure of the problem and leads to indirect solutions.
    • Instead, focus on how you can best solve the problem with OCaml, which could be with many, few, or no combinators.

    For example, List.map (Fun.flip List.cons ls) xs is over-using combinators. List.map (fun x -> x :: ls) xs is simpler.

  • See iterators in the official OCaml Guidelines for an extra terrible example.

Don’t be too clever

  • The most concise solution is not always the best one. Readability is about more than just concision.
  • Write the shorter code if doing so does not drastically increase complexity.
  • Point-free programming (i.e. living in the higher-order function world too much and not with actual data, e.g. especially excessive function composition and partial function application) is often clever but unreadable. Be explicit and simple without being overly verbose.

    For example, consider the straightforward

    let sum_squares xs =
      let squares = List.map (fun x -> x * x) xs in
      List.fold_left (+) 0 squares
    

    which is more straightforward than

    let sum_squares =
      Fun.compose (* excessive function composition here *)
        (List.fold_left (+) 0) (* a partial function application *)
        (List.map (fun x -> x * x)) (* another one.. so very clever! .. in a bad way *)
    

Use the weakest feature

  • Use the simplest feature that expresses your intent. More powerful features are valuable when they solve a problem that simpler features cannot, but they should not be used merely because they exist.
  • Every additional capability in the feature you use imposes a cost on the reader because they must be prepared for you to use it!
  • Search for the simplest type or feature that solves your problem, and then introduce mechanisms only when they buy you something.

    For example, do not use a list where a tuple suffices, or a first class module when you only need a higher order function.

Use the appropriate data structure

  • Think carefully about the structure of your problem and the data structure it requires. Much of the time, a standard variant or record type is the appropriate solution.
    • Don’t use integers-as-tags a-la-C, and use lists/maps only when that is exactly the form of data.
  • When an advanced data structure is required, such as a bitmap, there is probably a standard library module or an opam package for it, and these can be easy to drop into your code.

Libraries vs Frameworks

  • Frameworks are large dependencies that massively influence the structure of your program.
    • i.e. Frameworks are a box you are forced to live in, whereas with libraries you are in control
    • It can be very challenging to use more than one of them at a time because you have two parties wrestling for control.
    • Moral: adopt frameworks sparingly, and only when they are fundamental to your project.
  • A good library, on the other hand, is useful and not restrictive. It is easily pluggable and will not steer the direction of your code for you.

    An example framework is Lwt for concurrency. It will take over your code with monads, and you cannot always nest its use, so you may struggle to work with other libraries that use it themselves. Still, it is sometimes necessary to use.

    Some example libraries are the monotonic timing library Mtime, and the priority search queue data structure Psq. These define a few types and functions that are tightly contained and are unopinionated.

  • Source (Malcolm Matalka at FUN OCaml 2025)
  • In general, avoid depending on too many frameworks and libraries for single-use purposes because every dependency you have is one more that the reader of your code has to understand. Dependencies can be heavy. Make sure they are carrying their weight.

Abstract appropriately

  • Use abstraction to avoid duplication and to avoid hardcoding.
  • Abstraction should enhance readability by allowing the reader to focus on the fundamental problem the code at hand is solving.
  • A piece of code lacks abstraction when the main idea is buried under irrelevant detail or just when the same idea is repeated in several places.

    For example, consider this somewhat hard-to-read code:

    let passing_students students =
      List.filter (fun s ->
        let sum = List.fold_left (+) 0 s.grades in
        let count = List.length s.grades in
        count > 0 && float sum /. float count >= 60.0
      ) students
    

    Instead think about the algorithmic components and make named functions out of them:

    let average grades =
      float (List.fold_left (+) 0 grades) /. float (List.length grades)
    let is_passing student =
      student.grades <> [] && average student.grades >= 60.0
    let passing_students students =
      List.filter is_passing students
    
  • It should not hinder readability by being too implicit or clever, so choose the smallest abstraction that captures the common idea (See “Use the weakest feature”).

Parse, don’t validate

  • Use new types to parse your data into always-valid forms where illegal states are unrepresentable.
  • Avoid implicit invariants or frequent re-validation. Effective use of types can often uphold those invariants by themselves and will aid in readability.

    For example, parse a string into an abstract email type exactly once instead of checking at every use that it is a valid email string.

  • Source (Alexis King, Nov 5 2019)

Put big ideas first

  • Use expression structure to emphasize the main idea. The semantic center of an expression should draw the reader’s eyes.
  • Do use pipelines with |> when the sequence of transformations is itself the important idea.
    • But, don’t use pipelines when they are a poor fit: too much branching, shared intermediates, or one dominant operation.

    For example, piping might not be appropriate for the following code where the big idea is to count occurrences of each item in a list.

    items
    |> List.sort_uniq compare
    |> fun l ->
      List.map (fun x -> count (equal x) items) l
      |> List.combine l
    
  • A let-binding can separate data preparation from the important operation, and @@ can also help put that operation front and center. Small pipelines are often still useful for preparing arguments.

    The above overly-piped code can read more clearly with some let-bindings.

    let item_with_count item =
      (item, count (equal item) items)
    and uniq_items =
      List.sort_uniq compare items
    in
    List.map item_with_count uniq_items
    

Specific suggestions

DRY. Don’t Repeat Yourself. An oldie but goodie. Repeat yourself maybe once, but only if you did not have to copy/paste code to do so. Higher order functions (functions as arguments to other functions) make it especially easy to avoid repeating yourself.

Pattern match and destructure. Pattern matching and let-destructuring (e.g. let a, b = ... in ...) are some of OCaml’s clearest tools, and they express intent very explicitly. Use them liberally.

Do not write long anonymous functions. Anonymous functions should always be short. If one becomes long, then help your reader by naming it, thereby making it no longer anonymous.

For example, don’t write this.

  let update_prices products =
    List.map (fun p ->
      let discount =
        if p.quantity > 100 then 0.20 else 0.0
      in
      let price = p.price *. (1.0 -. discount) in
      { p with price = max 0.0 (price +. p.shipping) }
    ) products

Instead you want List.map update_price products where you have named the anonymous fun p .. above as update_price.

Options over exceptions. Exceptions for recoverable failures require the programmer to remember to catch them. Options force the programmer to handle them. Don’t leave anything up to chance, and favor options in your interfaces. Results allow you to express reasons for failure and can be a good alternative to options.

For example, to normalize a vector to a unit vector, you may prefer val normalize_opt : vector -> vector option instead of val normalize : vector -> vector because normalization is not total: it is not defined on the zero vector.

New types, not abbreviations. New types force structure and separation, while abbreviations may accidentally cross data because they do not create distinct types. New types also document themselves in interfaces.

For example, for a 2D integer point, prefer type t = { x : int ; y : int }, a new record type, instead of type t = int * int.

Prefer records over tuples. Tuples are useful for transiently packing data together, but they should not be used for meaningful, long-lived groupings of data. When defining types, choose to make a new record instead of abbreviating a tuple type, like the 2D point above.

Label function arguments of the same type. If any two arguments have the same type, you invite the caller to confuse them, or you require them to read a documenting comment. Instead, use labeled arguments to distinguish them.

For example, the standard library function String.split_first : sep:string -> string -> (string * string) option uses labels to distinguish its arguments, yet String.concat : string -> string list -> string does not because the types are different.

Avoid programming with indices. An index into a list is usually not the right idea. Using indices invites off-by-one errors that are not possible in a structural approach to the problem. Lean into pattern matching and forming your solution with structure rather than numbers because structure can be better visualized and verified.

For example, to get the element in a list before the first one satisfying p, this:

List.find_mapi (fun i x ->
  if p x then List.nth_opt xs (i - 1) else None
) xs

is less clear than:

let rec find = function
  | [] | [_] -> None
  | prev :: next :: _ when p next -> Some prev
  | _ :: tl -> find tl
in
find xs

to a seasoned functional programmer, especially because the first raises an exception in the singleton list case satisfying p!

Align delimiters. Matching delimiters (e.g. begin and end, or let and in) should appear on the same line or be aligned in the same column. This helps the reader find the end of an expression by scanning in only one direction (either to the right or down).

begin/end. Delimit program constructs with begin and end. They are especially preferable to parentheses around control flow constructs like match, if, and try. For example, in the following, begin match reads like one keyword, and it aligns better than any parentheses would.

match x with
| [] ->
  begin match y with
  | None -> ...
  | Some z -> ...
  end
| _ -> ...

Common module coding patterns

Name your types t. Most types should be named t. This naturally encourages each type to live in its own module, where the module name describes that type t. This keeps modules focused and small, and it separates responsibilities by type.

For example, the tree and dictionary types in Assignment 3 could both be in one module, but if you tried that, you would be violating this rule which is a bad sign! Simpledict.t and Simpletree.t was our solution.

module T/include T. When defining a type that will be passed to a functor, it is common practice to wrap the type and its operations in a module called T. Suppose you intend to implement this interface:

(* my_module.mli *)
type t = A of int
val compare : t -> t -> int
module Set : Set.S with type elt = t (* My_module.Set is then your set of t's *)

A good design to avoid redefinitions and aliasing in the .ml file is:

(* my_module.ml *)
module T = struct
  type t = A of int
  let compare (A x) (A y) = Int.compare x y
end
include T
module Set = Set.Make (T)

module type S. When a module type is implemented (i.e., returned) by a functor, it is typical to call that module type S. While not descriptive by itself, the enclosing module provides the context, just like types named t. For example, Set.S is the module type of sets, and Set.Make is a functor that produces modules with type Set.S. This helps to keep modules focused–the Set module in the standard library contains little more than S and Make.