Skip to content
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

Static types? #1048

Open
camertron opened this issue Aug 25, 2021 · 3 comments
Open

Static types? #1048

camertron opened this issue Aug 25, 2021 · 3 comments
Labels
help wanted

Comments

@camertron
Copy link
Contributor

@camertron camertron commented Aug 25, 2021

Feature request

Hey view_component people! I'm filing this issue to solicit opinions on whether we should adopt static type checking into view_component and which system(s) we should consider. Off the top of my head, there are three options (but please feel free to suggest others):

  1. RBI + Sorbet
  2. RBS + some type checker
  3. rux

Some definitions:

  • Sorbet: Stripe's type annotation and static type checking library.
  • RBI: So-called "Ruby Interface" files used by Sorbet. Generally .rbi files are used instead of having annotations in the code itself, i.e. when supplying types for a 3rd-party lib, etc.
  • RBS: A language for defining Ruby types written by the Ruby maintainers themselves.
  • rux: A Ruby transpiler I wrote that's capable of generating RBI files from inline type annotations.

Motivation

Type information can be extremely handy for developing and using Ruby libraries. Here are the major benefits as I see them:

  • Increased confidence in code correctness. Type checking can catch subtle (or even overt) bugs.
  • Helpful for IDE-level tooling like language servers and intellisense/autocomplete.
  • Makes code easier to understand because types help indicate intent (i.e. "is this thing a string or...?" vs "ah, this thing is a string, that means I can do x with it'). Leads to faster onboarding of new contributors.

Examples

In each of these examples I'm going to be annotating ViewComponent::Base#render_in.

Sorbet + RBI

Sorbet type annotations are written in Ruby and defined in the code itself. Separate .rbi files can be stored in the project's sorbet/rbi directory and are generally used to specify types in 3rd-party libraries, etc. RBI files are also Ruby code, but with method bodies and such missing.

# lib/view_component/base.rb
module ViewComponent
  class Base < ActionView::Base
    extend T::Sig

    HTML = T.type_alias { String }

    sig {
      params(
        view_context: ActionView::Base,
        block: T.proc.returns(HTML)
      ).returns(HTML)
    }
    def render_in(view_context, &block)
      # implementation here
    end
  end
end

Notice the nice little type alias for returning strings of HTML :)

Sorbet is perhaps the most mature of the three systems, but there are a couple of drawbacks in my opinion:

  • The sig blocks distract from the method bodies. I find my eyes having a harder time finding the method I'm looking for because the sigs get in the way.
  • sigs are executable Ruby code, meaning the sig method has to be defined on the class when the class is loaded. Sorbet comes with runtime type checking as well as static, but if you don't want it you have to ship your library with the sorbet-runtime gem anyway and disable the checks. This feels ugly and cumbersome to me. Why should I have to ship a dependency with my lib that effectively doesn't do anything?

RBS

RBS works differently than Sorbet in that all type definitions live outside the code in separate .rbs files. As of now, there's no type checker available that can actually check RBS-defined types, but the Stripe folks say Sorbet will eventually be able to use them.

# rbs/view_component/base.rbs
module ViewComponent
  class Base < ActionView::Base
    def render_in: (view_context: ActionView::Base) { () -> String }
  end
end

It's nice that all the types can be defined inline, but there are some drawbacks:

  • Separate .rbs files mean having to keep type info perpetually in sync with Ruby code, which seems like a huge hassle and something we're likely to forget. It's reminiscent of the old SASS/LESS days where you'd have both a .sass and an .html.erb file with identical hierarchies that you'd have to keep matched up. Ugh.
  • I personally don't like that wonky block syntax.

Rux

Rux was originally written as a JSX-inspired way to render view components, but is becoming a Ruby transpiler framework of sorts. I have a branch that can extract inline type annotations and spit out .rbi files that can then be type checked by Sorbet. Although totally accidental, rux and RBS use very similar syntax. The difference of course is that RBS types are defined in a separate file while rux types are specified inline. Rux types are heavily influenced by Python's mypy.

NOTE: Right now type aliases aren't supported, but they wouldn't be hard to add.

# lib/view_component/base.rux
module ViewComponent
  class Base < ActionView::Base
    def render_in(view_context: ActionView::Base, &block: Proc[String]) -> String
      # implementation here
    end
  end
end

Rux files are transpiled to Ruby using the ruxc tool, or optionally automatically on require.

Please let me know what you think!

@camertron camertron added the help wanted label Aug 25, 2021
@joelhawksley
Copy link
Contributor

@joelhawksley joelhawksley commented Sep 10, 2021

@camertron thanks for bringing this up! I've been wondering how types might benefit both writing ViewComponent itself and consumers of the framework. I'd be interested to hear your thoughts on using static types when authoring ViewComponents.

As for which tool I'd prefer, I'm not sure. I think you've assessed our options here fairly. I don't know if we have any sort of preference internally.

I know we've considered investigating autocorrect/intellisense for Primer ViewComponents. Perhaps we could start with an experiment that adds that functionality there? I believe it might be possible with our YARD annotations as well.

@camertron
Copy link
Contributor Author

@camertron camertron commented Nov 11, 2021

Yeah, our YARD annotations could be a great stepping-stone, and I'd love to experiment with a prototype 😄

Here are a few other thoughts from one of our Slack conversations:

  1. How would we type system_arguments? They can literally contain anything. Sorbet lets you specify a single type for keyword arguments, i.e. Float or String , but I don't think that's powerful enough for our use case. RBS has a record type that does what we want, but it's not clear if it works with keyword args.
  2. We define a lot of methods dynamically. Fortunately Sorbet and RBS both support putting type definitions in a separate file, meaning a tool could auto-generate the signatures for them. I don't think such tooling exists right now, so we'd have to write it ourselves.

@camertron
Copy link
Contributor Author

@camertron camertron commented Nov 11, 2021

Oh, and I was wrong that there's no type checker that uses RBS files. Check out steep!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted
Projects
None yet
Development

No branches or pull requests

2 participants