4 ms·
Lowtype: Elegant Types in Ruby
- graypegg 11mo agoIt'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/aaa079bf3dd2ac6b471facc3e26f38039172382a/lib/expressions/type_expression.rb#L26 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 11mo agoThough 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 11mo agoOne 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 11mo ago> 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 11mo agoI have something to learn here. How would you redesign the DSL to be less magical?
- ryukoposting 11mo agoMixed 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 11mo agoRuby 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 11mo agoIn fairness: RBS sucks though. This one here is a bit better than RBS.
- jonathaneunice 11mo agoI 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 11mo agoBut 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. :)
- Lio 11mo agoI 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/ https://literal.fun/
- lowtype 11mo ago[dead]
- dmux 11mo ago>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 11mo agoThey 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 11mo agoI 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 11mo agoRuby 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 11mo agoWhat's the advantage of using this over RBS?
- ceritium 11mo agoI 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 11mo agoI 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 https://github.com/maxim/portrayal [2]: https://max.engineer/portrayal-guards-poc https://max.engineer/portrayal-guards-poc
- nona 11mo agoFirst time I hear about this, I really like your guard approach. It's just quite plain ruby. I'd be interested in hearing your opinions on other runtime approaches like contracts.ruby, literal.fun and LowType of course.
- hakunin 11mo agoAppreciate it. I wrote this controversial thought[1] once, but for what it's worth, it applied to me just as much as to anyone else. Projects like these type gems are incredibly fun and satisfying to build. Your vision is clear, you've seen types before, you're proficient enough with Ruby to do clever things. The work seems cut out for you. Go nuts! Problem is, these kinds of solutions (I also see this in "service objects" world) take Ruby away from you, and offer you a whole new vocabulary. With time I started appreciating libraries that avoid solving a problem that plain Ruby can solve, because Ruby is incredibly clear and concise already. If you leave more opportunities for people to write plain Ruby, you will solve everything with much less library code. I think that's where the fun of building goes against the end developer experience. Builders love "owning" everything. E.g, "No, don't write `&&`, I have a special way for you to compose types!" These are general thoughts, but I'm happy to get concrete with specific examples too. [1]: https://x.com/hakunin/status/1960016559750914097 https://x.com/hakunin/status/1960016559750914097
- shevy-java 11mo agoLooks a bit python inspired. Also looks awful - not as bad as RBS but awful still.
- jez 11mo agoIt'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/definitions/redefiner.rb#L132-L136 https://codeberg.org/Iow/type/src/branch/main/lib/definition...
- 11mo ago
- jihadjihad 11mo agoSo, it's like Python? If only Ruby had something like Pydantic, too.
- jaredcwhite 11mo agoA 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 11mo agoAny 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 11mo agoThis 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.
- Alifatisk 11mo agoHow often do they release new versions? Looking forward to the rbs integration