Advanced Merlin Features: Destruct and Construct

by Ulysse Gérard on Dec 21st, 2022

Merlin is one of the most important tools for OCaml users, but a lot of its advanced feature often remain unknown. For OCaml newcomers who might not know, Merlin is the server software that provides intelligence to code editors when working on OCaml documents. It allows one to easily navigate the code, get meaningful information (like type information), and perform code generation and refactoring tasks. Merlin installation and usage is documented on its official webpage.

Merlin is distributed with both an Emacs and a Vim plugin. It can also be used in Vscode via the OCaml LSP Server and the corresponding plugin.

In this post, we will focus on two complementary features of Merlin: the venerable destruct and the younger construct. Both of these leverage OCaml's precise type information to destruct or create expressions.

Destruct

Destruct (sometimes called case-analysis) uses the type of an identifier to perform multiple tasks related to pattern-matching. It can be called with the following key bindings:

  • Emacs: C-d or M-x merlin-destruct
  • Vim: :MerlinDestruct
  • VSCode: Alt-d or 💡 Destruct

Destruct's behavior changes slightly depending on the context around the cursor. We are going to describe how it behaves in the next three sections.

Automatic Case Analysis

The primary use case for Destruct is to generate a pattern-matching for a given value. Let's consider the following snippet:

let f (x : int option) = x 

Calling destruct on the right-most occurrence of x will automatically generate the following pattern-matching with the two constructors of x's' option type:

let f (x : int option) = match x with
  | None -> _
  | Some _ -> _

What happened is that Merlin looked at the type of x and generated a complete pattern-matching by enumerating its constructors.

Notice that Merlin used underscores on the right-handsides of the matching. We call these underscores typed holes. These holes are rejected by the compiler, but Merlin will provide type information for them. These holes should not be confused with the wildcard pattern appearing on the left handside Some _.

After calling destruct, the cursor should have jumped to the first hole. In Emacs (resp. Vim), you can navigate between holes by using the commands M-x merlin-next-hole (resp. :MerlinNextHole) and M-x merlin-previous-hole (resp. :MerlinPreviousHole). In VSCode, you can use Alt-y to jump to the next typed hole.

Complete a Matching

Merlin can also add missing branches to an incomplete matching. Given the following snippet:

let f (x : int option) = match x with
  | None -> _

Calling destruct with the cursor on None will make the pattern-matching exhaustive:

let f (x : int option) = match x with
  | None -> _
  | Some _ -> _

Refine the Cases

Finally, Merlin can be used to make a pattern-matching more precise when called on a wildcard pattern _. Given the following snippet:

let f (x : int option opton) = match x with
  | None -> _
  | Some _ -> _

Calling destruct with the cursor on the _ pattern in Some _ will refine the matching:

let f (x : int option option) = match x with
  | None -> _
  | Some (None) | Some (Some _) -> _

Note that Destruct also works with other types, like records. Let's consider the following snippet:

type t = { a : string option }
let f (x : t) = x

Calling destruct on the last occurrence of x will yield:

let f (x : t) = match x with 
  | { a } -> _

And we can refine it by calling destruct again on a, etc.

let f (x : t) = match x with 
  | { a = None } | { a = Some _ } -> _

That wraps our presentation for destruct. Generating and completing pattern- matching cases can be very useful when working with large sum types !

Construct

Construct can be considered as the dual of Destruct, as they work complementarily. When called over a typed-hole _, Construct will suggest values that can fill that hole. It can be called with the following key bindings:

  • Emacs: M-x merlin-construct
  • Vim: :MerlinConstruct
  • VSCode: Alt-c of 💡 Construct an expression (the cursor must be right after the _)

For example, given the following snippet:

let x : int option = _

Calling construct with the cursor on the _ typed hole will suggest the following constructions:

Some _
None

Choosing the first one will replace the hole and place the cursor on the next hole:

let x : int option = (Some _)

Calling construct again will suggest 0 and result in:

let x : int option = (Some 0)

In the future, Construct might also suggest fitting values from the local environment instead of solely rely on a type's constructors.

Destruct and Construct

As stated previously calls to destruct and construct can be used in collaboration. For example, after calling destruct on x in the following code snippet:

type t = { a : unit; b : string option }
let f (x : int option) : t option = x

x is replaced by a matching on x with the cursor on the first hole:

let f (x : int option) : t option = 
    match w with
    | None -> _
    | Some _ -> _

One can immediately call construct and choose a construction for the first branch:

let f (x : int option) : t option = 
    match w with
    | None -> None
    | Some _ -> _

And again for the second branch:

let f (x : int option) : t option = 
    match w with
    | None -> None
    | Some _ -> Some _

Finally, like Destruct, Construct also works with records and most OCaml types:

Some _ → Some { a = _; b = _ } → Some { a = (); b = None }

Conclusion

When put to good use, these complementary features can remove some of the burden of working with big variant types. We encourage you to try them and see if they help your everyday workflow! If you encounter any issues or have ideas for improvement, please communicate them to us via the issue tracker.