Publishing practice: add an optional argument to an existing library function

This is a half-technical, half-community question.

Say you want to add a feature to an existing function, already published in the wild world (opam). Of course, you can just provide new functions, but that means multiplying their number each time you add an orthogonal option (or you only provide some combinations, and then your user wonders why some others are absent). In many cases, using optional arguments is more natural and more convenient. However, that implies changing the type of the existing function, hence my question: from the user perspective, how well-accepted adding an optional argument to an existing function is?

I don’t think it breaks anything at the source level, except rare scenarios where someone reuses the signature of your module to reimplement a drop-in replacement of your function—and then breaking is expected, and adding a new function would break just as well.

It does change the binary interface of the function, but, I don’t have a clear picture of this: does it happen that people plug in a new version of a library without recompiling their client code?

Also, it definitely breaks C code calling the OCaml function. I believe that this in infrequent, and that signaling the breaking change with the new release would be enough.

I have done that more than once, nobody directly complained. But it remains that any breaking change is always annoying. You can however give yourself a bit of peace of mind by assessing breakage yourself:

opam pin add . 
opam install $(opam list -s --depends-on $YOURPACKAGE)

It’s also nice if you file issues in breaking packages about the upcoming changes and, if you have the time, it’s extra nice if you PR fixes for the breakage.

4 Likes

@dbuenzli captures my opinion exactly, but just to add that adding an optional argument to a function can indeed break code at the source level, even without considering shenanigans such as module type of. Uses of functions as values can be sensitive to optional arguments, for instance with pretty-printers:

# module Lib = struct
    let pp ppf = Format.fprintf ppf "{ hello world }"
  end

  (* Initial user code *)
  let () = Format.printf "%t@." Lib.pp ;;

{ hello world }

# (* Next version of [Lib] adds an optional argument *)
  module Lib = struct
    let pp ?(bracket = `Curly) ppf =
      match bracket with
      | `Curly -> Format.fprintf ppf "{ hello world }"
      | `Square -> Format.fprintf ppf "[ hello world ]"
  end

  (* Same user code *)
  let () = Format.printf "%t@." Lib.pp ;;
 
Line 1, characters 28-30:
Error: This expression has type ?kind:[ `Curly | `Square ] -> Format.formatter -> unit
       but an expression was expected of type Format.formatter -> unit

In most cases, adding an optional argument is generally considered a “small” breaking change, but this ultimately depends on the context of how that function is intended to be consumed.

6 Likes

Thank you both. I learnt something with opam list --depends-on. :slight_smile: Well spotted @CraigFe, I oversaw this usage indeed.