Question about circular references

David Alayachew davidalayachew at gmail.com
Tue Jul 4 13:22:10 UTC 2023


Hello Gavin,

Thank you for your response!

Let me start by saying your F# example meets all the needs I had for my
original problem. Thank you for seeing my point. I will address your F#
example momentarily.

> As Ron has suggested - if you want to build a cyclic
> structure then we already have fantastic technology for
> that - use classes. You want all the other fields to be
> immutable? Use final! Once we support patterns in
> classes, then you’ll get many of the advantages of using
> records as a client.

Maybe you can help me out with that. I've had a very extensive back and
forth with Ron, and I'm not seeing how to accomplish this with classes
while maintaing directness and immutability.

If I were to model this as a class, I would want this class to be nothing
but the most relevant bits of data, transparent for all to see (you can see
why I instictively reached for records here). That way, I can maintain as
direct an abstraction as possible without any of the fluff.

So, my first thought would simply be to have a class something along the
lines of this.

```java

public final class Node
{

//Unmodifiable
public final Node t, u, v;

public Node(Node t, Node u, Node v)
{

this.t = t;
this.u = u;
this.v = v;

}

//equals, hash, etc.

}

```

But this still hits the same problem of not being able to reference a
variable that hasn't been assigned yet. How would I get past this while
using normal classes?

Ron suggested modeling my data as edges, but once I try and switch from 8
Nodes with references to other Nodes and instead use (up to) 64 edges, I've
blown up my example into something significantly more complex in order to
work around the fact that the language does not permit immutable, direct
self-references.

The best workaround that I have found thus far is just to create a set of
Nodes, then construct a Map lookup where I can find all edges that touch
the given key. But this is the very workaround that I suggested in my
original post, and it doesn't meet my needs because that strategy loses
clarity almost immediately after you get any sort of scale.

I want to be able to look at my code, and have the direct relationships
between nodes be tightly coupled to the Node. I want it to be as easily as
with a non-cyclic representation. Brian Goetz's Data-Oriented Programming
has a good example of a tree like structure that captures what my goal is.
And my original code example with T, U, and V shows exactly what I am
talking about. There, I have minimized overhead so that the relationships
can be as visible as possible.

> With records we have carved out a subset of classes that
> fit the design, for which we can give a great experience
> without massive changes to the language and its
> semantics. We could have gone further but we would have
> to have made compromises that weren’t worth the price.

Yes, I don't think that this is a problem with records per se. Rather, I
think that this is a more general problem in Java itself that limits the
realm of possible representations, and it just so happens that some of
those off-limits representations are applicable to records too. Does that
make sense?

> PS We also provide another option: Want to be a hacker? Make your record
with array components and mutate the array contents to your heart's content!
>
> record Loop(String head, Loop[] tail){};
>
> Loop[] tl = new Loop[]{null};
>
> var l = new Loop("hello", tl);
>
> // A loop of hellos
> tl[0]=l;
>
> var tmp = l;
> for(int i=0; i<100; i++){
>     System.out.println(i+": "+tmp.head());
>     tmp=tmp.tail()[0];
> }
>
> // Make l a loop of hello worlds
> tl[0]=new Loop("world", new Loop[]{l});
>
> for(int i=0; i<100; i++){
>     System.out.println(i+": "+tmp.head());
>     tmp=tmp.tail()[0];
> }
>
> (Don’t tell anyone I showed you this code :-) )

Hmmmmmmmmm, now that I think about, I remember reading somewhere else that
there is a proposal for being able to "freeze" arrays. Freezing an array
would make it unmodifiable, in the same way that Collections can be.

If/when that JEP comes out, I could swap out my t, u, v for numeric
transitions instead, then once I have finished, I can freeze my array to
make it unmodifiable. I'll concede that, if frozen arrays ever come out,
then this strategy meets all the needs I specified.

However, I would far rather we get something like your F# example, which I
will now address.

> I think what you are asking for is “why can’t I build
> cyclic record values”?
>
> This is a reasonable question - it’s a very common
> question in languages that are build to be immutable from
> the ground-up, e.g. functional languages. For these
> languages the solution is either to just add pointers
> (e.g Standard ML took this approach), or to support
> something more first class. If you look at F#, you’ll see
> that they support this - although it’s pretty hidden in
> the spec! They allow (i) let expressions to be recursive
> (with a REC keyword) and (ii) they provide the AND
> operator to define mutually recursive value bindings and
> type declarations. This leads to code like this (taken
> from
https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/records )
>
> // Create a Person type and use the Address type that is
> // not defined
> type Person =
>   { Name: string
>     Age: int
>     Address: Address }
> // Define the Address type which is used in the Person
> // record
> and Address =
>   { Line1: string
>     Line2: string
>     PostCode: string
>     Occupant: Person }
>
> // Create a Person type and use the Address type that is
> // not defined
> let rec person =
>   {
>       Name = "Person name"
>       Age = 12
>       Address =
>           {
>               Line1 = "line 1"
>               Line2 = "line 2"
>               PostCode = "abc123"
>               Occupant = person
>           }
>   }
>
> I think this is the sort of thing you are after, right?

Lol. This would absolutely meet my needs if this were possible.

> The issue is that this would create **all sorts of
> problems**, e.g. around DA/DU, initialisation semantics,
> pattern matching semantics, etc. etc.

I see what you mean. It sounds like what you are saying here is that, the
benefits this would provide would not be worth the costs it would incur? If
so, I am willing to accept that this particular strategy would not be an
effective fit for Java.

But since you broached the subject (:P), RedIODev raised a point that I
think might be the best solution out of this.

Here is a link to the response in question --
https://mail.openjdk.org/pipermail/amber-dev/2023-July/008141.html

RedIODev says "I think the main problem as I understand it is, that you
cannot forward declare the identity of an object. So it’s technically not a
record problem but a class problem."

The F# example you described makes sense to me and the costs also make
sense to me.

But how do you feel about the ability to forward declare the identity of an
object, and then use it before it's declared, with the compiler-enforced
promise that I will declare and populate that object before ever attempting
to dereference its pointer? I would even be fine with forcing the object to
be declared/populated/initialized (what's the right word here?) in the same
block that it's variable was declared at.

Would this solution instead be a viable strategy?

Thank you for your help and insight!
David Alayachew
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20230704/40208b2d/attachment.htm>


More information about the amber-dev mailing list