There are other ways to do this, but they may not have the properties you want.
Suppose you wanted to represent an immutable value tree constructed from leaves. It is pretty simple. You can have a node constructor that takes a value and a list of child nodes. This makes it quite easy to create new trees from old trees, and they are guaranteed to be acyclic.
Now suppose you want to represent an immutable oriented graph of values. Now you have a problem that nodes may have loops; there can be no βsheetβ for plotting. The solution is to abandon the principle that node knows what its neighbors are. You can imagine an immutable graph by creating an immutable set of nodes and an immutable list of edges. To add a node to an immutable graph, you create a new graph with this node added to the node set. Similarly for adding edges; you create a new list of edges. Now the fact that there are cycles in the graph topology does not matter; no object has a loop in which objects it refers to.
Without knowing more about your actual problem space, it's hard to say which immutable data structure will work for your application. Can you tell us more about what you are trying to do?
I am trying to simulate a collection of types. Each type has a name and several attributes. Each attribute has a name and type. There are several mutually recursive types and that this problem arose there.
Well, you should have said that first. If there is one thing that I know of, it is type analysis. Obviously, the compiler must be able to handle all kinds of crazy type situations, including types with cyclic base classes, loops with internal types, type arguments, type restrictions, etc.
In the C # compiler, we solve these problems mainly by making objects "delivered" in their immutability. That is, when we first create a set of types, each type of object knows its name and its position in the source code (or metadata). Then the name becomes unchanged. Then we enable the base types and test them for loops; then the base types become immutable. Then we check for type constraints ... then we resolve the attributes ... and so on until everything is parsed.
I looked at other ways to do this. For example, we could use the same technique that I just proposed for graphs: create an immutable object, called, for example, "compilation", to which you can add types, thereby creating new immutable compilations. Compilation can track the "edges" between a type and its base type in an immutable hash map and then check the resulting graph for loops. The downside is that the type does not know its base type; you should ask compilation what the base type of a type is.
Here you can do the same. You can have a βsetβ class that contains a set of immutable types, and a multi-map from type to set of immutable attributes. You can create a set of types and a set of attributes as you like; the thing that changes is a map, not a type.
The downside of this is that you no longer request a type for your attributes; you are requesting a set for type attributes. This may not suit your needs if you need to pass types independently of any set.