Long-winded post coming (and I’ll be repeating some of the arguments already made by others). TL;DR version: what you propose is not easy to implement, and even if we implemented it binding your functor applications would always be better.
Most functor applications create types. For applicative functors (most of them), this isn’t that much of a problem as Set.Make(String).t
is a valid type that is common to all applications of the Set.Make
functor to the String
module. For generative functors this is much more problematic. If we had defined Set.Make
as a generative functor, then the following code would fail to typecheck:
let empty = Set.Make(String)().empty
This is because the type of empty
would only be valid in the scope of the functor application, and by not binding the functor application you don’t have a scope associated to it.
Not having a good place to bind types is also a problem for applicative functors, but slightly less so as it’s more likely that the types created can stay valid in a larger scope.
Regarding compile-time evaluation of functor applications: functors are just regular functions under the hood. There is no way to know whether two successive applications of the same functor to the same argument are supposed to produce the same result, without deep inspection of the functor’s code.
For applicative functors, you do have the guarantee that two applications of the same functor to the same argument produce the same types, but that doesn’t mean that the values are the same.
To get around this, Flambda asks the front-end to flag function definitions that correspond to functors, and will try to inline those functions more aggressively than usual. In most cases, this will end up looking like compile-time evaluation, but with your approach of using applications at each use of the functor you’re making the compiler do the same work a lot of times, while binding the functor application to a name as early as possible means the work is only done once. Some work on purity annotations could improve the situation, but this is still not completely trivial (typically, if you have two different functions in your program that call the same pure function with the same arguments, it’s not always obvious that you can share the result as you would have to move the call to a scope that is visible from both use sites. This could result in unnecessary extra computations if the two functions are called at most once each and never both of them).
And I’m not fond of comparisons with generics. In a way, the generic Set<String>
corresponds in OCaml to string set
(for some parametric type set
). The reason why OCaml uses functors for sets and not regular parametric types is that a set is parametrized by its comparison function. In the generics variant, this requires that you can find the canonical comparison function for each type. With functors, you can have two different set types with elements of the same type, but with different comparison functions.
If you want to implement something that looks like generic sets in pure OCaml, here is one possibility (I’m choosing a list-like implementation for simplicity, but this works equally well with other data structures):
type 'a set = Empty | Cons of 'a * 'a set constraint 'a = < compare : 'a -> int; ..>
let empty = Empty
let rec mem x = function
| Empty -> false
| Cons (y, l) ->
let c = x#compare y in
if c = 0 then true
else if c > 0 then mem x l
else (* c < 0 *) false
let rec add x = function
| Empty -> Cons (x, empty)
| Cons (y, l) as l0 ->
let c = x#compare y in
if c = 0 then l
else if c > 0 then Cons (y, add x l)
else (* c < 0 *) Cons (x, l0)
(* Other functions here *)
The main drawback is that set elements must have a comparison function canonically associated to them, so it will only work with objects (or maybe with modular implicits in the future). But if you want to instantiate it with strings, all you have to do is to wrap your strings in objects:
let wrap str =
object method s = str method compare other = String.compare str other#s end
let _ = Set.mem (wrap "Hello") (Set.add (wrap "World") Empty)
This is basically how generics work in object-oriented languages. In OCaml it’s very inefficient, because little work has been done on optimising objects (and it would actually be very hard to do given the other constraints of the language), so the functor approach is recommended instead. Also, the functor approach guarantees that all sets of the same type agree on the comparison function, while it’s not enforceable in the object-oriented world.
Regarding simplified paths, currently compilation units cannot be compiled as functors, so whenever you need to use a functor you need to either have it bound locally (you can do module Set = Set.Make
if you want), or access it through an indirection. There is some ongoing work on lifting the restriction and allowing compilation units that are functors, but in most cases it’s better to pack together the functor itself and the various interfaces it uses (for the arguments and result) so I don’t expect this feature to be used for shortening functor paths.
Finally, a few thing I’ve seen to deal with the problem:
- Make a module, in your project, that contains all the functor applications you often need, along with short aliases for commonly used modules if you need to. You can then make this the only opened module in each of your files, giving good readability without making it too painful to resolve what a short alias refers to.
- When you define a module around a specific type, pre-instantiate commonly used functors. You would then use
Mytype.Set
in subsequent files, instead of Set.Make(MyType)
. You can even do this for standard library modules if you want:
(* myString.ml *)
include String
module Set = Set.Make(String)
module Map = Map.Make(String)
Combined together, these two tricks can likely remove most of the functor applications in your code, without making it too hard to understand.