scala 理解 trait 中 泛型 变量 F[_]

This very abstract syntax comes up all the time in Scala, I will try to give you an intuition of what it means and how to use it.
scala 首先支持泛型类,不过我们经常也会遇到泛型的 trait,对于泛型的trait 在我们使用 map flatmap操作集合的时候 中非常常见,对于出现 trait map[A,F[_]]

trait Functor[A, F[_]] {
  def map[B](f: A => B): F[B]
  def flatMap[B](f: A => F[B]): F[B]
}

/** Higher Functor Trait */
trait HigherFunctor[A, M[+_], F[_[_]]] {
  def map[B](f: A => B): F[M]
  def flatMap[B](f: A => M[B]): F[M]
}

/** Multi-Functor Trait */
trait MultiFunctor[A, State, F[_, _]] extends Functor[A, ({type λ[α] = F[α, State]})#λ] {
  def map[B](f: A => B): F[B, State]
  def flatMap[B](f: A => F[B, State]): F[B, State]
}

/** Higher Multi-Func

其实 F[_] 代表的就是泛型集合,例如 List[Int] Seq[Long],
对于 def mapB,that(
implicit bf :CanBuildFrom[Repr, B, That]) :That =(...)
)

Repr 是内部用来保存元素的集合类型,B是函数f 穿件的元素类型。 That 是我们想要创建的目标集合的类型参数,它可能与输入的原始集合相同,也可能不同。

Traits are used to share interfaces and fields between classes. They are similar to Java 8’s interfaces. Classes and objects can extend traits but traits cannot be instantiated and therefore have no parameters.

Defining a trait
A minimal trait is simply the keyword trait and an identifier:

trait HairColor
Traits become especially useful as generic types and with abstract methods.

trait Iterator[A] {
  def hasNext: Boolean
  def next(): A
}
Extending the trait Iterator[A] requires a type A and implementations of the methods hasNext and next.

Using traits
Use the extends keyword to extend a trait. Then implement any abstract members of the trait using the override keyword:

trait Iterator[A] {
  def hasNext: Boolean
  def next(): A
}

class IntIterator(to: Int) extends Iterator[Int] {
  private var current = 0
  override def hasNext: Boolean = current < to
  override def next(): Int = {
    if (hasNext) {
      val t = current
      current += 1
      t
    } else 0
  }
}

val iterator = new IntIterator(10)
iterator.next() // returns 0
iterator.next() // returns 1
This IntIterator class takes a parameter to as an upper bound. It extends Iterator[Int] which means that the next method must return an Int.

Subtyping
Where a given trait is required, a subtype of the trait can be used instead.

import scala.collection.mutable.ArrayBuffer

trait Pet {
  val name: String
}

class Cat(val name: String) extends Pet
class Dog(val name: String) extends Pet

val dog = new Dog("Harry")
val cat = new Cat("Sally")

val animals = ArrayBuffer.empty[Pet]
animals.append(dog)
animals.append(cat)
animals.foreach(pet => println(pet.name))  // Prints Harry Sally

The goal of this post is to understand this syntax and why you would need it. In order to do so we will gradually climb the ladder of abstractions and answer the following questions :

  • What is a value ?
  • What is a proper type ?
  • What is a first-order type ?
  • What abstracts over a first-order type ?
  • Why do I need F[_] ?

Values represent raw data. They have the lowest level of abstraction and are the simplest concept that we need to deal with.

Take a look at the right hand side of these examples — it’s just data and it’s trivial to understand.

If a child asks you what your funky BigPanda tshirt costs and you answer $12 then they’ll understand what you mean. They’ll certainly understand the value in your answer (2). But if they ask you what a dollar is then suddenly things get more complicated. Explaining money and currencies is a bit more tricky. This takes us to types.

Look at the information the REPL spits out. It keeps telling you about types: String, List[Int] etc. These are all proper types.

Proper types are a higher level concept than values. Let’s talk about how they are related: types can be instantiated to produce a value and values are a specific instance of a type.

String can produce all the string literals you can think up ("a", "ab", "algorithmic service operations" etc). If we go back to our pricing example, 2, 49,000,000](https://bigpanda.io/resources/bigpanda-expands-series-b-funding-49-million/)...) or any other amount.

Moving from values to proper types took us up a level of abstraction. What do we get if we go one higher?

In the previous example, we said that List[Int] is a proper type, but what isList?

This doesn’t compile. The compiler won’t let us say that a value is a List. It wants us to say that it is a list of something, a List[_].

There is a slot there. If we want the compiler to give us a type we need to put something in the slot. It’s like a parameter to a function that returns a type. There’s a name for this special kind of function: a type constructor.

You’ve probably met other type constructors: Option[_], Array[_], Map[_,_] and friends. Notice that Map is a little different; it needs a type for the key and for the value. It has 2 slots for 2 parameters.

First-order types are just types (List, Map, Array) that have type constructors (List[_], Map[_, _]) that take proper types and produce proper types (List[Int], Map[String, Int]).

Going from proper types to first-order types took us up a layer of abstraction. In most programming languages you can’t abstract any further. However, Scala let you go a step further. Let’s take that last step and see where it takes us.

Every step we’ve taken so far has added an abstraction over the previous abstraction:

In Scala you can abstract over a first-order type with this syntax:

Let’s forget about WithMap for a second to focus on the F[_] syntax. F[_] represents a first-order type with one slot. For example List[_] or Option[_].

Now the million dollar question: What is WithMap?

Answer: A second-order type

It’s a type which abstracts over types which abstract over types!!!

[图片上传失败...(image-a8a96c-1580368866563)]

Feel like inception, right? Hopefully, you followed until here and everything is starting to fall into place.

Let’s introduce one more piece of terminology, and then try and clarify how everything fits together.

A type with a type constructor (ie. a type with [_]) is called a higher kinded type. A type constructor is just a function that takes a type and returns a type.

Let’s do a quick analogy between types and functions :

  • A type constructor List[_] is just a function of type

T => List[T]

For example:

String => List[String]

Given a proper type it will return another proper type you can think about it as a function that works at the type level, a type level function.

But wait we returned only a proper type, what if we return another first order type :

List[] => WithMap[List[]]

Or generalized to any one-hole type

F[] => WithMap[F[]]

Give a type level function we return another type level function.

Higher order functions are functions that returns functions at the value level , you can see the analogy here at the type level

The type of a type is called kind and uses * as notation to communicate what order they are.

  • String is of kind * and is Order 0
  • List[_] is of kind * -> * (takes one type and produce a proper type, Order 1) takes a String and produce a List[String]
  • Map[_,_] is of kind: * -> * -> * (takes two Order-0 types and produce a proper type, Order 1) takes a String,Int and produce a Map[String,Int]
  • WithMap[F[_]] of kind : (* -> *) -> * (take a Order 1 type (* -> *) and produce a proper type, Order 2)

This gives a visual way to talk about the type of types.

We abstracted over all the first order types with one hole, we can now define common functions between all of them for example :

trait WithMap[F[_]] {def map[A,B](fa: F[A])(f: A => B): F[B]}

You can mentally replace F by List or Option or any other first-order types. This allows us to define a map function over all first-order types.

Yes, that’s it, it allows us to define functions across a lot of different types in a concise way, this is very powerful but is not in the scope of this post. Just remember that now you have a way to talk about a range of types based on how many holes they have and not on what they represent (Option, List)

  • 1, "a", List(1,2,3) are values
  • Int, String, List[Int]are proper types
  • List[_], Option[_] are type constructors, takes a type and construct a new type, can be generalized with this syntax F[_]
  • G[F[_]] is a type constructor that takes another type constructor like, Functor[F[_]], can be tough of higher order function at the type level

TOUR OF SCALA

HIGHER-ORDER FUNCTIONS

Language

  • English
  • Bosanski
  • Español
  • 한국어
  • Português (Brasil)
  • Polski
  • 中文 (简体)
  • ภาษาไทย
  • Русский
  • 日本語

Higher order functions take other functions as parameters or return a function as a result. This is possible because functions are first-class values in Scala. The terminology can get a bit confusing at this point, and we use the phrase “higher order function” for both methods and functions that take functions as parameters or that return a function.

In a pure Object Oriented world a good practice is to avoid exposing methods parameterized with functions that might leak object’s internal state. Leaking internal state might break the invariants of the object itself thus violating encapsulation.

One of the most common examples is the higher-order function map which is available for collections in Scala.

val salaries = Seq(20000, 70000, 40000)
val doubleSalary = (x: Int) => x * 2
val newSalaries = salaries.map(doubleSalary) // List(40000, 140000, 80000)

doubleSalary is a function which takes a single Int, x, and returns x * 2. In general, the tuple on the left of the arrow => is a parameter list and the value of the expression on the right is what gets returned. On line 3, the function doubleSalary gets applied to each element in the list of salaries.

To shrink the code, we could make the function anonymous and pass it directly as an argument to map:

val salaries = Seq(20000, 70000, 40000)
val newSalaries = salaries.map(x => x * 2) // List(40000, 140000, 80000)

Notice how x is not declared as an Int in the above example. That’s because the compiler can infer the type based on the type of function map expects. An even more idiomatic way to write the same piece of code would be:

val salaries = Seq(20000, 70000, 40000)
val newSalaries = salaries.map(_ * 2)

Since the Scala compiler already knows the type of the parameters (a single Int), you just need to provide the right side of the function. The only caveat is that you need to use _ in place of a parameter name (it was x in the previous example).

Coercing methods into functions

It is also possible to pass methods as arguments to higher-order functions because the Scala compiler will coerce the method into a function.

case class WeeklyWeatherForecast(temperatures: Seq[Double]) {

  private def convertCtoF(temp: Double) = temp * 1.8 + 32

  def forecastInFahrenheit: Seq[Double] = temperatures.map(convertCtoF) // <-- passing the method convertCtoF
}

Here the method convertCtoF is passed to the higher order function map. This is possible because the compiler coerces convertCtoF to the function x => convertCtoF(x) (note: x will be a generated name which is guaranteed to be unique within its scope).

Functions that accept functions

One reason to use higher-order functions is to reduce redundant code. Let’s say you wanted some methods that could raise someone’s salaries by various factors. Without creating a higher-order function, it might look something like this:

object SalaryRaiser {

  def smallPromotion(salaries: List[Double]): List[Double] =
    salaries.map(salary => salary * 1.1)

  def greatPromotion(salaries: List[Double]): List[Double] =
    salaries.map(salary => salary * math.log(salary))

  def hugePromotion(salaries: List[Double]): List[Double] =
    salaries.map(salary => salary * salary)
}

Notice how each of the three methods vary only by the multiplication factor. To simplify, you can extract the repeated code into a higher-order function like so:

object SalaryRaiser {

  private def promotion(salaries: List[Double], promotionFunction: Double => Double): List[Double] =
    salaries.map(promotionFunction)

  def smallPromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * 1.1)

  def greatPromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * math.log(salary))

  def hugePromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * salary)
}

The new method, promotion, takes the salaries plus a function of type Double => Double (i.e. a function that takes a Double and returns a Double) and returns the product.

Methods and functions usually express behaviours or data transformations, therefore having functions that compose based on other functions can help building generic mechanisms. Those generic operations defer to lock down the entire operation behaviour giving clients a way to control or further customize parts of the operation itself.

Functions that return functions

There are certain cases where you want to generate a function. Here’s an example of a method that returns a function.

def urlBuilder(ssl: Boolean, domainName: String): (String, String) => String = {
  val schema = if (ssl) "https://" else "http://"
  (endpoint: String, query: String) => s"$schema$domainName/$endpoint?$query"
}

val domainName = "www.example.com"
def getURL = urlBuilder(ssl=true, domainName)
val endpoint = "users"
val query = "id=1"
val url = getURL(endpoint, query) // "https://www.example.com/users?id=1": String

Notice the return type of urlBuilder (String, String) => String. This means that the returned anonymous function takes two Strings and returns a String. In this case, the returned anonymous function is (endpoint: String, query: String) => s"https://www.example.com/$endpoint?$query"

你可能感兴趣的:(scala 理解 trait 中 泛型 变量 F[_])