Critique of immutable classes with circular references design, and better options
I have a factory class that creates objects with circular references. I'd like them to be immutable (in some sense of the word) too. So I use the following technique, using a closure of sorts:
[<AbstractClass>]
type Parent() =
abstract Children : seq<Child>
and Child(parent) =
member __.Parent = parent
module Factory =
let makeParent() =
let children = ResizeArray()
let parent =
{ new Parent() with
member __.Children = Seq.readonly childr开发者_StackOverflow社区en }
[Child(parent); Child(parent); Child(parent)] |> children.AddRange
parent
I like this better than an internal
AddChild
method because there's a stronger guarantee of immutability. Perhaps it's neurotic, but I prefer closures for access control.
Are there any pitfalls to this design? Are there better, perhaps less cumbersome, ways to do this?
You can use F#'s support for recursive initialization even when creating an instance of abstract class:
let makeParent() =
let rec children = seq [ Child(parent); Child(parent); Child(parent) ]
and parent =
{ new Parent() with
member __.Children = children }
parent
When compiling the code, F# uses lazy values, so the value children
becomes a lazy value and the property Children
accesses the value of this lazy computation. This is fine, because it can first create instance of Parent
(referencing the lazy value) and then actually construct the sequence.
Doing the same thing with records wouldn't work as nicely, because none of the computations would be delayed, but it works quite nicely here, because the sequence is not actually accessed when creating the Parent
(if it was a record, this would be a field that would have to be evaluated).
The F# compiler cannot tell (in general) whether this is correct, so it emits a warning that can be disabled using #nowarn "40"
.
In general, I think that using let rec .. and ..
to initialize recursive values is a good thing - it is a bit limited (one of the references must be delayed), but it forces you to keep the recursive references isolated and, I think, it keeps your code simpler.
EDIT To add an example when this may go wrong - if the constructor of Child
tries to access the Children
collection of its parent, then it forces evaluation of the lazy value before it can be created and you get a runtime error (which is what the warning says). Try adding this to the constructor of Child
:
do printfn "%d" (Seq.length parent.Children)
I think that Tomas's answer is the way to go. However, for completeness I'll show how you could use recursive records to create cyclic immutable objects. This can potentially get quite ugly, so I've hidden the immutable record implementation behind some nicer properties:
type Parent = internal { children : Children option }
and internal Children = { first : Child; rest : Children option }
and Child = internal { parent : Parent }
let rec internal listToChildren = function
| [] -> None
| c::cs -> Some { first = c; rest = listToChildren cs }
let rec internal childrenToList = function
| None -> []
| Some { first = c; rest = cs } -> c::(childrenToList cs)
module Factory =
let makeParent() =
let rec parent = { children = children }
and child1 = { parent = parent }
and child2 = { parent = parent }
and child3 = { parent = parent }
and children = [child1; child2; child3] |> listToChildren
parent
type Parent with
member p.Children = childrenToList p.children
type Child with
member c.Parent = c.parent
I guess something like this can also be done:
type ParentFactory private (n) as X =
inherit Parent()
let childs = [for i=1 to n do yield Child(X :> Parent)]
override X.Children = childs |> List.toSeq;
static member Create n = (new ParentFactory(n)) :> Parent
精彩评论