[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)