0

I am trying to write immutable code in Dart. Dart wasn't really built with immutability in mind, that's why I need to write a lot of boilerplate in order to achieve immutability. Because of this, I got interested in how a language, like Scala, which was built around the concept of immutability, would solve this.

I am currently using the following class in Dart:

class Profile{
  List<String> _inSyncBikeIds = []; // private field
  String profileName; // public field

  Profile(this.profileName); // You should not be able to pass a value to _inSyncBikeIds

  void synchronize(String bikeId){
    _inSyncBikeIds.add(bikeId);
  }

  bool isInSync(String bikeId){
    return _inSyncBikeIds.contains(bikeId);
  }

  void reset(){
    _inSyncBikeIds = [];
  }
}

The same class in immutable:

class Profile{
  final List<String> _inSyncBikeIds = []; // private final field
  final String profileName; // public final field

  factory Profile(String profileName) => Profile._(profileName); // You should not be able to pass a value to _inSyncBikeIds

  Profile._(this._inSyncBikeIds, this.profileName); // private contructor

  Profile synchronize(String bikeId){
    return _copyWith(inSyncBikeIds: _inSyncBikeIds.add(bikeId);
  }

  bool isInSync(String bikeId) {
    return _inSyncBikeIds.contains(bikeId);
  }

  Profile reset(){
    return _copyWith(inSyncBikeIds: []);
  }

  Profile copyWith({
    String profileName,
  }) {
    return _copyWith(profileName: profileName)
  }

  Profile _copyWith({
    String profileName,
    List<Id> inSyncBikeIds,
  }) {
    return Profile._(
        profileName: profileName ?? this.profileName,
        inSyncBikeIds: inSyncBikeIds ?? _inSyncBikeIds);
  }
}

What I understand from Scala so far, is that for every class a copy method is automatically created. In order to be able to change a field using the copy method, it needs to be part of the constructor.

I want the field _inSyncBikeIds to be final (val in Scala). In order to change the value of the field _inSyncBikeIds I need to create a copy of the object. But in order to use the copy method, to change the field, it needs to be part of the constructor of the class, like class Profile(private val _inSyncBikeIds, val profileName). But this would then break encapsulation, because everyone can create an object and initialize _inSyncBikeIds. In my case, _inSyncBikeIds should always be an empty list after initialization.

Three questions:

  • How do I solve this in Scala?
  • When I use the copy method inside the class, can I change private fields using the copy method?
  • Does the copy method in Scala copy private fields as well (even when they are not part of the constructor, you can't mutate that private field then of course)?
Niklas Raab
  • 1,576
  • 1
  • 16
  • 32
  • 3
    It sounds like you're conflating a lot of ideas here. Not every class in Scala gets `copy`; only *case classes* get it for free, and those are specifically for use in cases where you *want* the data to be public and immutable. If you're encapsulating private data, you do *not* want case classes. You'd use a regular `class` and you would either not get `copy` or you'd implement it by hand to do whatever makes sense. – Silvio Mayolo May 20 '21 at 20:24
  • @SilvioMayolo Thanks! I understand, so even in Scala I need to write my own copy method in order to achieve true immutability for all classes :( – Niklas Raab May 20 '21 at 20:46
  • 2
    you wanted the field to be immutable, and also "initialized to empty". That means it would always be empty (you can't change it because it's immutable), no? – phongnt May 20 '21 at 21:06
  • I want to be able to have a make copy of the object, with a string added to the list. Similar to the `synchronize` method. – Niklas Raab May 20 '21 at 21:14
  • Can't you have a private constructor in dart? Seems like an obvious solution if it's a thing, no? – Dima May 21 '21 at 00:43

2 Answers2

0

Scala comes from a tradition that tends to view immutable data as a license for free sharing (thus public by default etc.). The interpretation of encapsulation is more that code outside an object not be able to directly mutate data: immutable data regardless of visibility satisfies this.

It's possible to suppress the auto-generated copy method for a case class by making it abstract (nearly always sealed abstract with a private constructor). This is commonly used to make the apply/copy methods return a different type (e.g. something which encodes a validation failure as a value without throwing an exception (as require would)), but it can be used for your purpose

sealed abstract case class Profile private(private val _inSyncBikeIds: List[String], profileName: String) {
  def addBike(bikeId: String): Profile = Profile.unsafeApply(bikeId :: _inSyncBikeIds, profileName)

  // Might consider using a Set...
  def isInSync(bikeId: String): Boolean = _inSyncBikeIds.contains(bikeId)

  def copy(profileName: String = profileName): Profile = Profile.unsafeApply(_inSyncBikeIds, profileName)
}

object Profile {
  def apply(profileName: String): Profile = unsafeApply(Nil, profileName)
  private[Profile] def apply(_inSyncBikeIds: List[String], profileName: String): Profile = new Profile(_inSyncBikeIds, profileName) {}
}

unsafeApply is more common for the validation as value use-case, but the main purpose it serves is to limit the concrete implementations of the abstract Profile to only that anonymous implementation; this monomorphism has beneficial implications for runtime performance.

Notes: case classes are Serializable, so there is a Java serialization hole: in application code this is solvable by never ever using Java serialization because it's broken, but it makes up for being broken by being completely evil (i.e. if you have a Scala application that uses Java serialization, you should probably re-evaluate the choices that led you there).

There's no way to encode sealedness in JVM bytecode AFAIK (Scala uses an annotation, IIRC, so Scala will limit extension of Profile to that compilation unit but, e.g, Kotlin won't), nor is the private[Profile] access control encoded in a way that JVM languages which aren't Scala will enforce (the unsafeApply method is actually public in the bytecode). Again, in application code, the obvious question is "why are you trying to use this from Java/Kotlin/Clojure/...?". In a library, you might have to do something hacky like throw an exception, catch it and inspect the top frames of the stack, throwing again if it's not hunky-dory.

Levi Ramsey
  • 18,884
  • 1
  • 16
  • 30
0

I have no idea if it is possible in dart, but in scala this would be done with a private constructor:

   class Profile private (val _foo: Seq[String], val bar: String) {
      def this(bar: String) = this(Nil, bar)
    }

This lets you define

   private copy(foo: Seq[String], bar: String) = new Profile(foo, bar)

This is fine as long the class is final. If you subclass it, badness ensues: Child.copy() returns an instance of Parent, unless you override copy in every subclass, but there is no good way to enforce it (scala 3 admittedly has some improvement over this).

The generated copy method you mentioned only works for case classes. But subclassing a case class would lead to some even more interesting results.

This is really rarely useful though. Looking at your code for instance, if I read the ask correctly, you want the user to not be able to do Profile(List("foo"), "bar") but Profile("bar").synchronize("foo") is still possible even though it produces exactly the same result. This hardly seems useful.

Dima
  • 39,570
  • 6
  • 44
  • 70