Reputation: 2342
I want to model a hierarchy in F#, where each node must have a single parent, exception, obviously, the root node, which has no parent. My naive solution
type Node = {
Id: int // meta data for node
Parent: Node option
}
let root = { Id = 1; Parent = None}
let child1 = { Id = 2; Parent = Some(root)}
let child2 = { Id = 3; Parent = Some(child1)}
But my entry into F# has been through @swlaschin and he's blown my mind with creating descriptive domains. So it feels smelly to me that Parent
is an option, when 99% of the time, it's required. My best effort:
type Node =
| Node of NodeMeta * Node
| Root of NodeMeta
and NodeMeta = {
Id: int
}
let root = Root({Id = 1})
let child1 = Node({Id = 2}, root)
let child2 = Node({Id = 3}, child1)
Is there a more idiomatic way?
Upvotes: 6
Views: 280
Reputation: 2795
The pure functional idiomatic way to generate a tree is to use a Tree<'a> with no reference to the parent....a tree...is a tree, the "parent" property is derived from being a child of an existing node.
You then use a Zipper to navigate through the tree from parent to child and child to parent...rather like a "cursor" in a list of items.
see http://tomasp.net/blog/tree-zipper-query.aspx/
You can, if you want create your own tree, with distinct root and non root types, where only non roots are allowed to be added to existing nodes. You would then probably have to roll your own zipper as well, so the cost is high.
Upvotes: 0
Reputation: 6510
If I was building this for my own model in a Domain-Driven Design, I would probably define nodes as follows:
[<Struct>] type NodeId = private NodeId of int
module NodeId =
let create id =
// Replace with the proper validation rules for a Node Id
if id < 0
then Error "NodeId must be non-negative" // I would actually use a DU with each error case
else Ok <| NodeId id
let value (NodeId id) = id
type [<Struct>] RootNode = {Id: NodeId}
type [<Struct>] ChildNode = {Parent: Node; Id: NodeId}
and Node =
| Root of RootNode
| Node of ChildNode
member node.Id =
match node with
| Root r -> r.Id
| Node n -> n.Id
member node.Parent =
match node with
| Root _ -> None
| Node n -> Some n.Parent
member node.IsRootNode =
match node with
| Root _ -> true
| Node _ -> false
member node.IsChildNode =
not node.IsRootNode
This gives us the following:
NodeId
that wraps int
and an accompanying module that encapsulates any business rules around what makes a valid identifierRootNode
and ChildNode
that allow them to have only the required fields for that type of nodeNode
type that allows us to represent RootNode
and ChildNode
as the same type without requiring them to have the same fields, but still providing direct access to the underlying fields and allowing us to easily distinguish between RootNode
s and ChildNode
sThen, I would have a module for creating Node
s like:
module Node =
let createRoot =
NodeId.create >> Result.bind((fun id -> Root {Id = id}) >> Ok)
let createChild parent =
NodeId.create >> Result.bind((fun id -> Node {Id = id; Parent = parent}) >> Ok)
Upvotes: 6