The short answer is you just copy it into a container, and rebuild the container when the original object changes. This isn't as burdensome as you might think.
In other paradigms, we accept the tight coupling of a mutable reference because of the fear the object we are referring to will change and we will not pick up that change. With immutability that fear does not exist. Changes are only made in easily-defined places. When you make changes to the underlying objects, you can easily make changes or rebuild the associated aggregates in the same place.
Mutability also makes programmers want to maintain only a single copy of any given piece of data. They come to equate this one place in memory with that data's identity. They sometimes build massive data structures to make pointers to places in memory to preserve that identity.
Functional programmers come to think of values as data's identity. The programming language may implement it as references to the same place in memory, but that is irrelevant to the way you think about it. Instead of one massive data structure that refers to the same place, you use several small data structures that refer to the same value.
That value sometimes is a little pointer-like. It might be a (row,column)
of a game board, for example. Then you can set up a Map
from (row,column)
to Piece
for all pieces. Then you can filter that map to get friendly pieces and enemy pieces. Then you might pass those lists into a function to get a list of all your valid moves this turn, where a Move
has a from (row, column)
, a to (row, column)
, and a Piece
. Then you pass that list of moves into an AI function that selects a move, and we go back to the top and start again.
Here, the (row, column)
value semantically ties together the game board data structure and the move data structure and the friendly pieces lists, and the AI decision trees, and so forth, but they are not physically part of the same data structure. Immutability allows us to splinter off many derivative data structures without worrying about them getting out of sync.
If you do have a big complex data structure, there are ways to traverse and modify it in a functional way, such as lenses or zippers, but these are for use cases where your data is already in a complex form, such as XML manipulation, tree algorithms, etc. You wouldn't use them in the typical cases you use aggregation for in object-oriented programming.