📜 ⬆️ ⬇️

Functional programming in Scala - is it necessary at all?

In this article, I would like to show, using the example of a simple, purely functional code, how it is modified by the addition of requirements for testability and extensibility, and what happens. I hope it will be interesting to anyone who is interested in software design and functional design in particular. It is also desirable to understand a little Scala language, well, or be able to quickly understand.

Let's try to write a simple program that calculates the expression 4 * x ^ 3 + 2 * x ^ 2 + abs (x). Since this is a post about functional programming, let's design everything in the form of functions, having carried out the operations of raising to a power and a module:

object Main { def square(v: Int): Int = v * v def cube(v: Int): Int = v * v * v def abs(v: Int): Int = if (v < 0) -v else v def fun(v: Int): Int = { 4 * cube(v) + 2 * square(v) + abs(v) } println(fun(42)) } 

Looks nice, isn't it? Now add a couple of requirements:

- we want to test the fun () function using our implementations of the square, cube and abs functions instead of the wired ones in the current implementation
- the cube function is slow - let's cache it
')
Thus, fun should take its dependencies as arguments, at the same time you can make a memoization of the cube function.

 object Main { def square(v: Int): Int = v * v def cube(v: Int): Int = v * v * v def abs(v: Int): Int = if (v < 0) -v else v //       //     (  ),        def fun( square: Int => Int, cube: Int => Int, abs: Int => Int) (v: Int): Int = { 4 * cube(v) + 2 * square(v) + abs(v) } //   -         , //     def memoize[A, B](f: A => B): A => B = new mutable.HashMap[A, B] { override def apply(key: A): B = getOrElseUpdate(key, f(key)) } val cachedCube = memoize(cube) // cachedFun -     ,   cube.   -     val cachedFun: Int => Int = fun( square = square, cube = cachedCube, abs = abs) println(cachedFun(42)) } 

In principle, the solution is working, but everything is spoiled by an ugly fun signature with four arguments scattered across two parameter lists. Let's wrap the first list in trait:

 object Test3 { trait Ops { def square(v: Int): Int = v * v def cube(v: Int): Int = v * v * v def abs(v: Int): Int = if (v < 0) -v else v } def fun( //  ,   ? ops: Ops) (v: Int): Int = { 4 * ops.cube(v) + 2 * ops.square(v) + ops.abs(v) } //     -       //   -      // ..      -  ,   Map , //   -  .  Guava  . val cachedOps = new Ops { val cache = mutable.HashMap.empty[Int, Int] override def cube(v: Int): Int = cache.getOrElseUpdate(v, super.cube(v)) } val realFun: Int => Int = fun(cachedOps) println(realFun(42)) } 

And the last thing you can get rid of is a partial use of the arguments of the fun function:

 object Main { trait Ops { def square(v: Int): Int = v * v def cube(v: Int): Int = v * v * v def abs(v: Int): Int = if (v < 0) -v else v } class MyFunctions(ops: Ops) { def fun(v: Int): Int = { 4 * ops.cube(v) + 2 * ops.square(v) + ops.abs(v) } } val cachedOps = new Ops { val cache = mutable.HashMap.empty[Int, Int] override def cube(v: Int): Int = cache.getOrElseUpdate(v, super.cube(v)) } val myFunctions = new MyFunctions(cachedOps) println(myFunctions.fun(42)) } 

Thus, we have a classic OOP design. Which is more flexible than the original version, which is more typed (Int => Int is certainly less understandable than MyFunctions.fun), which is effective in speed (the OP option will not work faster, but slower - easy), which is simply clearer .

Perhaps readers will have the question "Why not monads?". Monads in Scala are impractical - they work slower, they are difficult to combine, their types are too complex, which makes it necessary to write code that is very abstracted from types. That does not improve readability and certainly does not reduce the compilation time. Although, I would be very interested to see a practical solution to this simple puzzle on monads in Scala.

The title of this article ends with a question mark for a reason - I publish the thoughts that I have when studying AF, hoping to help others and in the hope that others will share their vision and their experiences in such a difficult area as simple, understandable, resistant to bugs and extensible software design.

Waiting for your comments)

Source: https://habr.com/ru/post/331130/


All Articles