Hi Ashish; Thanks for this mail, very useful! A couple of (naive!)
queries/comments inline.

On 14 October 2016 at 17:03, Ashish Agarwal <agarwal1...@gmail.com> wrote:
> Using polymorphic variants for the error type poses some challenges. Here
> are some things to think about. (The following code assumes `open Async.Std`
> because it already has the necessary combinators).
> Error Unification
> So is the annotation on x wrong? Well, it type checks, so it's not
> disallowed. But is it just bad style? Is there never a reason to force a
> closed type? Could adding functions like Bunzli's open_error_msg resolve the
> matter easily enough?

What was your experience here? Why *would* one force a closed type?

> Unification At Scale
> Now you have to manually handle the errors every time x and f are used in
> the same context. Should some support for that be standardized?

Is this got around by naming types per your suggestion below?

> Writing MLI Files
> The goal is to maintain precise error information, so you diligently define
> unique variants for all your functions. Now you've got all kinds of
> polymorphic variant types, getting unified, and growing into larger and
> larger types. Every function's signature is thus something like
> (int, [`Err1 of int | `Err2 of string | .... | `Err20 of unit] Result.t
> Type inference gives you this error the first time, but now you copy/paste
> into your mli. When some upstream code changes, your module no longer
> compiles because the error type changed. So you have to keep copying/pasting
> the updated error type. That gets annoying very fast.
> An idea to avoid that is to name your error types:
> module A : sig
>   type err1 = [`Err1 of string]
>   val f : int -> (string, err1) Result.t Deferred.t
> end
> module B : sig
>   type err2 = [`Err2 of int]
>   val g : unit -> (string, [err1 | err2]) Result.t Deferred.t
> end
> Now, even if you change type err1, the signature for module B remains valid.
> However, you've introduced a different annoyance; you have to name all your
> error types. Also, you've only avoided one case. If g's implementation is
> modified to start using yet another function, then you do have to change g's
> signature. Note this is very different from the usual case. Normally,
> calling out to additional code in a function's implementation doesn't change
> that function's return type.

That's true, but perhaps not unexpected given the error possibilities
are explicitly being exposed in the type?

> Now you're in a situation where return types
> change all the time.

"all the time" sounds a bit over-stated :) How often in practice did
this sort of thing happen? I presume it was only when either (a) an
extra error return type was generated inside `g`, or (b) when `g`
implementation was modified to call out to a new `g'` that added an
error return type. I wouldn't have thought either was such a common
code modification to make, but I haven't the same experience of
managing large codebases at scale.

> Worse, if g's implementation stops using f, the
> signature of g should be changed but the compiler won't tell you that.
> Quickly you'll have functions that say they can return error `Foo but
> actually they cannot.

That's true. Can't think of a useful way round that one.
(But is any alternative better?)

> Coding Expertise
> Historically beginners have been advised to avoid polymorphic variants.
> Although this seems to be less true now on, it is certainly the case that
> coding with them is a bit harder. Example:
> With this style, you end up wanting to define a function like:
> val with_file :
>      string ->
>      f : (t -> (‘a, ‘b) Result.t ->
>      (‘a, [> err | ‘b]) Result.t
> The idea is that opening the file might return an error err. If the file is
> opened successfully, you call f, and f might return error `b. Thus, the
> result type should have errors [err | `b]. However, the above doesn't type
> check. The signature actually has to be (thanks to Sebastien Mondet for
> teaching me this years ago):
> val with_file :
>      string ->
>      f : (t -> (‘a, [> err] as ‘b) Result.t ->
>      (‘a, ‘b) Result.t
> Do you want your user and developer base to deal with such signatures? It is
> unclear what this signature is saying. We now have err mentioned in the
> return type of f, but in our original intention err has nothing to do with
> f. Of course, there is an explanation that makes sense of it (but I'll skip
> it).

Well, I at least would be interested in knowing if you've time!
Off-list if you prefer or it would bore people... :)

> I seem to be arguing strongly against the use of polymorphic variants, but
> I'm not. I'm really unsure what the right answer is. A lot of the problems
> I'm mentioning would go away if there was a project wide standardization of
> the particular polymorphic variants used. The value of using them is high,
> so actually I hope they will be used.

Also, it seems to me that almost any step forwards is going to result
in a better place than we're at now... :/

Richard Mortier

MirageOS-devel mailing list

Reply via email to