We can instead give tree a proper type, give operations over the type proper signatures which help to explain what the operations do, and get verification from the compiler at the same time that we aren't mixing things up.
It's hard to beat
data Tree a = Node a [Tree a]
separation of effects from pure functions - this I cannot understand. Aren't these function pure?
And another part of "properness" - each node of a tree is a tree itself. Sometimes less types is more.)
Well, I'm old-fashioned, but I think that being able to put any kind of data into a list is the strength, not weakness of a language, and that user-defined ADTs and compound data-structures needs no explicit typing, they are nothing but conventions.
Types provide one important thing: static guarantees. Compile-time type checking can only work with strong, expressive data types, and ADTs provide a very good way of enforcing that. (Together with Haskell's `newtype` keyword, which really is nothing more than a convention, if you will, a different way of expressing a type synonym, but with the static guarantee that when you make up a `newtype Name = Name Text`, you cannot use `Name` wherever you can use `Text`, but you can access the `Text` `Name` is a wrapper for easily.
This makes it much easier for your intent to be expressed directly in the code, namely in the data types. Just compare:
makePerson :: Name -> Age -> Gender -> Address -> Person
makePerson' :: Text -> Int -> Text -> Text -> Person