Truly a nit-pick, but referential transparency is used wrongly by basically every FP-language for some reason. What they actually mean there is pureness, not referential transparency - it is pureness that allows the replacement of a function call with its result, not any other feature of a language.
Hell, Java may actually be more referentially transparent than Haskell, due to it not having macros - the one thing that can break this functionality.
Unfortunately it's very tempting to intorduce side-effects 'just this once', which can make those techniques less useful, and takes some discipline to avoid. For example, in Scala it's easier to just throw an exception instead of wrapping results in a 'Try' type; or likewise for null/'Option'; etc. mostly since those results then require map/mapN/flatMap/traverse/etc. to handle, rather than giving us values directly.
However, I think it's usually worth the effort. For example, those map/mapN/flatMap/traverse functions are essentially 'setting policies' for how effects should interact; whilst the 'core logic' can remain completely agnostic.
As a very simple example, if we have 'l: List[A]' and 'f: A => Option[B]', we can combine these in multiple ways, e.g.
We can replace 'Option' with 'Future' to talk about concurrency rather than emptiness. We can replace 'Option' with 'Try' to talk about exceptions rather than emptiness. All of those expressions remain identical.More esoterically, we can replace 'Option[T]' with 'Reader[X, T]' for dependency injection of an 'X'; etc.
I've used these techniques commercially in PHP, Python, Javascript, Haskell and Scala (and academically in Racket, StandardML, Coq, Idris, and Agda too)