So far, we’ve only shown how to use and define basic functions at the Scala syntax level. This chapter formally introduces how to apply the advanced features of Scala’s functions: higher-order functions, uniform accessibility principles, currie, partial functions, and so on, which are directly related to the subsequent branch of Functional programming in Scala.

In Scala, functions are first-class citizens. This means that not only can a function be used as a parameter or return value to another function, it also means that a function can assign a value to a variable. For Scala, a data type can contain not only a primitive type, a reference type, but also, from a more abstract point of view, a function.

1. Higher-order functions

Given a function F(x) whose parameter x is another function G(·), the function F of the form F(G(·)) is called a higher-order function. Here’s a simple example:

// This argument is something we haven't seen before in Scala.
// If you're used to the Java 8 version of lambda programming, this writing is familiar: you can think of it as a Function
      
        interface in Java, where T and R are Int values.
      ,r>
def higher_order(function: Int= >Int, parameter : Int) :Unit = {
	// Print the exact result of this operation.
	println(function(parameter))
}
Copy the code

Function: Int => Int is a function that receives an Int and returns another Int.

The functionality of the higher_Order function is actually determined by the function passed in. Let’s define twice, and pass it as an argument to the higher order function. Let’s see what higher_order does with the parameter:

// This function returns twice the value of the original argument.
def twice(parameter: Int) :Int = 2 * parameter

Println (twice(parameter))
higher_order(twice,2)
Copy the code

The console will print the result: 4.

In fact, not only can the function itself be treated as an argument, it can also be passed as a return value. Like:

def higher_order(function: Int= >Int) : Int= >Int = function
Copy the code

The logic for this higher-order function is simple. It takes a function of type Int => Int and treats it as the return value. In addition, when function is a function with multiple arguments, you should use parentheses (). Like:

// Function is a function that takes (Int,Double) as arguments.
def higher_order(function: (Int.Double) = >Int, parameter: Int) :Unit=???Copy the code

1.1 Call by name and call by value

When passing a function call as an argument to another function, the Scala interpreter has two ways to handle it, as shown in the following example. Given the following declaration, where function foo is a higher-order function that receives Int => Int.

// This is a function of type Int => Int.
def foo(i: Int) :Int = i
// This is a function of type Int => Int.
def twice(i: Int) : Int = 2 * i
Copy the code

In the following method of calling foo, Scala precalculates the result (40) of twice(20) and passes it as an argument to the function. This method is called call-by-value. The advantage of pass-value calls is that because the result of the calculation is obtained first, repeated calculations are avoided when the I variable is used again inside the foo function.

foo(twice(20))  // => foo(40)
Copy the code

In some cases, we don’t want the Scala interpreter to compute the result value of the function passed in. The reason for this is that the calculation of this function is expensive (requires a lot of I/O, or CPU resources), but there is no guarantee that this calculation will be used internally in foo’s higher-order function. In other words, we don’t want the program to spend a lot of resources doing nothing.

If you want Foo to be delayed in processing this function, only actually calculating the result when it is needed, then you need to change the pass-value call to call-by-name. To do this, modify the parameter list of function foo to look like this:

// Use a space between: and =>!
def foo(i: => Int) = i
Copy the code

The form of I has been changed from Int to => Int, which now represents an expected value of type Int that might be computed and returned by another function.

foo(twice(20)) // Twice (20) has been calculated late.
Copy the code

1.2 Distinguish between parameter lists()=> XXX => XXX

An argument of type () => XXX takes a function whose argument list happens to be empty (we call this an empty parenthesis function, which we’ll refer to later) and returns a value of type XXX.

def higher_order(function: () => Int) :Unit = {
  // Called a function that could cause side effects.
  println(function())
}
Copy the code

An argument of type => XXX actually represents a function or procedure that will be delayed in the calculation, resulting in a value of type XXX. It has a different meaning from () => XXX.

// The action of this higher-order function depends on the function passed in.
def higher_order(function: => Int) :Unit = {
	// Call a pure function with no side effects.
	println(function)
}
Copy the code

1.3 Function Side effects

The side effect of a function is when the logic inside the function interacts with the environment. For example, it modifies a global variable or commits or modifies data to a remote database.

// Declare an external param variable.

var param  = 0

// This function affects an external variable.
// This is an impure function.
def side_effect(int: Int) :Int ={
  param +=1 
  int * 2
}

// When this function is called, it causes the param variable in the external domain to change, so it has side effects.
println(side_effect(2))
Copy the code

Side effects are divided into benign side effects and malignant side effects. Sometimes we artificially modify external variables for the purpose of statistical global variables, and such cases are benign side effects. However, if the side effect of a function can cause an error that the programmer did not anticipate, or cause an unintended result, this is a bad side effect.

In a concurrent task, we want each process (function) to be independent of the external factors and not change the external factors, so as to reduce some of the trouble caused by data synchronization. Functions that satisfy this condition are called Pure functions. In contrast, functions that have side effects (whether or not they are expected to occur) are divided into Impure functions.

1.3.1 pure functions

The interaction between a pure function and the outside world is Explicit: the outside world can interact with it only through one channel: input parameters and return values based only on those parameters.

val a = 5
def pure_function(x: Int, y:Int) :Int =	2*x + y
val z =pure_function(3.2)
Copy the code

Take the above code for example: the value of z ** depends only on the input values x and y ** when the function is called, and not on any other variables. In other words, as long as x and y are constant, the result of this function, z, must also remain constant. All the mathematical formulas (functions) we learn can be classified as pure functions. For example, the binomial distribution probability model:


f ( n . p . k ) = C n k p k ( 1 p ) n k f(n,p,k)=C^k_n p^k (1-p)^{n-k}

Given the number of experiments n and the probability p of event occurrence, the probability of k occurrence can be obtained by f(n,p,k), and when n,p,k remain unchanged, the result is always the same, which is not affected by external objective factors.

1.3.2 Impure functions

In addition to returning a value (or not returning a value), impure functions include an implicit way of interacting with the outside world. The most common is to read or modify global variables, or to print information to the console/terminal.

// This variable needs to interact with the Impure function
// So you need to declare var.
var count = 0

def impure(param : Int) :Int = {

    // This function changes the state of the external variable.
    count +=1

    param * 2
}

val result : Int = impure(4)
println(The s"impure function is called$countTime. "")
Copy the code

1.4 Functions with empty parentheses and functions without arguments

When defining a function that requires no arguments, we can choose to omit the parentheses directly or simply use () to indicate that the argument list is empty. In fact, both functions with no arguments and empty parentheses are of type ()=>XXX. Even so, we need to make a distinction between the two for two reasons: one is whether functions have side effects, and the other is that no-argument functions are related to the uniform access principle.

1.4.1 Parameterless Method

When a function definition does not declare a parameter list in parentheses, or it only has a parameter list of implicit variables, the function is called parameterless Method. It is sometimes called a parameterless function.

// This is a function with no argument list.
// Formally, the definition of this function is closer to a variable.
def parmLessFunc1: Int = 30

// This is a function with only an implicit argument list.
def paramLessFunc2(implicit param :Int) :Int = 2 * param
Copy the code

Traditionally, we should define a function as parameterless when it requires no arguments, has no side effects, and is only useful from the return value. When calling a function without arguments, you do not need to, and should not, include () as an argument list unless you want to override the function’s implicit variables.

// No problem.
parmLessFunc1

// No parentheses may be used when calling a function with no arguments.
//parmLessFunc1()

paramLessFunc2
// Since paramLessFunc2 has an argument list composed of implicit variables, this call is legal because it overrides the implicit variables.
paramLessFunc2(3)
Copy the code

Note that there is no special connection between the parameterless function and the named call, which omit the argument list.

1.4.2 Empty Parenthesis Function (empty-paren Method)

An empty parenthesis function means that the function is defined with an empty parenthesis () to indicate that it accepts no arguments, and it retains an explicit parameter list compared to a no-argument function.

var a : Int = 2

def emptyParenFunction() :Unit = {
	// This function has an obvious side effect.
	a += 1 
}
// It is a good idea to call an empty parenthesis function with () to remind yourself that the function will have side effects.
emptyParenFunction()
// If you call an empty parenthesis function without (), the compiler will not report an error, but IntelliJ will tell you not to do so.
empthParenFunction
Copy the code

A function that returns a value of Unit (that is, it does not provide a value to use) is only useful as a side effect. A function that does not return any value and has no side effects is meaningless.

When we choose to define an empty parenthesis function, we are implicitly conveying to the caller of our code that although the function does not require explicit arguments to be passed, the function itself has side effects. Callers of the code should be aware of the changes to the external environment, such as the console, or the disk, or the database, caused by calling this function.

1.5 Uniform Access Principle

See wikipedia for an explanation of this principle:

The Uniform Access Principle was put forth by Bertrand Meyer. It states “All services offered by a module should be available through a uniform notation, which does not betray whether they are implemented through storage or through computation.” This principle applies generally to object-oriented programming languages. In simpler form, it states that there should be no difference between working with an attribute, precomputed property, or method/query.

There are two ways to get a value: either by calling a function and getting a return value, or by accessing a literal directly through a variable. Look at the following parameterless function str0 and variable str1. They have little difference except for a slight difference in keywords:

def str0 : String = "hello scala"  
val str1 : String = "hello scala"
Copy the code

In other words, we want to get a Hello Scala string, and we have two ways to get it:

Str0 is a String variable.
println(str0)

// Str1 is a no-argument function that returns String.
println(str1)
Copy the code

This can be confusing for code callers: Since Scala allows parameterless functions, it seems impossible to tell whether it is a variable or a function just by looking at an identifier. However, if the caller of the code just wants the string Hello Scala, does it really matter to him whether the identifier is a variable or a function?

This is the uniform Access principle advocated by Scala (and not just the language) : if the caller of the code only needs to get the expected result, he doesn’t need to know how the result was calculated. Everything in between, whether the result was computed, or whether it was evaluated directly, is covered by a short identifier.

As a result, the line between parameterless functions defined by def and variables decorated by val seems blurred. For Scala, the two can indeed be considered equivalent under certain conditions. As another example, here is a piece of Java code. This class has the property and function name of the same name.

public class PlainJavaBean {
    public String name;
    public String name(a){ returnname; }}Copy the code

In Java, the name identifier with the same name is not ambiguous because one is a property and the other is a class function. If you want to get it by attribute, you access it as.name; If it is to be obtained by a function, it is accessed as.name(). In Scala, however, a similar declaration is syntax incorrect:

class PlainScalaBean {
  var name : String = _
  // The Scala compiler will assume that you have declared a recursive function.
  def name : String = name
}
Copy the code

From the perspective of Low-level compilation in Scala, when compiling a class bytecode file, the compiler compiles the name function (which acts as Java’s getName) of the same name for the property name. Obviously, it conflicts with the function that we’ve already defined; From a unified access point of view: Both name identifiers are actually describing the same thing, so there is no need to define an extra attribute or method.

1.5.1 Improve code readability

For illustrative purposes, here is a simple implementation of a single linked list: Each time the user finds the length of the list, the size function evaluates the length in real time and returns it.

class Link[T] (t: T) {

  var head: Node[T] = new Node[T](t)

  // The real meaning of a no-argument function lies in the principle of uniform access.
  // The user only needs to call it as if it were a property to get the length, regardless of how the length was obtained.
  def size: Int = {
    // The first node is 1 length.
    var value = 1
    var probe: Node[T] = head
    while(probe ! =null && probe.Next! =null) {
      value += 1
      probe = probe.Next
    }
    value
  }

  // This is an empty parenthesis function, indicating that it is an impure function.
  // The value of this function is not the return value, but the side effect: the function removes the last node from the original list.
  def removeLast() :Unit = {
    var probe: Node[T] = head
    while(probe ! =null && probe.Next.Next! =null) {
      probe = probe.Next
    }
    probe.Next = null
  }

  def add(t: T) :Unit = {
    val tail = new Node[T](t)
    var probe: Node[T] = head
    while(probe ! =null && probe.Next! =null) {
      probe = probe.Next
    }
    probe.Next = tail
  }
}

class Node[T] (var data: T = null) {
  var Next: Node[T] = _}Copy the code

As a developer, he knows exactly how the size function is calculated and implemented. For the user of the list, size is a wysiwyg “property” identifier: access it to get the length of the list. Therefore, the user naturally wants to get the length in terms of “access properties.”

val link = new Link[Int] (1)
link.add(4)
link.add(5)

// He only needs to call the.size attribute as normal to get the length of the list.
println(link.size)
Copy the code

To sum up, the uniform access principle does not affect the program itself, but it improves the programmer’s code reading experience.

2. Functions are variables

2.1 Anonymous Functions

The previous higher-order function case uses Int => Int to describe a function that takes an Int and returns the result of another Int. In fact, this is the standard way to write a Scala function type in the format (argument list) => return value. Now, not only can you define a function using def, but you can also declare a function in the form of a variable assignment, which is written more like A Java Lambda expression. For Scala, these two definitions are equivalent.

// def add(x: Int, y: Int): Int = x + y
val add: (Int.Int) = >Int = (x, y) => x + y
Copy the code

Where the (x,y) => x + y part is an anonymous function, except that it is assigned to the named variable add, so we can still call it using that identifier.

println(add(1.3))  / / 4
Copy the code

Note that there can be only one return value for any function defined. However, we can still define the following functions:

 val f : (Int.String) = > (Int.Int) =???Copy the code

In fact, this definition does not violate the rule, because the return value of the function is a Tuple2 of the entire (Int,Int) type.

2.2 Assign a defined function to a variable

A function, either defined with the def keyword or assigned with val, can assign to another variable. For any function (except a function with no arguments), the identifier represents the function itself.

def twice(int: Int) :Int = 2 * int
val function1: Int= >Int = twice
println(function0())
Copy the code

But for a no-argument function, the identifier represents the value that is evaluated and returned by that function. To avoid this misunderstanding, when the function itself is represented without arguments, it is distinguished by the _ sign.

def str: String = "hello"
def function0: () = >String = str _
// Call with parentheses.
println(function0())
Copy the code

Again, it’s important to note that both functions with empty parentheses and no arguments are essentially () => return value types.

3. The partial functions

First, we give a general understanding of partial functions, given a set U1 = {x1,x2… }. When the function PF (x) operates on every element of the set U1 and expects to obtain another set U2, a filter condition exists: if xn in the set U1 satisfies certain conditions, the pf(x) transformation is used, otherwise it is skipped.

Partial functions allow the programmer to operate on parameters in a set that satisfy a specified type or are within a specified interval. Elements that are not satisfied are either ignored or handled by the programmer. This little bound shows you how to define a partial function and how to use it in a set.

3.1 Implementation of partial function

Partial functions occur in Scala as a attribute PartialFunction. Let’s first create a partial function, and then explore its composition:

new PartialFunction[Any.Int] {
  
  override def isDefinedAt(x: Any) :Boolean=???override def apply(v1: Any) :Int=??? }Copy the code

For a PartialFunction of the form of any PartialFunction[T,R], it means to filter the data of type T and transform the elements satisfying the conditions into type R. Take [Any,Int] as an example of a code block, which means that the partial function accepts Any and filters the remaining elements to Int.

IsDefinedAt is an abstract function that returns type Boolean. What it does is to pass in every element of type T in the set as a parameter x and use internal logic to determine whether to transform it (or whether the element satisfies the condition). And for every element that satisfies that condition, it’s passed in again as a parameter v1 to the apply method and it transforms to an element of type R.

Let’s define a partial function that accepts elements of any type and filters elements of type Int through isInstanceOf. After that, a +1 step is performed for each element that satisfies an Int.

val pf: PartialFunction[Any.Int] = new PartialFunction[Any.Int] {

    // Filter elements of type Int
    override def isDefinedAt(x: Any) :Boolean = {
        x.isInstanceOf[Int]}// + 1 for all Int elements and return.
    override def apply(v1: Any) :Int = {
        v1.asInstanceOf[Int] + 1}}Copy the code

The following shows how to apply the partial function PF to a list and perform the expected logic;

val list: List[Any] = List[Any] (1.2.3."Scala"."Python".1.00d)
/ / 2 and 4
println(list.collect(pf).mkString(","))
Copy the code

Just pass the set bias function into the collect method of the list as the parameter, and the list will be filtered and transformed according to the conditions, and finally get a new transformed list.

3.2 Processing logic of the Collect function

The execution logic of the collect function of the list is as follows:

3.3 Key points of partial function

  • Partial functions are constructed by rewritingPartialFunctionClass.
  • PartialFunctionIt’s a quality in itself.
  • Put the partial function into the setcollectFunction.
  • PartialFunctionThere are two processes, filter and transform.

3.4 Shorthand for partial functions

In fact, you will need to be familiar with the pattern matching section, which I will discuss in a moment, to accept the shorthand form of partial functions.

In place of the original isDefinedAt and Apply procedures, bias functions can be declared using groups of case branches similar to the pattern matching style.

val f1 : PartialFunction[Any.Int] = {// It combines two logic: if the element is an Int, return its value +1.
	case element : Int =>  element + 1
}
Copy the code

If the number of case branches is small, we can package it as a whole block and pass it into the collect function.

val ints: ListBuffer[Int] = buffer.collect {
	{
		case element: Int => element + 1}}Copy the code

Note: When a function is called with a large block of statements, the original () can be replaced with {} form. Another thing to note here is that elements that do not satisfy any of the case branches are discarded.

4. Supplementary concepts of higher-order functions

Higher-order functions lead to many more concepts, and here we introduce function closures, function currying. Let’s start with a simple function:

// f(x) = g(x,y)
def f(x: Int) :Int= >Int = {
    // The function f returns a new anonymous function.
    y: Int => x + y
}

// This is a function assignment. Because g receives the anonymous function returned by f.
// g = x + y. Its higher-order function f already determines the magnitude of x.
// When x = 1, f(1) = g(1,y) = 1 + y.
val g: Int= >Int = f(x = 1)

// g(1,3) = 1 + 3 = 4
println(g(3))

// You can also assign f.x and g.y at once:
// This is equivalent to: g = f(1); g(2)
println(f(1) (2))
Copy the code

Let’s go through each of these concepts and understand why we do this.

4.1 Constrained variables and free variables

In mathematics, suppose a function like this:


f ( x ) = x + y . x { 0 < x < 5 } f(x) = x + y , x \in \lbrace 0 \lt x \lt 5 \rbrace

X is constrained, so x is called a constrained variable. Y is not conditioned, so y is called a free variable. For a function of a program, a variable is treated as either a constrained variable or a free variable, depending on whether the variable is in the function’s parameter list.

{
    var y = 0
    def inner(x : Int) :Int = x + y
}
Copy the code

Take the inner function, for example, for which the value of x is controlled by incoming arguments, or constrained variables. And y is outside of the inner function, so y is an unconstrained free variable for it.

4.2 Closure and Closure functions

A closure refers to an entity that is the combination of a function and its associated reference environment. The function will be unaffected by anything outside of the closure. In our example, the inner function and the domain of y together form a closure. We wrap this closure inside another function, closureFunction, which allows the free variable y of inner to be set.

For an inner function, a closureFunction saves the environment in which it runs, so it can also be called a closureFunction.

def closureFunction(y: Int) :Int= >Int = {
	def inner(x: Int) :Int = x + y
	inner _
    // You can simplify the declaration internally by using anonymous functions.
    // x: Int => x + y
}
Copy the code

According to the data on the Internet, closures are widely used in the JavaScript language, which is related to its own characteristics and application scenarios. Closures themselves are not a fancy concept; they are meant to protect the environment in which a function is run from external factors. With OOP languages, they prefer to define a class that encapsulates data.

// Use classes to implement closures
class closureObj(private val a:Int){
  def action(x : Int, y : Int) :Int= a * x + y   
}
Copy the code

4.3 Currying function

In the author’s opinion, the so-called curization is to split the definition domain mixed with various variables and functions into nested closures with hierarchical structure, and distinguish the order and hierarchical relationship between variables. For the previous example, we wrapped the original function F1 and its dependent variables into the larger function F2 as closures. Similarly, the function f2 and its dependent variables can be wrapped in a closed form inside the larger function F3, as follows:

  def f3(a: Int) :Int= >Int= >Int = {
    def f2(x: Int) :Int= >Int = {
      def f1(y: Int) :Int = a * x + y
      f1
    }
    f2
  }
Copy the code

Scala allows us to use a multiargument list (syntactic sugar) to curify functions directly. Here’s the equivalent declaration of the above code block:

def f3(a: Int)(x: Int)(y: Int) :Int = a * x + y
Copy the code

Currie can transform the chaotic and huge system into an orderly and progressive assembly line with hierarchical relations.

As an example, you will now implement a logic that accepts two strings of file names, filename1 and filename2, and determines whether they are of the same type by the suffix of the filename. The usual solution goes like this:

def isSameType(fileName1: String, fileName2: String) :Boolean = {
    var suffix = ""
    if(! fileName1.contains(".") | |! fileName2.contains(".")) return false

    suffix = fileName1.split('.')(1)
    fileName2.split('.')(1) == suffix
}
//true.
println(isSameType("Dance to this.mp3"."1999.mp3"))
Copy the code

For now, fileName1 and fileName2 are siblings, and the function always accepts both arguments and then compares them.

Now, we transform it to progressive logic that first accepts the first file name, fileName1, and saves its suffix. Any subsequent fileName2 is compared to the saved fileName1 suffix and returns a Boolean value.

By making a small change to the argument list, we can curlize the function and have it perform this logic step by step:

def isSameType(fileName1: String)(fileName2: String) :Boolean = {
    var suffix = ""
    if(! fileName1.contains(".") | |! fileName2.contains(".")) return false

    suffix = fileName1.split('.')(1)
    fileName2.split('.')(1) == suffix
}

// The first file name is saved.
val compareWith: String= >Boolean = isSameType("Music.mp3")

// All filenames will be compared to "music.mp3".
compareWith("playBoy.mp3")
compareWith("image.png")
Copy the code

As you can see, because of the parameter reuse nature of Currie, instead of putting both parameters in at once, we can put them in one by one as required. In other words, curlization can be used to perform either pre-validation of parameters (also known as pre-storage of parameters) or delayed operation.

5. Control abstraction

The control abstraction actually describes a special function that has no input value and no return value. The function type is () => Unit. Obviously, any block of statements goes into such a function, so we use control abstractions to decouple and aggregate duplicate code.

As an example, suppose that any business on a platform has to be “authenticated” before execution and “logged” after execution. Obviously, we can extract the middle business logic into a control abstraction and aggregate it into the following code:

  def produce(before: () => Unit)(operation: () => Unit)(after: () => Unit) :Unit = {
      
    // The assumption is the logic of identity processing.
    before()
    
    // Assume the logic to handle the intermediate business.
    operation()
    
    // Assume logging logic.
    after()

  }
Copy the code

If you’re familiar with AOP, the aspect oriented programming advocated by Spring, you’ll find this code familiar. The advantage of Scala is that we don’t use any complex JDK or CGLIb dynamic proxies, just using higher-order functions to achieve hierarchical logic. If we consider using function currying, we can assemble the logic in a modular way.

5.1 Self-implementing while loops

Here’s another interesting example: mock a while loop.

  def While(condition: ()=> Boolean)(block: ()=> Unit) :Unit = {
    // If the conditions are met
    if (condition()) {
      // Execute the body of the statement
      block()
      // Call recursively
      While(condition)(block)
    }
  }
Copy the code

Note that you need to pass two argument lists to call the While function because it is a curryized function. However, When you pass in a long block of statements, Scala allows you to use curly braces {} instead of () for the argument list.

// The second curly brace is the argument list for the argument receiving ()=> Unit.
While(() => i<10) {
    () => {
        i+=1}}Copy the code

However, since the While function requires an empty parenthesis function to be passed, each code block is always preceded by ()=>. To write more closely to the actual While loop, we can also change the => XXX parameter of () to a named call of the form => XXX.

  def While(condition: => Boolean)(block: => Unit) :Unit = {
    // If the conditions are met
    if (condition) {
      // Execute the body of the statement
      block
      // Call recursively
      While(condition)(block)
    }
  }
Copy the code

Finally, we implement a self-implementing “While” loop.

// The true "while" loop
While (i < 10) {
  i += 1 
}
Copy the code

6. Reference links

  • Side effects of functions

  • Scala has no-argument functions and uniform access rules

  • Uniform access in Python

  • Currying functions from a JavaScript perspective