Is this a good use case for a class instead of a struct?

https://play.crystal-lang.org/#/r/6mcr

struct Strongbox
  property opened = false
end


test = Hash(Int32, Strongbox).new

test[123] = Strongbox.new

if strongbox_variable = test[123]?
strongbox_variable.opened = true
end

# This is false
puts test[123].opened


# Second Test ----
test1 = Strongbox.new
test1.opened = true
puts test1.opened # true

When accessing the struct with an assigned local variable, the property is changed, but only inside that condition, not the struct itself.

What exactly is strongbox_variable? A local copy of the struct inside the conditional?

And if I were to use a class instead, it wouldn’t be a local copy, but a reference? Which makes the property become mutable?

If so, how come in the Second Test, the opened is mutable?

This leads me to believe… structs should never be used for mutable data, correct?

Now that I think of it. Why am I allowed to do this:

if strongbox_variable = test[123]?
strongbox_variable.opened = true
end

This code doesn’t do what the developer thinks it does?

It does exactly what it should be doing. Since it’s a struct strongbox_variable is a copy of the one you set into test[123]. Thus, when you edit strongbox_variable, you’re not editing the one that is in the hash, just the local one called strongbox_variable.

Do know however that structs are mutable. It’s just that they are passed by value, not reference. See Structs - Crystal

1 Like

That’s really odd or is it just me? If it’s not editing the one that “is in the hash”, why are we allowed to modify it in the first place?

Not really. All has to do with it being defined as a struct. strongbox_variable is being assigned a copy of whats in the hash. If it were a class, then it would be assigned a reference to the one in the hash, that would thus allow edits to be reflected in the original in the hash.

EDIT: But yes, structs are ideally used for immutable data.

1 Like

Should the compiler tell the developer they are trying to modify a local copy of a struct?

No, that’s documentation, that you can found here.

I like using struct, it’s more efficient than class and limit “deep” side effects (like modifying a class that is inside a class inside a class)

Take this code:

if strongbox_variable = test[123]?
strongbox_variable.opened = true
end

Why would a developer need to modify a property that doesn’t actually get modified?

opened is set to true, but only inside that if condition block. That doesn’t make sense? If a developer is setting a property of a struct or class, the developer is inherently intending to modify that property? I’m so confused

simply reasign the variable

struct Strongbox
  property opened = false
end

test = Hash(Int32, Strongbox).new

test[123] = Strongbox.new

if strongbox_variable = test[123]?
  strongbox_variable.opened = true
  test[123] = strongbox_variable
end

puts test[123].opened #=> true

Using structs prevents things like that:

class Strongbox
  property opened = false
end

test = Hash(Int32, Strongbox).new

box = Strongbox.new
test[123] = box

if strongbox_variable = test[123]?
  strongbox_variable.opened = true
end

# This is true
puts test[123].opened

# This is printed
puts "Oops this box isn't supposed to be opened, only the one in the test Hash" if box.opened

So you have to “reassign” the variable to the struct (when inside an assigned local variable condition).

But if you are not, dot notation works just fine, like a class

struct Mutable
  property value

  def initialize(@value : Int32)
  end
 

end

mut = Mutable.new 0

mut.value = 2
puts mut.value # => 2

Because here you’re editing the actual struct. Whereas in the other examples you’re passing a copy of that struct.

https://play.crystal-lang.org/#/r/6mdw

Notice how when you pass the object to the method, obj is a copy of mut. Thus when you edit it, it changes the value on that copy, but the original value is still the same.

Why doesn’t the assigned local variable edit the actual struct then (using dot notation)?

If it lets the developer use dot notation already on the “copy of that struct”, it should work like my example above, no?

Id imagine because when it fetches the obj from the hash, it’s getting a copy, not a reference to the obj in the hash. Since it is a struct.

I always thought dot notation is tied directly to its parent object. I guess that’s not the case with “local copies of structs”. It’s tied to its parent object, but of that local copy, not the actual struct itself. If I got that right?

If true, I need to remove “dot notation is modifying its parent (a reference)” out of my head.

Or, leave it in my head but make a distinction it’s only for classes.

Dot notation just accesses/executes a given property/method on the given object. Doesn’t have anything to do with what obj its tied to. That would be more related to a struct vs class.

Doesn’t make sense.

A developer USES DOT NOTATION to modify a property they want modified.

If it’s modifying a “local copy” it’s an illusion to the developer that it’s being modified, but it really hasn’t.

Something isn’t right

I’m sorry. You’re just not getting the concept of struct vs class. I’d reread the docs https://crystal-lang.org/reference/syntax_and_semantics/structs.html.

It’s a struct, so it gets passed by value. Thus you’re modifying a copy of that struct; which is the expected behavior since its a struct. If it were a class it would be passed by reference and modification would be reflected on the original object.

But this behavior is only happens when the object is passed. As you saw in your example when you were modifying the obj all in the same scope, your modification worked. But when it was passed to the method, it was no longer the “same” object, thus only affected that specific object.

Maybe looking it another way is better: don’t use struct unless they are immutable or unless you are willing to take copies, modify them and putting them back where they are stored, for performance reasons. If performance is not a problem when using a class instead of a struct then you should absolutely use a class.

Maybe something that the compiler could do is that there are changes in a value type and the value is never used again after the last change.


There is one “quirk” semantic in the language regarding value types that make things work when nested value types are used. I hope it won’t bring confusion or despair :slight_smile:

The following works

struct Foo
  property bar = Bar.new
end

struct Bar
  property value = 0
end

foo = Foo.new

foo.bar.value # => 0
foo.bar.value = 1
foo.bar.value # => 1

Only if the Foo#bar is a single line body method. Because that let the compiler inline the getter, hence a copy of the inner value is avoided.

It’s safe to view them kinda like Tuples without dot notation? For example, my modify item tuple method returns a new tuple, but with certain values changed.

I got hung up on dot notation, kept thinking it was acting like a class. When it’s a local copy. However, in my mut.value = 2 example above, dot notation modification does work when you are accessing the “struct” itself, but not when accessing the local copy of the struct. Well, it does work… on that LOCAL copy, but not the main struct.
example:

if strongbox_variable = test[123]?
  strongbox_variable.opened = true
  # If this reassignment is not done, modifying "opened" is an illusion!
  # test[123] = strongbox_variable
end

Knowing the differentiation between a local copy of a struct, and the struct itself is extremely beneficial. They can feel ambiguous sometimes. Especially when dot notation inherently implies the developer wants to change that property. When a developer is now modifying a property that is a local copy and not reassigning it…it now becomes an illusion to the developer and might create bugs?

Yeah, this is why I originally wanted to use a struct. Cause I have 2 properties (x and y). Which are immutable. And then I had 1 mutable property opened, and I remember people on gitter saying structs are mutable, so I thought it might be a good idea.

Quite frankly (not directed to you), I’m really tired of the “performance” mantra. I see it everywhere on gitter, reddit, etc.

For example, my JSON benchmark thread. If you use JSON.mapping with a struct it’s only negligibly faster. It’s actually 3 times more faster if you don’t call from_json, because the type checking slows it down significantly. But you have to use from_json. Which completely nullifies the entire benchmark. And also nullifies every point, every single user said that claimed “using a struct with JSON.mapping is faster!”

In fact, I might make an issue on GitHub about that. That’s not right, using a struct should be far faster. Hell, even @oprypin recommended and helped me significantly with a struct Message instead of using JSON.parse. from_json should not gimp performance that much, when the performance difference is over 3 times faster without it.

Because now, that just completely ruins the idea of a “statically typed” language. “Why create a struct when we can just do JSON.parse and use type checking?!”