January 29, 2023

On Sunday, 29 January 2023 at 03:29:45 UTC, FeepingCreature wrote:

>

No, I mean

num1 = num2;
onlineapp.d(22): Error: cannot modify struct instance `num1` of type `Foo!int` because it contains `const` or `immutable` members

Sorry for writing an answer without doing the necessary unit tests. I forgot to put the type in parentheses. Here is the corrected version:

struct S(T)
{
  import std.conv   : to;
  import std.traits : ImmutableOf;

  immutable(T)[] data;

  this(R)(R[] data)
  {
    this.data = data.to!(ImmutableOf!T[]);
  }

  string toString() const
  {
    import std.format : format;
    return format("%s: %s", typeid(data), data);
  }
}

unittest
{
  S!char test1, test2;
  alias TestType = typeof(test1.data);

  import std.traits : isSomeString;
  assert(is(TestType == string) &&
            isSomeString!TestType);

  test1 = S!char("bcd");
  test1.data ~= "234";

  test2 = test1;
  assert(test1 == test2);
}

void main()
{
  import std.stdio;

  string str = "bcd";

  auto str1 = S!char(str.dup);
  auto str2 = S!char("abc");

  str1 = str2;

  str1.writeln; // const(immutable(char)[]): abc
  str2.writeln; // const(immutable(char)[]): abc
}

SDB@79

January 29, 2023

On Sunday, 29 January 2023 at 17:41:26 UTC, Salih Dincer wrote:

>

On Sunday, 29 January 2023 at 03:29:45 UTC, FeepingCreature wrote:

>

No, I mean

num1 = num2;
onlineapp.d(22): Error: cannot modify struct instance `num1` of type `Foo!int` because it contains `const` or `immutable` members

Sorry for writing an answer without doing the necessary unit tests. I forgot to put the type in parentheses. Here is the corrected version:

struct S(T)
{
  import std.conv   : to;
  import std.traits : ImmutableOf;

  immutable(T)[] data;

  this(R)(R[] data)
  {
    this.data = data.to!(ImmutableOf!T[]);
  }

  string toString() const
  {
    import std.format : format;
    return format("%s: %s", typeid(data), data);
  }
}

unittest
{
  S!char test1, test2;
  alias TestType = typeof(test1.data);

  import std.traits : isSomeString;
  assert(is(TestType == string) &&
            isSomeString!TestType);

  test1 = S!char("bcd");
  test1.data ~= "234";

  test2 = test1;
  assert(test1 == test2);
}

void main()
{
  import std.stdio;

  string str = "bcd";

  auto str1 = S!char(str.dup);
  auto str2 = S!char("abc");

  str1 = str2;

  str1.writeln; // const(immutable(char)[]): abc
  str2.writeln; // const(immutable(char)[]): abc
}

SDB@79

I think you're missing the concept a bit:

test1 = S!char("bcd");
test1.data ~= "234";

This should emphatically *not* work. You're mutating a field of `test1`. That's what we don't want to ever happen!

Instead, you should have to write `test1 = test1(test1.data ~ "234");`

This looks extremely similar, but the key is that the assignment has to go through the constructor. That way, `test1` can only go from "valid value" to "valid value".
January 30, 2023

On Sunday, 29 January 2023 at 20:48:11 UTC, FeepingCreature wrote:

>

This should emphatically not work. You're mutating a field of test1. That's what we don't want to ever happen!

Instead, you should have to write test1 = test1(test1.data ~ "234");

This looks extremely similar, but the key is that the assignment has to go through the constructor. That way, test1 can only go from "valid value" to "valid value".

In order to use it as you say, the following opCall() should be implemented:

  this(R)(R[] data) { opCall(data); }

  alias opCall this;
  @property opCall() inout { return data; }
  @property opCall(R)(R[] data)
  {
    return this.data = data.to!(ImmutableOf!T[]);
  }

Then it works like this:

  immutable arr = [ 1, 2, 3];

  auto num1 = S!int(arr.dup);
  auto num2 = S!int(arr);

  assert(is(typeof(num1) == S!int));
  assert(is(typeof(num1.data) == immutable(int)[]));

  num1(num1 ~ 4); // or:
  num1 = num1 ~ 5;

  assert(num1.length == 5);
  assert(num1.length > num2.length);

  num2 = num1;
  assert(num1.length == num2.length);

But num1 ~= 6 doesn't work because it doesn't have an overload:

>

Error: cannot append type int to type S!int

SDB@79

January 30, 2023

On Monday, 30 January 2023 at 03:09:14 UTC, Salih Dincer wrote:

>

But num1 ~= 6 doesn't work because it doesn't have an overload:

>

Error: cannot append type int to type S!int

SDB@79

But what prevents num1.field ~= 6?

Making it private and giving it an accessor.

And then you're at this:

struct Struct {
  @ConstRead
  private int[] field_;

  mixin(GenerateAll);
}

Which is exactly what we're trying to get away from because it's too much overhead!

So I appreciate your suggestions, but I hope it's clear that they don't compete with

rvalue struct Struct {
  int[] field;
}
January 30, 2023

On Wednesday, 25 January 2023 at 16:23:51 UTC, FeepingCreature wrote:

>

Before I take on the effort of writing up and submitting a DIP, let me solicit feedback and see if anyone can see a reason why this idea is dumb and doesn't work.

tl;dr: immutable struct was a mistake: it's too weak. rvalue struct is what we really want.

Isn't this what private/@system (DIP1035) fields with public/@trusted getter functions are for?

January 30, 2023

On Monday, 30 January 2023 at 13:53:53 UTC, Dukc wrote:

>

On Wednesday, 25 January 2023 at 16:23:51 UTC, FeepingCreature wrote:

>

Before I take on the effort of writing up and submitting a DIP, let me solicit feedback and see if anyone can see a reason why this idea is dumb and doesn't work.

tl;dr: immutable struct was a mistake: it's too weak. rvalue struct is what we really want.

Isn't this what private/@system (DIP1035) fields with public/@trusted getter functions are for?

Yes, the behavior of "private fields with public getters" is exactly what we're aiming for. (@system - not really; we're not interested in doing un @safe things to them. This is purely about functional programming style.)

Having an in-language way to declare "immutable rvalue everything" would pose some advantages over getters though:

  • we wouldn't have to figure out when we'd need to dup a field on access to avoid mutation-at-a-distance: sSince immutable is transitive, the answer is "never", which also makes the GC happy
  • we avoid the template overhead of having to generate accessors for every field, with all the required analysis to figure out attributes etc.
  • even struct methods cannot mutate fields, for maximum purity
  • it just looks nicer.
January 30, 2023

On Monday, 30 January 2023 at 14:10:39 UTC, FeepingCreature wrote:

>
  • we wouldn't have to figure out when we'd need to dup a field on access to avoid mutation-at-a-distance: sSince immutable is transitive, the answer is "never", which also makes the GC happy

This can be accomplished by making return value of the getter const.

>
  • we avoid the template overhead of having to generate accessors for every field, with all the required analysis to figure out attributes etc.
  • even struct methods cannot mutate fields, for maximum purity
  • it just looks nicer.

These are all true. However, the proposed feature just feels inconsistent with how the rest of the language works. I can't pinpoint exactly why, but I think it has to do with that Immutability and visibility are usually treated as separate issues. This just does not play togeteher with rest of the language.

To be fair, it wouldn't be the first feature to be like that. protected and lazy for example also feel out of place IMO.

I feel you use D very differently than I do. I bump to these issues rarely enough that I don't feel bothered at all to write getters when I do. Plus I consider the struct itself (and rest of the module) being able to modify the field mostly a good thing. I wonder how people feel in general. If your style is widely spread, there might indeed be a case for a language feature.

January 30, 2023

On Monday, 30 January 2023 at 14:43:21 UTC, Dukc wrote:

>

I feel you use D very differently than I do. I bump to these issues rarely enough that I don't feel bothered at all to write getters when I do. Plus I consider the struct itself (and rest of the module) being able to modify the field mostly a good thing. I wonder how people feel in general. If your style is widely spread, there might indeed be a case for a language feature.

That's fair. We're coming at D like it's a functional language at heart with mutability bolted on, rather than a mutable language which can borrow some functional idioms. See my 2019 DConf talk https://www.youtube.com/watch?v=nKMOFaAdtAc ; the basic summary of how we use the language still holds.

The key insight is that we treat D like a functional language at the domain level, like an object-oriented language at the application level, and autogenerate everything at the infrastructure level. Almost all our structs live at the domain level, meaning they don't actually own state, they just represent domain knowledge. State-ownership happens at the application level, ie. in classes.

I do feel like std.algorithm, std.functional and ranges in general invite this sort of pure-data modelling though.

January 30, 2023

On Monday, 30 January 2023 at 15:21:02 UTC, FeepingCreature wrote:

>

The key insight is that we treat D like a functional language at the domain level, like an object-oriented language at the application level, and autogenerate everything at the infrastructure level. Almost all our structs live at the domain level, meaning they don't actually own state, they just represent domain knowledge. State-ownership happens at the application level, ie. in classes.

I need to emphasize this point. For instance, say that a struct represents "the arrival of a train at a station at a certain time." When we find that the train has a delay, we could mutate the arrival.time field on that struct. But what would that mean? The information given was accurate at the time. The struct, representing our knowledge at a certain point in time, is still valid. We have now gained new information, and wish to now replace it with an alternative value that has a different arrival time. But we don't want to change the original, ever, by any means! The original value is and remains a legitimate representation of the domain. That's why we don't use ref, or pointers, or state mutation in structs. Instead, we "patch" the variable with something like Haskell arrows [1]:

alias updateTimes = visit => visit
    .rebuild!(a => a.arrival = newArrivalTime)
    .rebuild!(a => a.departure = newDepartureTime);

return visits.map!(visit => visit.id == visitId ? updateTimes(visit) : visit);

or some such.

The goal of marking structs immutable (or rvalue) is to make any idiom that does not operate like this fail to compile.

[1] https://en.wikibooks.org/wiki/Haskell/Understanding_arrows

January 30, 2023

On Monday, 30 January 2023 at 15:31:59 UTC, FeepingCreature wrote:

>

I need to emphasize this point. For instance, say that a struct represents "the arrival of a train at a station at a certain time." When we find that the train has a delay, we could mutate the arrival.time field on that struct. But what would that mean? The information given was accurate at the time. The struct, representing our knowledge at a certain point in time, is still valid. We have now gained new information, and wish to now replace it with an alternative value that has a different arrival time. But we don't want to change the original, ever, by any means! The original value is and remains a legitimate representation of the domain.

I see. The idea about immutability is good IMO, but your ways to achieve that are different from mine. My way would be to have the original struct be immutable, which protects the fields, but have the value under construction be mutable with public fields. Data pointed to by any arrays or pointers in the struct would still be immutable.

I don't see a need for fields to be non-referenceable since immutability protects them if the data in question is already committed.