GithubHelp home page GithubHelp logo

Comments (6)

tindzk avatar tindzk commented on May 28, 2024

Thanks for the thorough analysis! I like your idea of considering the case class mapping just as a special use case, and find an abstraction for it.

The main advantages for choosing case class were:

  • They enforce named arguments
  • If we have a set of routes, we can match them easily
  • We can map them back to an URL while enforcing correct types
  • IDE support

One thing we need to explore more is the case where we map from a route back to an URL. I would like to retain the possibility to do this for case classes with your proposed architecture. Let's consider this example:

val details = Route("details" :: Arg[Int] :: HNil)  // Or shorter: Root / "details" / Arg[Int]

While we can instantiate an URL via Router.url(details, 42 :: HNil), we do not know the names of the arguments.

I would argue against including the HTTP method in the route. This library may be used for other protocols like FTP which do not have the notion of GET/POST requests.

Furthermore, I would like to propose the following syntactic changes:

val details  = Router.route(Root / "details" / Arg[Int], (i: Int) => i.toString)
val userInfo = Router.route(Root / "user" / Arg[String], (u: String) => u)

val table    = Router.table(details :: userInfo :: HNil)
val run1     = Router.run(details, "/details/42") // "42"
val run2     = Router.run(table,   "/user/test")  // "test"

It is important that the compiler is able to infer the correct types, e.g. it should be possible to know beforehand that run1 and run2 are strings.

from trail.

anatoliykmetyuk avatar anatoliykmetyuk commented on May 28, 2024

val run1 = Router.run(details, "/details/42") // "42"

How would you turn that into a Finch/Scalatra/Play handler?

One thing we need to explore more is the case where we map from a route back to an URL. I would like to retain the possibility to do this for case classes with your proposed architecture. Let's consider this example:

We'll just need to have two functions: Route => (String => CaseClass) and Route => (CaseClass => String).

For me, there are two real problems:

  • Inability to integrate MetaRouter with other web frameworks
  • Case classes are heavy

Personally, for me the fact that you need to do at least three operations to create a new route (the route itself, the case class and the mapping between the two) overrides the benefits you named. It may seem like nothing, but every time you want to define a new route, you need to remember all these operations, and where the code for each should go. This creates mental overhead for such a purely declarative task. If a framework distracts me from my primary task, this is a big disadvantage. IMO case classes should be an option, not an enforced style.

from trail.

tindzk avatar tindzk commented on May 28, 2024

How would you turn that into a Finch/Scalatra/Play handler?

The definition of Route would stay the same, so the user could write a regular route and then call fold in place:

val r = Root / "details" / Arg[String] / Arg[Int]
val f = ...        // Rewrite route for Scalatra
Router.fold(r, f)  // "/details/:string/:int"

We'll just need to have two functions: Route => (String => CaseClass) and Route => (CaseClass => String).

What do you think about changing MappedRoute to:

case class MappedRoute[ROUTE <: HList, Args <: HList, Result](route: Route[ROUTE], f: Args => Result)

Then we could write:

val r = Root / "details" / Arg[String] / Arg[Int]  // R = Route[String :: Arg[String] :: Arg[Int] :: HNil]
Router.url(r, "test" :: 42 :: HNil)                // "/details/test/42"

case class Details(s: String, i: Int)
val m = Router.route(r, Details.apply)  // MappedRoute[R, String :: Int :: HNil, Details]

Now we could introduce the route product a little differently:

case class RouteProduct[Results <: HList, Routes <: HList](results: Results, routes: Routes)

This maps each result type to its corresponding route. An alternative would be to use an HMap here.

val p = Router.product(m :: HNil)  // RouteProduct[Details :: HNil, R :: HNil]

Finally, we can overload url with RouteProduct:

Router.url(p, Details("test", 42)) // "/details/test/42"

While doing my recent refactoring, I was wondering whether we should eliminate RouteData and the fill operation altogether. What is your take on this?

from trail.

anatoliykmetyuk avatar anatoliykmetyuk commented on May 28, 2024

What do you think about changing MappedRoute to:

Looks good.

If we have fold for a simple Route, we need a similar function for a MappedRoute. In case of Scalatra, we must first do fold on the Route to get "/details/:int", then apply another function to register a handler: get("details/:int") { handler }. The question is, do we need a framework support for this pattern (handler function composed with the fold catamorphism), or will it be convenient for the user to do perform the pattern by hand?

Finally, we can overload url with RouteProduct:

This def url considers case classes as a special case. How about something like this:

case class IsoMappedRoute[ROUTE <: HList, Args <: HList, Result](route: Route[ROUTE], f: Args => Result, g: Result => Args)

def Route.url(route: IsoMappedRoute, r: route.Result): String = {
  val args = route.g(r)
  Route.url(route.route, args)
}

def Route.url(route: Route, args: Args): String = ???

val route: IsoMappedRoute = Router.route(r, Details.apply, Details.unapply)
val path = Route.url(route, Details(42))

While doing my recent refactoring, I was wondering whether we should eliminate RouteData and the fill operation altogether. What is your take on this?

Probably; if one does need such a functionality, it is just a MappedRoute[Route, Args, FilledRoute](originalRoute, args => substituteArgsIntoRoute(originalRoute, args)).

By the way, all these Route, MappedRoute, and IsoMappedRoute should have the same parent and IMO these classes should be hidden from the user. Route can become RawRoute:

trait Route[A]
case class RawRoute[Path <: HList, Args <: HList] extends Route[Args]  // Raw route's ultimate goal is to extract arguments
case class MappedRoute[A, B](route: Route[A], f: A => B) extends Route[B]
case class IsoMappedRoute[A, B](route: Route[A], f: A => B, g: B => A) extends Route[B]

implicit val f1: Functor[Route] = ???
implicit val f2: Invariant[Route] = ??? // https://static.javadoc.io/org.typelevel/cats-core_2.11/0.9.0/index.html#cats.functor.Invariant

val route = Root / "details" / Arg[Int]

// Warning: Functor <: Invariant, collision will happen here
val mapped = route.imap(route, Details.apply, Details.unapply)

val fromDetails: String = Route.url(mapped, Details(42))
val toDetails: Details = Route.parse(mapped, "/details/42")

val handler: FinchHandler = Route.toHandler(mapped, folder, handler: Details => Unit)

Why invariant: one, two.

from trail.

tindzk avatar tindzk commented on May 28, 2024

Instead of encoding the operations in MappedRoute, we could also express the routing table as an HList and provide a parse method that can be used as follows:

val details = Root / "details" / Arg[String] / Arg[Int]
val routes  = details :: HNil
Router.parse(routes, uri).map {
  case RouteData(details, s :: i :: HNil) => renderDetails(s, i)
}

This way, we can retain the current functionality of mapping routes to case classes. Would that be a satisfying solution?

from trail.

anatoliykmetyuk avatar anatoliykmetyuk commented on May 28, 2024

Instead of encoding the operations in MappedRoute, we could also express the routing table as an HList

That's a nice idea: no need to define a class if you can do with HList.

This way, we can retain the current functionality of mapping routes to case classes. Would that be a satisfying solution?

Looks good.

from trail.

Related Issues (17)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.