So exactly what is an address? When I think about an address to a particular node in a tree, I think of some kind of description of getting to that node. Since your tree is n-ary, an address could be a list of indices, so e.g.
val someTree =
T [ (* address: [] *)
T [], (* address: [0] *)
T [ (* address: [1] *)
T [], (* address: [1,0] *)
T [] (* address: [1,1] *)
],
T [] (* address: [2] *)
]
So if your datatype tree
type were extended to hold values in a certain node, knowing the address of a node would be a sure way to navigate to the right value. Until then, the addresses still conceptually make sense even though they're maybe less useful.
[...] list of all addresses for the parent nodes, and by appending the adresses of all children nodes it eventually delivers the list of all possible adresses.
Assuming my interpretation is correct, then yes, the strategy you describe makes good sense.
You could start off by solving it for a single layer in the tree, e.g.
fun range n = List.tabulate (n, fn i => i)
val zip = ListPair.zip
fun addresses (T ts) =
let val ts' = zip (ts, range (length ts))
in ts'
end
Trying this,
- addresses someTree;
> val it = [(T [], 0), (T [T [], T []], 1), (T [], 2)] : (tree * int) list
it is supposed to say that
T []
, the first sub-tree, should have all sub-addresses prefixed with 0 : ...
.
T [T [], T []]
, the second sub-tree, should have all sub-addresses prefixed with 1 : ...
.
T []
, the third sub-tree, should have all sub-addresses prefixed with 2 : ...
.
Next I might have to think of a way to make addresses
call itself recursively for each sub-tree. If we ignore the above index-adding function, the recursion could look like:
fun addresses (T ts) =
List.concat (List.map (fn t => addresses t) ts)
where List.map (fn t => ...) ts
addresses each sub-tree T ...
in ts
. Because addresses t
itself produces a list of something, List.map (fn t => addresses t) ts
produces a list of list of those. To avoid an infinitely circular type of list of list of list of ..., List.concat
collapses that "list of list of ..." into a "list of ..." at each recursive step.
However, running this,
- addresses someTree;
! Warning: Value polymorphism:
! Free type variable(s) at top level in value identifier it
> val it = [] : 'a list
is pretty useless, since it doesn't actually do anything beyond address each node.
The pattern List.concat (List.map f xs)
is pretty common, so let's make a helper function that does both, just because it will make the code tidier. Also, let's try to combine the two strategies of prefixing each sub-result with an index of the current node with a full recursive traversal of each node:
fun range n = List.tabulate (n, fn i => i)
fun concatMap f xs = List.concat (List.map f xs)
val zip = ListPair.zip
fun addresses (T ts) =
let val ts' = zip (ts, range (length ts))
in concatMap (fn (t, i) => ...) ts'
end
Now, the ...
part is supposed to both do addresses t
to get a list of sub-results, each being a list of indices, and it's supposed to add i
to the front of each of those lists (e.g. with map (fn addr => i :: addr)
).
Assuming you solve this part, you may still run into generating zero results:
- addresses someTree;
> val it = [] : int list list
This is because addresses (T [])
should actually give [[]]
: The list of all addresses for the empty tree is the list of the address to the root node, and the address to the root node was defined (by me) as []
. So [[]]
would be a list with a single address to the root node.
Fixing that,
fun addresses (T []) = [[]]
| addresses (T ts) = ...
you're able to get a full list of addresses to all nodes:
- addresses someTree;
> val it = [[0], [1, 0], [1, 1], [2]] : int list list
I've come to realize that []
is not part of this list, so you may want to add that.
The above is just one suggestion. There are plenty of alternative ways to structure this solution. You could build addresses
using an explicitly recursive helper function that takes a second argument being an index:
fun addresses (T ts) = ...
and addresses_helper [] _ = []
| addresses_helper (t:ts) i =
map (fn addr => ...) (addresses t) @ addresses_helper ts (i+1)
Or you could generalise this into a foldl
/ foldr
like you were doing initially. Or you could define a zipWith
and rather than concatMap (... map ...) (zip ...)
you could have concat (zipWith ...)
:
fun range n = List.tabulate (n, fn i => i)
fun curry f x y = f (x, y)
fun zipWith f (x::xs) (y::ys) = f x y :: zipWith f xs ys
| zipWith _ _ _ = []
val concat = List.concat
fun addresses (T []) = [[]]
| addresses (T ts) =
let fun prefix t i = map (curry op:: i) (addresses t)
in concat (zipWith prefix ...)
end
I'd recommend going with a concatMap
or zipWith
-based solution rather than one using foldl
/ foldr
because I think they're much easier to read. They may cause some extra traversals because you with foldl
/ foldr
technically could build i
as you're recursing instead of calling range
, but it comes at the cost of readability.