Does anyone know if there’s something roughly equivalent in Crystal?
The api is something like:
class MyApp < Roda
route do |r|
r.get("/users") do
json User.all
end
# throw is called after these blocks are run
# so the request doesn't run more code
# than it needs to
r.get("/users/:id") do |id|
json User.find(id)
end
end
end
The closest would be to use unions and return a given type. However, you have to handle it for each method call if they are nested.
For certain cases, passing a Proc could do it too.
Yea, they are roughly equivalent in their usage but, in Ruby, they don’t deal with stacktraces and other error-related things which makes them much lighter. I will most likely go forward with raising errors for now.
I’m a bit confused by your response. Maybe we are both misunderstanding? In a typical routing library (kemal for example) when you declare a route it builds a list of routes and their connected block of code to run at compile time or at least before it handles a request. This is meant to all run when a request is given, which makes it different, hence the need for a throw.
I’m not sure I follow. If Roda knows of each route and the related block to handle a given request, what extra code would run? Or are you saying that without the throw Roda would check other defined routes, i.e. /users/:id even if the path was /users and matched the first route block?
I guess throw/catch could be implemented similarly to exceptions in Crystal, maybe, but without unwinding the stack. I’m not sure… But I do see its use case
As @Blacksmoke16 said, not sure to follow. Usually routers store the routes as Procs (in lists, and/or hashes, or other data structures). When a request is made, the corresponding Proc matching the path is executed - that’s it.
Maybe Exceptions can be used similarly, but it is not a good idea to use them as a regular basis. Exceptions, as their name says, should occur exceptionally (both for performance reasons and code clarity).
Crystal could take some inspiration from Swift, where errors don’t involve stack unwinding.
Ok, maybe that’s common in the Ruby world? In the Crystal ecosystem, all the routers I’ve seen work as I described above: find an object corresponding to a route path. A Proc (or more) can be stored inside, and possibly other things.
Here the answer can be there are other ways to design differently the router to achieve the same goal.
@j8r yes, I get that it’s different than the other routers. That and a couple other reasons are why I’m interested in it. I apologize for giving a bad example. I dont even think what I wrote is valid roda so that definitely caused some confusion.
I think it’s pretty firmly hammered into most of us that using gotos is bad, so I’d love to know about more scenarios where this throw/catch dynamic could be useful.
I guess we could almost implement this entirely in Crystal. For example:
require "benchmark"
class ThrowError(T) < Exception
getter value : T
def initialize(@value : T)
end
end
def catch(value : T) forall T
begin
yield
rescue error : ThrowError(T)
if error.value == value
return
else
raise error
end
end
end
def throw(value)
raise ThrowError.new(value)
end
# This is similar to `raise(Exception)` except that it doesn't compute a callstack.
def raise(exception : ThrowError(T)) : NoReturn forall T
unwind_ex = Pointer(LibUnwind::Exception).malloc
unwind_ex.value.exception_class = LibC::SizeT.zero
unwind_ex.value.exception_cleanup = LibC::SizeT.zero
unwind_ex.value.exception_object = exception.as(Void*)
unwind_ex.value.exception_type_id = exception.crystal_type_id
__crystal_raise(unwind_ex)
end
Benchmark.ips do |x|
x.report("raise/rescue") do
begin
raise "OH NO!"
rescue
end
end
x.report("throw/catch") do
catch(:foo) do
throw :foo
end
end
end
The only thing else we’d need to do is to avoid rescuing ThrowError when you do rescue e or rescue e : Exception. Maybe try to allocate even less memory, but I’m not sure it’s possible.
But then, I’m not sure this is all worth it. If it’s only going to be twice as fast, maybe it doesn’t make much difference.
An alternative solution would be to avoid setting callstack on a throw exception. If CallStack hand an initializer that doesn’t unwind and just creates an empty stack, we could just assign that value to the special exception type’s callstack property and the existing ::raise would work.
Then it’s really trivial to implement throw/catch in user code without having to touch any runtime details. We could consider adding it to stdlib or it can easily be implemented externally.
I’ve also thought about throw/catch in Crystal for this exact same reason — I’ve preferred Roda for small Ruby APIs for a long time. The result of that was this mixin (gist includes an example of usage). I’m currently using it in 2 production apps and it works pretty well.
One thing that led to this implementation, though, was realizing that throw is not actually that impactful for this use case in Crystal. Roda uses throw to throw a value back up the stack (not simply to avoid calling other route matchers) because of Rack’s interface being based on the return value of call(env), but Crystal’s HTTP::Server::Context gives you the request and response to work with as IO objects. So you can, for example, serialize DB records directly to JSON without having multiple representations in memory all at the same time (all the DB records, the intermediate hashes/arrays, and the JSON string output) as you would need to do with a Rack-based framework.
For rendering HTML content within a layout, I typically end up doing stuff like this (render calls are this macro to render an ECR template to the response):
def call(context)
route context do |r, response|
render "app_header"
# do routing in between to render content
render "app_footer"
end
end