Labels for components of returned types of functions?

One thing I like about argument labels in OCaml is that they allow to differentiate in a non-verbose way between different arguments with identical types in a function.
Unfortunately, AFAIK there is no equivalent for components with identical types in a return type for a function. Suppose for example that I have a function f : int -> string * string where the two returned strings have two very different roles (say for example that one is contained in the other), and I would like to include that in the type information.
The only way I know to achieve this is through writing an additional adhoc type (such as

type contained_string = string
type container_string = string
val f : int → contained_string * container_string

or

type containment_pair = {contained:string;container:string}
val f :int->containment_pair

I find that cumbersome and sub-optimal.

1 Like

There’s another possible approach (though you might find it cumbersome as well): continution passing style.
In your example:

val f : int -> (contained:string -> container:string -> 'a) -> 'a

The labels on function arguments work because OCaml goes beyond regular curried functions and has first class support for multi-parameter functions. The same is not true for return values, and OCaml functions always have a single return value (which can be a tuple, of course, but from the language’s point of view it is always a single value).
But if you feel it would improve your code significantly, you can open a feature request for implicit record declarations (so that val f : int -> {contained: string; container: string} would be a valid type, that implicitly declares a record type for its return argument).
You can also use objects for this, although they’re not as efficient as regular records or tuples (val f : int -> < contained: string; container: string >).

1 Like

I feel it would improve the code because it would add more information without bloating, indeed.

It could be very useful indeed, in particular if this feature allows to introduce existential type variables in the return data type. In some pieces of code where I use GADTs, I tend to need to declare a new type for the return value of each function. Typically, instead of writing

type ('a, 'b) add = Exists : { value : 'c nat; plus : ('a, 'b, 'c) plus } -> ('a, 'b) add
val add : 'a nat -> 'b nat -> ('a, 'b) add

(where 'a nat is the type of the natural number 'a and ('a, 'b, 'c) plus is the witness that 'a + 'b = 'c), it would be convenient to write something like

val add : 'a nat -> 'b nat -> 'c . { value : 'c nat; plus : ('a, 'b, 'c) plus }

(I don’t know what would be the best notation for existential variables: 'a . ... notation is currently used in records to denote universally quantified variables so overriding it is probably not the best choice…)

1 Like

I think usually defining the record is the right decision, but sometimes another reasonable option is returning something like

`Contained x, `Container y

(I might do that if I was always going to unpack the values into separate variables immediately)

4 Likes