The Crystal Programming Language Forum

Immutable references to mutable reference types

Sometimes one wants to create a variable or constant which refers to some mutable data type, but which is immutable itself. A realistic but slimmed down example might be something like

# In the project main file, e.g. project.cr
class Project
  class_property config : Config = Config.new
end
# In `config.cr`
class Config
  record User, name : String
  property users = Array(User)
end
# in spec/fixtures.cr
module Fixtures
  CONFIG = Config.new users: [User.new "test user"]
  def reset_config
    Project.config = CONFIG
  end
end
# in spec/spec_helper.cr
Spec.before_each { Fixtures.reset_config }
# in spec/add_user.cr
describe "Config" do
  describe "adding a user" do
    it "works" do
      Project.config.users << Config::User.new "added user"
      Project.config.users.map(&.name).should contain "added user"
    end
  end
end
describe "An immutable constant" do
  it "has not been changed" do
    Project.config.users.map(&.name).should_not contain "added user"
  end
end

This would fail under the current system, because, while you can’t do Fixtures::CONFIG = # something, you can do Fixtures::CONFIG.method_that_mutates!. It is a common request in the chat to be able to create some constant for which not only the reference, but the value to which the reference is pointing is immutable.

A good start would be a macro which uses an annotation like @[Immutable] or @[Read_Only], which overloads setter methods to raise a compile-time error.

This should, in my opinion, be a standard library feature. However, barring that, I would like to begin the process of creating a shard which implements this feature via macros, if the currently implemented macros are up to the task.

I think this is impossible to implement with macros. You need it to be a language feature. But constants like that are really hard to implement or they make the type system really complex. See D language for example.

1 Like

I’ve been playing a bit with the idea of const

Now, don’t get too excited about this: it’s just an experiment, it results in a lot of duplicated code, and almost anything else other than what is shown there isn’t working. Also, I’m not sure how useful this is, compared to the complexity it brings (both in terms of the language and the implementation).

There are already immutable types in the language, however all of them have some usablility problems.

There are of course Structs, Tuples and NamedTuples. There are also Slices, however the read-only check there is a runtime one.

I would propose avoiding const, we’ve all seen the damage already. Instead the way forward in my opinion is a) extending type system, maybe something in a way of Scala/Rust/Haskell and b) avoiding sharing pointers to the data.

Your example deals with a global constant exposing its users directly. What you could do instead is hide the data behind a custom interface that only gives away the immutable methods. Now, unfortunately Crystal doesn’t have private instance vars, so if you need extra protection what you could do is start a fiber sitting atop of a channel and allocate the data inside that fiber so nobody else has the pointer.

(This is a separate can of worms though.)

This is what TypeScript’s ReadonlyArray does. It basically is an interface that doesn’t have any the mutable methods on it.

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

Would it be any easier/harder to have a generic that removes methods from a type? Then if you did like:

ARR : ReadOnlyArray(Array(Int32)) = [1, 2, 3]
ARR << 4 # => Undefined method '<<' for ReadOnlyArray(Array(Int32)) 

That was my original idea – the one I said I thought could be implemented through macros. A macro or annotation could create a duplicate of the original type with all of the methods ending in = overwritten to raise a compile-time exception. But that wouldn’t work of course – a method might mutate an ivar without ever calling a setter method. :slightly_frowning_face:

The problem is much harder than just removing all setters though. Basically it has a lot to do with indirection.

Here’s an entertaining read: http://yosefk.com/c++fqa/const.html

oh wow