Basic Haskell is lovely though. It's why I liked Elm initially although it does take it a bit too far in the basic direction.
The problem with Haskell is it's purity and thirst for abstraction means more and more complex type definitions and ideas - monad transformers (yes I know they are supposed to be 'easy') - and GADTs for example. And if you want to use libraries, you need to get these concepts to merely be able to read the example code. It's table stakes like known what "npm i" means when looking at a node module doco.
Javascript on the other hand, for all it's warts, allows you to cheat by wrangling directly with the data. This has a lot of issues but it is a lot easier to grok - to the point where non programmers use it as a scripting language. Sometimes people do some weird shit with proxies/promises - and you'll get that type of code in any Turing-Complete and useful language, but on the whole most libraries do keep it simple and scrutable.
> The problem with Haskell is it's purity and thirst for abstraction means more and more complex type definitions and ideas [...]
If that's the case something goed wrong. Proper abstraction means having complex definition with simple types!
It's not difficult to create a stateful computation in a non-strict runtime environment. Laziness is compatible with (local) state. Haskell's random package is designed around explicitly threading the PRNG state through the computation:
import System.Random (Random, RandomGen, random)
import Data.List (unfoldr)
randomList ∷ (Random a, RandomGen g) ⇒ g → [a]
randomList = unfoldr (Just ∘ random)
Since the function passed to `unfoldr` always returns a `Just` value, the resulting lazy list never ends. If you're not familiar with `unfoldr`, it's essentially the opposite of `foldr`: instead of applying a reduction function to a list it produces a list by iteratively applying a generator function to a seed—in this case the PRNG state.As it happens, this function already exists as `System.Random.randoms`. You use it like this:
main = do
(xs :: [Int]) ← randoms <$> newStdGen
mapM_ print xs
There are related functions `randomR` and `randomRs` which take a lower and upper bound in addition to the state.If you were trying to achieve this effect with `randomIO` or `randomRIO`—perhaps to avoid explicitly threading the state—then you will run into difficulties. Not because of laziness; rather the contrary. The problem is that IO actions are strict by default, so you can't easily combine them to create a lazy list. You would need to use something like `System.IO.Unsafe.unsafeInterleaveIO` to defer the generation of the random values until they were actually demanded, but then you're mutating the shared, global PRNG state each time an item is evaluated from the list and the result would not be deterministic or repeatable even if you started with a known seed.
To the grandparent, assuming the trouble was as I understand it, the idea is to split your state and advance one new state down the lazy list while the rest of the computation uses the other new state.
Eliding constraints for brevity, if you have:
split :: g -> (g, g)
random :: g -> (a, g)
then you can say lazyRandomList :: g -> ([a], g)
lazyRandomList oldState =
let (listState, newState) = split oldState
in (makeList listState, newState)
where
makeList inState =
let (value, nextState) = random inState
in value : makeList nextState
When you match on the outer tuple, the split gets forced, but `makeList listState` is still a thunk. You can go ahead and use `newState` all you want without forcing it.Once you force it, you have a cons, with two thunks that can be forced independently. The tail evaluates to a similar cons produced from a different state. A lazy list.
It's true that it's infinite, but you can fix that (if you want to) by taking the head (perhaps a random number of elements); it's easier to control the distribution of length that way than with some fixed chance you produce nil at each step, although that approach works too.
Maybe I'm missing something?
Of course in a strict language with a decent type system like Scala you eventually find you want to use that style anyway. But you can come to it in your own time and see the benefits for yourself rather than having it forced on you.
The problem is while you can adapt and bolt on some functional programming techniques to existing languages like C# - to bring the power of the type system to an existing language is all but impossible. There are a few areas it has started to sneak in, like nullable types. But from my naive perspective you just can't bolt on an expessive type system like that, let alone a dependently typed one.