|
Pure-functional techniques can give advantages 'down the road', regardless of what language they're used in. For example, pure functions can be tested without any mocking; they can trivially be made concurrent/parallel; their contents can be rearranged/refactored (referential transparency); etc. 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. l.map(f) : List[Option[B]] // Run f on all elements, keeping all results
l.flatMap(f) : List[B] // Run f on all elements, discarding empty results
l.traverse(f): Option[List[B]] // Run f on elements in sequence; abort if any are empty
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) |