[HN Gopher] Magical Handler Functions in Rust
___________________________________________________________________
Magical Handler Functions in Rust
Author : bkolobara
Score : 98 points
Date : 2022-10-17 10:28 UTC (12 hours ago)
(HTM) web link (lunatic.solutions)
(TXT) w3m dump (lunatic.solutions)
| efficax wrote:
| someone explain to me the point of backend webassembly for a web
| framework? you're running the compiler already, why not emit...
| actual assembly
| wyldfire wrote:
| I think it's similar to the whole javascript/node concept.
|
| In many ways, web development is like a single distributed
| application between client and server. So if you write your
| client code in Rust/Zig/C++/etc and emit wasm, wouldn't it be
| cool if you could share data structures and logic for that code
| on the backend that would handle client requests?
| kevincox wrote:
| But you can share code between Rust compiled to wasm on the
| frontend and rust compiled to native on the backend. In fact
| I did this for a game where you can either play locally on
| the frontend or play networked with optimistic updates
| provided by the client-side but the rules enforced on the
| server-side.
| pjmlp wrote:
| CleverLikeAnOx wrote:
| Could be to allow running untrusted backend code. E.g., so kind
| of third-party extensions system for your product. Webassembly
| is good for sandboxing.
| dathinab wrote:
| - Sandboxing of each request (is performant when the right
| kinds of snapshot+CoW technique is used)
|
| - PaaS for running WASM compiled untrusted web applications,
| especially nice for "edge service style PaaS"
|
| - Node style async of rust, can be a bit more simple then rust-
| async (but also likely more limited)
|
| - Similar "compile once run everywhere" as JVM (but better due
| to limited interface and server focused?), i.e. you have the
| wasm runner compiled for each target but you need to compile
| the program only once and then you can run it on all kinds of
| platforms including x86_64, arm64 etc. Especially good for PaaS
| but also good for more flexible deployment pipelines in large
| companies.
|
| And especially for many web use-cases, especially as run on a
| lot of "edge" services the drawbacks of single threaded WASM do
| not matter much. E.g. the workloads are of the kind which tend
| to work very well with WASM performance wise.
| quotemstr wrote:
| > someone explain to me the point of backend webassembly for a
| web framework? you're running the compiler already, why not
| emit... actual assembly
|
| Once you're in this industry, you stop trying to figure out why
| fads come and go. There really isn't any reason. It's all
| fashion. You'll go crazy asking "Why?".
| losvedir wrote:
| As a relative newbie in Rust who recently dabbled with Axum, I
| wholeheartedly agree that the handlers and extractors are magical
| and hard for first-time users. Another issue I ran into that's
| not mentioned in the post, is the compile-time type errors are
| pretty cryptic, if you get it wrong. I was trying to use the new
| state extractor, and didn't have my state properly wrapped in an
| Arc and it was complaining to me in a way that was totally
| unhelpful. The docs mention there's an `axum-macros` crate which
| can help here, but I feel like if you need macros to get around
| confusing type errors, you're going against the grain pretty
| hard.
|
| On the whole I still found experience pretty compelling, though,
| as I did eventually get a little web app running with great
| performance and very low memory usage.
| jjwiseman wrote:
| The bad error messages are really a big deal. I like Axum, but
| this type-driven extractor technique leads to such awful
| messages for some trivial mistakes.
| Dowwie wrote:
| Extractor types aren't magical. You can clearly see the types
| involved. How and when are those types constructed? That's up to
| you to research.
| kibwen wrote:
| _> Rust developers generally like these properties,
| predictability and explicitness, instead of magic. That's why I'm
| quite surprised that most Rust web frameworks went another
| route._
|
| Ecosystems are rarely monoliths, and I assume that the people
| writing web frameworks in Rust are likely to have experience with
| (or at least be inspired by) more magical frameworks in other
| (usually more dynamic) languages, whereas the libraries that
| eschew magic are presumably written by people with prior
| experience in low-level languages. The fact that Rust has some
| amount of appeal to both of these crowds (not to mention the
| functional programming folks) makes it a bit of a melting pot.
| pjmlp wrote:
| As if macros weren't their own kind of magic.
| bryanlarsen wrote:
| `cargo install cargo-expand` to demagickify.
| pjmlp wrote:
| I bet many won't understand the runes of serde, when there
| is a whole conference talk explaining how it works.
| fire wrote:
| I wonder if macros are a type of magic that can be reasoned
| about more sanely than other types of magic?
| dathinab wrote:
| The handlers are less hard to reason about then a lot of
| macros.
|
| You don't have to understand the type magic which makes
| them work.
|
| At least if you look at the more relevant frameworks
| (actix-web, axume) they basically boil down to calling a
|
| fn foo(x: A, y: B) -> Result<Body, Error>
|
| as roughly
|
| let x = A::from_request(&req, &mut payload).await?; let y =
| B::from_request(&req, &mut payload).await?; foo(x, y)?;
|
| and _nothing_ more.
|
| There is no special handle first/last parameter different
| logic or anything.
|
| They way they handle not copying the payload (request body)
| is by moving it (a reference to it) out of the request (or
| here payload).
|
| The reason the performance for micro-benchmarking
| extractors is different depending on the order has less to
| do with the general design and more the typical "I
| reordered parameters and now optimizations trigger
| differently" problem. (Probably some in-lining and const
| propagation allowing eliminating some checks but just on
| one order, anyway not specific to web frameworks at all and
| can also happen with their approach, but probably less
| likely if I should guess).
| twic wrote:
| No, indeed, the opposite. In Rust, these will surely be
| procedural macros, which comprise functions written in Rust
| which operate on Rust code, represented as token streams.
| The compiler compiles the macro, lexes your code, feeds it
| to the macro, gets a token stream back, parses that, and
| that's what it compiles.
|
| Understanding a magic handler function involves
| understanding a particular subset of features of the Rust
| type system, and operating that mechanism in your head to
| see what happens.
|
| Understanding a proc macro involves understanding arbitrary
| Rust.
| fire wrote:
| ...oh dear
| mckirk wrote:
| To me, this kind of 'magic' is definitely a double-edged sword.
| For the people that are familiar with the framework, I assume it
| makes it much nicer to write code and to reason over it (at the
| above-framework level), since there's no boilerplate to write or
| understand.
|
| But as someone who often has to jump into unknown codebases and
| get up-to-speed quickly, I've certainly been confused by
| framework magic before, which makes it almost impossible to
| reason 'up' from the concrete implementation of e.g. a request
| handler. Since the keywords to grep for aren't explicitly
| mentioned in the handler anywhere and the 'dispatch' side of
| things is hidden under mountains of abstraction, you usually have
| to resort to tutorials to understand what the hell is going on,
| and even then the codebase might use some extra-magical
| properties of the framework that aren't clear to anyone that
| isn't at home within it.
|
| So it could be argued that this magic saves you code in exchange
| for a need for more documentation, at which point you might just
| be better off being more explicit in your code -- at least
| explicit enough so things remain greppable.
| nicoburns wrote:
| I agree with this:
|
| > So it could be argued that this magic saves you code in
| exchange for a need for more documentation,
|
| But not this:
|
| > at which point you might just be better off being more
| explicit in your code
|
| Reason being that you only need to add documentation once (for
| the framework), and then you can have cleaner more readable
| code for every project using it. Yes, there's magic enabling
| it, but shouldn't need to worry about how it works if it's well
| documented enough. And if you really need to know then you can
| read a post like this one which explains it.
| mckirk wrote:
| That is a fair point. I guess it basically comes down to
| preference:
|
| Should code be essentially self-explanatory, without needing
| to resort to external documentation to understand, or can
| frameworks be terse to the point of being a kind of DSL?
|
| I suspect most low-level programmers prefer the former ("It's
| all right there"), while dynamic-language coders feel
| comfortable with the latter ("I only want to be explicit
| where it's absolutely necessary.")
|
| As another top-level comment pointed out, Rust is interesting
| because it attracts both kinds of programmers, so it's no
| wonder the question of which approach to go with for a Rust
| framework isn't a trivial one to answer.
| kirse wrote:
| _But as someone who often has to jump into unknown codebases
| and get up-to-speed quickly_
|
| I'm not sure how long you've been in development but eventually
| you just learn to "expect magic" with frameworks and after
| awhile they all blur together. Show me the docs on the
| decorators, the pipeline, how it handles global context etc and
| it's all pretty much the same core concepts repeated across
| different languages.
|
| To your later point, yes, frameworks should be a terse DSL. I
| expect a framework to make good "magic" assumptions that enable
| me to benefit from packaged functionality that would often be
| more verbose in lower-level implementations.
|
| _without needing to resort to external documentation to
| understand_
|
| That's not a thing. RTFM. Or a good code inspection tool if you
| really hate docs.
| junon wrote:
| What an assertive take that couldn't be more subjective and
| counter to what many (including myself) have experienced.
| kirse wrote:
| Your ability to pop open vscode and slap some JS in it to
| start a web-server in a few lines is the product of decades
| of SW frameworks that have made thousands of assumptions
| along the way to increase expressive power while tucking
| complexity under the hood. Feel free to provide a counter-
| example vs a generic statement.
| runevault wrote:
| You just presumed everyone is a JS dev. Despite how hard
| it is trying to take over the world but not everyone
| writes javascript.
| cercatrova wrote:
| Same as in this Rust example then. The Axum example is
| basically the same as its equivalent in JS. I've also
| seen similar abstraction in Python (Django, Flask), Ruby
| (Rails), Elixir (Phoenix) etc.
| runevault wrote:
| I will agree insofar as webdev seems to proliferate use
| of magic. ASP.NET core also uses a ton of it, which I
| hate as well. I'm not sure why that one particular area
| draws the people who love magic for their tooling, but it
| at least partly explains why I wish I could just hide in
| a backend hole and avoid 90% of it (Dependency Injection
| can be very full of magic as well but that's another tech
| tool I wish would go away).
| SevenNation wrote:
| > ... I don't know the "official" name for it, so I just stole
| the name from a recent reddit post (magical handler functions).
|
| What's the difference between programming "magic" and "using the
| language to its fullest"?
|
| Wikipedia has an article dedicated to the concept of magical
| programming. It starts with:
|
| > In the context of computer programming, magic is an informal
| term for abstraction; it is used to describe code that handles
| complex tasks while hiding that complexity to present a simple
| interface. The term is somewhat tongue-in-cheek, and often
| carries bad connotations, implying that the true behavior of the
| code is not immediately apparent. For example, Perl's polymorphic
| typing and closure mechanisms are often called "magic". The term
| implies that the hidden complexity is at least in principle
| understandable, in contrast to black magic and deep magic (see
| Variants), which describe arcane techniques that are deliberately
| hidden or extremely difficult to understand. However, the term
| can also be applied endearingly, suggesting a "charm" about the
| code. The action of such abstractions is described as being done
| "automagically", a portmanteau of "automatically" and
| "magically".
|
| https://en.wikipedia.org/wiki/Magic_(programming)
|
| I think that "true behavior of the code is not immediately
| apparent" is the key. The harder it is to figure out how
| something works, the more likely it is to be viewed as "magic."
|
| Do the magical handlers in this article fit the bill? I'm not so
| sure. All that's going on is generics applied to functions. True,
| it's not as common as generics applied to other types. But it's
| also not fringe.
|
| Maybe as a technique becomes more established, it starts to seem
| less magical. The author notes:
|
| > Exploring this pattern was an interesting journey. I still
| can't tell why it's more popular than the alternative that
| contains less magic and gives more flexibility to developers. I
| can only assume that even Rust developers long for some magic and
| coolness in their tooling.
|
| Maybe it's more popular because it's now widely used across Rust
| web frameworks and therefore expected as a convention?
| insanitybit wrote:
| I'm in agreement that a simple macro can be just as readable as
| any other code.
| brundolf wrote:
| I agree with most of the criticisms of the "magic" functions,
| although I really have to disagree that the macro approach (with
| a totally custom DSL) for routing/authorization is _less_ magical
| than the alternative. It also cripples IDE support. With a
| builder-pattern you can get smooth autocompletion and clear error
| messages, while macros usually sabotage error messaging and
| completely sabotage autocomplete.
|
| Bigger picture: I think the reason so many Rust frameworks favor
| this kind of magic is to lower the bar (accessibility, but also
| development velocity). Being lower-level, Rust starts off with a
| disadvantage when it comes to quickly building out high-level
| application logic like you would on a typical web server, so when
| people are trying to expand its reach into areas like that, they
| try to close the gap by making their libraries as easy-to-use and
| boilerplate-free as they can. Rust's macro and type systems are
| the mechanisms it provides for eliminating boilerplate and
| generally making it act like a higher-level language, so that's
| what people use. And that makes well enough sense, even though it
| comes with trade-offs.
| gigatexal wrote:
| Dislike. Give me verbosity and pragmatism. I like simple
| functions that so one thing and are easy to reason about and
| test.
| kubota wrote:
| This was an interesting read for a Java / JVM language developer
| that has recently begun learning and using Rust in personal
| projects.I was curious how the magic was working begind the
| scenes. In the Java world, our most popular framework (Spring
| Boot) takes Magic to another level, since Java allows reflection,
| lots of magic is possible.
| tarkaTheRotter wrote:
| Luckily, there are saner alternatives on the JVM. If you read
| at "Your server as a function" paper from Twitter, you'll see
| that the composing of middleware and simple handler functions
| can be used to develop powerful stacks which are a doddle to
| write, reason about and test.
|
| The SaaF design was used to design Finagle (Scala), and then it
| inspired http4k (Kotlin). There may be more that I'm not aware
| of. These libraries are often described as micro-frameworks -
| but this is misleading - it is only because the core is very
| small and can be easily extended by bolt-on modules.
| twic wrote:
| The JDK has a server-as-a-function-ish web server built in
| (and has done for years):
|
| https://docs.oracle.com/en/java/javase/17/docs/api/jdk.https.
| ..
|
| Because HttpHandler is a functional interface, you can write
| lambdas for it. Here is a little outline of a server:
|
| https://gist.github.com/tomwhoiscontrary/b4888b86057c74a636c.
| ..
|
| Sadly, Filter is not a functional interface, so it's much
| clunkier. It's also not really clear why you would use a
| filter when you can do function composition; i suspect they
| added filters because previous Java web frameworks had them.
| dathinab wrote:
| While extractors are sometimes doing copies, it is basically
| never in situations where it matters.
|
| For example extractors on the json body will always _move the
| body out of the request_ (independent of the position the
| extractor is in).
|
| Sure there is some copying for e.g. request parameters, but you
| very often do that anyway as you e.g. do some urldecoding,
| parsing and similar on the parameters, e.g. extracting a Uuid
| type from a `/user/9313ba53-955e-4ce1-8b53-de8f92656196/bar`
| path.
|
| Similar if the extractor doesn't return the error type you want
| you can wrap it an map the error when extracting. Sure with
| actix-web and warp there are some error handling situations which
| they don't handle well, but that got improved on in axume.
|
| Application state is shared across threads (or at least across
| time) so it's anyway in a `Arc` or `Rc` so you are just cloning
| the reference pointer but not the data behind it.
|
| Lastly if you have a very special situation you can decide to
| just accept a `Request` moving (not copying) it into the handle
| and do the rest yourself by hand.
|
| Without question the system has potential for improvements, but
| for now it has become the de-facto standard due to it being very
| convenient, does work well for many use cases and does not add a
| relevant performance overhead for a lot of use cases.
|
| Now their solution is targeting backend WASM so they probably use
| will focus in single threaded WASM with some snapshot mechanics
| to easily sandbox each request so their requirements are anyway a
| bit different but looking into history routing macros where
| common in the past as far as I remember, and they are pretty much
| all gone by now.
|
| But then they probably will go through a bunch of revisions of
| their system, so it's not like they can't add it back in later
| one.
|
| EDIT: Just to be clear their solution is a fully valid reasonable
| approach.
| chrismorgan wrote:
| > _I don't know the "official" name for it, so I just stole the
| name from a recent reddit post (magical handler functions)._
|
| I'd want a term mentioning that it's type- or signature-guided or
| -driven. "Magical" is way too non-specific. How about "signature-
| driven handler functions"? Seems both accurate and fairly clear.
___________________________________________________________________
(page generated 2022-10-17 23:01 UTC)