Comment by zahlman
> Unsurprisingly, the equivalent Rust code is much more explicit.
Okay, but you can do the same in dynamically typed Python, while still using familiar exception logic and not requiring the type annotation on `req` (although of course you can still use one):
def authenticate(req):
match req.cookies.get("token"):
case None:
raise AuthenticationFailure("Token not included in request")
case cookie_token:
pass
match req.db.get_user_by_token(cookie_token):
case None:
raise AuthenticationFailure("Could not find user for token")
case user:
return user
Although I normally use plain old `if`/`else` for this sort of thing: def authenticate(req):
cookie_token = req.cookies.get("token")
if cookie_token is None:
raise AuthenticationFailure("Token not included in request")
user = req.db.get_user_by_token(cookie_token)
if user is None:
raise AuthenticationFailure("Could not find user for token")
return user
Nothing ever forces you to pass "null objects" around in dynamically-typed languages, although it might be more idiomatic in places where you don't care about the reason for failure (or where "failure" might be entirely inconsequential).The nice thing about the Rust syntax shown is that constructs like `let` and `match` allow for a bit of type inference, so you aren't declaring manifest-typed temporaries like you'd have to in many other languages.
> It's possible to write sloppier Rust than this, but the baseline is quite a bit higher.
The flip side: the baseline for Python might be low, but that's deliberate. Because there are common idioms and expectations: dictionaries have both a `.get` method and key-indexing syntax for a reason (and user-defined types are free to emulate that approach). So indeed we could rewrite again:
def authenticate(req):
try:
cookie_token = req.cookies.get("token")
except KeyError:
raise AuthenticationFailure("Token not included in request")
user = req.db.get_user_by_token(cookie_token)
if user is None:
raise AuthenticationFailure("Could not find user for token")
return user
And for that matter, Pythonistas would probably usually have `req.db.get_user_by_token` raise the exception directly rather than returning `None`.You can always add more checks. The Zen says "explicit is better than implicit", but I would add that "implicit is better than redundant".
> In essence, dynamically-typed languages help you write the least amount of server code possible, leaning heavily on the DSLs that define web programming while validating small amounts of server code via means other than static type checking.
Well, no; it's not because of access to the DSLs. It's because (as seen later) you aren't expected to worry about declaring types for the interfaces between the DSLs and the main code. (Interfaces that, as correctly pointed out, could fail at runtime anyway, if e.g. an underlying SQL database's column types can't be checked against the code's declared data structures at compile time.)
The main thing that personally bothers me with static typing is that, well, sometimes the type calculus is difficult. When your types don't check, it's still on you to figure out whether that's because you supplied the wrong thing, or because you had the wrong specification of what to supply. And understanding the resulting compile-time error message (which is fundamentally written in terms of what the type is) is typically harder than understanding a run-time dynamic type error (which can usually be understood in terms of what the object does).
> Okay, but you can do the same in dynamically typed Python
But the rust code is still safer, e.g. you haven't checked for an `AttributeError` in case `req.cookies`, the point is Rust protects you from this rabbit-hole, if you're prepared to pay the price of wrestling the compiler.