Some of the code is based on Scala 3, mainly using optimized enumeration class declarations. See: New Scala 3 features – Gold Mining (juejin. Cn)

The advantage of throwing an exception is that it provides logic for handling exceptions in your code centrally, called a catch block. But this mechanism itself undermines the referential transparency mechanism of THE FP idea. In other words, the result of a pure function may be context-dependent.

def MayThrowEx(x : Int) : Double =
  try
    val y  = 10
    y / x 
  catch
    case e : Exception= >999.9d
end MayThrowEx

def MustThrowEx(x : Int) : Int =
  try
    // Scala can proactively guide the return type of a throw type, which is set in a catch block.
    val y  = ((throw new Exception("non")) : Int)
    x + y
  catch
    case e : Exception= >12
end MustThrowEx
Copy the code

Obviously, the return value of MayThrowEx does not depend entirely on x + y, but may actually depend on the return value in a catch block, such as when the user calls MayThrowEx(0). MustThrowEx is a more extreme example, where the imposition of an exception causes the result of the operation to actually depend entirely on the value of the catch block. In short, pure functions with try-catch structures are always at risk of becoming “impure”.

Also, exceptions are type unsafe. The function Int => Int itself does not tell the user what type of exception might be thrown, and of course the compiler does not force the user to handle runtime exceptions. But if the user forgets to handle potential exceptions, the program will only detect the problem at runtime.

Java forces the user to check for checked exceptions or to pass them on to a superior. However, this may cause the suffix of the function signature to be forced with throws XXX, XXX, XXX… . This mechanism does not work for highly generalized higher-order functions, which have no sense of what exceptions the function it receives might throw.

Alternatives to exceptions

At the same time, we don’t want to throw away the benefit of exception handling: integrating centralized error handling, so we need to find other ways to express exceptions (this doesn’t mean we have to give up throwing exceptions altogether, it just doesn’t work in FP). One way is to use C to express exceptions in the style of a specific error code, such as by pre-specifying that MayThrowEx returns a non-negative number, so that any negative number -1D, or -999.9D, can be used to indicate a calculation error. But we rejected this approach for three reasons:

  1. Causes errors to spread in a “silent” way.
  2. Resulting in a large number of lateifCode inspection templates.
  3. Cannot be used for generic code because it cannot be definedTOutliers of.

On the other hand, in order for functions like MayThrowEx to work, the user may need to add additional constraints, such as pre-checking that the value of x cannot be zero. In FP programming, these rules are difficult to pass along with MayThrowEx to other higher-order functions, because higher-order functions themselves do not discriminate between passed arguments.

The second way is to upgrade a function to a total function and let the user decide what value to return when MayThrowEx fails.

def MayThrowEx(x: Int, ifPanic: Double) :Double =
  val y = 10
  if x == 0 then ifPanic else y / x
end MayThrowEx
Copy the code

The downside of this approach is that users often don’t know which default is the most appropriate. Second, assuming that the program has already accepted illegal input, it is wise to stop the computation, or to choose another branch of the computation, rather than always using a default value. Therefore, it is desirable to introduce a mechanism that can defer the decision of what to do when an accident occurs, so that it can be resolved at the appropriate time.

Fpinscala /01.answer.md at second-edition · fpinscala/fpinscala · GitHub

Option

Option is a typical Algebraic Data Type, so it is more appropriate to use the enumeration class of Scala 3 for definition. The Scala library already includes Option and Either. Here’s an implementation of Optional:

// for example Father >:> Son,
// then Some[Father] >:> Some[Son]
enum Optional[+A] :
  case Some[+A](get : A) extends Optional[A]
  case None extends Optional[Nothing]
Copy the code

Scala 2 can express algebraic data types like this:

trait Optional[+A]
case object None extends Optional[Nothing]
case class Some[+A] (v : A) extends Optional[A]
Copy the code

Exceptions can be replaced with just one value: None. Scala’s covariant mechanism ensures that Optional[Nothing] is a subtype of any Optional[X]. Now we can treat the result as Optional regardless of whether it is wrong:

def MayCompute(x: Int) :Optional[Double] =
  import Optional. *val y = 10
  if x == 0 then None else Some(y / x)
end MayCompute
Copy the code

A function like this that returns Optional (Option, Either, or similar concepts) does not produce a meaningful output for all inputs. Such a function is called a partial function.

The following is an Optional implementation, including the basic methods in FP paradigm programming, map, flatMap, filter, etc.

// for example Father >:> Son,
// then Some[Father] >:> Some[Son]
enum Optional[+A] :case Some[+A](get: A) extends Optional[A]
  case None extends Optional[Nothing]

  def map[B](f: A= >B) :Optional[B] = this match
    case Some(a) => Some(f(a))
    case_ = >None

  def flatMap[B](f: A= >Optional[B) :Optional[B] = this match
    case None= >None
    case Some(a) => f(a)

  def getOrElse[B> :A] (`default` : = >B) :B = this match
    case None= > `default`
    case Some(a) => a

  def orElse[B> :A](ob: => Optional[B) :Optional[B] = this match
    case None => ob
    case_ = >this

  def filter(f: A= >Boolean) :Optional[A] = this match
    case Some(a) if f(a) => Some(a)
    case_ = >None
Copy the code

In order to keep the method extensible, the methods in this paper have type parameters. For example, getOrElse and orElse declare the type parameter B >: A, which allows the user to return an instance that is more abstract than itself. The difference is that getOrElse returns unboxed A or B, while orElse returns Optional[A] (itself) or Optional[B].

FlatMap and Map have different semantics. The function f received by flatMap produces another Optional, resulting in the following method map2: itself Optional[A] receives another Optional[B], and returns another Optional[C] using the (A,B) => C function.

def map2[B.C](b: Optional[B])(f: (A.B) = >C) :Optional[C] =
  this flatMap (aa => {
    b map {
      bb => f(aa, bb)
    }
  })
Copy the code

This flatMap + map combinatorial logic is more intuitive when written as a For expression:

val value: Optional[(String.Int)] = for {
    a <- Some("key")
    b <- Some(200)}yield (a, b)
Copy the code

For an in-depth look at Scala For expressions, see Scala + : Type Inference, list operations, and For Loop-digging (juejin. Cn).

The Sequence and Traverse by

In addition to map, flatMap, filter and other generic methods, sequence and traverse are common in FP. Take Optional[A] as an example, the two functions are:

  1. sequence: Receive aList[Optional[A]]Sequence, flip it over toOptional[List[A]].
  2. traverse: Accept oneList[A]Sequence andA => Optinal[B]Function, and then flips the sequence toOptional[List[B]].

Traverse can be thought of as A more general method than sequence, which simply requires passing an additional Optinonal[A] => Optional[A] function, the simplest form being x => x (where A =:= B), So you can implement the traverse method first, and then treat the Sequence as a special case of it. On the other hand, both methods receive external Optional[A] processing and return, so it is more appropriate to declare them as utility functions in Optional companion objects.

object Optional:

  def traverse[A.B](os : List[A])(f : A= >Optional[B) :Optional[List[B]] = os match
    case Nil= >Some(Nil)
    case h :: tails => f(h).map2(traverse(tails)(f))(_ :: _)

  def sequence[A](as: List[Optional[A]]) :Optional[List[A]] = traverse(as)(x => x)
Copy the code

Enhance Lift

A normal mapping function can be lifted to map Optional (Option, Either), similar to the decorator pattern, so no changes to any previous function signatures are required.

object Optional:
  // - traverse
  // - sequence
  def lift[A.B](f: A= >B) :Optional[A] = >Optional[B] = _ map f // oa : Optional[A] => oa.map(f)
Copy the code

Either

At the heart of this chapter is the use of common value classes to uniformly express program failures or exceptions. Optional is one solution, but not the only one, nor the best. The reason: Optional does not tell the user the exact cause of the error. Instead, it simply throws None.

For this problem, we create another ADT type Either to report error messages in detail. The implementation is as follows:

enum _Either[+E, +A] :
  case Left[+E](ex : E) extends _Either[E.Nothing]
  case Right[+A](value : A) extends _Either[Nothing.A]
  
/* define in scala 2: trait _Either[+E,+A] case class Left[+E](ex : E) extends _Either[E,Nothing] case class Right[+A](v : A) extends _Either[Nothing,A] */
Copy the code

Where Left represents the result when an error occurs, and Right represents the value when it is evaluated correctly (Right itself is a pun, and they are also defined as such in Scala’s native library). To recap the MayCompute example, if MayCompute(0) is called now, the program returns a Left(x shouldn’t be 0).

def MayCompute(x: Int): _Either[String.Double] =
  import _Either.*
  val y = 10
  if x == 0 then Left("x shouldn't be 0.") else Right(y / x)
end MayCompute
Copy the code

Of course, you can choose to catch the native Exception, because it also carries stack call information, which makes it easier for users to troubleshoot problems.

def MayCompute(x: Int): _Either[Exception.Double] =
  import _Either.*
  val y = 10
  try Right(y / x) catch case e : ArithmeticException= >Left(e)
end MayCompute
Copy the code

Use a nominal function to postpone the calculation

See my early notes for the famous calls: Scala: The Beginning of Functional Programming – Nuggets (juejin. Cn)

However, if an expression instead of a literal is passed in, the function call fails:

println(MayCompute(10 / 0))
Copy the code

The current MayCompute method is a pass-by call. The program evaluates the 10/0 expression before calling the function, but this is not in the try-catch block of MayCompute. The solution is simple: make x a generic call and postpone the calculation.

def MayCompute(x: => Int): _Either[String.Double] =...Copy the code

Thus, the program will only turn to the 10/0 expression if it reaches Right(y/x), which is already inside the try-catch area.

If we purify the entire logic of MayCompute, we get the more generalized Try function:

def Try[E< :Exception.V](v : => V) : _Either[E.V] =
  import _Either.*
  try Right(v) catch case e: E= >Left(e)
Copy the code

A supplement to the problem of metamorphosis

The following are declarations of methods that include Either versions of map, flatMap, and so on.

enum _Either[+E, +A] :case Left[+E](ex: E) extends _Either[E.Nothing]
  case Right[+A](value: A) extends _Either[Nothing.A]

  import _Either.{Left.Right}

  def map[B](f: A= >B): _Either[E.B] = this match
    case Left(ex) => Left(ex)
    case Right(value) => Right(f(value))

  def flatMap[EE> :E.B](f: A => _Either[EE.B]): _Either[EE.B] = this match
    case Left(ex) => Left(ex)
    case Right(value) => f(value)

  def orElse[EE> :E.B> :A] (`default`: => _Either[EE.B]): _Either[EE.B] = this match
    case Left(ex) => `default`
    case_ = >this

  def map2[EE> :E.B.C](b: _Either[EE.B])(f: (A.B) = >C): _Either[EE.C] =
    for aa <- this; bb <- b yield f(aa, bb)


object _Either:
  def traverse[E.A.B](as: List[A])(f: A => _Either[E.B]): _Either[E.List[B]] = as match
    case Nil= >Right(Nil)
    case h :: tails => f(h).map2(traverse(tails)(f))(_ :: _)
  def sequence[E.A](es: List[_Either[E.A]]): _Either[E.List[A]] = traverse(es)(x => x)
Copy the code

Compared to Optional and Either flatmaps, orElse adds more upper and lower bound rules because both exceptions and values are covariant. Either of the following methods will return an error:

@Deprecated
def flatMap00[EE.B](f: A => _Either[EE.B]): _Either[EE.B] = this match
  case Left(ex) => Left(ex) // error
  case Right(value) => f(value)

@deprecated
def orElse00[EE.B] (`default`: => _Either[EE.B]): _Either[EE.B] = this match
  case Left(ex) => `default`
  case Right(value) => Right(value) // error
Copy the code

Start with the flatMap00 method. Left(ex) is of type _Either(E,Nothing), but the function signature requires that the type _Either[EE,B] be returned. So to satisfy the covariant relationship defined by Either, declare EE >: E to indicate that _Either(EE,Nothing) is the parent of _Either(E,Nothing).

OrElse00 method is the same. Right(value) is of type _Either(Nothing,A), but the function signature requires the return of type _Etiher[EE,B], where B >: A is also declared to fill the variable relationship. For a more detailed look at the type variation section, see: Liskov philosophy in Scala Generics – Nuggets (juejin. Cn)

The resources

Scala – “promoted” using the ETA extension in “Functional Programming in Scala”? – Thinbug

function – What is “lifting” in Scala? – Stack Overflow

(4 messages) How to evaluate the scalaz library? – zhihu (zhihu.com)