When I read or write functional style code, I am often amazed by the extraordinary abstraction of functional ideas. As a programmer who has always believed in OO, you are no stranger to “abstraction”. I even defined the essence of object-oriented thinking as two words: Responsibility and Abstraction. Design is good as long as responsibilities are properly assigned; When properly abstracted, programs become leaner and more extensible. If you’re familiar with GoF’s design patterns, you can read “abstract” into almost every pattern.
However, in any case, object-oriented thought actually constructs a noun world, which to a large extent limits its world view, and it can only take Entity as the core. Although we can still extract common characteristics for entities, these characteristics cannot exist in isolation as behaviors, which is the Achilles’ knees of object-oriented thinking.
If the object – oriented thought is the philosophical view of the material world, then the functional thought shows the pure mathematical thinking. Functions, as first-class citizens, do not represent any substance (object), but only a transformation behavior. Yes, any function can be considered a transform. This is the highest abstraction of behavior, representing some kind of action between types. Functions can be extremely atomic operations, combinations of atomic functions, or a more semantically distinct layer of function representation encapsulated on top of the combination.
To understand the transformational nature of functions, we must learn to “see” the transformational nature in concrete behavior. This “insight” can be interpreted as deconstructive analysis, in the same way that nuclear analysis is used to calculate the number of atoms in the carbon-14 isotope when dating fossils. The “atomic” functions we deconstruct often have extraordinary abstraction abilities. For example, we can deconstruct the atomic fold function for the sum and product operations of a set. Although sum is the sum and product is the quadrature from the perspective of behavior characteristics, both of them start from an initial value and perform operations on set elements in turn from the perspective of abstraction. The operation itself is another abstract transformation operation, thus introducing the concept of higher-order functions. To make a fold more than just a specific type, you can introduce the type system of a functional language. Folds can be classified as foldRight or foldLeft depending on the direction of the fold. FoldRight (or Flodr) functions are defined as follows:
/ / the scala language
def fold[A.B](l: MyList[A], z: B)(f: (A.B) = >B) :B = l match {
case Nil => z
case Cons(x, xs) => f(x, fold(xs, z)(f))
}Copy the code
- haskell
foldr f zero (x:xs) = f x (foldr f zero xs)
foldr _ zero [] = zeroCopy the code
Understanding Scala provides an interesting case study of Scala’s options, revealing an abstraction similar to fold. This example shows how to construct a variable from multiple variables that may not be initialized. Option is perfect for handling this situation. The nature of Option is described in my blog not so simple as Null Object, which I won’t repeat here. This example wants to create a connection from database configuration information. Because the configuration information may be wrong and the connection may be null, the API using Option is more robust:
def createConnection(conn_url: Option[String],
conn_user: Option[String],
conn_pw: Option[String) :Option[Connection] =
for {
url <- conn_url
user <- conn_user
pw <- conn_pw
} yield DriverManager.getConnection(url, user, pw)Copy the code
Now, let’s make this function infinitely abstract, which is to remove some of the complex and redundant representational information, like filtering out the dazzling array of colors and presenting only the simplest black and white. First, we erase the “create connection” feature, and then we erase the type information. Fact is, we can see the createConnection to DriverManager. GetConnection conversion, after the transformation, to create a connection, can be introduced into three Option [String] parameter of type, Gets the result of type Option[Connection]. Then, by removing the concrete String, we can abstract the following “conversion” operation:
(A, B, C) : = > D convert (Option [A], Option [B], Option [C]) = > Option [D]Copy the code
Note that this conversion operation is a function-to-function conversion.
The book finds the right concept to aptly describe this “transformation” operation, called lift:
def lift[A.B.C.D](f: Function3[A.B.C.D) :Function3[Option[A].Option[B].Option[C].Option[D]] =
(oa: Option[A], ob: Option[B], oc: Option[C]) = >for (a <- oa; b <- ob; c <- oc) yield f(a, b, c)Copy the code
Function3 is actually A wrapper around the (A, B, C) => D function in Scala. I prefer the higher-order form:
def lift[A.B.C.D](f: (A.B.C) = >D) : (Option[A].Option[B].Option[C]) = >Option[D] =
(oa: Option[A], ob: Option[B], oc: Option[C]) = >for (a <- oa; b <- ob; c <- oc) yield f(a, b, c)Copy the code
Lift functions are broad, abstract the DriverManager. Before the getConnection () function is a concrete object is converted. It can be passed as an argument to the lift function:
val createConnection1 = lift(DriverManager.getConnection)Copy the code
The lift function returns a function that is essentially the same as the createConnection() function defined earlier. Because Lift wipes out specific type information, it can promote getConnection not only to functions with options, but also to all functions of the form (A, B, C) => D. Let’s customize a Combine function:
def combine(prefix: String, number: Int, suffix: String) :String =
s"$prefix - $number - $suffix"
val optionCombine = lift(combine)Copy the code
Distinguish the execution results of combine function from opitonCombine function:
Ultimate abstractions such as fold or lift are common in functional language apis, such as monad filter, flatMap, and map for collections, sequence, and andThen for function composition. We can also name this basic transformation in conjunction with transformation semantics, making the code more concise and readable. For example, for the following three function definitions:
def intDouble(rng: RNG) : ((Int.Double), RNG)
def doubleInt(rng: RNG) : ((Double.Int), RNG)
def double3(rng: RNG) : ((Double.Double.Double), RNG)Copy the code
We can abstract out the generic schema for RNG => (A, RNG) and semantically name it Rand. In Scala, we can alias this transformation using the type keyword:
type Rand[+A] = RNG= > (A.RNG)Copy the code
When we take a look at object-oriented thinking after using functions as basic units of abstraction, we see that most of the design principles and design patterns in OO can be reduced to functions. Scott Wlaschin gives a very graphic comparison in Functional Design Patterns:
Obviously, functions are the purest abstraction. As the saying goes, simplicity can mean everything sometimes.