The neophyte ' s Guide to Scala part 12:type Classes

Over the past two weeks, we have discussed some functional programming techniques that keep us dry and flexible, in particular function combinations, the application of partial functions, and currying. Next, I'll continue to discuss how to make your code as flexible as possible.

However, this time we will not discuss how to use a function as a class object for this purpose, but instead use the type system, this time it is not to hinder us, but to make our code more flexible: You will learn about type classes knowledge.

You might think that this is a less relevant concept and is being brought to the Scala community by noisy Haskell fans. However, it is clear that this is not the case. Type classes is already an important part of the Scala standard library and is becoming more and more important for many popular and widely used third-party open source libraries. So you really should be familiar with them.

I'll discuss the concept of type class, why it's useful, how to benefit from the type class as a user, and how to implement your own type class and make it work.

Problem

Instead of giving an abstract explanation of what type class is, I will deal with this topic in a simpler but more practical way--for example.

Imagine that we want to write a cool statistics-related library. This means that we will provide many functions for working with numeric collections, most of which are used to aggregate their values. Further, if we are limited to accessing the elements of this collection by index only, and we need to use the reduce method of the Scala collection library. We added this restriction for ourselves because we wanted to re-implement something that the Scala standard library has already provided-just because it's a good example of not having a lot of restrictions, and it's small enough for a blog post. Finally, our implementation assumes that the values we have obtained are sorted.

We begin with the implementation of the median, quatiles, and IQR of the double type.

object Statistics {def median (xs:vector[double]): Double = xs (xs.size/2
) def quartiles (Xs:vector[double]): (Double, double, double) =
/4), median (XS), XS (XS.SIZE/4 * 3 = Quartiles (xs) match { case (Lowerquartile, _, upperquartile) = Upperq Uartile- Lowerquartile} def mean (xs:vector[double]): Double = {Xs.reduce (_ + _)/ Xs.size}}

Median (median) divides the dataset into two halves, and the smallest and largest of the four-bit (quartile) (the first and third elements of the tuple returned by our quartile method) separates the first 25% and the last 25% from the data set. Our IQR method returns a four-bit range (Interquartile range), which is the difference between the maximum four-bit and the smallest four-cent.

Now, we want to support a number other than double. So, let's re-implement these methods for int again, right?

Of course not! First of all, there will be some repetition, won't it? And, in cases like this one, we will soon encounter situations where we have to use dirty tricks to cover the methods, because the type parameters are erased.

if int and double inherit the same base class or implement the same trait, such as number, then we can modify the parameter type and return type of our method to use that more general type. Our method parameters will look like this:

object Statistics { =??? = ??? = ??? = ??? }

Thankfully, in this case, int and double do not have a common trait, so this astray is out of the way. In other cases, it is possible that some types will have a common parent class or trait--but this is still a bad idea. Not only because we've lost the type information we've previously available, our API has closed the door for other future extensions that we can't control: we can't increase the number trait type in a third-party extension (meaning, in a third-party extension, We may not be able to inherit numeric this trait, but still want to let the class in the third-party extension be statistics this API call).

Ruby's answer to this question is monkey patching. Expands the new type at the expense of the polluting global namespace, making it behave like number. Java developers who were defeated by Gang of the four (four authors of design mode) would have thought that the adapter (Adaptor) would solve all the problems.

object Statistics {trait numberlike[a] {def get:a def plus (Y:numberlike[a]): Numberlike[a] def minus (y: Numberlike[a]): Numberlike[a] def divide (y:int): Numberlike[a]} Case classNumberlikedouble (x:double)extendsNumberlike[double] {def get:double=x def minus (y:numberlike[double])= numberlikedouble (X-y.get) def plus (y:numberlike[double])= numberlikedouble (x +y.get) def divide (Y:int)= numberlikedouble (X/y)} type Quartile[a]=(Numberlike[a], numberlike[a], Numberlike[a]) def Median[a] (Xs:vector[numberlike[a]): Numberlike[a]= XS (xs.size/2) def Quartiles[a] (Xs:vector[numberlike[a]): Quartile[a]=(XS (xs.size/4), median (XS), XS (XS.SIZE/4 * 3) def Iqr[a] (Xs:vector[numberlike[a]]): Numberlike[a]=quartiles (xs) match { Case(Lowerquartile, _, Upperquartile) =Upperquartile.minus (Lowerquartile)} def Mean[a] (Xs:vector[numberlike[a]]): Numberlike[a]=Xs.reduce (_.plus (_)). Divide (xs.size)}

Now we've solved this problem in an extended way: the person using our library can pass in an numerlike adapter written for int (we can also provide ourselves with an int adapter that we can provide ourselves as an API). or pass in any adapter that wants to behave like numer, without recompiling the implementation module of our statistical method.

However, always wrapping your numbers in an adapter is not only hard to write and read, it also means that you have to create a lot of adapter objects to interact with your library.

**Type class to save you**

A powerful option beyond the workaround provided above, of course, is to define and use the type class. The type class, as a powerful feature of the Haskell language, is also called class, but it has nothing to do with the concept of class in object-oriented.

A type Class C defines a number of actions that must be supported by any type T that wants to be a member of C. But whether T is a member of type Class C is not determined by T itself, and any developer who wants a type to be a member of the class, just need to provide the type of operation that must be supported. Now, once T becomes a member of type Class C, a function that restricts one or more of its parameters to a member of C can use T as a parameter.

Like this, the type class allows for immediate and traceable polymorphism. Code that relies on the type class is open to the extension without creating an adapter object.

Create a Type class

In Scala, the type class can be implemented through a combination of techniques. This is more than what needs to be done in Haskell, but it also gives developers more control.

Creating a type class in Scala consists of several steps. First, let's define a trait. This is the actual Typc class.

Object Math { trait numberlike[t] { def plus (X:t, y:t): T def divide (X:t, y:int): T def minus (X:t, y: T): T }}

We have created a type class called Numberlike. The type class always has one or more types of parameters, and they can often be designed to be stateless, for example, the methods defined in our numberlike trait depend only on their parameters. It should be noted that the methods of the adapters above depend on the objects of the type T that they fit and a parameter, but the method defined in our Numberlike type class has two parameters of type T--and in Numberlike, This object of type T is the first parameter of the operation.

Provide a default member

The second step in implementing a type class is usually to provide some of the default implementations of your type class trait in its accompanying objects (companion object). We'll see in a minute why this is a good strategy. First, let's take the double and int as a member of the type class Numberlike.

object Math {trait numberlike[t] {def plus (x:t, y:t): T def divide (X:t, y: INT): T def minus (X:t, y:t): T} object numberlike {Implicit object numberlikedouble extends Numberlike[double] {def plus (x:double, y:double): Double = x + y def divide (x:double, y:int): Double = x/ y def minus (x:double, y:double): Double = x- y} Implicit object Numberli Keint extends Numberlike[int] {def plus ( X:int, y:int): int = x + = x/ y def minus (X:int, y:int): Int = x- Y}}}

Two things: First, you'll see that the two implementations are basically the same. However, this is not always the case when you create a member of type class. Our numerlike trait is a relatively small area. At the end of this article, I will give some examples of type class, and there will be much less room for repetition when implementing their members. Second, ignore the precision that we lose when we divide integers in numerlikeint, just to keep the example simple.

As you can see, the members of type class are usually singleton objects. Also notice the implicit keyword in front of each type class implementation. This is the key member that makes the type class possible in Scala, which makes the members of type class implicitly available under some conditions. This is discussed in more detail in the next section.

Programming for the Type class

Now we have the type class and its two default implementations. Now let's program the type class in the statistic module. Let's take a moment to focus on the mean method.

object Statistics { import math.numberlike = ev.divide (Xs.reduce ( Ev.plus (_, _)), Xs.size)}

This may seem scary at first, but it's actually very simple. Our method accepts a type parameter T, and the only one parameter vector[t]

Restricting the parameters to a specific type class is accomplished by the second implicit parameter in the parameter list. What does that mean? Simply put, the value of a numerlike[t] type must be implicitly available in the current scope. This means that a implicit value must be declared and made available in the current scope. Typically this is achieved by import of implicit value in a package or object.

The compiler looks for the associated image (companion object) of the type of the implicit parameter only if no other implicit value is found. So, as a library designer, putting your default type class implementation in your type class trait's companion object will make it easy for your library's users to overwrite your implementation with their own implementations, which is what you want to do. The consumer can also pass in an explicit value at the location of the implicit parameter to overwrite the implicit value in the current scope.

Let's take a look at the default type class implementation parameter is recognized.

Val numbers = vector[double] (23.0, Max, Max, Max, 199, 420, 3839) println (Statistics.mean (numbers)) /c0>

Very good. If we want to try it with vector[string], we will have to get an error in the compiler that there is no implicit value available for parameter e:numberlike[string]. If you don't want to see this error message, you can customize the error message by adding a @implicitnotfound annotation to your type class trait.

object Math { import annotation.implicitnotfound @implicitNotFound ("No member of Type class Numberlike in scope for ${t} ") trait Numberlike[t] { def plus (X:t, y:t): T def divide (X:t, y : Int): t def minus (X:t, y:t): T }}

Contextual Bounds Context Definition

It is cumbersome to include an implicit argument list that expects the type class member in the parameter list. As a simplification of implicit parameters for only one type parameter, Scala provides a syntax called the context bounds. To demonstrate how to use this syntax, we will then use this syntax to implement our statistics method.

object Statistics { import Math.numberlike def Mean[t] (xs:vector[t]) (implicit ev:numberlike[t]): T = EV. Divide (Xs.reduce (Ev.plus (_, _)), xs.size) def Median[t:numberlike] (xs:vector[t]): T = xs (xs.size/2) def Quartiles[t:numberlike] (Xs:vector[t]): (T, T, T) = (XS (xs.size / 4), median (XS), XS (XS.SIZE/4 * 3 = Quartiles (xs) match { case (Lowerquartile, _, Upperquartile) => Implicitly[numberlike[t]].minus (Upperquartile, Lowerquartile)}}

A t:numberlike-style context binding means that a value of type numberlike[t] must be available, so it is the same as adding a second implicit parameter in the argument list numberlike[t]. However, if you want to use this implicitly available value, you must call the implicitly method, as we did in the Iqr method. If your type class requires more than one type parameter, you cannot use the context bound syntax.

Self-made type class member

As a user of a library that uses the type class, you would have wanted to make a class like this for a member of this type class. For example, you might want to use our statistical library for the duration type of Joda time. For some, we certainly need to put Joda time on our classpath.

Librarydependencies + = "Joda-time"% "joda-time"% "2.1" + = "Org.joda"% "Joda-convert"% "1.3"

Now all we need to do is create an implicit value that implements the Numerlike (make sure Joda time is on your classpath when trying)

object Jodaimplicits { import math.numberlike import Org.joda.time.Duration extends numberlike[duration] { = x.plus (y) = Duration.millis (X.getmillis/ y) = x.minus (y) }}

If we introduce a package or object that contains this numberlike, we can calculate the mean value for some time periods.

Import statistics._ Import jodaimplicits._ Import = Vector (Standardseconds, Standardseconds), Standardminutes (2), standardminutes (17), Standardminutes (Standardminutes), standardhours (2), standardhours (5), standardhours (8), Standardhours (+), standarddays (1), standarddays (4)) println (mean (durations). getstandardhours)

Case

Our numberlike type class is a good practice. But Scala has its own numeric type class, which allows you to use sum or product for a set when the numeric[t of T is present. Another type class that you commonly use in the standard library is ordering, which allows you to provide an implicit ordering for your type to be used by the sort method of the Scala collection.

There are more type classes in the standard library, but as a regular Scala developer, they are not often used.

A common use case in a third-party library is serialization and deserialization, especially conversion to or from JSON. By making your class A member of the formatter type class required by this library, you can customize your class to serialize JSON, XML, or something. (Typical is Spary-json)

The mapping between the Scala type and your database-driven need type is also usually customized and extended using the type class.

Summarize

Once you have really taken Scala to do something serious, you will inevitably encounter the type class. I hope that after reading this article, you are ready to take advantage of this powerful technology.

Scala's type class allows you to have Scala code that is open for backtracking and to keep type information as much as possible. Compared to other languages, Scala's type class allows developers to have full control, meaning that the default type class implementation can be overwritten without hindrance, and the implementation of type class is not visible in the global namespace.

You will find this technique useful when you want to write a library for use by others, but the type class is also useful in program code, which can reduce coupling in different modules.

[Translate]the neophyte ' s Guide to Scala part 12:type Classes