> On Apr 10, 2020, at 19:46, Chris Angelico <ros...@gmail.com> wrote:
> 
> On Sat, Apr 11, 2020 at 12:26 PM Soni L. <fakedme...@gmail.com> wrote:
>> They used to say that about Rust.
> 
> Rust prides itself on not having exception handling, forcing everyone
> to do explicit return value checking. I'm not sure how this factors
> into the current discussion, since it forces all these functions to
> return two values all the time (an actual return value and an OK/Error
> status),

That’s not quite true. A Result is effectively a ragged union of a value or an 
error; you don’t explicitly specify a return value plus an OK or an Error, you 
specify either an Ok(return value) or an Err(error value). Sure, technically 
that’s the same amount of information, but the way it’s done, together with the 
syntactic sugar provided by the language, makes a big difference, which I’ll 
get to below.

> which you can quite happily do in Python if you so choose,

Now, this is the key point. You can do this in Python, but it won’t work like 
it does in Rust. It’s not just that you won’t get the compiler verification 
that you covered all code paths (it might be possible to come up with types 
that mypy can verify here; I haven’t tried…); it’s that your code will look 
like 2 lines of boilerplate for every line of code, and still won’t 
interoperate well with the stdlib or the vast majority of third-party libraries 
unless you explicitly wrap the hell out of everything.

There are multiple error handling schemes that can be made to work well—but as 
far as I know, nobody has discovered one that can be made to work well if it 
isn’t used ubiquitously. Python, Rust, Scala, Swift, C#, Haskell, etc. are all 
good languages to deal with errors in—they all picked one, implemented it well, 
and strongly encourage everyone to use it everywhere. Meanwhile, C++ has three 
different error handling schemes, two of which are implemented very nicely, but 
it’s a nightmare to use, because every complex C++ program has to deal with all 
three of them and map back and forth all over the place.

An even more relevant illustration is JavaScript async code. Before there was a 
consistent convention it was next to impossible to use different libraries 
together without frequent bugs of the “callback never got called because we got 
lost somewhere in the chain and there’s no way to tell where” variety, and that 
couldn’t be fixed by just the language, it had to be fixed by every single 
popular library being rewritten or replaced by a new library.

And that’s the problem with Soni’s proposal. It’s a good system, and designing 
a new language around it and letting an ecosystem build up around that language 
might well give you something better than Python. But that doesn’t mean 
changing the language now will give us something better. That only worked in JS 
because the status quo ante was so terrible and the benefits so big (not to 
mention Node coming along and completely changing the target surface for the 
language toward the end of the transition). But here, the status quo ante is 
fine for most people most of the time, and the benefits small, so we’re never 
going to get the whole ecosystem updated or replaced, so we’re never going to 
get the advantages.

> and in fact has already been mentioned here. It's not an improvement
> over exception handling - it's just a different way of spelling it
> (and one that forces you to handle exceptions immediately and reraise
> them every level explicitly).

No it doesn’t. That _is_ true in Go, which gives us a perfect opportunity to 
compare. Let’s take something non-trivial but pretty simple and compare 
Pythonesque pseudocode for how you’d write it in different languages:

Python:
   val = func()
   val = func2(val)
   try:
       val = func3(val)
   except Error3:
       return defval
   return func4(val)

Rust:
   val = func()?
   val = func2(val)?
   val = match func3(val):
       Ok(v): v
       Err(v): return Ok(defval)
   return func4(val)

Go:
   val, err = func()
   if err:
       return None, err
   val, err = func2(val)
   if err:
       return None, err
   val, err = func3(val)
   if err:
       return default, None
   val, err = func4(val)
   if err:
       return None, err
   return val, None

Go does force you to explicitly handle and reraise every error, which not only 
drowns your normal flow in boilerplate, it makes it hard to notice your actual 
local error handling because it looks almost the same as the boilerplate. But 
Rust lets you bubble up errors unobtrusively, and your local error handling is 
immediately visible, just like Python. (I know this is a silly toy example, but 
read actual published code in all three languages, and it really does pan out 
like this.)

The Rust argument is that this isn’t just as good as Python, it’s better, 
ultimately because it’s easier for a compiler to verify and to optimize—but 
those are considerations that aren’t normally top priority in Python, and 
arguably wouldn’t be achievable in any realistic Python implementation anyway. 
On the other hand, Rust has less runtime information and dynamic control in the 
rare cases where you need to do tricky stuff, which are considerations that 
aren’t normally top priority in Rust, and arguably wouldn’t be achievable in 
any realistic Rust implementation anyway. But for the vast majority of code, 
they’re both great.
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/AFMUGWRN7RK2DFI5QCY4V5TTXSGWSHGJ/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to