Import Either (Either(..))

I occasionally wish for Left and Right to be available as a common language feature, but I certainly wouldn’t want to open Either and bring in map and who knows what else from that module.

Does some standard prelude do this for Option - importing Some and None without the rest? Is there a way to do this with Either?

I’m guessing “no”, because if there were such a feature, people would use it and there wouldn’t be such a bad smell around open.

Option does the opposite: option is in the core library, and Option.t is just an alias for option:

That code also shows what’s probably the best way to do what you’re asking for: define a type type ('l, 'r) either = ('l, 'r) Either.t = Left of 'l | Right of 'r. You could also do open (Either : sig type ('l, 'r) t = Left of 'l | Right of 'r end), but then Either.t is called t for the rest of the scope (or t/2, if you define a new type t afterwards), which is probably not what you want.

(I don’t think there’s a way to get Left and Right into scope unqualified without also having their type.)

2 Likes

It sure isn’t, if I got that right. The type t that comes out of this has a Left and Right, but isn’t recognized as the original Either.t, so it doesn’t work with List.partition_map for example. Where the first example does work.

One thing worth keeping in mind is that the compiler will use type inference to allow unqualified use of constructors. E.g., in a fresh top level we can write

# List.partition_map (fun x -> Left x) [1;2;3;4];;
- : int list * 'a list = ([1; 2; 3; 4], [])

Using the unqualified Left because the type of List.partition_map tells the compiler that the Left is coming from Either. An upshot is you can use a sprinkling of type annotations to avoid needing an open or qualified use. So

# let l : _ Either.t list = [Left 1; Right "Foo"];;
val l : (int, string) Either.t list = [Either.Left 1; Either.Right "Foo"]

I often use scoped open sugar too, and may well construct that list as

# Either.[Left 1; Right "foo"];;

This is all just to say that, IMO, existing language constructs make it possible to use constructors in a quite light weight way, with the benefits of qualified use but many times having it appear unqualified.

That all said, afaict, @liate’s suggestions for type aliases does indeed bring the type constructor in scope in the desired way:

# type ('a, 'b) either = ('a, 'b) Either.t = Left of 'a | Right of 'b;;
type ('a, 'b) either = ('a, 'b) Either.t = Left of 'a | Right of 'b
# Left 1 = Either.Left 1;;
- : bool = true

And if we add a type constraint we can use the sealed open too:

# open (Either : sig type ('a, 'b) t = Left of 'a | Right of 'b end with type ('a, 'b) t = ('a, 'b) Either.t);;
type ('a, 'b) t = ('a, 'b) Either.t = Left of 'a | Right of 'b
# Left 1 = Either.Left 1;;
- : bool = true

If we wanted this open regularly without so much ceremony, you could name the module type and then do the sealed open like this:

# module type EitherType = sig type ('a, 'b) t = Left of 'a | Right of 'b end with type ('a, 'b) t = ('a, 'b) Either.t;;
module type EitherType =
  sig type ('a, 'b) t = ('a, 'b) Either.t = Left of 'a | Right of 'b end
# open (Either : EitherType);;
type ('a, 'b) t = ('a, 'b) Either.t = Left of 'a | Right of 'b
# Left 1 = Either.Left 1;;
- : bool = true
1 Like

…I guess that’s what I get for only checking that a Left or Right exist, sorry. (The t/2 thing also should have been a clue, it doesn’t happen if you do the sealed open with a type constraint like what @shonfeder said.)

The other way you could reduce the ceremony would be to define a module with the constrained type, either by just naming the sealed module or putting the type alias in a module, then opening that:

module EitherType : sig
  type ('a, 'b) t = Left of 'a | Right of 'b
end
with type ('a, 'b) t = ('a, 'b) Either.t =
  Either

(* … *)

open EitherType
Left 1 = Either.Left 1

(I also agree with @shonfeder that type inference-based constructor choice and local open sugar generally reduces the need for qualified constructors a lot; I’d make sure there’s not a convenient place to stick a type annotation or local open before trying any of this.)

1 Like

That didn’t have any effect on the following type inference that I could see - I still get “Unbound constructor Left” for using it in a subsequent function definition. I’m no expert on Ocaml type inference; it seems plausible to me that one could prime the pump with a prior use of the type, but not sure that’s what you intended?

As partition_map pn_function value_list requires pn_function to have already been declared, it isn’t obvious to me how the type of partition_map is going to help with a let-defined function.

Type-directed disambiguation works at the binding level, so if you’ve used it in one binding, it’s not automatically imported for use in a later binding.

1 Like

Can an extra module solution work from a separate compilation unit? As I understand it, the sig ... end for a separate compilation unit is in a .mli … anyway, I have not found a way to get the … with type ... expression past the compiler, at the compilation unit level. (I.e., an external module to the one where I actually need Left / Right.

The with type ('a, 'b) t = ('a, 'b) Either.t bit is equivalent to defining the type as type ('a, 'b) t = ('a, 'b) Either.t = (* … *) in the signature (or at least is in this case), so you can just do that. (If you’re going to the effort of making an entire extra file for this, I’d probably do the type alias version.)

Thanks! In case anyone else could use an explicit account – my import file stddef.mli:

(* Provide Either type, without Either.map etc. *)

type ('a, 'b) either = ('a, 'b) Either.t = Left of 'a | Right of 'b

So where I may want to use Left/Right, I only have to

open Stddef
2 Likes

A related tip:

In your own codebase, where you use a bunch of different more elaborate types all over the place, define your types within a submodule T - e.g. in mytype.ml:

module T = struct
  type t = ...
end
include T

...

… this way you can simply use the types in other modules, without getting values and modules in scope, by:

open Mytype.T

...

… and in a module where you don’t need the type all over, just do a local open of the top module:

let v = Mytype.(Mytype |> map f)
2 Likes