There are several approaches, neither of them devoid of code smell.
First, notice that <=<
is the composition in Kleisli categories,
so while you can not write f(g(h(x)))
you can write (f.g.h) x
provided the three functions live in a Kleisli
category.
Second, others have resorted to domain-specific languages, where you can write ordinary lambda functions but have a probabilistic choice operator at your disposal, too. The drawback is that these DSLs must be interpreted, and your PRNG approach is effectively one way to embed such a probabilistic lambda calculus into ordinary Haskell. Unfortunately I know no quasi-quoters that do this translation for you.
You might want to experiment with LazyPPL which internally uses a technique similar to the previously suggested value-supply package: At the core of one of the monads is an infinitely wide and deep tree of (possibly PRNG-generated) random numbers.
data Tree = Tree Double [Tree]
newtype Prob a = Prob (Tree -> a)
Regarding referential transparency, as @Ambrose mentioned it depends on the point of view: If the semantics of a variable x
is a probability distribution, then we might very well call a probabilistic program referentially transparent, since one can talk about equality of distributions. If the semantics is a stream of PRNG samples, however, then one must ask what a good equality relation for these streams is.