I’m interested in low-magic (little opinionated, minimalistic) web frameworks. They give me a way to respond to endpoint requests with ByteStrings, and little/nothing else.
The ones I know about are Snap, Scotty, Spock. Are there any others?
Which are the most maintained these days? Are they (in 2023) suitable for heavy-duty production software?
I really like the look of Twain, but am a bit worried at the routing performance. From what I can tell, its basically brute force matching over all routes, rather than being a bit more intelligent and building a trie. It’s certainly simple, but feels unfortunately wasteful. I’d love something that looked like twain but had a tiny bit more routing smarts. Maybe that can just be layered on top though
I don’t know if a trie is the best way to approach this. It’s certainly ideal when you have a lot of paths, but for a small app, you can probably get away with something like
grouped :: (Request -> Maybe Request) -> (Request -> Request) -> [Middleware] -> Middleware
grouped match restore ms app req = case match req of
Nothing -> app req
Just req' -> foldr ($) ms (\req'' -> app (restore req'')) req'
type RoutePrefix = ...
stripPrefix :: RoutePrefix -> Request -> Maybe Request
stripPrefix = ...
restorePrefix :: RoutePrefix -> Request -> Request
restorePrefix = ...
prefixed :: RoutePrefix -> [Middleware] -> Middleware
prefixed prefix = grouped (stripPrefix prefix) (restorePrefix prefix)
main :: IO ()
main = do
run 8080 $
foldr ($) (notFound missing) routes
routes :: [Middleware]
routes =
[ get "/" index
, post "/echo/:name" echoName
, prefixed "/foo"
[ get "/" fooIndex
, post "/echo/:name" fooEcho
, get "/bar" fooBar
]
]
I think if you do anything much more intelligent, you lose the simplicity of everything being a Middleware.
Couldn’t the run function merge all the routing middlewares into a trie under the hood? Then I think you can just keep the same interface that Twain already has.
The run function isn’t a part of Twain. In this case (because I’ve just pasted part of the example in the readme with some changes), it’s from Warp, but it could be from anything that can serve a WAI application. The Middleware type used is also not from Twain, it’s from WAI, and as a consequence there is no way to see if a Middleware “matches” a given route. Twain is a very thin layer on top of WAI.
I use lucid2 for writing html. I don’t use an eDSL for CSS & JS because I often times have to compare code snippets and doing the translation in my head is harder. Moreover this prevents fluid collaboration with UI experts.
Why is there a separate lucid2 package on Hackage?
What about caching (as Snap/Heist has), performance differences? TBH maybe I don’t need any caching. If someone can tell me lucid is just as fast as any HTML output from a Node.js program, I’m fine.
Why is there a separate lucid2 package on Hackage?
Separate from what? lucid2 generates HTML.
What do you mean when you say “as fast as any HTML output from a Node.js program”? There are a lot of ways to generate html from Node, each with different performance characteristics, and the same is true for Haskell.
If you mean why separate from lucid (1), then it’s because the author believes in the Immutable Publishing Policy (which is an idea with a lot of merit in my opinion).
There are absolutely things you could do, but at this point I don’t really think it’s Twain anymore. I think that’s fine, Twain doesn’t really need to change, it’s just a fairly fundamental limitation of this type of chaining-middleware framework. For some applications I’m sure that’s absolutely fine.
Re routing: A path-element based trie is a pretty obvious optimization, and shouldn’t be difficult to implement. You would just need a “helper” middleware that takes a dictionary of “subdirectories” to sub-handlers, inspect the request to pop off the first path element, look up the matching sub-handler (and serve a 404 if it doesn’t exist), and delegate to that sub-handler. And then you can nest those dispatchers for whatever tree depth your routes need. I’ve done this against plain WAI before, and it works fine. The trickiest part is deciding how you do the “popping” - you can either actually modify the request, so that for each nested sub-handler, the first element in the path it processes is the one that it cares about; the advantage is that the sub-handler is blissfully unaware of the part of the route that it doesn’t need to care about, but the downside is that if you’re serving something that needs to link to other resources, then absolute links you generate based on the “current path” as per the HTTP request will be wrong. Alternatively, you can add the handler’s local path to its configuration, and make it skip that prefix when processing a request - downside of that is that it makes the setup a bit awkward.
Re caching: you can just make a caching middleware and run your WAI application behind it for full-response caching, and this should give you the same kind of performance that you’d get from any other web framework’s full-response caching. IIRC, there are ready-to-use packages on hackage for that, but don’t quote me on that. If you want to cache fragments, then you will need a solution specific to whatever HTML rendering system you use anyway, but whatever you are using, integrating it with WAI (or Twain, or Scotty, or whatever you end up using) should be fairly straightforward.
@Kleidukos@gilmi, etc.: Given the posts that came later into this thread, about Twain’s potential performance issues with routing, and (what looked to me like) “hacks” that are currently needed to fix them, would you still recommend Twain?
Maybe these performance issues won’t be a problem unless for very large (e.g. thousands of?) routes? Please share more details if you have time, @ocharles etc.