I might just not understand your meaning but I don't think what you're saying about abstractions becoming indistinguishable from language features is true. Here's a (pure) Haskell function that is passed a random number generator and "returns" an infinite list of random numbers:
infiniteRandoms :: (RandomGen g, Random r) => g -> [r]
infiniteRandoms = unfoldr (Just . random)
This just uses a standard function from the ubiquitous `Data.List` module (`unfoldr`) and the most basic functionality from the `System.Random` module. Nothing about that is "indistinguishable from a language feature", IMO.
In other languages, of course, this operation would look different and potentially have side effects depending on the libraries used.
> Nothing about that is "indistinguishable from a language feature", IMO.
You are right in this case.
The parent comment said "You'd want some additional abstractions to make that work well.". Maybe passing around a random generator explicitly is not what we want. We abstract things away because we want to focus on "what" some code does, not necessarily "how". Most of the time, I want my random numbers to be random; the case where I need to replicate a specific sequence of random events is actually rare. That's why "random" is available as a system facility and does not need to take the random generator's state explicitly ("There is a single, implicit, global random number generator", https://hackage.haskell.org/package/random-1.1/docs/System-R...)
My point is that hidden state is not bad when the explicit simulation of state is cumbersome to use.
As an aside, that's also the reason I dislike Go's approach to error handling. There is so much praise about having everything visible, based on the claim that "there is no happy path"
There is no happy path. This is a very common misconception which causes so many programs to be unreliable POSes... handling errors is very important, just as important as handling success. Hell, 99% of the time, it's not an error, it's just an alternate option. Hey, the file isn't there... that's not an error, it's just a different possible state of the universe. Error code is application code.
And yet, error handling code follow certain patterns that can be abstracted away (99% of the time!).
Besides, in Go forums or blogs, you can spot code samples where "error checking is omitted for clarity", which I find quite representative of the kind of noise explicit error handling code introduce:
> Maybe passing around a random generator explicitly is not what we want.
That's fair. What I often want is to pull a function out of my program and test it without a lot of work. This is easy if the function is pure.
> My point is that hidden state is not bad when the explicit simulation of state is cumbersome to use.
That depends on your goal. If your intention is to make state explicit, then by definition there has to be more code to make it so. Again, a preferable abstraction for this makes the boilerplate go away, while keeping everything pure. That added effort gives you testability but I know there are instances where that effort is not worth it.
In other languages, of course, this operation would look different and potentially have side effects depending on the libraries used.