Scala - Abstract Member

A member of a class or trait is abstrat if the member does not have a complete definition in the class. Scala  goes beyond the abstract member idea in its full generality, besides methods, you can also declare abstract fields and even abstract type as the member of classes or trait. 

in this chapter, we will describe all four kinds of abstract member, vals, vars, methods, and types. and we will also discuss the following. 

  • pre-initialized fields
  • lazy vals
  • path-dependent types
  • and enumerations. 

A quick tour

trait Abstract {
  // -- abstract types
  // 
  type T // type declaration can also be a member of the abstract class, think that of the type parameter of some genric ? but better...
  def transform (x : T) : T
  
  val initial : T
  val current : T
}
and a concrete implementation of abstract class is as follow.
class Concrete extends Abstract { 
  type T = String // Now T becomes a concrete type, String, which is an alias to String 
  def transform(x : String) = x + x
  def initial = "hi"
  def current = initial
}

Type members

the term abstract type in Scala means a type declared (with the "type" keyword") to be a member of a class or trait. It does not mean the class itsel or trait is abstract. 

you can think of a non-abstract (or 'concrete') type member, such as the String in the concrete class as a way to define an alias, for a type. In class Concrete, for example, the type String is given the alias T. As a result, anywhere T appears in the definition of class Concrete, it means String. 

One reaons to use a type member is to define a short, descriptive alias for a type whose real name is more verbose, or less obvioius in meaning , than the alias. Such type member can help clarify the code of a class or trait. The order main use of type members is to declare abstract types that must be defined in subclass. 

Abstract vals.

abstract val are vals that does not have a initial value. 

Before we goes in to the abstract vals, we need to take the nature vals into consideration. for one, abstract vals should stay constant in its subclasses, but the reverse does not hold true, a abstract method declaration can be implemented by a concrete val definition. so given the abstract Apple class deifnition as follow, 

// -- abstract vals
// abstract method declaration, on the other hand, may be implemented by both concrete method definition and concrete val definitions..

abstract class Fruit {
  val v : String  // v for value 
  def m : String // m for method 
}
this is a valid Apple inheritance
abstract class Apple extends Fruit {
  val v : String  // v for value 
  val m : String // OK to override a 'def' with a 'val' 
}
and this is not a VALID fruit inheritance. 
abstract class BadApple extends Fruit {
  def v : String  // ERROR: cannot override a 'val' with a 'def' 
  def m : String  
}

Abstrat vars

like the abstract vals, abstract vars are vars with a name and type but not initial value. and example is as follow. 

// -- abstract vars
trait AbstractTime { 
  var hour : Int
  var minute : Int
}
as we know that var is equivalent to a getter method and a setter method. such as for a var defined as def hour: int and a setter method def  hour_= (x: Int)...
// as we know that var is like a getter and a setter automatically by the compiler 
trait AbstractTime { 
  
  def hour : Int
  def hour_=(x : Int)
  def minute: Int
  def minute_=(x : Int)
}

Initializing Abstract vals

the problem with the Abstract vals members is that if you want to provide a concrete implementation, there is two condition that must be met, 1. the val can not be reassigned once it is already assigned. 2. there are certain initializing order that is required for a sucessful initialization. this is true important for trait, because trait does not have constructor. 

first let's consider a simple example with trait.

// -- initializing abstract vals

trait RationalTrait { 
  val numberArg : Int 
  val denomArg : Int 
}
to instantiate a concrete instance of that trait, you need to implement the abstract val definition. 
// anonymous classthat mixes in the trait and is defined by the body 
new RationalTrait { 
  val numberArg = 1
  val denomArg = 2
}
this new TraitName { class_body } is a way to construct some anonymous class that mixes in the trait and is defined by the body.

there are some ordering issue related to this type of object creation, suppose that we have a named concrete type called Rational. 

the expression 
new Rational(expr1, expr2)

and 

new RationalTrait {
  val numerArg = expr1
  val denomArg = expr2
}

are different in terms of when the expr1 and expr2 is called, in the first example, the expr1 and expr2 are evaluated before the constructor of Rational object, while in the second case, the expr1 and expr2 are evaluated after $iw object - the anonymous object which mixes in the trait. this is all ok for this simple case - the order of vals is irrelevant to the correctness of the object. 

however, consider the following code. 


// initialization order 
//   numberArg and denomArg 
// are evaluated as part of the initialization of the anonymous class
// but the anonymous class is initialized after the RationalTrait, 
// so the value of the numberArg and denomArg  is not available of 
// RationalTrait, so you might see errors if the RationalTrait is defined as below. 

trait RationalTrait { 
  
  val numberArg : Int 
  val denomArg : Int
  
  require(denomArg != 0)
  
  private val g = gcd(numberArg, denomArg)
  val number = numberArg / g
  val denom = denomArg / g
  
  private def gcd(a : Int, b : Int) : Int = 
    if (b == 0) a else gcd(b, a % b)
  
  override def toString  = number + "/" + denom
}

and below will violate the following constraint. 



// this will violate the require constraint 
val x = 2
new RationalTrait {
  val numberArg = 1 * x
  val denomArg = 2 * x
}
that is because before the evaluation of the val numberArg = 1 * x and denomArg = 2 * x; the internal of the RationalTrait will be evaluated first, and the require statement will fail.

Scala has an feature called Pre-initialized fields, which helps to solve the problem.
new  {
  val numberArg = 1 * x
  val denomArg = 2 * x
} with RationalTrait
the syntax is that you put the curly bracket {} before the mixing Trait and this willl guarantee the initialization section will be called first. 


or the following is also valid syntax.


object twoThirds extends  {
  val numberArg = 2
  val denomArg = 3
} with RationalTrait

Or the following. 

class RationalClass(n: Int, d : Int) extends {
  val numberArg = n
  val denomArg = d
} with RationalTrait { 
  def + (that : RationalClass) = new RationalClass(
    number * that.denom + that.number * denom,
    denom * that.denom
  )
}

there is an known gotchas related to the initialization section. check the following. 

// initialization section gotchas
new 
{
  val numerArg = 1
  val denomArg = this.numerArg * 2 // this now refers to $iw, because when the initialization section refers to the object containing the class or object that is being constructed, not the constructed one.
} with RationalTrait

the reason as it has been stated above is in the initializaiton section, the "this" references goes to $iw (synthetic object), because when the initialization section refers to the object containing the class or object that is being constructed, not the constructed one.

Lazy vals

Another way to solve the initializing issue is the use of lazy vals.

there is a keyword called "lazy", the meaning of lazy is that it allows the system to sort out things should be initialized. 

object Demo {
  lazy val x = { println("initializing x" ); "done" }
}
and when you run the code, you will see the output as in code below 
scala> Demo
initializing x
defined module Demo

scala> Demo.x
res0: java.lang.String = done
and if we change the code as follow. 
object Demo {
  lazy val x = { println("initializing x"); "done" }
}
running the same code, and the output is 


Demo
// defined module Demo
Demo.x
//initializing x
//res0: java.lang.String = done
nowe, let's change the code to use lazy vals. 
// lazy are never executed more than once 
// becareful of the initialization order
trait LazyRationalTrait { 
  val numberArg : Int 
  val denomArg : Int
  
  lazy val number = numberArg / g
  lazy val denom = denomArg / g
 
  private lazy val g = { 
    require(denomArg != 0)
    gcd(numberArg, denomArg)
  }
  
  private def gcd(a : Int, b : Int) : Int = 
    if (b == 0) a else gcd(b, a % b)
  
  override def toString  = number + "/" + denom  
}
and now it is fine
// with lazy val, this is fine
val x = 2
new LazyRationalTrait { 
  val numberArg = 1 * x
  val denomArg = 2 * x
}

Abstract types

Let's see an example and then we will introduce the abstract types. 

// -- abstract types

class Food
abstract class Animal { 
  def eat(food : Food)
}

class Grass extends Food
class Cow extends Animal { 
  override def eat(food : Grass) {} // this won't compile
}
it won't compile because you have to match the exact signature when compiling. otherwise, see the following code. 
//class Fish extends Food
//val bessy : Animal = new Cow
//bessy eat (new Fish) // see the problem here ?
A better way is to use the Abstract types. 
// a better way

class Food 
abstract class Animal { 
  type SuitableFood <: Food
  def eat(food : SuitableFood)
}

class Grass extends Food
class Cow extends Animal { 
  type SuitableFood = Grass
  override def eat(food : SuitableFood) {}
}
now, this won't compile (a good sign)...
// now this won't work

class Fish extends Food
val bessy : Animal = new Cow
bessy eat (new Fish) // this won't compile

Path-dependent types

If we input the last code into Scala compiler, what we may get for the error message is like this:

: 12 : error: type mismatch
found : fish
required : bessy.SuitableFood
   bessy eat (new Fish)
              ^
odd, isn't it? the type rquired is by the eating method, bessy.SuitableFood. It consists of a object references, bessy and followed by a type field, SuitableFood.

a type called bessy.SuitableFood is called path-dependent type. the path here means a references to an object..

the type depends on the path. for instance 

// -- path-dependent type 

class DogFood extends Food
class Dog extends Animal { 
  type SuitableFood = DogFood
  override def eat(food : DogFood) {}
}
and feed a bessy the cow tihe dogfood, it won't compile. 
val bessy = new Cow
val lassie : Animal = new Dog
lassie eat (new bessy.SuitableFood) // this won't compile , type mismatch
while, this can pass compilation.
// this will
val bootsie = new Dog
val lassie = new Dog
lassie eat (new DogFood)
a clear comparison against OuterClass.InnerClass can help in understanding how the path-dependent types works.
// a note on the inner class
class Outer { 
  class Inner 
}

val o1 = new Outer
val o2 = new Outer

// this is how you create instance of inner classes, an instance of the outer class is required, 
new o1.Inner
// but  you cannot do this way 
new Outer#Inner // -- we will show how this notation becomes useful

Structural subtyping

when a class inherit from another class, the first is said to be a nominal subtype of the other one. it is nomimal subtype because each type has a name, and the names are explicitly supportred structural subtyping. 

scala support what is called structural subtyping, where you get a subtyping relationship simply because two types have the same members. to get structural subtyping in Scala, use Scala's refinement types

suppose that you want to define a animals that eat grass. so instead of AnimalThatEatGrass, you may do the following. 

Animal { type SuitableType = Grass }

// given this, define a pasture class
class Pasture { 
  var animals : List[Animal { type SuitableFood = Grass}] = Nil
}

supose that you will need to write some load pattern code, like you load a resource such as the file for a short while and after use, you will return the file handle back to the operation system, it is like the C#'s using pattern. 

// e.g. 
// remember the load patterns?
using (new PrintWriter("data.txt")) { 
  writer => writer.println(new Date)
}
using (serverSocket.accept()) { 
  socket => 
      socket.getOutputStream().write("Hello, world\n".getBytes)    
}

like the code above, for both the file and the socket object, they both have a close method, but they don't have the same inheritance (like inherit from same trait such as IDiposable), it is hard to write code that can use both the file object or the socket object. 

we might want to write some code as such , while it does not compile. 


// and we will make a general function for this 
def using[T, S](obj : T) (operation : T => S) = { 
  val result = operation(obj)
  obj.close() // compilation error, obj does not have this close method 
  result
}


however, you can use the structural typing, and there is one special syntax called view bound, The desire upper bound is { def close(): Unit }.,

and here is the example.


// two small differences in the refinement 
//  one is 
//   no base type is specified, scala compiler will use AnyRef instead ? 
//  second is 
//   AnyRef do not have the close mtehod, technically speaking, the second is a structural type
def using[T <: { def close(): Unit}, S](obj : T) (operation : T => S) = {
  val result = operation(obj)
  obj.close
  result
}
the major difference of this type and the refined type 4Animal { Type SuitableFood = Grass } is that ther is no base class is provifded , technically speaking, thi s is structral type. 



Enumeration

scala does not have a built-in language construct for enumeration, however, it has a Enumeration class that you can extend. 

Enumeration is an application of path-dependent  type is found in the Scala's...

an example of simple enumeration is like the following. 

object Color extends Enumeration { 
  val Red, Green, Blue = Value 
}
and another example is the Direction enumeration .

object Direction extends Enumeration { 
  val North, East, South, West = Value  
}
the type of Color.Red, Color.Green and others are of type Color.Value.  Direction.East, Direction.West and others are of type Direction.Value; They are of different types. 

Value has variant that allow you to give some names to the enumertion values. 

// or you can  use the Variant of Value methods

object Direction extends Enumeration { 
  val North = Value("North")
  val East = Value("East")
  val South = Value("South")
  val West = Value("West")
}
we can iterate over the values of the values of a enumeration like below. 

// let's iterate through the enumerations
for (d <- Direction.values) print(d + " ") // the values of the enumeration
each enumeration value has a unique value, 

Direction.East.id // id has unique names
and you can convert an id back to the enumeration value. 

// construct a Direction value back from the Id
Direction(1)

Case study: Currencies

we are going to design a system which  allow represent currency in different designation such as dollars,  yen, euro and others.. 

also, it should support conversion from one curency to another, or operation between two different kind of currencies.

below is the code, and we will highlight some key point later. 

// file : 
//   currency.scala
// description: 
//   Currency example with the abstract member, such as abstract type, abstract vals, and abstract vars and etc.., and path dependent types

abstract class CurrencyZone { 
 
  type Currency <: AbstractCurrency
  def make(x : Long) : Currency
    
	abstract class AbstractCurrency {
	  val amount : Long
	  def designation : String
	  
	  def + (that : Currency) : Currency = make(this.amount + that.amount)
	  def * (x : Double) : Currency = make((this.amount * x).toLong)
	  def - (that : Currency) : Currency = make(this.amount - that.amount)
	  def / (that : Double) : Currency = make((this.amount / that).toLong)
	  def / (that : Currency) = this.amount.toDouble / that.amount
	  
	  def from(other: CurrencyZone#AbstractCurrency): Currency = 
	    make (math.round(
	        other.amount.toDouble * Converter.exchangeRate(other.designation)(this.designation)))
	  
	  private def decimals(n : Long) : Int = if (n == 1) 0 else 1 + decimals(n / 10)
	  
	  override def toString = ((amount.toDouble / CurrencyUnit.amount.toDouble) formatted ("%." + decimals(CurrencyUnit.amount) + "f") + " " + designation)
	}
    val CurrencyUnit : Currency 
  
}

object Converter { 
  var exchangeRate = Map(
    "USD" -> Map("USD" -> 1.0, "EUR" -> 0.7596, "JPY" -> 1.211, "CHF" -> 1.223),
    "EUR" -> Map("USD" -> 1.316, "EUR" -> 1.0, "JPY" -> 1.594, "CHF" -> 1.6233),
    "JPY" -> Map("USD" -> 0.8257, "EUR" -> 0.6272, "JPY" -> 1.0, "CHF" -> 1.018),
    "CHF" -> Map("USD" -> 0.8108, "EUR" -> 0.6160, "JPY" -> 0.982, "CHF" -> 1.0)
  )
}


object US extends CurrencyZone { 
  
  abstract class Dollar extends AbstractCurrency { 
    def designation = "USD"
  }
  
  type Currency = Dollar
  def make(x : Long) = new Dollar { val amount = x } 
  val Cent = make(1)
  val Dollar = make(100)
  val CurrencyUnit = Dollar
}

object Europe extends CurrencyZone { 
  abstract class Euro extends AbstractCurrency { 
    def designation = "EUR"
  }
  type Currency = Euro
  def make(cents : Long) = new Euro { val amount = cents } 
  val Cent = make(1)
  val Euro = make(100)
  val CurrencyUnit = Euro
}


object Japan extends CurrencyZone { 
  abstract class Yen extends AbstractCurrency { 
    def designation = "JPY"
  }
  type Currency = Yen
  def make(yen : Long) = new Yen { val amount = yen } 
  val Yen = make(1)
  val CurrencyUnit = Yen
}
and the use of the code is 

// use of the classes 
Japan.Yen from US.Dollar * 100
Europe.Euro from res12
US.Dollar from res13
US.Dollar * 100 + res14

as for the class design, we have this for the abstract class.

abstract class CurrencyZone { 
 
  type Currency <: AbstractCurrency // Currency is subtype of AbstractCurrency
  def make(x : Long) : Currency

}

	abstract class AbstractCurrency {
          val amount : Long
	   def designation : String
	  
	  // ... +/-/*//
	}
}
and for each type of curency, make a CurrencyZone which handle creation of such currency as such ..

object US extends CurrencyZone { 
  
  
  abstract class Dollar extends AbstractCurrency { // Dolalr is still abstract, you cannot make Dollar directly instead from the CurrencyZone..
    def designation = "USD"
  }
  // Currency type
  type Currency = Dollar
  def make(x : Long) = new Dollar { val amount = x } 
  val Cent = make(1) // dollar unit Cent
  val Dollar = make(100) // dollar unit dollar
  val CurrencyUnit = Dollar // the CurrencyUnit is Dollar
}
few notes: first Dollar is defined as the an asbtract class, the value member is not defined, you cannot create Dollar directly. but instead use the Dollar or cent with arithmetic operation to operate on the currency.

second , it alias the Currency as Dollar, which satisify the abstract type member of the AbstractCurrencyZone.. 

though you created two Unit, Cent and Dollar, while to fulfill the need for CurrencyUnit required  by the AbstractCurrencyZone. 


转载于:https://my.oschina.net/u/854138/blog/139402

你可能感兴趣的:(Scala - Abstract Member)