Unless i’m mistaken, ppx_import allows you to do that.
The syntax is:
type t = [%import: Test_self_import.t]
module type S = [%import: (module Test_self_import.S)]
(per its tests)
Unless i’m mistaken, ppx_import allows you to do that.
The syntax is:
type t = [%import: Test_self_import.t]
module type S = [%import: (module Test_self_import.S)]
(per its tests)
I’ve started using more and more .ml-only modules, with the following
patterns:
(* as usual *)
module type FOO = sig … end
(** The docs go here *)
let some_public_fun (x:a) (y:b) : c = …
open struct
module Foo = Imported_foo
exception Oops
(** Doc here too for LSP hover *)
let private_fun x y = …
end
let other_public_fun x y : unit =
try private_fun x y
with Oops -> false
cuts down on repetition for modules that are either:
open struct … endI like this. Do you still tend to use the starting-with-underscore convention for private functions? I really like to see which functions are public and which are private without requiring an .mli file (and I tend to avoid mli files anyway), but this method is more structured, like public and private sections of a class.
I think I still use _ in places, but the downside is that the compiler
will not warn you about dead code. I sometimes use foo_ as the “this
is an implementation detail but please warn me”.
That’s a cave-man version of a private keyword !!
I think some the issues with mli could be addressed by better editor support. If your editor noticed when a type was different between the ml and the mli and you had a button to chose which version to promote, this would already be nicer than whatever we have now.
Also, the editor could annotate the ml file to give the information of what is public and what is private. That would save some time when reading code.
my real pain point is OCaml’s value restriction as described here OCaml - Polymorphism and its limitations where OCaml assign weak type in function wrapper that use GADT.
Consider this:
type (_, _) path =
| Nil : ('a, 'a) path
| Custom :
{ parse : string -> 'param option
; format : 'param -> string
; rest : ('a, 'b) path
}
-> ('param -> 'a, 'b) path
(** more constructor, not shown here *)
If this GADT constructor isn’t exported, and instead I allow user to use this function instead:
let custom : type a.
parse:(string -> 'param option)
-> format:('param -> string)
-> ('param -> a, a) path
=
fun ~parse ~format -> Custom { parse; format; rest = Nil }
(* user code, OCaml assign weak type here, it evaluate to (string -> _weak1, _weak1) path instead of (string -> a, a) path *)
let slug = custom ~parse:parse_slug ~format:Fun.id
The only solution I found is to encourage user to wrap it in function
let slug () = custom ~parse:parse_slug ~format:Fun.id
Which is strange. ![]()
Is there a similar trick to remember the order of arguments for functions taking both a container and an item (Set.add, Set.mem, Hashtbl.add…?) Usually, but not consistently, the container goes at the end if it’s persistant (so one can use the |> operator, I guess).
I decided long ago that I would not pay the mli tax, ever. And I ended up doing the same as what @c-cube does: declare interface signature in the .ml, use trailing underscore as “private”, etc… Until recently, when I went full circle and started to write mli again, to write specifications for AI waiting for them to flesh out the ml, turning OCaml into some sort of higher level language. So I guess, everything comes to him who wait ![]()
The effort to keep ML and MLI files in sync comes up a lot. I personally don’t feel that pain but I was intrigued by a comment by @raphael-proust
Does this not suggest that a type declaration in an ML file that is abstract (i.e., no right hand side) could take the meaning of the declaration in the MLI file where it is provided? Such a language extension would at least be syntactically conservative since all existing code has to provide a full declaration in an ML file.
Language and compiler developers: what are the barriers?
There’s no warning if the slug variable is inside a function body, there’s only a warning if it’s at the toplevel, which makes sense because if it’s bound at the toplevel then multiple later bindings have access to it and can potentially change its type later.
yes, that caught me off guard, this is work if I use the GADT constructor directly – ie no need to wrap it in a function. But I won’t export the GADT constructor to public API of course.
There’s related discussion here (and surrounding): Has there been a syntax proposed for combining .mli into .ml? - #25 by shonfeder , in case you are interested!
My main pain point is that I really do not like using dune but am also not aware of a good alternative. The doc is unstructured and incomplete, I find the s-exps hard to read and it is also very hard to remember the correct forms, it is very difficult to do anything “by hand” on top of what dune does due to how much it mangles names, moves everything around, hides everything etc., there is a lot of bureaucracy (like how even the most basic project needs an empty dune project file and an empty opam file), a lot of default behaviours are annoying (warn-error on unused variables in dev mode??), and so on.
I feel the opposite. OCaml went through a lot of build systems before settling on dune and they all were slower and it was more difficult to capture dependencies correctly. Dune serves the common case well. In particular, not every additional source file requires updating dune files but there is an organic hierarchy of libraries and directories. It is also easy to vendor libraries that are themselves managed with dune. Dune is opinionated - so I would not be surprised if it’s difficult to organise a project in a way where libraries and directories don’t align - but I just avoid that. I don’t care how dune organises its _build/ hierarchy and treat it as transient.
Just a small point, dune can automatically generate that, so there’s no need to manage it manually.
So you can do stuff like this?
Failure “something” |> raise
Another thing to strongly “not recommend” in a style guide, I guess.
I get this and didn’t say dune was worse than not having anything. The dependency management aspect is great and that’s why I use it. But its inflexibility serves my uses very poorly. I’m not a professional programmer, I’m an academic, and I often do very small projects for personal utilities or for research purposes that may start as a single-file script, later a proper executable, then later a split between a library and an executable, which I want to be calling without installing and perhaps in exotic ways, reading some files or interfacing with a python script or whatever, and dune does not support this well at all with the way it enforces directory structures and naming conventions and forces you to go through it to call the code you wrote.
But then even with more organized projects, I just don’t like the time I spend every time rediscovering how you do X or Y, and having to experiment to understand what some option means, and the gazillion mostly empty files you need to maintain. Certainly in part just that I don’t use it often enough since it’s not the main part of my job, but I don’t really have this problem for other tools.
Fair enough. There is an initial cost to get off the ground with dune. Dune has evolved to help with the initial setup by providing dune init.
dune init {project, library, executable} for thatDune was designed to speed up using OCaml in education for students who are not familiar with programming, building, etc. It’s certainly good at that.
If you build software as a profession, you should invest in a proper, generic build system; one that will no doubt require more work for the simple cases but will also allow you to build anything from anything.
In the project I’m currently working on, I have rules to build native executables from OCaml and C, wasm from OCaml and JS, OCaml source from configuration files, PNG from SVG, CSV from various JSON using custom OCaml tools, SQL from JSON and CSV, and that’s just on the top of my head. I rarely work on a project where the build is much simpler than that; sometimes it’s actually way more convoluted with lots of generated code and data.
I’m been using some variant of makefiles for more than 3 decades at that point and never felt the need for anything different. If you plan to spend some time on a unix environment in the forcoming years, I would recommand you invest in learning such a tool.