内容简介:I’veI don’t want to get into the details too much of the specific proposal, but here’s a sketch of one way thisThis function returns
I’ve long been a proponent of having some sort of syntax in Rust for writing functions which return results which “ok-wrap” the happy path. This is has also always been a feature with very vocal, immediate, and even emotional opposition from many of our most enthusiastic users. I want to write, in one place, why I think this feature would be awesome and make Rust much better.
I don’t want to get into the details too much of the specific proposal, but here’s a sketch of one way this could work (there are a number of variables). We would add a syntactic modifier to the signature of a function, like this:
fn foo() -> usize throws io::Error { //.. }
This function returns Result<usize, io::Error>
, but internally the return
expressions return a
value of type usize
, not the Result type. They are “Ok-wrapped” into being Ok(usize)
automatically by the language. If users wish to throw an error, a new throw
expression is added
which takes the error side (the type after throws
in the signature). The ?
operator would behave
in this context the same way it behaves in a function that returns Result
.
If this is your first introduction to this topic, you should know it has a very long history of discussion that preceeds this blog post. We’ve been having discussions on this theme since around the 1.0 release of the language, and they’ve never made any progress.
I find these discussions exhausting because I feel my position is consistently misrepresented, often by people using very emotionally charged language, sometimes suggesting (or in some cases outright stating) that I am ruining Rust, or at least proposing to, or maybe just leading everyone astray. This is quite frustrating since I have devoted a lot of time - for some periods paid, but for many (including at this moment) unpaid - over the past 5 years to try to improve Rust, and I think I’ve made a lot of very valuable contributions.
Some very kind and generous people try to counteract this by always showing gratitude toward me when I get these sorts of responses. I appreciate their effort and it makes me think better of them, but I have to be honest that it doesn’t realy make me feel better. Gratitude is very weak in the face of ungratitude. So I’d say, not only on this issue but on all issues, would be for everyone to take the responsibility for how their comments will be recieved, to demonstrate emotional maturity and respect for other people, and to be empathetic with the person whose work they are commenting on. Also, put a bit of effort to be more confident that you understand the work you’re commenting on before you make your comment.
Ok wrapping would be a huge boon
I have a file in a project I’ve been working on (not open source yet) which feels pretty
representative of the kind of code I write. It contains a lot of functions, some of which return Result
and some of which don’t. As I edit it this keeps changing, as I introduce new code that
could raise errors, and realize new points at which errors should be “caught” and no longer
returned.
Because I’m using fehler, each time a function “enters or exits the Result monad” (as it were), I
only make one edit: I add or remove the #[throws]
annotation on that function. This is the power
of Ok wrapping that I never see acknowledged: without Ok wrapping, users need to 1 + N edits, for
every happy path return in their function, every time they change whether this function returns a
Result.
The file in question contains 16 functions. Here is the distribition of happy path returns among those functions:
Number of functions with.. | ..this many happy path returns |
---|---|
8 | 1 |
4 | 2 |
1 | 4, 5, 7, and 17 |
Half of my functions only have 1 return (so that’s only twice as many edits without ok-wrapping), but a few of them (which you can imagine, are quite central to the operation of this module) have many paths. These functions are the real source of pain without ok-wrapping.
Most of my functions with many return paths terminate with a match
statement. Technically, these
could be reduced to a single return path by just wrapping the whole match in an Ok
, but I don’t
know anyone who considers that good form, and I certainly don’t. But an experience I find quite
common is that I introduce a new arm to that match as I introduce some new state to handle, and
handling that new state is occassionally fallible.
Without fehler, this becomes an ordeal as I now edit the 17 other match arms to be wrapped in Ok
,
in addition to changing the function signature. With fehler, I just change the function signature to
document that this function is now fallible.
And of course, an experience I have as well is being uncertain if this error should be thrown, if it should be caught, if we need to call the fallible function in this branch at all, if maybe that function should be catching the error - etc. And without fehler I’d be making 18 edits every time I change my mind.
And not for nothing, these are not very easy edits to make because they require a wrap-around call
that involves making edits in multiple places: exactly the reason ?
was preferred to try!()
in
the first place. Of course I already know that someone will say we just need a postfix operator for
the Ok
constructor, and while that would strictly speaking be an improvement, it wouldn’t collapse
the number of edits from O(n) in the number of return paths to O(1).
I wouldn’t be surprised if, right now, people are putting error boundaries in suboptimal places because the edit cost of changing those boundaries to the better position is too high! I’m sure I’ve done it. How terrible!
It would be consistent with other effects/monads
These are the core “effects/monads” of Rust as most people use it today:
- Option
- Result
- Iterator
- Future
- Stream
We have seen with Future, and will someday see with Iterator and Stream, a syntactic pattern for dealing with “being in the monad” in Rust:
- A syntactic annotation on the function which is “in the monad” (marking the function async, fallible, a generator, etc)
- An annotation for “introducing an effect” (throwing an error, yielding a value)
-
An annotation for “forwarding an effect” from a subroutine (
?
,await
, Python’syield from
)
Let me just say, I’m not very optimistic about any proposals to introduce polymorphism over these different effects. There are two different avenues these proposals take.
First is some sort of “Monad trait.” There have been proposals for how this could be done with the addition of a heapful of new abstractions in Rust’s trait system, but I’m very suspicious of their ability to integrate well with type inference, unification, etc. I suspect any modelling like that would be incredibly unergonomic to actually use, because you’d have to type ascribe loads of things.
Second would be proposals to add a new axis of polymorphism over effects. This seems like it has the possiblity of at least not being terribly unergonomic, but I see two problems. The first is that it needs to be done in a way which reduces, rather than increases, cognitive load, which is going to depend a lot on the syntax we choose and so on. The second is that there’s just no bandwidth to implement this sort of thing in any forseeable time horizon. Ask me again in 3 years, maybe.
Despite this, the fundamental consistency is valuable for reducing cognitive load. One response I
often see to talk of this feature is that it make Rust harder to teach, but I think exactly the
opposite is true. Right now, ?
is difficult to teach because you have to teach it in terms of
matching on a Result
. This would be as if you add to teach await
in terms of polling a future,
and it’s because we have an incomplete effect system for fallibility - we only have the notation for
forwarding the effect, we don’t have the notation for being effectful.
Instead, since most code would just use some combination of ?
, throw
and throws
, new users
could have an intuitive understanding of fallibility in the same way async/await gives them an
intuitive understanding of asynchrony. They would learn about how to process failure and “leave the
monad” at the second stage of their understanding, in the same way that they learn about the task
system at the second stage of understanding async/await (but here the cliff would also be much
lower, because Result is much simpler than Future).
It could go hand in hand with ABI-based optimizations
Panicking is actually faster in many cases than using Result
, because the happy path is cheaper.
If there are many function calls between where an error is raised and where it is handled, it can
result in faster code to not have to check the result value for fallibility at all of those calls
during the happy path.
This is along the same lines as why I recommend using a trait object for errors in application code: making the happy path as cheap as possible is a huge win in cases where errors represent an uncommon systemic failure (whether programmer error or an issue arising from another program in a distributed system). By using anyhow::Error, for example, the error type’s stack representation is just a single pointer, which avoids making the Result’s stack representation unnecessarily large.
But by just eliminating the Result type from the representation at all at an ABI level, we could make things even better. It is not impossible to imagine an optimization pass which converts a use of the Result pattern into a stack unwinding pattern on platforms which support onwinding, when it has no effect on program behavior, Syntactically nothing is different, but users could get the performance benefits of exceptions when they would benefit.
This is orthogonal to the syntactic issue - these changes can be made completely independent of one another - but it would involve exactly the same type of language integration as the syntactic change, and opposition to this kind of language integration is usually one of the main objections.
Counterarguments to common objections
The error path should not be invisible
This is the most common counterargument and it is very easy to respond to: I agree that the error
path should not be invisible. That’s why I’ve never advocated making any change to how the error
path is handled. It is still necessary to use ?
to “rethrow” an exception.
And yet it’s always one of the first and most popular comments any time this discussion is brought up again. I wish people would put a bit more effort in understanding things before making comments on them, but all evidence suggests this is not the natural behavior of humans on the internet.
Let me just repeat: no one is proposing to make the error path invisible. If you think that is the consequence of throwing/catching function proposals, you have misunderstood these proposals and you should start again.
I actually find this complaint quite ironic, because ok-wrapping syntax would make it easier
to
identify all of the error paths because of the way it interacts with implicit final return. Today,
when a function call returning a result ends in an implicit return, if that function is also
returning a result, it is totally unmarked. But this syntax would require, even in that case, that
that terminal function call be annotated with ?
, identifying it as fallible. With this syntax, now every
fallible path is marked with ?
or a throw
expression. This is more consistent and more
explicit!
Having to edit the happy path returns adds meaningful value
I’ll have to be honest, a large part of the time this argument is made, I just don’t understand the value the person is claiming that this adds. Usually they are using terminology that feels handwavy and ideological to me, and I cannot find a basis in concrete user experience from which to judge the value they claim is added.
There is one argument along these lines that I have understood practically, which is this: suppose I
am adding a fallible case to a function. Perhaps this is a scenario in which now, every other return
path should be examined to see if it should in fact stop being an ok-path return, but instead now
return an error. In such a case, applying Ok
to each of those return paths gives me an opportunity
to make that examination.
First of all, I don’t think I have ever experienced this scenario in my life. In any case it is diminishingly uncommon, whereas the example I described above of needing to move things in and out of the Result monad is something I experience with extreme frequency. This alone is enough for my to deprioiritize this problem, but I have considered the possibility, and I would make these further comments.
If you have a return path you want to move from the happy path to the error path, values of that path must have been represented in the return type before. If you were using Rust’s type system at all effectively, this would probably mean modifying the return type to remove those values from its set of values (for example, perhaps previously one variant of your enum you now realize should actually be a separate error type). By making this edit to the type definition, you can follow the compiler’s guidance to find the happy paths that should now be error paths, which is far better than a manual and error-prone examination of every happy path anyway.
If you were previously representing errors in a way that had no type assistance whatsoever (for
example, you were just returning an i32
that is negative in error cases), well, yea, you get no
help. But this code would be extremely unidiomatic, and it would be obvious
to you that you need
to transform this function carefully. In that case, you should not just
make a change to mark your
function as “throwing,” but use an “explicit Result” intermediate step to help you catch all of the
return paths and examine them. But this feels like an extremely uncommon problem with obvious alerts
to danger.
I don’t like exception terminology
I’m completely open to different terminology if someone can come up with something good. Most
counterproposals have also modified return
, in a fallible context, because they are proposed by
people who hold to the previous objection. Since I think modifying return would annull the key
benefit of this proposal, I’m not myself enthusiastic about any of these alternative terminologies.
But I personally do like the exception terminology. Most programmers coming to Rust will have
extensively used languages that use this terminology. Of course, some of them hate those systems in
those languages - that’s part of why many hobbyists chose to use Rust in the first place - but I
am always designing for people who don’t have a choice about what language they use. For these
people, I believe that understanding Rust’s try/throw behavior as relatively similar,
syntactically, to whatever language they come from, but with the modification that fallible
function calls must be annotated with ?
, and with a value-type reification of fallibility into Result
that try
evaluates to, is a smaller pivot than starting from scratch with new keywords.
Of course this is a point of debate: is it similar enough that its a small pivot, or dissimilar enough that they will be confused by the way it behaves differently from their expectations? To take an existing example: I don’t think the differences in our async/await syntax from the behavior of languages like Python and JavaScript would justify choosing different keywords for the feature. But maybe here the trade off is different. This is the kind of discussion I am open to having, but usually these responses are some conflation between this and complaints about what users don’t like in the exception systems of other languages , properties that no proposal for Rust actually has.
This would make IDE integration worse
This argument is interesting, but the situation is no different from async/await and generators. Basically, yes, like all syntactic changes to Rust, IDE integration would need to handle it properly. Usually, we consider this as a cost, but not a particularly overwhelming one.
I’d be interested in learning if there are any particular challenges that make this seem more difficult for IDEs than other syntactic changes, the authors of rust-analyzer should feel free to get in touch with me if that’s the case.
std types should not be special cased
Adding lang items is always a cost, but a small one. It’s the reality today that std defines a
large number of types which have special handling by the syntactic sugar and even the type system of
the language. In some cases, we make special cased types open-ended with traits (and this is what
the Try
trait is supposed to be for), and it’d be great to support an open-ended set of
“result-like” types. In particular, having some level of support for both Result and Option (even if
Result is given some preference) seems like it should be at least a soft constraint for feature
proposals.
But it’s pretty telling that we have still not stabilized the Try
trait some 4 or 5 years after
stabilizing ?
. There just isn’t a lot of pressure from users to define their own Try
types.
Obviously, only some problems justify having special syntactic integration between the language and std. But fallibility is not an uncommon problem, any more than iteration or asynchrony are. We have clear demonstration that it is one of the major issues users need to manage when writing code. We have clear precedent that we handle these effectful problems using a set of effectful syntactic sugar that naturally integrate with the language. Opposition to this is opposition to the way that Rust is designed already.
Conclusion
I’ll be honest, I don’t expect many people who have already staked out a position in opposition to
this feature to be convinced by this blog post. I think with most of you I have differences of
fundamental values and I can never convince you that this is a net positive. There are still a
handful of people who hold to their orignal position that ?
was a mistake, after all.
But if you feel open-minded that maybe you have not understood the value proposition of this
feature, try the fehler
crate in a real project and see if you can see the benefits of
ok-wrapping when you write code.
以上所述就是小编给大家介绍的《A brief apology of Ok-Wrapping》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
UML基础与Rose建模案例
吴建 / 人民邮电出版社 / 2004-10 / 29.00元
《UML 基础与Rose建模案例》介绍了用UML(统一建模语言)进行软件建模的基础知识以及Rational Rose工具的使用方法,其中,前8章是基础部分,对软件工程思想、UML的相关概念、Rational Rose工具以及RUP软件过程等进行了详细的介绍;后3章是案例部分,通过3个综合实例,对UML建模(以Rose为实现工具)的全过程进行了剖析;最后的附录中给出了UML中常用的术语、标准元素和元......一起来看看 《UML基础与Rose建模案例》 这本书的介绍吧!