May 14, 2021 Scala
Sometimes, you don't need to specify that one type is equal/sub/more than another class, and you can disguise this association by converting that class. O ne viewer specifies that one type can be "considered" to be another type. This is useful for object read-only operations.
The implicit function allows the type to be converted automatically. R ather, implicit functions allow on-demand functions to be applied when they can help satisfy type inference. For example:
scala> implicit def strToInt(x: String) = x.toInt
strToInt: (x: String)Int
scala> "123"
res0: java.lang.String = 123
scala> val y: Int = "123"
y: Int = 123
scala> math.max("123", 111)
res1: Int = 123
Vision, like a type boundary, requires the existence of such a function for a given type.
You
<%
specify type limits, such as:
scala> class Container[A <% Int] { def addIt(x: A) = 123 + x }
defined class Container
This says A must be "considerable" as Int. Let's try.
scala> (new Container[String]).addIt("123")
res11: Int = 246
scala> (new Container[Int]).addIt(123)
res12: Int = 246
scala> (new Container[Float]).addIt(123.2F)
<console>:8: error: could not find implicit value for evidence parameter of type (Float) => Int
(new Container[Float]).addIt(123.2)
^
Methods can enforce more complex type restrictions by implying parameters. F
or example, List supports sum for digital content, but not for other content. H
owever, Scala's numeric types don't all share a superseed, so
T <: Number
I
nstead, to make it work, Scala's math
library
defines an implicit
Numeric[T]
Then use it in the List definition:
sum[B >: A](implicit num: Numeric[B]): B
If
List(1,2).sum()
you do not need to pass in an num parameter;
But if you call
List("whoop").sum()
that it can't set num.
Methods may require a specific type of "evidence" when no unfamiliar object is set as Numeric. The following type-relationship operators can be used at this point:
scala> class Container[A](value: A) { def addIt(implicit evidence: A =:= Int) = 123 + value }
defined class Container
scala> (new Container(123)).addIt
res11: Int = 246
scala> (new Container("123")).addIt
<console>:10: error: could not find implicit value for parameter evidence: =:=[java.lang.String,Int]
Similarly, based on the previous implicit conversion, we can relax the constraints to visibility:
scala> class Container[A](value: A) { def addIt(implicit evidence: A <%< Int) = 123 + value }
defined class Container
scala> (new Container("123")).addIt
res15: Int = 246
In the Scala standard library, views are primarily used to implement common functions of collections. For example, the "min" function (on Seq) uses this technique:
def min[B >: A](implicit cmp: Ordering[B]): A = {
if (isEmpty)
throw new UnsupportedOperationException("empty.min")
reduceLeft((x, y) => if (cmp.lteq(x, y)) x else y)
}
Its main advantages are:
scala> List(1,2,3,4).min
res0: Int = 1
scala> List(1,2,3,4).min(new Ordering[Int] { def compare(a: Int, b: Int) = b compare a })
res3: Int = 4
As a side note, there are views in the standard library to convert Ordered to Ordering (and vice versa).
trait LowPriorityOrderingImplicits {
implicit def ordered[A <: Ordered[A]]: Ordering[A] = new Ordering[A] {
def compare(x: A, y: A) = x.compare(y)
}
}
Scala 2.8 introduces a shortcut to series and access implicit parameters.
scala> def foo[A](implicit x: Ordered[A]) {}
foo: [A](implicit x: Ordered[A])Unit
scala> def foo[A : Ordered] {}
foo: [A](implicit evidence$1: Ordered[A])Unit
Implicit values may be accessed through implicitly
scala> implicitly[Ordering[Int]]
res37: Ordering[Int] = scala.math.Ordering$Int$@3a9291cf
When combined, less code is often used, especially when the view is in series.
Scala can abstract the type of "higher order". F or example, suppose you need to process several types of data with several types of containers. Y ou may have defined a Container interface that can be implemented as several types of containers: Option, List, and so on. You want to define an interface that can use the values in these containers, but you don't want to determine the type of values.
This is similar to the function Curryization.
For example, although the "binary type" has
List[A]
which means that we have to satisfy a "level" type variable to produce a specific type (just as a function without Curry needs to provide only a list of arguments to be called), higher-order types need more.
scala> trait Container[M[_]] { def put[A](x: A): M[A]; def get[A](m: M[A]): A }
scala> val container = new Container[List] { def put[A](x: A) = List(x); def get[A](m: List[A]) = m.head }
container: java.lang.Object with Container[List] = $anon$1@7c8e3f75
scala> container.put("hey")
res24: List[java.lang.String] = List(hey)
scala> container.put(123)
res25: List[Int] = List(123)
Note: Container is a polymorphic ("container type") of a paramethic type.
If we use containers in conjunction with implicit transformations, we get "ad hoc" polymorphism: the ability to write generic functions to containers.
scala> trait Container[M[_]] { def put[A](x: A): M[A]; def get[A](m: M[A]): A }
scala> implicit val listContainer = new Container[List] { def put[A](x: A) = List(x); def get[A](m: List[A]) = m.head }
scala> implicit val optionContainer = new Container[Some] { def put[A](x: A) = Some(x); def get[A](m: Some[A]) = m.get }
scala> def tupleize[M[_]: Container, A, B](fst: M[A], snd: M[B]) = {
| val c = implicitly[Container[M]]
| c.put(c.get(fst), c.get(snd))
| }
tupleize: [M[_],A,B](fst: M[A],snd: M[B])(implicit evidence$1: Container[M])M[(A, B)]
scala> tupleize(Some(1), Some(2))
res33: Some[(Int, Int)] = Some((1,2))
scala> tupleize(List(1), List(2))
res34: List[(Int, Int)] = List((1,2))
It is usually necessary to access a specific subseter class of (generic) traits. For example, imagine that you have some generic traits, but you need to be able to compare them to one of its subsystes.
trait Container extends Ordered[Container]
However, the comparison method is now necessary
def compare(that: Container): Int
Therefore, we cannot access specific sub-types, such as:
class MyContainer extends Container {
def compare(that: MyContainer): Int
}
Compilation failed because we specified the Ordered trait for Container, not for a specific subspecies.
To reconcilice this, we switched to
F-
polymorphism.
trait Container[A <: Container[A]] extends Ordered[A]
Strange type!
But you can see how ordered parameterization is implemented for A, and
Container[A]
So, now
class MyContainer extends Container[MyContainer] {
def compare(that: MyContainer) = 0
}
They're orderly:
scala> List(new MyContainer, new MyContainer, new MyContainer)
res3: List[MyContainer] = List(MyContainer@30f02a6d, MyContainer@67717334, MyContainer@49428ffa)
scala> List(new MyContainer, new MyContainer, new MyContainer).min
res4: MyContainer = MyContainer@33dfeb30
Since they
Container[_]
of Container, we can define another
Container[_]
and create a mixed list of Containers:
scala> class YourContainer extends Container[YourContainer] { def compare(that: YourContainer) = 0 }
defined class YourContainer
scala> List(new MyContainer, new MyContainer, new MyContainer, new YourContainer)
res2: List[Container[_ >: YourContainer with MyContainer <: Container[_ >: YourContainer with MyContainer <: ScalaObject]]]
= List(MyContainer@3be5d207, MyContainer@6d3fe849, MyContainer@7eab48a7, YourContainer@1f2f0ce9)
Notice how the result type
YourContainer
by the
MyContainer
types. T
his is the work of type inference. I
nterestingly, this type doesn't even need to make sense, it just provides a logical maximum underlying uniform type for the list.
What happens if we try Ordered now?
(new MyContainer, new MyContainer, new MyContainer, new YourContainer).min
<console>:9: error: could not find implicit value for parameter cmp:
Ordering[Container[_ >: YourContainer with MyContainer <: Container[_ >: YourContainer with MyContainer <: ScalaObject]]]
The unified type
Ordered[]
exists.
Scala supports structure types structural types - type requirements are represented by interface constructs, not by specific types.
scala> def foo(x: { def get: Int }) = 123 + x.get
foo: (x: AnyRef{def get: Int})Int
scala> foo(new { def get = 10 })
res0: Int = 133
This may be pretty good in many scenarios, but reflection is used in this implementation, so pay attention to performance!
In traits, you can keep type members abstract.
scala> trait Foo { type A; val x: A; def getX: A = x }
defined trait Foo
scala> (new Foo { type A = Int; val x = 123 }).getX
res3: Int = 123
scala> (new Foo { type A = String; val x = "hey" }).getX
res4: java.lang.String = hey
This is often a useful technique in situations such as dependency injection.
You can use the hash operator to refer to a variable of an abstract type:
scala> trait Foo[M[_]] { type t[A] = M[A] }
defined trait Foo
scala> val x: Foo[List]#t[Int] = List(1)
x: List[Int] = List(1)
As we all know, type information is lost at compile time due to erasing. S cala's Manifests feature allows us to selectively recover type information. The manifest provides an implied value that is generated by the compiler as needed.
scala> class MakeFoo[A](implicit manifest: Manifest[A]) { def make: A = manifest.erasure.newInstance.asInstanceOf[A] }
scala> (new MakeFoo[String]).make
res10: String = ""
See also: https://github.com/twitter/finagle
trait Service[-Req, +Rep] extends (Req => Future[Rep])
trait Filter[-ReqIn, +RepOut, +ReqOut, -RepIn]
extends ((ReqIn, Service[ReqOut, RepIn]) => Future[RepOut])
{
def andThen[Req2, Rep2](next: Filter[ReqOut, RepIn, Req2, Rep2]) =
new Filter[ReqIn, RepOut, Req2, Rep2] {
def apply(request: ReqIn, service: Service[Req2, Rep2]) = {
Filter.this.apply(request, new Service[ReqOut, RepIn] {
def apply(request: ReqOut): Future[RepIn] = next(request, service)
override def release() = service.release()
override def isAvailable = service.isAvailable
})
}
}
def andThen(service: Service[ReqOut, RepIn]) = new Service[ReqIn, RepOut] {
private[this] val refcounted = new RefcountedService(service)
def apply(request: ReqIn) = Filter.this.apply(request, refcounted)
override def release() = refcounted.release()
override def isAvailable = refcounted.isAvailable
}
}
A service can authenticate requests through filters.
trait RequestWithCredentials extends Request {
def credentials: Credentials
}
class CredentialsFilter(credentialsParser: CredentialsParser)
extends Filter[Request, Response, RequestWithCredentials, Response]
{
def apply(request: Request, service: Service[RequestWithCredentials, Response]): Future[Response] = {
val requestWithCredentials = new RequestWrapper with RequestWithCredentials {
val underlying = request
val credentials = credentialsParser(request) getOrElse NullCredentials
}
service(requestWithCredentials)
}
}
Note how the underlying service needs to authenticate the request, and it is static authentication. Therefore, filters can be thought of as service converters.
Many filters can be combined:
val upFilter =
logTransaction andThen
handleExceptions andThen
extractCredentials andThen
homeUser andThen
authenticate andThen
route
Enjoy the safe type!