-
Notifications
You must be signed in to change notification settings - Fork 594
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
add monad syntax and tests #1925
Conversation
Thank you for your pull request! After a quick sanity check one of the team will reply with 'OK TO TEST' to kick off our automated validation on Jenkins. This compiles the project, runs the tests, and checks for things like binary compatibility and source code formatting. When two team members have also manually reviewed and (perhaps after asking for some amendments) accepted your contribution, it should be good to be merged. For more details about our contributing process, check out CONTRIBUTING.md - and feel free to ask! |
Hmm, very interesting proposal, thanks! I may not get to review it today but will soon |
OK TO TEST |
Test FAILed. |
Test FAILed. |
Test FAILed. |
Test PASSed. |
As I wrote in #1970, I hope to use def optionalCookieSession: Directive1[Option[(Session, User)]] =
(for {
sessionId <- cookie(SessionCookieName).map(_.value)
if sessionId != ""
SessionSearchResult(Some(session)) <- onSuccess(loginService ? FindSessionById(sessionId))
UserSearchResult(Some(user)) <- onSuccess(userService ? FindUserById(session.userId))
} yield Some(session -> user))
.recover(_ => provide(None)) I tested the code above with this PR. But it shows the following error:
, clearly because of the return type of the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I think our 'nested' DSL is actually quite nice for this use case, also supporting for comprehensions as introduced here seems quite nice and not too heavy - thanks for the proposal!
I'm curious if it could be extended to support the example given by @pocorall
Test PASSed. |
Hi @winitzki, Thank you for your contribution! We really value the time you've taken to put this together. Before we proceed with reviewing this pull request, please sign the Lightbend Contributors License Agreement: |
Test PASSed. |
I agreed to the CLA. Not sure why this was not being checked before. |
Test PASSed. |
Not speaking for the entire team, but just to voice my opinion: I think I'd prefer this to be something maintained outside of Akka HTTP. I don't think yet another way of doing things in vanilla Akka HTTP is a good idea, it is complicated enough as it is for newcomers (and intermediate users). (I also don't personally find it a super good match, the routes for anything but super simple example cases are more of a tree than a sequential computation which doesn't match the sequential style well, but that is more my opinion so if this is what you want in your project, by all means, feel free to pursue it) |
Test PASSed. |
In my view, routes should not necessarily be trees. The logic of a route is more or less linear: extract most of the code should be extracting various data from the request, then there might be some filtering based on validation, some logging or performance metrics, then there are calls to some functions to compute responses, and finally a response is returned. I think the API for routes and the code would have been a lot simpler, had Akka-HTTP used the monadic Another advantage of a monad is that it is a standard and well-understood design pattern. Refactorings are straightforward. Also, for instance, you can apply a monad transformer and enrich the route with extra effects. For example, swagger annotations could be generated automatically by enriching the route directives with swagger meta-information. |
Hi @winitzki,
They are trees however, and what you explained is a single traversal of such tree -- an depth-first-ish (-ish since rejections) traversal of the routes tree is indeed what routes basically do at runtime. However, in face of rejections, the traversal is not as simple as you painted it here. Even a
That could not have been the solution -- the reason we did so is in order to have feature parity with Java which is a hard, non negotiable, requirement for all of our APIs. An abstracted, using hlists, version of directives existed in Spray which became Akka HTTP, and one of the tradeoffs we had to make was this abstraction, among other things. As for the general monad argument -- yes and no, I don't think it enchances composability over how routes already are able to compose. I do see that the API proposal can be nice, however the tradeoff is that we'd confuse newcomers even more, by providing yet another style inside the core library. This is not to say that the PR isn't cool and a great idea, I think it is, however I'd wage it against the increased "choices" issue which we continue to face on a day to day basis. Having all that said... I wonder if you would be okey with releasing this directive as a separate library, and we would link to it in the official docs -- would this be a nice compromise? You'd also get exposure in the community that it's your cool monadic akka routes library etc :-) |
I understand about the Java compatibility requirement. So perhaps another person in your team could look over this and weigh in, and if there are 3 people all saying that this functionality belongs outside the core library, so be it, let that person close the PR. I will make a small add-on library then. |
Hi @winitzki This is really impressive work and I understand that some people will find it attractive to define the routes with for comprehension. However, we wouldn't like to add confusion about how to use the DSL by adding another way to define the same thing. Questions like "when to use what?". Therefore I suggest that you release this as a small external library and we would be happy to include a reference to it from the Akka HTTP documentation. |
Hi there, seems we're in agreement that this is very cool but best not in the main library -- however we're happy to link to your project once you send us a link or PR such PR that would add such docs :-) Closing for now, thanks and I hope we can get that separate mini project running :-) |
Thank you for the useful input that helped me understand where this API should be going. We are already using this for our production code at my day job. Perhaps once we see a few more use cases that require more development, I'll make this into a separate library. Especially, I would like to figure out whether monad transformers could be harnessed to generate swagger annotations or other stuff automatically from the route code. |
Support the Scala
for
/yield
syntax in route directives, simplify route code.Writing routes in
for
/yield
syntaxThe default Akka-HTTP style is to use nested functions for defining routes, for example:
Each of the functions
get
,path
,extractLog
,entity
etc. are defined by the Akka-HTTP library as values of typeDirective[L]
.A
Directive[L]
has atapply()
method that takes as an argument a function of typeL => Route
and returns aRoute
.Therefore, each directive in a route requires you to write one more nested function of type
L => Route
.It also follows that a
Directive[L]
is equivalent to a value of type(L => Route) => Route
, which is known in functional programming as the "continuation monad".Being a monad, this type can be used in
for
/yield
syntax, which is idiomatic in Scala, and avoids having to write several nested functions.The only missing part is the methods
map
,flatMap
, andwithFilter
as required by thefor
/yield
syntax.The class
Directive[L]
already has methodstmap
,tflatMap
, andtfilter
but their type signatures are not suitable because of the type class constraint onL
and because of using magnets.This PR adds syntax helpers that enable the
for
/yield
syntax for routes, so that the route above can be rewritten like this:All Akka-HTTP directives can be used without any changes in this syntax.
Directive operations such as
&
can be used as well, in the right-hand side of<-
.The new syntax is added using a zero-allocation wrapper,
WrapDirective[L]
, which can be created out of aDirective[L]
or aDirective1[L]
automatically via an implicit conversion.There are also implicit conversions from
WrapDirective[L]
back toDirective[L]
andDirective1[L]
for convenience.Some comments:
get
) need to be used with the syntax_ <- get
(these directives are of typeDirective0
and have an argument of typeUnit
).for
block are written asa = b
instead ofval a = b
.Unit
, such aslog.info(...)
, are written as_ = log.info(...)
.if x.length > 0
are permitted within thefor
block as usual; the route will be rejected if the condition fails.complete(xyz)
, just writeyield xyz
, wherexyz
must be a marshallable value (a string, a case class, or an http response.)yield
can be also used on aRoute
, so thatyield complete(xyz)
,yield reject(...)
, etc. are also supported.recover()
,&
etc., can be used on aWrapDirective
(i.e. on the result offor/yield
) as well.The advantages of this syntax:
for
/yield
syntax is a standard, idiomatic Scala syntax for sequential computationsExample of refactoring several routes with a common prefix: