Then how do you obtain such a random number that you use as a parameter? After all, it cannot be the result of a function, because then we'd be in a 'dysfunctional world'. Yet, it cannot be a constant either. I guess you'd have to handle it like file IO, that is, you are forced to introduce that impurity into your code.
"Functional" doesn't necessarily imply "pure", but some functional languages do offer great tools for reasoning about purity and state. Here is a simple example in Haskell:
showTenRandoms :: IO ()
showTenRandoms = do
gen <- getStdGen
let randoms = unfoldr (Just . next) gen
putStrLn $ show $ take 10 randoms
We create the random number generator "in the `IO` monad", where non-pure actions are allowed. Then we pass it to `unfoldr`, which is a pure function (you can tell because it's in a `let` binding), to generate an infinite list of random integers. This works because when we use the generator to get a random number, it isn't mutated, but instead returns a random number and a new generator. The `unfoldr` call basically just iterates on that operation.
If you don't know Haskell, this code will probably look like nonsense, so you'll just have to take my word for it. I'd be happy to explain or clarify further.
The same way that purely functional languages do input/output. (think, how do purely functional languages even write to the console, if they are in fact pure?). Creating the initial random generator is an effectful operation much like writing to the console; it get's handled in the Main/root part of the program.
Understanding exactly how input/output in pure languages work involves Understanding the distinction between the RTS (Run Time System - the thing that runs the effects) and your Program (the recipe for what effects to run).
You use a generator that receives a state and returns a random number and the modified state. You'd want some additional abstractions to make that work well.
The abstraction then becomes indistinguishable from a language feature, and you have come back full-circle to where you started. You call "random" without managing the state explicitly, except when you need to.
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.