[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] Re: [MirageOS-devel] Using Result instead of Option in libraries
Hi Ashish; Thanks for this mail, very useful! A couple of (naive!) queries/comments inline. On 14 October 2016 at 17:03, Ashish Agarwal <agarwal1975@xxxxxxxxx> 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 richard.mortier@xxxxxxxxxxxx _______________________________________________ MirageOS-devel mailing list MirageOS-devel@xxxxxxxxxxxxxxxxxxxx https://lists.xenproject.org/cgi-bin/mailman/listinfo/mirageos-devel
|
Lists.xenproject.org is hosted with RackSpace, monitoring our |