In fact, you do not hold on to the root when you lower the left subtree.
go n (Tree _ lr) = go (n - 1) l . go (n - 1) r
Thus, the root turns into two tricks, composed together. One contains a link to the left subtree, the other contains a link to the right subtree. The root node itself is garbage.
The left and right subtrees themselves are just thunks, because the tree is created lazily, so they do not consume much space yet.
We evaluate only go n (Tree _ lr) because we evaluate depthNTree nt , which is go nt [] . Thus, we immediately force the two created go calls to simply turn the root into:
(go (n - 1) l . go (n - 1) r) [] = (go (n - 1) l) ((go (n - 1) r) [])
And since this is lazily evaluated, we first make an external call, leaving ((go (n - 1) r) []) as thunk (and therefore do not generate more r ).
The recursion in go will force l , so we will create more of this. But then we do the same thing again one level down; again, that the node tree becomes garbage immediately, we generate two tricks, holding the left and right sub-trees, and then we force only the left one.
After n calls, we will evaluate go 0 (Tree x _ _) = (x:) . We created pairs of ten n and forcibly left n left, leaving the necessary in memory; because the right subtrees are unvalued thunks, they are constant space each, and there are only n of them, therefore only O(n) space. And all the nodes of the tree leading to this path are now not found.
In fact, we have an external list constructor (and the first element of the list). Forcing more of this list will examine these regular subtrees expanding along the composition chain, but there will be no more than n .
Technically, you linked the link to tree 1 in the treeOne with global reach, so in fact you could keep the link to every node that you ever produced, so you rely on GHC to treeOne that treeOne only ever used once and should not be saved.