Hacker News new | ask | show | jobs
by aerovistae 2042 days ago
This looks great in general, but does anyone else find Sorbet's signature formulation offputting? Feels cluttered and not at-a-glance readable.

> sig {params(name: String, id: String).returns(Integer)}

My ruby isn't quite good enough to parse the language constructs that make up that line, but it's not pretty. I guess....sig is a class method which takes a block....and params is a class method (Added to BasicObject or something, perhaps?) to which you can pass any number of keyword params, and which returns an object that has a return method, to which you can pass any object type. I guess I don't know why you need to call sig at all, as opposed to just params( args ).returns( type ). I also have no idea how the sig call gets associated with the method that follows it.

I just wonder if there was a cleaner way to phrase this that's still syntactically viable.

3 comments

The block passed to sig is evaluated in a different context, one where the local object has those methods. The methods aren't added globally, which is why you need the first method to switch the context. It's generally a good policy to avoid adding those class methods at the top level, which Sorbet does assiduously.

Sig also turns into a no-op if you have runtime verification turned off, which is another good reason not to call params right away, because (in ruby) you can ignore everything in the block if the block is not called, sort of like a debug macro in C, but you can't do that with a method that is called - it must evaluate its parameters.

That's super informative, thank you! Can you also add a word of explanation as to how the `sig` call gets associated with the method that follows it? What ties them together, is there some static parser?
Sorbet has its own written-in-C++ parser that does the actual parsing. At runtime, the sig call basically sets a flag for the next method that is defined which hooks into it to validate that the parameters passed in are as defined, and that the return value is as expected. I believe they're delving into the dark magic in the interpreter directly, the docs are here: https://sorbet.org/docs/runtime
For the runtime, there is not much dark magic. Each type that has an `extend T::Sig` has `method_added` hooks registered, which notifies Sorbet runtime whenever a method is defined on the type. When that `method_added` hook is called, Sorbet runtime uses the sig flag that you mention to associate the `sig` with the method definition that follows it.

It is, more or less, an implementation of this idea: https://yehudakatz.com/2009/07/11/python-decorators-in-ruby/

Yes indeedy, I'd consider `method_added` to be pretty dark magic though :)
The actual signature checking is complicated, but associating the sig call with the method is pretty simple in Ruby — you just hook `method_added`. (Of course the actual implementation in something like Sorbet is a lot more sophisticated than just `def method_added`, but that's the basics of how you make a method call alter the next method definition.)
I imagine they could (in theory) simplify it to

  sig(name: String, id: String).returns Integer
Losing the block maybe has some undesirable performance implications, but it looks a little nicer without the curly braces.
I think they did this originally but the performance implications were a deal breaker, and had to move to blocks. Iirc, which is questionable.
params in rails is pretty standard for controllers. wrapping it in something like sig makes the namespacing not collide.

https://github.com/search?l=Ruby&q=params&type=code