Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Advanced type


May 14, 2021 Scala


Table of contents


Advanced type

Vision ("Type Class")

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)
        ^

Other types of restrictions

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:

  • A: - B A must be equal to B
  • The A-lt;;B A must be a sub-class of B
  • A slt;%-lt; B A must be considered B
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

Use views for generic programming

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:

  • The elements in the collection do not have to implement the Ordered trait, but the use of Ordered can still perform static type checks.
  • You can also define your own sort without any additional library support:
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)
  }
}

Context boundaries and implicitly

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.

More advanced polymorphism types and ad hoc polymorphisms

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))

F-world polymorphism

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.

The type of structure

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!

Abstract type members

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)

Type wipe and manifest

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 = ""

Case study: Finagle

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!