Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Ruby adds a core class called Data to represent simple immutable value objects (saeloun.com)
293 points by feross on Nov 29, 2022 | hide | past | favorite | 181 comments


As I type this, every top-level comment here is a criticism or a nitpick. A cool new feature is being added to Ruby. People have done difficult and meaningful work that benefits others. And this is the response here.

I'm psyched for this feature. As someone who has worked primarily with Ruby and Elixir, I really appreciate the design and safety affordances of immutable data. I'm glad there's a bit more immutability in Ruby and that, in general, the language keeps progressing. Thank you Ruby language devs.


Yeah, I'm happy to see this. Every Ruby shop I've worked for has some kind of hand-rolled immutable struct, possibly in addition to various implementations provided by libraries. The backwards compatibility reasoning for not adding an immutable flag to Struct (my first instinct) makes sense. Struct has always been kind of wonky anyway. Getting both positional and kwargs for free is great. I also appreciate that they kept the implementation minimal for now. The pull request shows a very elegant way of implementing default arguments using only language features, which is a good sign:

      Measure = Data.define(:amount, :unit) do
        def initialize(amount:, unit: '-none-') = super
      end


The libraries that provide immutability often provide stronger enforcement than this new Data class. I like the idea of something like this as part of Ruby core, but this was a chance to do it right and I don't think they took it far enough.


You're thinking deep immutability? I'm not sure that's ever going to make sense to bundle with the language. Too many caveats.

I'd rather see incremental additions to this minimal Data API than trying to do too much at once. Lots of love for the Ruby dev team but they don't always have the clearest idea of what problems their users are trying to solve.


Yes, that's exactly what I was thinking. The problem is that without deep immutability this is still open to abuse/miss-use. When I'm creating an immutable struct I'm trying to prevent future developers from accidental mistakes.


Reinventing the wheel is cancer to support. There's already Concurrent::ImmutableStruct.


Ruby is one of those things people love to hate.

I've worked with Ruby for nearly ten years and have been hearing some variation of "Ruby is dying" since forever.


Yeah, I do not even get why people hate Ruby so much. They do rarely even attack the actual weaknesses of Ruby but instead goes on about Rails specific stuff, the slow performance (a Ruby < 1.9 issue, these days it is as fast as Python if not faster) or just imagined stuff which has never been true.

Ruby is a flawed language (I especially dislike the lack of real namespaces) but the hate it gets is often not based in reality.


I associate Ruby with a particular era of hipster web and app development, ~2007-2010. I don't know why but it's not quite attractive.

That said the serious reason I don't try it out is because I fear it would be like Python, and I don't need two Pythons in my life. If Ruby had been dominant I'd likely be using Ruby and thinking the same of Python.


    a particular era of hipster web and app 
    development, ~2007-2010. I don't know why 
    but it's not quite attractive.
That was a really fun era.

~1997-2007 was fun but it was a mess. It seemed like every project was its own bespoke "framework" and everything was a Java or PHP spaghetti monstrosity.

2007-2014 was the era when "majestic monolith" MVC frameworks with a strong emphasis on automated tests really began to coalesce. Rails led the way here, I think. It was a brief island of sanity if you ask me. MVC may not be perfect but it's a fine choice most of the time for anything CRUD.

I suppose there was kind of a "hipster" aesthetic around Ruby/Rails (Ruby devs kind of prided themselves on being quirky or whatever, as opposed to staid corporate Java drones, I guess?) but I did not find it to really be a factor.

2015-present has been a nightmare. Nearly every web application is now two apps: front and back-end. For apps with highly interactive UIs this is an obvious win but for the vast majority of web applications it has been a disaster. User-facing performance of the modern web is awful and I have never felt less productive as a developer. Every developer thinks they need to do things "like Amazon" or "like Facebook" and just.... no. The Javascript community has absolutely lost all connection with sanity and the era of the full-stack developer is over.


During that timeframe, I dismissed Ruby/Rails for similar reasons.

First of all, regardless of what everyone says regarding how "inovative" Rails was, Tcl and Python did it first, with AOLServer and Zope.

During the .com wave, I was part of a startup with a product for CMS development, that was basically inspired from AOLServer, although we did our own thing.

It supported Windows 2000/NT, Aix, HP-UX, Solaris, Linux (only for dev purposes), SQL Server (Sybase and Microsft variants), Informix, Oracle, Access (only as demo concept), and we were in the process of adding DB 2 support as well.

We went down in the .com crash, and Lisbon isn't SV, so hardly anyone heard of us, but it doesn't change the fact we were already better than Rails v1.0 in 1999.

Second, I know Python since version 1.6, so Ruby hardly brings anything other to the party than a Smalltalk inspired workflow, without the IDE experience.


Yeah it is definitely from that era 2007~ ruby though is like the friendly python. Dependency’s usually just work and it doesn’t get mad at your style - it’s very accepting and importantly IMO the stack traces are much easier to use to identify the root cause of a bug compared to python …


I do think the same thing about python. The occupy a ver similar space and I already know ruby, so I have very limited incentive to start doing stuff in python. But that doesn’t mean I dislike it, or believe it’s a bad language.


Having worked in PHP and Ruby, the parallels to the hate is obvious to me. Enough people have done enough meaningful work in both languages that any possible downside or nitpick has been discovered and voiced.

It's like when a game/band/tv show becomes popular, the more popular the more toxic the community becomes, and the louder the hate seems to be. I don't want to pull names out of hats, but pick any of the most used languages and you'll be able to find people banging on about objectively bad parts of the language, and yet they're still the most popular languages.

Admittedly, I don't really prefer Ruby over other languages, but just like PHP I wish people would stop being so dramatic over it. It's plenty good enough to get shit done, if someone likes working with it power to them.


Is Ruby that much hated?

Let’s make a very simple naive test using Google search, "I love Ruby" matches 98,400 results, while "I hate Ruby" returns 7,740 results (from my current terminal).

There is even a book named "I love Ruby" it seems.[1] Compare that to the existence of "The UNIX-HATERS Handbook".[2]

Can you be more precise about what features you miss in Ruby modules compared to "real namespaces"?

[1] https://i-love-ruby.gitlab.io/ [2] https://en.wikipedia.org/wiki/The_UNIX-HATERS_Handbook


I don't know, to me Ruby just feels like a slightly more user friendly PHP.

Vaguely similar in performance to Python and PHP (not an order of magnitude off), slower than Java and .NET (for typical web based workloads with mainstream frameworks, .e.g Rails vs Django vs Laravel vs Spring Boot vs ASP.NET), still good enough for most web projects out there.

Even Rails just seems like your average flagship framework, if not an entire platform - certainly on the heavy side, with its own quirks and opinions, but definitely a safe option for getting things done.

It's jarring to see Ruby's popularity be on the decline, though you could also say the same about PHP in many cases.


There are namespaces, there just aren't standards like Python.

And there aren't many good Ruby developers. There are too many noobs who write Ruby code that make my eyes bleed. I know, because I have to see this at work and they're supposedly "rockstar" developers. Ha! I swear most so-called "engineers": 0. don't grok software engineering, 1. weren't formally-trained in software engineering AND computer science, and 2. happily push shitty code that there will be no incentive to refactor through automated means.

If you want to enforce coding standards in Ruby, you better learn LISP / S-expressions, because that's what Rubocop uses.


There are modules, but they aren't real namespaces.


https://www.youtube.com/watch?v=06x8Wf2r2Mc - at 10:52 Simon Peyton-Jones talks about life of programming languages.

Ruby, having at least million users around the world, is in immortal category.

What fascinated me in the original post is "...data model on the principles of the Value Object, introduced by Martin Fowler."

I mentioned Haskell because there is a long, long history of the use of immutable objects in computing science, dating back to Church and Turing's communication. The immutability is what differentiates lambda calculus and Turing machine.

That is why I remembered Haskell and the video above.

Yet, it is "introduced by Martin Fowler," for some reasons.

This is very sad.


Those Java Enterprisey guys as Martin Folwer is the reason i stop caring about Enterprise. Shitty stuffs get complicated for no reason.


People love to bash Java for what has been typical enterprise architectures since large corporations have used computers.

Or do people think that before Java we didn't have any architecture astronauts on large corporations?!?


Hacking on Java frameworks is mostly a pain. Both from IDE tooling (too much RAM, configuration fighting,...) to the documentation. Basically it made simple problems harder for just "JAVA" reason.

For comparison, i've spent many months to hack on Java framework in a company before to deliver a hackable feature.

By the same time, i also released 3 production applications from scratch with Rails.

The issue is not Java is impossible to have such productivity, it's the mindset, the culture issues from Enterprisey people.


Again, that culture was already there decades before Java came to be, COM, SOM, CORBA, Taligent, ER model, Booch method, Rational Unified Process, Structured systems analysis and design method. Yes, I am an old dog at this game.

Had Ruby and Rails been embraced at the same scale, you can bet there would exist a comparable flavour of Ruby EE.


As i said, it's about the mindset that matters (rather than throwing me a bunch of design patterns).

If you're at work and want to get shit done fast for the salary reason: There's no reason NOT to use Rails.

If you want to go enterprise, use something lightweight and modular, composable instead. Throwing a bunch of inheritable BeanClass serves no purpose on the job.

UNIX philosophy came long ago, before JAVA.


UNIX philosophy is cargo cult from a book worshiped by FOSS folks.

CORBA was born on UNIX, original implementation targeted C, C++ and Smalltalk.

Java was years away to be created, again by a UNIX company.

Java EE, was reborn from an canceled Objective-C project created by Sun and NeXTSTEP, again two UNIX companies.


No, unix philosophy is what make things work together as i see. A big mess of inheritance tree doesn't solve large scale problem. That's the misunderstanding of "large scale engineering" that i've seen from most companies.

You don't solve problem by solving it. You solve it by decomposing it, and compose again.


The technical architects, solution architects and business analysists couldn't care less about what a worshiped book says about the UNIX philosophy.

On the enterprise mines, the minions do what they say.


As someone who has done a lot of "enterprise" Java, I feel your pain.

> The issue is not Java is impossible to have such productivity, it's the mindset, the culture issues from Enterprisey people.

I do think that a lot of it will depend on what sorts of projects people work with, as well as how old the frameworks are. Some design choices made in the development of those will certainly show their age. Consider this article, for some nice arguments in that regard: https://earthly.dev/blog/brown-green-language/ (I'd say this also applies to frameworks, not just languages themselves)

I actually recently used Dropwizard to create a proof of concept system for storing and querying millions of files by their metadata and using Java wasn't too bad in the end: https://blog.kronis.dev/tutorials/1-4-pidgeot-a-system-for-m...

I'd vaguely rank the frameworks and libraries as follows:

  - Java/Jakarta EE: pretty dated at this point, you'll mostly see it in legacy projects, generally not a "modern" feeling experience, if you need to configure a separate web server instead of an executable .jar file, you're going to have a bad time
  - Spring: if you see plain old spring, you'll have a bad time, it's the definition of "enterprise" code and will waste lots of your time
  - Spring Boot: can be good if you start a new project with a recent version, still somewhat enterprise but lots of integrations are just plug & play
  - Dropwizard: takes a bunch of idiomatic Java packages and glues them together, comparatively lightweight, you can even get by without dependency injection, most of the cruft that you need to deal with ends up being because of the language itself instead of a framework with bunches of weird dynamic class loading and reflection
  - Quarkus/Vert.X/...: new frameworks and libraries that try to build something more modern and performant, generally you can look in the direction of these for many of the modern projects that you want to do, though their lack of maturity might show sometimes (e.g. if you try doing something unsupported, or just run into their learning curve, e.g. how the Vert.X event loop works)
I guess I can sum up my thoughts with:

  Is Java as a language inherently a bit more annoying to work with than some of the other web dev languages? Definitely.
  Have its frameworks and the language itself gotten better in the past years, especially after newer versions than Java 8 came out? Yes.
  Are many out there going to pick it for its decent static type system, good runtime performance (JVM is great) or bunches of stable packages? Sure.
  Are some going to be pigeonholed into working with horrible legacy projects that are absolutely demotivating and show off the worst of the language? Sadly, also yes.
  Are others going to look in the direction of something like Kotlin, which attempts to solve some of its nuisances, or maybe other languages altogether? Also yes.
But at the end of the day, a lot of it comes down to familiarity: if you feel like you'll be more productive in .NET, Node, Ruby, Python or another language, and there aren't external external factors that force your hand in a particular direction, pick whatever fits your needs the best.


Okay, I'll bite.

Java could be a much nicer language with much less allowance for architectural space programs.

Type classes were introduced into Haskell as early as 1988-1989. At 1991 you could have programming in language with parametric polymorphism, type inference and principled (albeit called ad-hoc) overloading, there was at least one.

The ST monad, one of the first effect-containing abstractions, was introduced as early as 1992 - I think it was close to introduction of IO monad.

So, at the time Java was introduced in 1995, it could have all these nice properties which greatly simplify programming and make architectures easier to understand and more future-proof (Haskell's solution to an Expression Problem [1] is one of the simplest, it uses what was available in 1995, more or less).

[1] https://en.wikipedia.org/wiki/Expression_problem

Thus, we could have a programming language that makes architecture rocket launch much harder. But we did not.

The slogan was "write once, run anywhere" without any regards on how much is needed to be written or how to safely compose what is written.


I guess you never seen Haskell code in production with endless GHC feature flags...


I saw, of course. I use Haskell professionally for 15 years now.

These feature flags are not "architecture space odyssey," though. Usually they reduce boilerplate, often significantly.

They are also artifact of Haskell being "research language," as mentioned in video above.


Programming languages exist primarily to provide pretty paths for certain kinds of development, which is to say by the structure of the language they make certain kinds of solutions more natural to the developer, while making others more painful. Java's pretty path is large, object oriented towers of classes, with state scattered rough-shod throughout the application.

To be fair, there has been some effort to ease the insanity. Record classes, lambdas, and the like lessen the pain of trying to write Java against its original orthodoxy. But before that, a simple "sort list of lists by first value" method involved making a class available in the current scope that implemented Comparator<List<X>> which contains a method "compare".

As opposed to "sortby (comparing fst) list_o_lists"


Languages are orthogonal to the desires of technical architects, solution architects and business analysts, that control the helm of enterprise shops.

There was no Java when GoF book came out, CORBA and DCOM were used all over the place, SOAP was initially driven by Microsoft and its COM/.NET group (see Don Box), when enterprise architects get FP into the enterprise, scalaz happens.


I don't hate Ruby, but at my previous workplaces the Ruby evangelists would absolutely lose their minds if you criticized anything about it. They'd even jump to ad hominem and personal attacks. "Man it seems Ruby has a lot of pitfalls for engineers that aren't intimately familiar with each and every one of its ins and outs!" ... would get a response, "it's because you're just fucking stupid and a horrible engineer."


curious. Ehat are you using Ruby for? Rails? I use Python bc it is almost everywhere but I always liked Ruby a lot.


> As I type this, every top-level comment here is a criticism or a nitpick.

This is maybe going to sound snarky, but honestly I really am being serious: at this point, what else do you expect from this website? HN has a well-earned reputation of being a bunch of kneejerk-critics and naysayers.


Personally, I do expect this behavior, but I also call it out, because I don't accept this behavior.

For better or worse HN is my community. Its given me so much over the years, even as it's infuriated me much of the time. As I recognize patterns of dysfunction I highlight them, so that the community can understand & overcome them.

And I do think I see incremental progress. Not to say I caused that progress, I have no idea what if any role I played from my modest soap box (I suspect very little), but I think there's every reason to hope for the better.


Not all critique is knee-jerk just because it comes quickly.


Good point. I can rattle off criticisms of an idea instantly if it happens to touch my little corner of the industry I've been working in for over eight years. It's rare at this point that I ever encounter a novel idea that I haven't already given thought to.


I hear you and I agree. The HN vibe is nitpicky, for sure. I was a little surprised the comments here were 100% negative (or not positive, to be fair). I usually wouldn't bother reading all the comments, but I care about Ruby, so I was a little more irked by the negativity than usual. :)


The core of that reputation is because people on HN want the best and many of them know that expecting the best can be an effective way of getting it.

Yes, there are critics who are not ‘in the arena’, but for the rest I see a lot of care (AKA taste as defined in Ira Glass’ words on “The Gap”) put in to the comments.


I'm sure there is a lot of care, but I also see a lot of people abusing the concept of care in order to troll.

I have had/seen this conversation over and over again on HN:

Commenter: This topic sucks, I wish people would stop posting it to HN.

Me: I understand this topic isn't interesting to you. Why don't you ignore it and click on something else?

Commenter: I care so deeply for this community, I hate to see this topic reduce it's SNR.

Does the commenter care about HN? Presumably. Does that make their complaint legitimate? No. They're bothered by how a subset of people on the site want to discuss some topic, and they're manufacturing a reason that sounds better than, "I don't want to see it."

But no one is entitled to have articles that are to their taste posted to HN. Sometimes this is expressed more like, "this isn't appropriate for the HN audience." But the audience of an HN post isn't what anyone imagines HN looks like, it's the subset of HN members who find that post in particular to be interesting and worthy of discussion.


Have you ever been in a room with someone trying to change the subject you are deep in thought about? Two things happen: A) you don't notice, and B) they might start trying to find new tactics if they are still willing to engage at all.

I speak from a good deal of experience with this. Active listening was a good crutch for me in a lot of ways, but it works very poorly with people more and more often these days (especially online) and by the time you reach texting and forum posting, you basically just have to read walls of text before you can respond.

I hate to say it but sometimes I just need to interrupt you! Same goes the other way. Please, PLEASE, chime in with some reverb or retaliation. (Hell, maybe one day I'll earn a slap on the back ;)

Then you have the next problem. It spirals... like a positive feedback loop. You find yourself in a thrilling conversation/argument with someone only to have completely alienated the rest of the room. Now what? Is anyone else even paying attention? Would they have liked to, but you both just flew past them and dove straight into your threads?

I think this stuff is insanely hard in the real world sometimes, and only harder online. I like HN partially just because I feel like a lot of people here kinda get that.

P.S. I'm also the kind of person who types really long emails, then spends a while trying to edit them down. Sometimes I just don't have the energy. Sorry for the hypocrisy.


Are you telling me I long-windedly went on a tangent?

I'm open to this criticism I'm just not entirely sure I'm correctly interpreting you. (It certainly wouldn't be the first time I was told I was unclear or long-winded.)

I don't mean to speak out of turn, but just to offer you information, your experience really reminds me of how friends have described ADHD to me. I'm just putting that out there in case it's useful, make as much or as little of it as you care to.


> your experience really reminds me of how friends have described ADHD to me

I can't speak to how often this happens for people without ADHD, but it is a very common trait for those with ADHD (and describes me rather well on most days).

For what it's worth, my interpretation was that they were criticizing you so much as elaborating/describing/explaining a different perspective on the same phenomenon you described (or at least, a similar phenomenon)... and then that turned into its own tangent. of a tangent of a tangent.

So, bringing these tangents all the way back to the OP: ADHD is hard, communication is hard--especially with people. Communication patterns are context sensitive. Toxic communities are easy, toxic communication patterns are easy, and establishing constructive communication patterns for a broad community of people is hard. Ruby's community tends to be mostly nice, although it's a lot less popular and energetic than it was 12 years ago. Ruby itself is marvelous--haters be damned--even when there's a little truth in the criticism. I think that its positive impact on the industry as a whole is hard to measure (almost every commonly used web framework before Rails, Rails, almost every commonly used web framework before Rails; and the languages like Rust and Elixir are hugely influenced by the idioms, ergonomics, and aesthetics of Ruby, IMO). And adding Data in ruby 3.2 is a great little building block and will probably replace 90% of my Struct usage. And I've been wondering if I should backport it, wait for all of my projects to hit 3.2+--I'm hoping there will be a good implementation in the "backports" gem soon--or just use one of the gems that will create an adapter layer over it (I'm sure one or more of the dry-* gems will be built on it, and I'm using them in a few places anyway).

So, I was going to type a six word reply ("yeah, this is a common ADHD trait.") and this accidentally got blurted out. So yeah. ADHD. ;)


crucial edit: s/my interpretation was that they were criticizing you so much as/my interpretation was that they weren't criticizing you so much as/


I never said it was perfect, but care as it’s expressed in the world never is. There is always a vocal majority who cut down and even a vocal minority who actively work to ruin what is good, but care is like the sun: no matter what it’s always out there helping things grow.


This is a very polite and optimistic way of putting it. Having been in this industry for 15+ years now, I have a more jaundiced view.


I'm about 15ish years in and I have a jaundiced view of the jaundiced view. I've learned a lot of "the right way" things from HN that are genuinely better. True shop knowledge from people "in the arena" with expertise building and running the kind of systems I build and run. There are a lot of pricks though, to be sure.


I have definitely learned a lot too but it’s rarely from the knee-jerk first responses on tech posts.


That can be true, though it can also be a sort of cheap way to sound smart in a group of smart people.


Appreciated your conversation here as well, https://www.codewithjason.com/podcast/11693222-164-oop-desig...


Thanks! That was a fun conversation.


I can see why this was added, but I don't see myself using it anytime soon.

If I just need a bundle of attributes and don't care about immutability then I will continue to use Struct.

If I do need immutability I will continue to use dry-struct (https://dry-rb.org/gems/dry-struct/1.0/) which has much deeper guarantees of immutability and type enforcement.


Concurrent::ImmutableStruct is thread-safe and tested. Dry is cute, but too new.


5 years old is too new? concurrent-ruby is only 3 years older.


Dry is mature and has additional support for Monads, thread safe containers, schemas. I'm leveraging more than just struct from Dry.


Remember HN comments go in waves, at a point top rated comments are positive, then they turn to negatives as people react to negativity and viceversa.

You should not worry about what top-level comments are.


Love this attitude. We need more of it around here.


People who like commenting on this website do so because they like to debate. If you agree with something you can upvote and move along. Like this comment, for instance, is an attempt to refine an idea posited by you regarding the mentality of HN commenters. For some reason, I enjoy that.

Regarding this feature, it's cool but I'm surprised they introduced a new concept just to add immutable structs.


I agree. I come here for the comments. Most of the time I barely care about TFA itself. I just want to learn something new by reading what smart people are saying. Formed and refined a lot of opinions during my time here.


Aren't the type of comments "Yay, I'm glad, I was so waiting for this" kinda useless, forbidden, and should we downvote and flag them.


My only criticism is that Ruby doesn't need more features.

ES6 suffers from this disease to a fairly extreme degree, and you can see this schism in the JS community back to the semicolon war in the early 2000s. Kotlin as well, unfortunately, as it's otherwise generally a really nice language.

There are so many language structures and shortcuts. And unless you spend a large amount of time becoming truly fluent in all of them, you're never going to be able to read all the "clever" code that uses them.

These structures are not common across languages, either, so if you jump between codebases in multiple languages... good luck.

tl;dr: Readability über alles.


The one feature Ruby absolutely needs is proper modules and libraries, like Python and Javascript. The current system modifies global name space and interpreter state, it's essentially #include from the C preprocessor.


Toxic positivity is what's causing the slow decline in HN. Inventing potential new features is easy; quickly culling bad ideas is the most vital part of real progress.


Design and safety in Ruby? Common. While my experience is mostly ROR I believe it reflects how Ruby is supposed to be developed. It's an insane convoluted system built to resemble the writing style of VB. So many aliases. So many different ways of doing things that are subtly different.

The amount of time I have lost because of some piece of code (a block) that gets executed somewhere else and I have no clue how that works is insane. And every time I need to go and dig in the implementation details to identify the params passed in because there is no enforcement at write time. I want my types!!! Why would you voluntarily write code where you throw out the types. The thing that is most useful...


So… I was trying to figure out why freezing a Struct wasn’t a suitable solution here and the article pretty much answered it: Data is not enumerable.

That alone is enough reason to create a separate class. Struct has completely wrong ergonomics for certain things.

I’m sure there are a bunch of other reasons but I didn’t feel the need to dig further.


If you want to use something like this right now (with a threadsafe bonus) concurrent-ruby has some useful specialized structs that offer similar behavior:

* ImmutableStruct: Immutable struct where values are set at construction and cannot be changed later.

* MutableStruct: Synchronized, mutable struct where values can be safely changed at any time.

* SettableStruct: Synchronized, write-once struct where values can be set at most once, either at construction or any time thereafter.

https://github.com/ruby-concurrency/concurrent-ruby


Yep. There was no reason to add trivial functionality that already exists. Just something else to go wrong, support, and add bloat.


I love Ruby, but I honestly don't get the point of Struct and now Data. You can do the same thing as both Struct and Data with Class. An immutable data container is a Class without setter functions. Whenever you start with a Struct that now needs methods to operate on private data, you just have a Class.

Whatever, I still love you Ruby. You can always just not use these things and life goes on.


They’re both shortcuts to creating class-like things.

Take @dataclass in python for example, that’s similar to Struct for Ruby.

‘Data’ documentation from the GitHub PR (https://zverok.space/ruby-rdoc/Data.html):

    Data provides no member writers, or enumerators: it is meant to be a storage for immutable atomic values. But note that if some of data members is of a mutable class, Data does no additional immutability enforcement

    See also Struct, which is a similar concept, but has more container-alike API, allowing to change contents of the object and enumerate it.


> Take @dataclass in python for example, that’s similar to Struct for Ruby.

From a cursory look data classes seem to do a lot more than structs. Structs seem like a mutable version of namedtuples, or js objects, down to behaving like a collection / iterable.


In reality there is a lot of overlap between structs and classes that the intent seems to be a bit lost on modern devs.

They were intended (going back at least to C++) to convey different things. A struct is a light abstraction of related things. So instead of having int x, int y everywhere you have Struct Point.

A class has responsibilities, implies state, implies interfaces.

Given the signaling that structs send they should be value objects and as such the original Ruby struct is a bad implementation. The Data object is basically a concession of this with an understanding that changing struct would break "all the things" that already use struct.


In C++ a struct from the start was a class with default public visibility. You can define member function on them.


Yes I'm aware. You can define methods/functions on most (if not all implementations of struct); Ruby's is no exception to this.

The parent indicated they didn't understand why both struct/class exist because there is so much overlap. The primary reason is to signal intent. This has largely been lost as the masses just use classes for everything or they use simple data types like hashes.


Data is shorter to write and gives you automatic structural equality. You really could do the same thing with a class, but if you want something exactly like Data, it's nice to have it built in.


I'm a big fan of value objects. In Ruby, many of my projects have a simple baseclass, ValueObject, that is only a little simpler than Data.

I love it that I can now use it from the language instead. It saves me just one class. At most five methods.

But it saves me having to explain and defend this for colleagues who insist that an email is Just A String, or a price being a float (i know) has worked Just Fine. I can now point them at stdclass documentation. And it being part of std weighs far more than the opinion of that contractor that keeps on nagging about design patterns and crap.


Very often, in engineering or design or even art, the best feature is actually a constraint.


from a reddit comment:

"Glad we'll have a built-in struct class that's not Enumerable. I remember passing a Struct instance to a method which had a check for object.is_a?(Enumerable), and then proceeded to do something else than what I intended. It's fine to offer a way to iterate over attributes, but it was unnecessary to make the whole Struct Enumerable."


This is the use-case for me. Here's an actual example of a Struct I will probably convert to Data in the file-identification library I've been working on. Right now they just have their `#to_a` overridden to disable some of their annoying automatic Enumerable behavior: https://github.com/okeeblow/DistorteD/blob/dd2a99285072982d3...


Honestly they could have made Struct simply be a hash with some syntactic sugar, I can't say I've ever used Struct in anything I've written in my 8+ years writing Ruby professionally

With this new data class... Idunno I'm shrugging my shoulders. The whole mutability/immutability debate has always seemed so pointless. Don't go assigning shit so willy-nilly?


I use Struct for some things because it enforces and documents the shape of the data.

This is especially helpful when code gets complicated and you have a lot of hashes being thrown around.

Instead of trying to document and enforce a nested hash structure through multiple layers, you can use Structs to make mini classes that do that for you.


"Readability counts."


Ruby gonna Ruby. I decided it wasn't for me when I realized that blocks had magic syntax and were separate from, but convertible to, anonymous procedures. But some people love it, and if it's to your taste then it's to your taste.


I believe this is a part of 3.2 and not 3.1? Couldn’t find it in the docs anyway.



It seems to have been around unofficially for some time before that even. https://stackoverflow.com/q/7306004/580412


> @nobu (Nobuyoshi Nakada) proposed Data, which used to be a class for extension library authors, but deprecated since ruby 2.5 and removed since 3.0. We might reuse it now.

https://bugs.ruby-lang.org/issues/16122

Ed:

https://ruby-doc.org/core-2.5.0/Data.html

> Data

> This is a deprecated class, base class for C extensions using Data_Make_Struct or Data_Wrap_Struct.

Vs

https://ruby-doc.org/core-2.4.0/Data.html

> Data

> This is a recommended base class for C extensions using Data_Make_Struct or Data_Wrap_Struct, see doc/extension.rdoc for details.


> However there is one caveat. If some of the data members are of a mutable class, Data does no additional immutability enforcement.

Seems like an area of concern / gotchas. Either restrict Data to not allow mutable nested objects, or provide immutable versions of stdlib object types as well and enforce that those types are used.

Yet another Rubocop-that-should-just-be-built-into-the-language coming in 3...2...1...


This is pretty common when adding immutability to a language that wasn't designed around it. Whether it's JavaScript's `const` or Java's `final`, languages almost always add shallow immutability rather than deep. My sense is that it's quite difficult to add deep immutability in later.

Rust is the main exception I'm aware of, and it had deep immutability from the get-go.


Scala has immutable types in the standard library too, but I don't think they make much sense without the language being statically typed. By which I mean adding immutable types to Python or whatever's stdlib would not be that hard, but it would most likely be confusing since you don't know for sure what you have until you look at it and see.


Rust's deep immutability is different than most in that it allows setting a reference as recursively immutable even if the data type it's pointing to has mutable fields.

Scala gives you immutable data types but unless I'm mistaken there's no way to say "recursively disable mutating functions for all elements in this list"—you just have a list that cannot be mutated. If you stick a Java object in a Scala list, it's liable to mutate, so Scala has shallow immutability.


Yeah that's true, it is different. Neat :). With Scala you would put immutable types in your list if you wanted to, but I get what you mean, it's not the same.


Scala's immutable types are also "shallow". For example you can put mutable objects inside an immutable container and change them when you want. Rust avoids this using lifetimes.

It would be interesting if mutability was actually part of Scala's type system, for example if immutable Seq was defined as

    class Seq[T <: Immutable]


> I don't think they make much sense without the language being statically typed.

They absolutely do, in part because you son’t even have a type system you’d need to undermine in order to achieve mutation.

There are “immutability first” dynamically typed langages, and they work rather well (erlang, clojure).


> Whether it's JavaScript's `const`

const is JS is referring to the reference and has nothing to do with the value, it's not even shallow immutability, there is none at all.

    const arr = []
    arr.push(1)
is valid JavaScript.

Bit nitpicky maybe, but wouldn't want people to get the impression const gives you any sort of immutability.


> wouldn't want people to get the impression const gives you any sort of immutability.

const makes the binding immutable but not the object. Object.freeze makes the object immutable but not it’s children.


Accidentally mutating globals was a big problem with the language. Const didn't solve it completely: you can still implicitly create globals by forgetting to declare the binding locally. But at least if you have a global you can make it an immutable binding and get warned about mutating it some of the time (ie assignments). As far as I'm concerned that is all it is useful for. For a local binding you don't really get much benefit since it's not deeply immutable, and it isn't an improvement to the ambiguity of local/global scopes, so you're frankly better off not using it as it will trick newcomers.

The same argument doesn't apply to Data's shallow immutability. It will give you errors when mutating at least some of the fields. If your code can catch you mutating a number, then you can notice the bug and be reminded to make deep copies etc. It's an improvement, just like Object.freeze.


It's really the same problem extended to attributes of Data. The "immutability" in Data _also refers to the reference_.

To take Python tuples (which are morally pretty close to Data)

  my_data = ([],false)
  my_data[0].push(1)
tuples are immutable, but this happens, because the "immutability" is in the same vein as JS's const: "we are always pointing to the same object".


This is exactly the same problem that OP was drawing out with Ruby's Data: the references inside the Data object are immutable and cannot be changed to point to other objects, but once you've dereferenced the (immutable) pointers there are no immutability guarantees. Hence, shallow immutability.

The only conceptual difference between this and JS's `const` is that you can't use `const` to declare object properties immutable; for that you need Object.freeze().


For JavaScript, there's an active proposal to add deeply immutable objects and arrays: records and tuples. [0]

[0]: https://github.com/tc39/proposal-record-tuple/


> Rust is the main exception I'm aware of, and it had deep immutability from the get-go.

C and C++ have deep immutability too, but both come with many other footguns as baggage.


Perhaps even more egregiously: given Ruby's dynamic runtime, we may take an apparently immutable instance of a data class and variously prepend, extend, refine or define methods upon that object's class or eigenclass such that, one way or another, different values are returned, for subsequent uses of that object, with your choice of lexical or global scope, including effecting mutability to the extent of providing viable setter methods.

Actually doing so breaks the covenant of Data, but an enforced prohibition breaks the language. As with Struct before it, a data class is merely a shorthand.


> Actually doing so breaks the covenant of Data, but an enforced prohibition breaks the language.

Fortunately in my almost 20 years of ruby, dark magic does not survive a reasonable amount of sunlight. Most of the time someone (including me) has written impolite ruby, they know what they are doing and why (laziness 90% of the time) and respond appropriately to a gentle prod to be more polite.


This has been my experience as well.

Ruby gives you footguns and you can certainly do ill-advised things... but in practice I just don't see it happening very often either in open source projects or in private corporate stuff.

Why?

Because IME there's rarely even a need to go down those paths at all, unless you are writing a framework or doing something else "meta."

99.99% of Ruby I have actually seen is either simple scripts or bog standard OO stuff.


> a framework

We do find such horrors lurking in the bowels of Rails, unsurprisingly. ActiveSupport clamps on to several fundamental modules, but Rails only inflicts the worst of its metaprogramming gymnastics either upon itself, or client objects/classes that expect a ton of magic (viz. models, controllers, views). When peeking at some of the machinery I'm sometimes unsure whether to be appalled or astounded.


Agreed. And if you have a rigid tests-are-necessary-to-merge regime you end up finding that the simplest way to write quality tests are simple approaches to implementations.


Java made the same compromise with records, and likely for similar reasons. Limiting fields of such structures to only contain immutable objects ends up limiting their adoption too much even though it may be the way you would like people to use them in the long term.


The notion of value objects reminds me of certain types in Common Lisp, dating back to the definition of the language in the 1980s.

The numeric and character types in Common Lisp are immutable value types. The standard says they may be copied at any time, so the object equality operator (eq) may or may not return true on them. There is a separate equality operator, eql, that performs value equality on value types (so two numbers whose value is the same integer, even if they are bignums with a representation stored on the heap, will return true when compared with eql.) Eql has the same meaning as eq on types that are not immutable value types.

While this is not standardly possible in CL, it would be nice if users could extend operators like eql with new methods, thereby defining new immutable value types.

A nice thing about these value types is they play well with distributed computation or with saving of values to offline storage.


Ruby dev since early 00's. WTF. There's #frozen. It signals something is immutable. Why add another type this late into the game, add complexity, do it a completely different way, and break everything that came before with this unnecessary shit?


    break everything that came before
How is this breaking anything, let alone everything?

I'm not sure I see the value of Data either, but I also don't see how it breaks anything. One of us is severely misunderstanding it.


er... whats #frozen?



> irb(main):005:0> john.name = "John Doe"

> (irb):5:in `<main>': undefined method `name=' for <...> (NoMethodError)

> Did you mean? name

This error message is a bit roundabout. I assume the implication is that `name` is the getter and `name=` is the setter. And so, immutable objects don't have the setter method? But definitely could be worded much better than NoMethodError.

Disclaimer: I'm more of a Python/JS/TS person, and haven't used Ruby much.


This is how Ruby works — you don't directly access an object's instance variables outside of the object, you call getter methods (commonly created with attr_reader) or setter methods (commonly created with attr_writer, or attr_accessor). In this case, there is no setter. This is the same error you'd get if you tried to set a read-only attribute anywhere else.


And it's worth noting that, unlike with Python, this isn't Ruby internals letting an exception bubble up from somewhere; rather, `foo.bar = baz` is literally syntactic sugar for `foo.bar=(baz)`.

(All Ruby operators work this way: something like `foo + bar` is short for `foo.+(bar)` for example.)


Or ... you just ignore the rules

  john.instance_variable_set(:@name, "John Doe")

  john.send(:"instance_exec") do @name = "John Doe" end
I love ruby.


Hah, true. But good luck getting it past code review, right?


Yeah. Non-Ruby devs are probably thinking, "what is the point of rules if the rules can be sidestepped so easily?"

But IMO/IME Ruby strikes a good balance here. You can break the rules but it really sticks out like a sore thumb. We avoid these backdoors like the plague.


Sharp knives and all that


Interesting, so immutable is basically syntactic sugar for read-only on every attribute. That's a good backwards-compatible sort of implementation.


> I assume the implication is that `name` is the getter and `name=` is the setter

Yes. This is extremely conventional/idiomatic Ruby. You'd be likely to come across this pattern not long after Hello World.


> But definitely could be worded much better than NoMethodError.

But that is the root cause - there is no such method.

The thing about 'Did you mean? name' is just a guess to help you.


Data could implement better messaging via method_missing though, since they know any calls to a setter will fail


> The thing about 'Did you mean? name' is just a guess to help you.

And instead it throws anyone who hasn't extensive experience with Ruby's quirks into a spiral of misery.


How so? That's a day 1 experience for Ruby. Typos and calls for the methods on the wrong objects will always get you a NoMethodError. Anybody past their first Ruby tutorial will be familiar with this exception.


This is the kind of problem you encounter once, scratch your head, look it up on stack overflow, understand why it is, and then commit this little quirk to memory and it never really troubles you again.

(Or more realistically, your IDE underlines your error in red and you never even see the error message)


I like it because it encourages the newcomer to understand that the `=` "operator" in `foo.bar = 123` is actually just a part of a method named `bar=`.

Though, to be honest, rather than an "instructive" error message... I sort of wish the language syntax itself forbade that space and simply forced you to write `foo.bar= 123` instead of `foo.bar = 123`.

I wonder if Matz himself has ever second guessed that choice he made in the 90s? =)


> I sort of wish the language syntax itself forbade that space

This syntactic sugar is definitely a bit weird, but it seems necessary in the "everything is an object" design, and to simultaneously meet Ruby's prime directive of maximizing developer happiness.

Can you imagine the howling from the haters if `x = 1` was a syntax error?


This is a pretty basic facet of Ruby. You do not need extensive experience to be familiar with Ruby's approach to getters and setters. This will be surprising to people whose expectations come from other languages, but I doubt it would surprise any Ruby developer.


Agreed. The second chapter of Programming Ruby, "Classes, Objects, and Variables", shows you how to manually implement 'attributes' before introducing you to the attr_* methods. This is fundamental stuff.


If you don't like dealing with confusing and unexpected `NoMethodError` exceptions, stay away from Ruby.


I was not making any judgement or criticism of Ruby, just stating that the error message in this specific instance could be more helpful.


> the error message in this specific instance could be more helpful.

What do you think it should have said? Because this:

john.name = "John Doe"

is, literally, calling `name=` on `john`. But there is no such method.


Fair enough. In this case, since it subclasses/implements Immutable, it might be nice to call that out specifically in the error. You could immediately avoid thinking it's a typo or programmer error, but a deliberate feature.


I'm also not a ruby person but I'd think it would not be too hard to say "method 'name=' does not exist. Value objects do not provide setter methods by default." Or similar. It would almost be better if it didn't make the "did you mean?" suggestion which confuses the issue.


Ruby is a duck typed language.

What matters isn't "is" but "can act like".


Why can't giving an informative error message be part of "can act like"?


I definitely understand your point.

IMO the root issue is the language syntax allows you to write `foo.bar = 123` instead of `foo.bar= 123` which kind of obscures the fact that `bar=` is a method.

If you've never written Ruby I 100% agree that the NoMethodError is perhaps confusing. However as others have noted this is just a really simple core concept you learn right away! This is your standard "oops, I called something that doesn't exist" message and contains everything you need to see where you went wrong.

99% of the time that message is all you need; for the other 1% you can use a debugger to pause things and see e.g. what methods `foo` does support.


To be honest I did understand what was wrong from the error message on first read, despite not knowing ruby (but probably aided by experience in other languages with similar patterns). I think what the distant grandparent was noticing is valid though: the error message is accurately but tangential to the reason for the error. Sure, you can figure it out. Probably you can figure it out without needing to be a great master of the language. But to criticize somebody for wondering whether an error message could be more informative/relevant, when that is clearly possible, I find galling.


> I was not making any judgement or criticism of Ruby,

I was.


NoSuchMethod makes me think of the bad old days when I was writing VB6 code and getting Method ~ of Object ~ not found... ugh.


Did anyone else do a "double take" (what the...?) when they read this part: <<[the Value Object] has the following properties: Immutable, ..., Extensible>> ???

I'm confused. How can an immutable object be extensible?

In Java parlance, would that mean something like:

    public /*not final!*/ class GinormousData {
        // all public final immutable members
        public final String name;
        public final int name;
        ...
    }
Then, you are supposed to extend it? Weird. I never saw this style once. Is it a good idea?

The scary part, since the class is not final, you need to constantly worry if someone did something crazy like inherit and add mutable fields. Then legacy code that operates on references of GinormousData is clueless.

If I need to compose value objects, I would do this:

    public final class GinormousData {
        // all public final immutable members
        public final String name;
        public final int name;
        ...
    }

    public final class EvenBiggerData {
        public final GinormousData gdata;
        // other public final immutable members
    }
Regardless of my language concerns above, this is great new feature for Ruby.

When I write Python, I go wild with namedtuples and dataclasses(frozen=True). Love them immutable data objects -- so easy to reason about when reading code.


I don't know why you're being downvoted so hard—I've never seen a definition of values objects that includes extensibility. I'm not even really sure what the point would be with the way they're typically used.


> (irb):5:in `<main>': undefined method `name=' for #<data Person name="John", dob=#<Date: 1990-01-01 ((2447893j,0s,0n),+0s,2299161j)>> (NoMethodError)

> Did you mean? name

Is that the best they could do to express that `person.name` is readonly?

The "did you mean" part especially is really where they could add info that would make sense to a human, but clearly that didn't happen.


So I get your point, but to this reasonably-Ruby-familiar human, this is not surprising. `name=` is the name of the setter method that you'd need to have here if you were to set the field; it doesn't exist, so Ruby's telling you so. It's a pretty common idiom.

It could be improved--not easily; a setter method does not necessarily have to map to a data type, and Ruby itself doesn't know what you mean here because the Data type is in the stdlib but doesn't appear to be a compiler special case--but this is a bit nitpicking, and I would perhaps gently push back on changing the error message because it's representing what you really do need to know in Ruby once you're past the level one "driving Rails" usage of Ruby. Getting an error message other than this one would obfuscate what's happening and leave me as a long-time Ruby user pretty confused.


Maybe we could do something like reparse the line and check the arity? We know the reader doesn't take a parameter, but the user clearly provided one, so maybe we could eliminate suggestions that have the wrong arity.


That's a neat idea! I might limit it to 0-arity vs. N-arity suggestions, to not create odd behavior for other situations (assuming Data is, as I expect, just Ruby and doesn't have any compiler special-sauce to it).

I guess it would then also provide a better message for `attr_reader` vs `attr_accessor` usage, too, right?


I submitted a pull request to did_you_mean for a simpler solution: only suggest methods ending with = if the user tried to call one, and vice versa.


That's not always the right behaviour either.

`#set_table_name` was replaced by `#table_name=` in ActiveRecord, so (especially around the time this changed when you might have projects in older and newer versions) it would be a likely mistake to make. I'm reasonably certain this is not the only case where a setter existed without `=` as part of the name where later library updates changed it to one with `=` (or the other way around).


That's a good point, but according to my tests DYM already won't suggest `table_name=` for `set_table_name`. The spellchecker is based on Jaro-Winkler and Levenshtein distances, not substring matches. An alternative would be just to not suggest "foo=" for "foo" and vice versa.


Data could override method_missing to throw a different error for method names ending in =


Does anything else in Ruby do so idiomatically?

My point is that would be different, and as a rule, different is surprising and surprising is wrong.


If you do ruby for any length of time, you’ll have run into assignment actually being a method call.

object.attr = 1

is

object.send(:attr=, 1)

And yes, setters can take multiple arguments. They are just methods. Nothing special about them.


Not true, there is special things about them, like always returning their argument.


You are confusing setters with the assignment evaluation. Setters are just normal methods.

    ?> def you_are_wrong=(a)
    ?>   a * 2
    >> end
    => :you_are_wrong=
    >> send(:you_are_wrong=, 24) 
    => 48

However yes, the `foo.bar = 24` expressions translates to `(foo.send(:bar=, 24); 24)`.

The reason is so that `foo = bar.baz = 12` works as you expects, it's absolutely not some kind of cruft or black magic.


Discussed further down here: https://news.ycombinator.com/item?id=33793994

tldr You tried to call name= on an object that has no such method defined on it. So you get...NoMethodError. name is defined, however, so you got a "is this the typo you made?" message.


They could have intercepted the message and handled it differently, but that would likely break someone else’s expectations about what should happen.


Programming error messages are almost always so terrible.


I wonder if immutable strings are considered.


Aren’t immutable string’s already present when you use the frozen_string_litral: true pragma?


That only freezes literals. But you can of course also call freeze on any string.


This makes me sad and confused.

Isn't "Data" the worst possible name one could use for anything in programming? I am not exaggerating; it is literally the example I have always used.

If Data is an immutable struct, call it ImmutableStruct. If that's too long, there's ImmuStruct or IStruct. Any of those names is better than Data. They are all more concise.


See https://bugs.ruby-lang.org/issues/16122 for lengthy deliberation of the justification (in particular, of the relative utility vs merely enhancing Struct), and discussion of the naming options, but let's crucially note that Data itself is not an immutable struct, it is a factory for classes when the instances of those classes are intended to be immutable structs.

Hence

    Product = Data.define(:name, :price)
creates a Product class, most likely during initialisation; then in application code we could subsequently write

    cheese = Product.new("Wensleydale 200g", 499)
and here it's cheese that is immutable; the point being, we're still naming our domain objects purposefully.

In that light, I think it's appropriate for a language to use generic terms for fundamental elements. For example, almost everything is descended from Object and we're fine with it.


Thank you for the link and the explanation. I still think Data should not have been used


Hell, Struct already accepts keyword argument options (keyword_init), they could have just added a new one (immutable: true)

IMO ImmutableStruct is exactly as verbose as BasicObject, something that has been in Ruby for a long time now. ImmutableStruct is a massively better name for this.


> Struct already accepts keyword argument options (keyword_init), they could have just added a new one (immutable: true)

The article explains the reason they didn't do that:

Ruby maintainers decided that Struct could not be made to suit the requirements of a value object. The core Struct class is a “somewhat alike” value-object; it is compared by value and is,

mutable

collection-alike (defines to_a and is Enumerable)

dictionary-alike (has [] and .values methods)



"ImmutableStruct" does not communicate the key detail that these are compared by value, not identity. "Data class" is a common term for this in other languages, including Java, Kotlin, and Python. "Data" is definitely more concise (almost by definition) and I would argue it's more precise as well.


In Java, "data class" might be a common term that existed for a long time. But a language-level feature was introduced in SE 14 as "records": https://docs.oracle.com/en/java/javase/14/language/records.h...


>Discussions started in Ruby forums to build a data model on the principles of the Value Object, introduced by Martin Fowler.

are they trying to say that martin fowler invented immutable data structures???


Value objects are equal if they have the same values (although are still different objects) - there's a little more to it than simply immutable data structures.


Well, what is it then? Is it something more than

* An immutable composite value that

* Has an equality operation not based on pointer identity

Those have been known for a long time, at least 30 years. It is the default type of value of Haskell, a language that is older than Ruby.


> Well, what is it then? Is it something more than

Straight from the horse's mouth: https://martinfowler.com/bliki/ValueObject.html


Yes, they can still be separate objects too

https://wiki.haskell.org/Physical_equality


yeah and martin fowler did not invent that.


Within the domain of enterprise software, or maybe just 'applied CS', I think he did. What's an earlier reference than his 2003 use of it?


Ward Cunningham's CHECKS Pattern Language of Information Integrity, published in 1994, where it's called Whole Value


pretty sure lisp had it for decades


No, they’re trying to say that Martin Fowler introduced the Value Object.


Value Object is older than that. It was called Whole Value in Ward Cunningham's CHECKS Pattern Language of Information Integrity, published in 1994




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: