[HN Gopher] Lowtype: Elegant Types in Ruby
       ___________________________________________________________________
        
       Lowtype: Elegant Types in Ruby
        
       Author : birdculture
       Score  : 60 points
       Date   : 2025-11-28 16:50 UTC (4 days ago)
        
 (HTM) web link (codeberg.org)
 (TXT) w3m dump (codeberg.org)
        
       | graypegg wrote:
       | It's not always a good thing, but I love Ruby's ability to define
       | new runtime-valid syntax that looks pretty much native to Ruby
       | itself.                   def method(thing: String | "default
       | value")
       | 
       | the pipe operator seems to be defined here, as just a regular
       | method:
       | https://codeberg.org/Iow/type/src/commit/aaa079bf3dd2ac6b471...
       | the type gets picked out by the module included in the class you
       | want typechecked, which reads the default value from all methods
       | (which is the "real" ruby syntax here, where `thing` is assigned
       | a default value of the result of calling `String | "default
       | value"`) and uses that for type checking.
       | 
       | I like that over-flexibility... it's regularly too clever and
       | makes it difficult to follow the flow of an application, but I
       | like it all the same.
        
         | brudgers wrote:
         | Though the ability to arbitrarily create first class syntax is
         | why Rails is Rails, my favorite use case is FizzBuzz as a
         | property of Integers. YMMV.
        
           | shevy-java wrote:
           | One can be creative here, but at the end of the day, a lot of
           | what is generated, is just a waste of time. People love to
           | tinker. Then they move on, often before having written any
           | documentation. This is also a curse in ruby - people hate
           | writing documentation. There is a reason ruby lost to python
           | - over-tinkering and dislike of documentation are two smaller
           | reasons.
        
         | shevy-java wrote:
         | > it's regularly too clever and makes it difficult to follow
         | the flow of an application
         | 
         | Too much magic means nobody can understand the code anymore.
         | 
         | Most people who design DSLs don't understand this. It's a
         | problem in ruby - ruby's syntax is so flexible that one ends up
         | with kind of dialects in it.
        
           | kelseyfrog wrote:
           | I have something to learn here. How would you redesign the
           | DSL to be less magical?
        
       | ryukoposting wrote:
       | Mixed feelings here. Type annotations are a thing Ruby lacks,
       | that other languages have, that I like using in other languages.
       | Ergo, I'd like to have them in Ruby, right?
       | 
       | My knee-jerk reaction is "yes I'd like that" but when I pause to
       | think about how I actually write Ruby code... hmm. I tend to use
       | Ruby when its flexible syntax and type system are helpful. How
       | much would I actually benefit from something that _restricts_ the
       | flexibility of the type system?
       | 
       | Bear in mind, I'm not a "Ruby dev" per se. It's a well-loved tool
       | in my mostly firmware-focused repetoire. I use it for little CLI
       | tools, and toy game engines too (mri embeds into C really
       | cleanly). Fun little things that most folks would use Python for,
       | I just like Ruby better.
        
         | vidarh wrote:
         | Ruby has had "officiak" type annotations since 3.0.0 via RBS.
         | 
         | Exactly because of the concerns you described, RBS originally
         | used only separate files for the type annotations, so it can be
         | selectively and gradually applied. You can add Ruby signatures
         | inline as comments as well, but frankly both options looks
         | ugly, and so does many of the alternatives like Sorbet
         | signatures.
        
           | shevy-java wrote:
           | In fairness: RBS sucks though. This one here is a bit better
           | than RBS.
        
         | jonathaneunice wrote:
         | I had exactly this reaction when gradual typing came to Python.
         | "Do we really need this??"
         | 
         | But over time, I've grown to love it. Programming is
         | communication--not just with the machine, but with other
         | developers and/or future me. Communicating what types are
         | expected, what types are delivered, and doing so in a natural,
         | inline, graceful way? Feels a big win.
        
           | shevy-java wrote:
           | But you use types not to communicate with other people - you
           | use them to give more hints to the python interpreter.
           | Otherwise you could use comments. :)
        
             | zem wrote:
             | type annotations in python are essentially structured
             | comments with special syntactic support and runtime
             | introspection facilities (i.e. you can get at the
             | annotations from within the code). they are explicitly not
             | "types" as far as the interpreter is concerned, you can say
             | e.g. `x: str = 42` and python will be fine with it. the
             | value comes from tooling like type checkers and LSPs that
             | work with the annotations, and from metaprogramming
             | libraries like dataclasses and pydantic that introspect
             | them at runtime and use them to create classes etc.
        
         | shevy-java wrote:
         | Completely agree with you.
         | 
         | For some reason some people want to slap down types onto EVERY
         | language. I call them the type-o-maniacs.
        
         | skipants wrote:
         | I think you're 110% on the nose. Types in Ruby really depends
         | on the use-case. I don't think your use cases fit the bill and
         | types would be annoying if anything.
         | 
         | I work on big Ruby on Rails monoliths and I curse every day
         | that I don't have types to work with. It's almost a requirement
         | for large refactors.
        
         | pizza234 wrote:
         | This sentiment reflect the type of project worked on - small
         | ones. As projects get bigger, more type information gets lost,
         | and that's why it needs to be compensated, typically via
         | automated (unit) testing.
         | 
         | After having worked with gradual typing, unless the application
         | is very disciplined, IMO automated testing is not enough to
         | document the code, as Ruby makes it very easy to use flexible
         | data structures which very easily become messy.
        
       | Lio wrote:
       | I love these new approaches to type checking such as this and
       | Literal[1]. I think they really show how far we could go with
       | runtime ruby syntax.
       | 
       | For both though I have questions:
       | 
       | A. How do I use this day to day to improve my tooling and
       | developer experience?
       | 
       | B. If at some point in the future I decide to get rid of this how
       | easy is it to eject?
       | 
       | I've seen too many adandoned dependencies over the years to trust
       | anything I can't easily remove when it's time to upgrade.
       | 
       | These runtime typing efforts look nicer than Sorbet but, as far
       | as I can see, you still have to have complete test coverage to
       | trigger runtime checks if you want to spot correctness issues
       | before you deploy into production.
       | 
       | Sorbet doesn't have that problem right now. Maybe something
       | clever using Prism might be a way round that?
       | 
       | 1. https://literal.fun/
        
         | dmux wrote:
         | >These runtime typing efforts look nicer than Sorbet but, as
         | far as I can see, you still have to have complete test coverage
         | to trigger runtime checks if you want to spot correctness
         | issues before you deploy into production.
         | 
         | I think you're right, and if that's the case, aren't these
         | libraries (Lowtype, Literal) more akin to Design by Contract
         | mechanisms?
        
       | thiago_fm wrote:
       | They should integrate this to ruby-core and make it even better
       | by changing the parser and making it faster in terms of
       | performance, as optimized as it can.
       | 
       | But I have a hard time believing ruby-core will want to hear
       | community feedback... people have been talking about this for
       | ages... Ruby is omakase?
       | 
       | RBS and Sorbet suck. One is very limited, the other isn't part of
       | ruby-core and makes you rewrite the function arguments again,
       | similar to Java's annotations... Doesn't look like Ruby at all,
       | or DRY, mostly like a workaround!
       | 
       | LowType is what it should have been -- hard to believe we are in
       | 2025 and we still don't have a decent, programmer-friendly
       | solution in ruby-core.
       | 
       | Meanwhile Python has it right since a long time. No wonder it is
       | so stagnated with people going for other stacks.
       | 
       | Ruby is slowly becoming what Perl did, a very niche language.
        
         | shevy-java wrote:
         | I agree with your analysis that RBS and Sorbet suck.
         | 
         | I disagree that this here should be part of ruby-core, largely
         | because I don't think any of this type madness should
         | infiltrate ruby.
         | 
         | Ruby does not necessarily follow "DRY" - that appears to have
         | been coined by either DHH or the pickaxe guys. More than one
         | way to do it, is kind of orthogonal to DRY too. Note: I do not
         | disagree that DRY has value. What I am saying is that ruby's
         | design does not necessarily follow DRY as a guiding principle.
         | 
         | > hard to believe we are in 2025 and we still don't have a
         | decent, programmer-friendly solution in ruby-core.
         | 
         | I do not think the year has anything to do with it. If they
         | suck - and types suck - then they should not be in ruby core. I
         | understand you have another opinion, but that's the beauty - we
         | have orthogonal opinions there. One says must be part of ruby
         | core; the other says should not be part of ruby-core ever, no
         | matter the year.
         | 
         | > Meanwhile Python has it right since a long time.
         | 
         | The question is: how many use it there?
         | 
         | > No wonder it is so stagnated with people going for other
         | stacks.
         | 
         | Lack of types aren't the reason ruby declined. That is a wrong
         | assumption here.
         | 
         | > Ruby is slowly becoming what Perl did, a very niche language.
         | 
         | That is true, but not due to lack of types. Python without
         | types would still be at rank #1 at TIOBE for instance. Your
         | analysis is simply wrong here.
        
         | rco8786 wrote:
         | Ruby needs a Typescript. Leave ruby-core as it is. Let us write
         | Ruby with type annotations, where the compiler does type
         | checking, strips the annotations, and leaves us with plain
         | runnable Ruby.
        
       | theoldgreybeard wrote:
       | What's the advantage of using this over RBS?
        
       | ceritium wrote:
       | I love it, I liked what dry-initializer and dry-struct do, and I
       | wanted something similar but simpler than RBS or Sorbet.
       | 
       | I tried once myself to implement something like lowtype, but
       | without success.
        
         | hakunin wrote:
         | I built portrayal[1] (a much simpler replacement for those dry
         | libs), and was also experimenting[2] with runtime-enforced
         | types based on this lib.
         | 
         | My general thoughts is that declaring types in Ruby is
         | unnecessarily complicated, because you're basically just
         | running values through pieces of boolean logic, and nothing
         | else. Might as well make that explicit, which is what my
         | experiment did. I didn't however publish the types library, but
         | the concept was proven.
         | 
         | [1]: https://github.com/maxim/portrayal
         | 
         | [2]: https://max.engineer/portrayal-guards-poc
        
       | shevy-java wrote:
       | Looks a bit python inspired.
       | 
       | Also looks awful - not as bad as RBS but awful still.
        
       | jez wrote:
       | It's an interesting approach. From my skim, the way it works:
       | 
       | 1. Parse the files with a Ruby parser, collect all method
       | definition nodes
       | 
       | 2. Using location information in the parsed AST, and the source
       | text of the that was parsed, splice the parameters into two
       | lambda expressions, like this[1]:                    "->
       | (#{method_node.parameters.slice}) {}"
       | 
       | 3. Evaluate the first lambda. This lets you reflect on
       | `lambda.parameters`, which will tell you the parameter names and
       | whether they're required _at runtime_ , not just statically
       | 
       | 4. In the body of the second lambda, use the `lambda.parameters`
       | of the first lambda in combination with
       | `binding.get_local_variable(param_name)`. This allows you to get
       | the _runtime_ value of the statically-parsed default parameters.
       | 
       | This is an interesting and ambitious architecture.
       | 
       | I had though in the past about how you might be able to get such
       | a syntax to work in pure Ruby, but gave up because there is no
       | built-in reflection API to get the parameter default values--the
       | `Method#parameters` and `UnboundMethod#parameters` methods only
       | give you the names of the parameters and whether they are
       | optional or required, not their default values if they are
       | optional.
       | 
       | This approach, being powered by `binding` and string splicing,
       | suffers from problems where a name like `String` might mean
       | `::String` in one context, or `OuterClass::String` in another
       | context. For example:                   class MyClass
       | include LowType           class String; end           def
       | say_hello(greeting: String); end         end
       | MyClass.new.say_hello(greeting: "hello")
       | 
       | This program does not raise an exception when run, despite not
       | passing a `MyClass::String` instance to `say_hello`. The current
       | implementation evaluates the spliced method parameters in the
       | context of a `binding` inside its internal plumbing, not a
       | binding tied to the definition of the `say_hello` method.
       | 
       | An author could correct this by fully-qualifying the constant:
       | class MyClass           include LowType           class String;
       | end           def say_hello(greeting: MyClass::String); end
       | end              MyClass.new.say_hello(greeting: "hello") # =>
       | ArgumentTypeError
       | 
       | and you could imagine a Rubocop linter rule saying "you must use
       | absolutely qualified constant references like `::MyClass::String`
       | in all type annotations" to prevent a problem like this from
       | happening if there does not end up being a way to solve it in the
       | implementation.
       | 
       | Anyways, overall:
       | 
       | - I'm very impressed by the ingenuity of the approach
       | 
       | - I'm glad to see more interest in types in Ruby, both for
       | runtime type checking and syntax explorations for type
       | annotations
       | 
       | [1]
       | https://codeberg.org/Iow/type/src/branch/main/lib/definition...
        
         | radiospiel wrote:
         | > how you might be able to get such a syntax to work in pure
         | Ruby, but gave up because there is no built-in reflection API
         | to get the parameter default values
         | 
         | what I have done successfully here
         | https://github.com/radiospiel/simple-service/blob/master/lib...
         | is to install a TracePoint which immediately throws, and then
         | call the method. The tracepoint then receives the value of the
         | default arguments.
         | 
         | Not pretty, and I wouldn't run this in production critical
         | parts of a system, but it works.
        
       | jihadjihad wrote:
       | So, it's like Python? If only Ruby had something like Pydantic,
       | too.
        
       | jaredcwhite wrote:
       | A side comment: I love that this is on Codeberg. I'm seeing more
       | and more projects migrating there or originating there to begin
       | with, and it's awesome. (And I've been doing that as well.)
       | 
       | More please! =)
        
       | inopinatus wrote:
       | Any type system for Ruby objects that isn't based on
       | message/method response signature (i.e eigenclass), but instead
       | relies on module ancestors (or worse, class), is fundamentally
       | misaligned with the architecture of the language.
       | 
       | A remarkably high proportion of folks that self-identify as Ruby
       | aficionados will make this error.
       | 
       | I'm not even talking about respond_to? / method_missing tricks.
       | If an object prepends a module to its singleton to become a proxy
       | for something else, or a library offers refinements (which are
       | lexical) so its clients may declaratively align method
       | expectations, or (bad style, looking at you Rails, but
       | nevertheless) just evals whatever method definitions it likes
       | after messing with the three implicit contexts, then it should
       | still pass.
       | 
       | Leaning on class and mixin is just _one_ of the ways in which
       | Ruby object anatomy evolves, and although that's a familiar
       | default to many, there are other styles in common use, especially
       | in framework /library code. Any app relying on such a framework
       | may either not pass, or may silently bypass, such type checking.
       | And I foresee a myriad of edge cases if one slings around
       | closures as a habit (why, yes I do).
       | 
       | Symbolic message passing is the basis of object collaboration in
       | Smalltalkish OO, and in Ruby class/mixin is merely one of the
       | ways to get there. The conceptual gap means that what you get
       | from oversimplification isn't just a half-baked type system, it
       | also becomes an incomplete straitjacket for style.
       | 
       |  _Edit to add_ : after reviewing the internals of this library,
       | note that for a dash of irony, it is indeed prepending modules to
       | class singletons to redefine methods with proxy wrappers. That is
       | to say, it could not type-check itself.
        
       | rco8786 wrote:
       | This is super interesting. Is it possible to turn the runtime
       | checks off in certain environments? I would love to have these
       | type checks just happen during unit tests, for example, and then
       | no-op in production so that performance is unimpacted.
        
       ___________________________________________________________________
       (page generated 2025-12-02 23:01 UTC)