Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: handling partial functions in Rust libraries #1945

Closed
catamorphism opened this issue Mar 8, 2012 · 6 comments
Closed

RFC: handling partial functions in Rust libraries #1945

catamorphism opened this issue Mar 8, 2012 · 6 comments
Labels
A-typesystem Area: The type system

Comments

@catamorphism
Copy link
Contributor

I don't have a specific proposal, but I do want to discuss principles for how to handle partial functions in the standard libraries. Examples of partial functions are option::get (undefined on none), vec::last (undefined on empty vectors), and map::get (undefined for keys not bound in the map).

One approach is to "totalize" these functions by lifting their results into the option type: for example, vec::last would return none if invoked on an empty vector and some(x) on a non-empty vector, for some x. (That was the behavior until today.)

Another approach is to use typestate to specify preconditions so that we write total functions on a smaller domain, rather than partial functions on a bigger domain.

Finally, perhaps the simplest approach is for library functions to fail with an uncaught exception when error conditions arise.

I prefer either the first or second approach over the third (ideally I would prefer the second, but clearly there has been some hesitance to use typestate in anger). My preference is informed by my experience with Haskell where the use of functions like fromJust or head in cases where the invariants don't really hold (and the compiler doesn't help by checking those invariants) is a major pain. If you know an invariant, you should be able to write it down and express it either through types or typestate (whether it's types or typestate is more of an implementation detail to me).

However, I just changed vec::last to fail on an empty vector, for consistency with some of the existing vec code. I'd like to arrive at some guiding principles here.

@ghost ghost assigned catamorphism Mar 8, 2012
@graydon
Copy link
Contributor

graydon commented Mar 9, 2012

I'd differentiate cases here.

  • For functions where the partial-ness arises from failure to handle sparse refinements of relatively dense and pervasive input datatypes (out of range integers, wrong-case options, etc.) I'd prefer to fail for now and work on ways of possibly expressing static refinement types at the typestate level, in a way that's not too clunky for the user.
  • For functions that interface with the OS or unpredictable environment, I'd like to fail when they fail.
  • For functions where the partial-ness is either based on user-provided helper functions (find, filter, etc.) or associative queries of predictably-sparse input/key domains (hashmaps or such) I think it's more reasonable to provide versions that return options.

The basis of this preference is, in a word, "frequency". Datatypes involved in the first set are pervasive (numbers, containers), have a lot of operations, and most operations fail on few and sparsely-distributed cases (0, empty vector, out-of-range index) relative to the dense set of successful cases. IOW these are cases most easily understood as exceptions-to-a-rule. Currently we model exceptions-to-rules (in terms of proportion) as failures, so I'd carry on with that.

I understand that frequency is not always easy to judge, and sometimes you'll want "both interfaces" (hashmaps I think usually have a failing and option-returning variant). When frequency is hard to judge, provision of one or the other, or both, becomes more a matter of taste. I think it's generally ok to support both if a user asks for both, in which case we're faced with an even smaller question: what to name them when providing both. For this I think the one that returns non-option should be the shorter of the two names -- that is, provide foo and maybe_foo -- because each function appeals to a different audience. The one that fails appeals to the programmer who prefers "implicit, quiet, dynamic code"; the one that is more work to call appeals to the programmer who prefers "explicit, bureaucratic, static" code.

Aside from all that, I also want to reiterate, when fail is unacceptable in the first two cases, my general wariness about encoding very-rare-frequency exceptional cases as option or result in the libraries. I don't think reinventing monadic style in rust is a good path to go down. It's expensive-by-default at runtime, and produces compositionality problems that take extra machinery to solve. If "handling" very-exceptional cases (rather than magnifying them as faults and crashing the subsystem) remains an important behavior people really want to support, I will suggest as I did last year that we revive the earlier non-unwinding signal/handler system I had in earlier versions of rust (similar to the system in Mesa).

The design is sketched here:

https://mail.mozilla.org/pipermail/rust-dev/2011-November/000999.html

It's important to understand that this design works quite differently from exceptions. Handlers are dynamically scoped as in exceptions, but It never unwinds unless it's failing, and failure remains idempotent. Non-failure does not unwind, just recovers at the signal site.

@kud1ing
Copy link

kud1ing commented Apr 5, 2012

Would it be possible or even sensible for Rust to have return-type polymorphism?
That would allow a head to return an option<T> and an unsafe head which returns a T.

@kud1ing
Copy link

kud1ing commented Apr 5, 2012

When i read "very-rare-frequency exceptional cases" i understand this as "such problems will almost never occur, we should not bother too much". Do i misread you here?

I wonder because similar complains regarding functions like head are raised again and again in the Haskell world:
http://www.reddit.com/r/haskell/comments/lf71l/deprecate_preludehead_and_partial_functions/

@graydon
Copy link
Contributor

graydon commented Apr 6, 2012

When i read "very-rare-frequency exceptional cases" i understand this as
"such problems will almost never occur, we should not bother too much".
Do i misread you here?

Yes, you are misreading. I'm not saying they're no big deal; I'm saying that the mechanism of "returning an option" for functions with very-rare failure modes is sufficiently unpleasant that we have more language-engineering to do to solve it. I think using options, like using strings, booleans or integers, is a matter of taste; having the flexible and useful hammer of sum types tempts us to view any "finite set of alternatives" problem as a potential nail. I don't think returning option types for all failure cases (and combining them together when there are multiple forms of failure, a la multiple nested exception monads) is always the right approach. It's too awkward for casual users on interfaces that rarely fail. We need a different, more subtle technique for those cases. I have suggested a couple. I'm open to others.

@catamorphism
Copy link
Contributor Author

I don't see clear consensus on this and it seems like we have plenty to do for 1.0, so I suggest we postpone this discussion.

@catamorphism
Copy link
Contributor Author

Closing since @graydon 's condition system is now checked in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-typesystem Area: The type system
Projects
None yet
Development

No branches or pull requests

3 participants