Duplicated definition of signature

I have two files (a.ml , a.mli)

a.mli defines signatrues for a structure ABC and DEF
a.ml actaully is written with structure body of ABC and DEF.

a.mli
module type ABC = sig ... end
module type DEF = sig ... end

a.ml
module Abc (S : DEF) : ABC = struct ... end

However, this code is not compiled with unbound module DEF. so, it forces to copy of DEF signature into a.ml.

If I need some additional function in DEF, I should also update ml and mli.Tthis is duplicated work.

How can I improve it?

Thank you.

1 Like

I think you can use ppx_import to do this. You define in a.mli, and in a.ml you [%import ...] the module type. When I saw that, I thought: “clever, heh”.

1 Like

What is the best introduction document to ppx?
Sometimes, I feel I should use them, but some other times they remind me of camlp4/camlp5 and I prefer to not put my hands anywhere near those dangerous gears.

It seems to be a feature of library, not the ocaml-builtin.

In my project currently, other coworkers do not use ppx_imprt. it means that I cannot use it for now.

Thank you!

Well,
(1) there’s -using- PPX rewriters, and there’s -implementing- PPX rewriters.
(2) to -use- 'em, well, that’s pretty easy, and you can follow the instructions for 'em on their homepages. It’s pretty straightforward, to the point where truly, there’s not much need for an introduction to them generally. I mean, it’s as simple as just adding “-package ppx_import” to your compile lines, and

type t = [%import ...argle bargle gargle ...]

in your code. [I’m not writing something real b/c seriously, the ppx_import docs are not bad, and the testsuite is comprehensive and didactic.

Also, the -syntax- of PPX extensions/attributes is -fixed- in the camlp5 grammar, so in that sense it’s not like camlp4/5.

One option is to not have the a.mli file at all because you don’t need it. I assume you actually want to make all three public:

  • ABC
  • DEF
  • Abc

In that case you just need the implementation file a.ml, you don’t need the redundant interface file a.mli. The implementation already exposes exactly what you want to expose.

Maybe, but .mli files are also useful to accelerate compilation.

I might have accidentally thrown away the PPX baby with the water of his bath. :slight_smile:

Another non-PPX solution is to have a file a_intf.ml:

module type ABC = sig ... end
module type DEF = sig ... end

module type A = sig
  module type ABC = ABC
  module type DEF = DEF

  module Abc (S : DEF) : ABC
end

Then in a.mli you can just write include A_intf.A, and in a.ml you can include A.

Or I suppose you could do the inverse and have a signature file with a different name than a.mli:

signature.mli:

module type ABC = sig ... end
module type DEF = sig ... end

a.ml:

module Abc (S : Signature.DEF) : Signature.ABC = struct ... end

And that would suggest that a signature file is unnecessary - a.ml could include the signatures and all should be well.

Unless the compiler has special support for this, I don’t think you can have an mli file with no corresponding ml file. Put differently, in a phrase like (S : Signature.DEF), “Signature” refers to a module, but the signature.mli file only gives a module type. Edit: I am wrong about this.

So long as a.ml remains exactly as it is in this example that’s true, but the cost of having no explicit interface is that you can’t add anything at the top level of the module without exposing it.

Besides what matters to the compiler, there is also something to be said for having a reference where one can read the interface of a module without having to sift out the implementation details.

That may be right but this seems to work for me:

signature.mli:

module type ABC = sig
  type t
end
module type DEF = sig
  type t
end

impl.ml:

module Abc (S : Signature.DEF) : (Signature.ABC with type t = S.t) = struct
  type t = S.t
end

test.ml:

module M = Impl.Abc(struct type t = int end)

Makefile:

all: test

signature.cmi: signature.mli
	ocamlc -c signature.mli

impl.cmo: impl.ml signature.cmi
	ocamlc -c impl.ml

test: test.ml impl.cmo
	ocamlc -o test impl.cmo test.ml

Yes that is true.

Well in these kinds of situations the main goal is to define a functor, so all or most interesting stuff should go inside the input and output signatures and the body of the functor anyway. The rest of the file itself should be empty.

Huh, shows what I know!

You can, the compiler supports it, and it’s supported by most build systems (ocamlbuild/oasis/dune/…)

I think it is misunderstood how … useful macro-extensions can be. Perhaps the difficulty and opacity of writing PPX extensions is partially at fault. Certainly in writing any sort of AST-processing software, the opportunities for making code more succinct, maintainable, and -understandable- by using macros, are everywhere. I think a sign that PPX is healthy will be if many people write small one-off PPX rewriters to solve small problems in their packages, sort of like how many scheme programmers have small collections of macros they cooked-up to solve specific problems that bothered them.

This is interesting: dune does indeed appear to support this with the ‘modules_without_implementation’ field. I have not done it before, but the result seems to be that you can have two interface files, one for ‘DRY’ purposes and one for ‘limit the interface’ purposes. This seems to work:

dune:

(executable
  (modules_without_implementation a_dry)
  (name test))

a_dry.mli:

module type ABC = sig
  type t
end
module type DEF = sig
  type t
end

a.mli:

module Abc : functor (S : A_dry.DEF) -> sig type t = S.t end

a.ml:

module Abc (S : A_dry.DEF) : (A_dry.ABC with type t = S.t) = struct
  type t = S.t
end

test.ml:

module M = A.Abc(struct type t = int end)

I am less certain that this is worth the hassle though, and whether I would want to do it in real life. Putting the signatures of ABC and DEF in a.ml and repeating them in a.mli doesn’t seem that bad.