What are the advantages of an import.ml?

I’ve seen a few OCaml libraries that use (eg dune-engine) or expect that one uses (eg accessor) an import.ml file, seemingly to collect together multiple opens and fix-up definitions. I’ve never used import.ml, mainly because these days I tend to like keeping things explicit, but I’m now wondering:

  • What are the advantages of structuring common imports this way? Are there any disadvantages?
  • Is this a standard project convention? If other people worked on my OCaml projects, would they be confused by the fact that all my imports are scattered in the files that import them?
  • Say I have a project named foo where the libraries are all named Foo_bar, Foo_baz, etc, but are in directories /bar, /baz etc. Should I be using an import.ml type thing with module Bar = Foo_bar;; module Baz = Foo_baz;; and so on to hide the discrepancy, or is this obfuscating?
  • When is it a good idea to open libraries into the import.ml wholesale, and when should I just make shorthand aliases? (Certainly Base would be opened, and maybe some of the Accessor definitions, but otherwise… hmm.)
  • Should these sorts of files always be open Import'd, or should I be setting the compiler to open them implicitly (and how would one do that in Dune)?
1 Like

We do have something similar that is used to replace some unsafe functions, extend stdlib modules, add modules as if they were in the stdlib, … We open this module automatically in the whole project using the -open option of the ocaml compiler.

(* example of what we have in our import.ml like file *)

let (==) = `You_probably_want_to_use_non_physical_compare_instead
let (!=) = `You_probably_want_to_use_non_physical_compare_instead

module ListOverrides = struct
  let hd = `Pattern_matching_is_the_way
  let tl = `Pattern_matching_is_the_way
end

module List = struct
  include List
  include ListOverrides
end
-open Module
    Opens the given module before processing the interface or implementation files. If several -open options are given, they are processed in order, just as if the statements open! Module1;; ... open! ModuleN;; were added at the top of each file. 

https://caml.inria.fr/pub/docs/manual-ocaml/native.html#s:native-options

I just open Printf.
All other things are aliased, or opened very locally using something like

BatList.(
 ... 1 to 3 lines of code using BatList heavily
)
1 Like

Once, I worked on some code at INRIA, even open Printf was not allowed.
All modules were explicitly named or short aliased.

Did they allow local opens?

If you find yourself repeating the same open or aliases everywhere, I think that’s when you should introduce an import.ml factor these things into a single place. This helps to keep all these definitions uniform across the code base.

  • What are the advantages of structuring common imports this way? Are there any disadvantages?

Advantage: less boilerplate
Disadvantage: slower recompilation in some cases. possibly bigger binaries

  • Is this a standard project convention? If other people worked on my OCaml projects, would they be confused by the fact that all my imports are scattered in the files that import them?

I’ve learned this convention from Jeremie Dimino. I’m guessing that he either introduced this convention at Jane Street or picked it up there. So it’s definitely not standard.

  • Say I have a project named foo where the libraries are all named Foo_bar , Foo_baz , etc, but are in directories /bar , /baz etc. Should I be using an import.ml type thing with module Bar = Foo_bar;; module Baz = Foo_baz;; and so on to hide the discrepancy, or is this obfuscating?

That seems excessive. However, if you’re using only a few modules from a particular library, it makes sense to alias them in import.ml.

  • When is it a good idea to open libraries into the import.ml wholesale, and when should I just make shorthand aliases? (Certainly Base would be opened, and maybe some of the Accessor definitions, but otherwise… hmm.)

I wouldn’t mind keeping everything in import by doing include Base. However, I’d make sure this doesn’t affect compilation/binary sizes as base is pretty big.

  • Should these sorts of files always be open Import 'd, or should I be setting the compiler to open them implicitly (and how would one do that in Dune)?

You could pass the -open Import flag in theory. In practice, there’s no way to skip this flag for the import module itself. In any case, I can’t stand -open so I wouldn’t use this either way. I don’t mind writing open Import as it’s usually the only pre-amble I need to write.

2 Likes

Yeah, I realised this after I’d mentioned that (I think I had a brain-fart and thought opening things in modules would make them publicly available, oops!).

Generally at the moment, while I’m playing around with accessor, I’ve got something like

(*
 * import.ml
 *)

(* JS accessor libraries do this, and it seems like a decent trade-off
   between implicitness and convenience *)
module Accessor = Accessor_base
include Accessor.O

(* if I've got quickcheck generating code, this appears;
   I'm a bit sceptical of open-ing Base_quickcheck in the global scope.
   I think I'll eventually make the alias a bit more self-documenting
   than Q, though! *)
module Q = Base_quickcheck

(* These are, indeed, arguably quite excessive; the main rationale I have
   is that where they appear, they tend to stutter and obfuscate the code.
   NB: this import.ml would come from the Foo package, and only the Foo
   libraries used by this library would appear here. *)
module Bar = Foo_bar
module Baz = Foo_baz
(* etc *)

(*
 * client code
 *)
open Base
open Import

I think I’ll continue not to use -open; the stanzas open Base and open Import feel like they’re a much better signifier that the global namespace is altered from the usual OCaml expectations.