We define what we have first (after all this is mathematics):
EmailList = a list of Email
Email = a string that goes "something" then "@" then "domain"
Now what we want:
DomainList, no duplicates.
Ok so no we're set up. We want a function that takes us from an EmailList to DomainList, as defined above. There are no objects so we simply say the relationships between things. I will introduce the tools we need:
map = takes a function and a list and applies that function to every element of the list, returning a new list.
We use map over EmailList with the function extractDomain which we will write later (it's trivial to do without mutation so I won't show here).
So now we have a list of domains, but there might be duplicates.
So now I have DomainList(with duplicates) and I want DomainList(without duplicates), so I want a function from DomainList to DomainList.
Well, what's the difference between what I have and what I want? What I have has too much, and I want less (ie. the list I have has too many domains (duplicates) and I want fewer (unique)).
filter = takes a list and a function. That function it takes, that you will supply to filter, we'll call f. Function f takes one list element (so filter will call f for every element) and it must return a boolean to mean whether that element should go into the new list, or if it should not go into the list.
So we could use filter to go from a DomainList with too much, to a DomainList with fewer things. However each time our function f is called, we don't know if this function has already been called before with that same domain; and because we don't have state to change a variable to keep a tally, it seems impossible.
So let's approach the problem in a different way. One thing we could do is sort the list, then group adjacent similar items, then map over the list taking the first element of each item. But we won't do that.
We need something more generic than filter. Let's look at fold.
fold = takes a list, a function f, and a starting point z (that we call 'zero'). For each element of the list, it calls your supplied function f with the current element and the zero; but then, it would be useless because it would be the same zero all the time. So what fold does, to be useful, is that whatever the result of calling your function f with a list item and the zero is, THAT value (so the value the call to yourFunctionF(list_item, zero) returns) will be the next 'zero'. So now we have a little machine called fold that can thread something we have through its inner mechanism and play along with us.
Using that we can say the zero is the list of elements we've seen so far.
So now our DomainList -> DomainList function will be a fold. That fold will take a the DomainList we have, and a zero that will be the empty list (to start with!) and a function f. That function f will simply check if the item is inside zero. If it is, then simply return zero (because that will be the zero for the next call to f!); if it's not, then return a new zero with that item added to it.
Now we've achieved the goal without any mutation (ie. without us talking/concerning ourselves with any mutation. of course there's lots of mutation going on in RAM but the point is not to eliminate that!)
Does that help?