I wouldn’t call it “bad”, but it’s definitely not as clean as I’d like it to be. A quick rundown of how I think about it.
A regular struct might look something like this:
struct Message
getter id : UUID
getter content : String
getter metadata : Metadata { Metadata.new }
getter sent_at : Time
getter processed_at : Time?
def initialize(@id, @content, @metadata, @sent_at, @processed_at = nil)
end
def processed?
!processed_at.nil?
end
end
The getter
s and the args to initialize
are duplicated and, in most cases, must change in lockstep. There’s no single source of truth for the object’s state and the initialization of that state, so people reach for record
because it does exactly that:
record Message, id : UUID, content : String, sent_at : Time, metadata : Metadata = Metadata.new, processed_at : Time? = nil do
def processed?
!processed_at.nil?
end
end
I lose the benefit of the lazy generation of the metadata
ivar because the parser doesn’t parse blocks that way.
And, as @ysbaddaden mentioned, the attributes are too crammed onto a single line, so then you rewrite like this:
record Message,
id : UUID,
content : String,
sent_at : Time,
metadata : Metadata = Metadata.new,
processed_at : Time? = nil do
def processed?
!processed_at.nil?
end
end
Now you’ve lost the visual boundary between the getters and regular methods. In regular structs, I use that boundary to indicate “this is where you can look to see what state an object has” and crystal tool format
also creates that boundary for me. But with this structure, even if you put an empty line in between, the formatter removes it.
So that’s annoying, so I change my structure to this:
record(
Message,
id : UUID,
content : String,
sent_at : Time,
metadata : Metadata = Metadata.new,
processed_at : Time? = nil,
) do
def processed?
!processed_at.nil?
end
end
This adds that clear boundary between state and behavior, but sacrifices other aesthetics and I wouldn’t call this more readable.
For the record (YEAAAAAAAAH!), I don’t think the record
macro is the right answer to defining initialize
and getter
s in a single place for structs with behavior. It’s amazing for objects that are simply used for holding state. It’s right there in the name — “record” is also the term many FP languages use for types of state. However, it’s the only thing we’ve got right now. It would be really nice if we had something better.