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 首先支持泛型類赞辩,不過我們經(jīng)常也會(huì)遇到泛型的 trait,對(duì)于泛型的trait 在我們使用 map flatmap操作集合的時(shí)候 中非常常見螟蝙,對(duì)于出現(xiàn) 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
其實(shí) F[_] 代表的就是泛型集合,例如 List[Int] Seq[Long],
對(duì)于 def mapB,that(
implicit bf :CanBuildFrom[Repr, B, That]) :That =(...)
)
Repr 是內(nèi)部用來保存元素的集合類型咒锻,B是函數(shù)f 穿件的元素類型许蓖。 That 是我們想要?jiǎng)?chuàng)建的目標(biāo)集合的類型參數(shù),它可能與輸入的原始集合相同投蝉,也可能不同。
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
<pre style="box-sizing: inherit; color: rgb(54, 54, 54); font-weight: 400; line-height: 1.5; margin: 0px 0px 1.2em; word-break: break-all; font-family: Consolas, Monaco, "Andale Mono", "Source Code Pro", "Liberation Mono", Courier, monospace; display: block; padding: 15px; overflow-wrap: break-word; white-space: pre; overflow: auto; font-size: 1.1rem; background-color: rgb(247, 247, 247); border: none; border-radius: 3px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">T => List[T]
</pre>
For example:
<pre style="box-sizing: inherit; color: rgb(54, 54, 54); font-weight: 400; line-height: 1.5; margin: 0px 0px 1.2em; word-break: break-all; font-family: Consolas, Monaco, "Andale Mono", "Source Code Pro", "Liberation Mono", Courier, monospace; display: block; padding: 15px; overflow-wrap: break-word; white-space: pre; overflow: auto; font-size: 1.1rem; background-color: rgb(247, 247, 247); border: none; border-radius: 3px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">String => List[String]
</pre>
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 :
<pre style="box-sizing: inherit; color: rgb(54, 54, 54); font-weight: 400; line-height: 1.5; margin: 0px 0px 1.2em; word-break: break-all; font-family: Consolas, Monaco, "Andale Mono", "Source Code Pro", "Liberation Mono", Courier, monospace; display: block; padding: 15px; overflow-wrap: break-word; white-space: pre; overflow: auto; font-size: 1.1rem; background-color: rgb(247, 247, 247); border: none; border-radius: 3px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">List[] => WithMap[List[]]
</pre>
Or generalized to any one-hole type
<pre style="box-sizing: inherit; color: rgb(54, 54, 54); font-weight: 400; line-height: 1.5; margin: 0px 0px 1.2em; word-break: break-all; font-family: Consolas, Monaco, "Andale Mono", "Source Code Pro", "Liberation Mono", Courier, monospace; display: block; padding: 15px; overflow-wrap: break-word; white-space: pre; overflow: auto; font-size: 1.1rem; background-color: rgb(247, 247, 247); border: none; border-radius: 3px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">F[] => WithMap[F[]]
</pre>
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 aString
and produce a List[String] -
Map[_,_]
is of kind:* -> * -> *
(takes two Order-0 types and produce a proper type, Order 1) takes aString,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 :
<pre style="box-sizing: inherit; color: rgb(54, 54, 54); font-weight: 400; line-height: 1.5; margin: 0px 0px 1.2em; word-break: break-all; font-family: Consolas, Monaco, "Andale Mono", "Source Code Pro", "Liberation Mono", Courier, monospace; display: block; padding: 15px; overflow-wrap: break-word; white-space: pre; overflow: auto; font-size: 1.1rem; background-color: rgb(247, 247, 247); border: none; border-radius: 3px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">trait WithMap[F[_]] {def map[A,B](fa: F[A])(f: A => B): F[B]}
</pre>
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 syntaxF[_]
-
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
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"