0
votes

I'm just starting out using abstract types. I'm running into an error that I'm not able to fully understand. Here is some of my code for some BACKGROUND

abstract class DbfReader( fileName: String )
{
    type DBFDataType <:Any
    type Key <:Any
    type Value <:Any

    abstract class FieldMapping
    {
        type FieldType
        def acronym: Key
        def longName: Key 
        def fieldNum: Int
        def getField: FieldType
        def getFieldLength: Int
    }

    def fieldMappings: Map[ Key, FieldMapping ]
    def getFieldCount: Int
    def hasRecord(): Boolean
    def getRecord(): DBFDataType
    def getFieldVal( fieldName: Key )( rowData: DBFDataType ): Value
    protected def createFieldMapping( fieldAcro: Key, 
                                      fieldLongName: Key, 
                                      fieldPosition: Int ): FieldMapping
....
}

THe Abstract class DbfReader is meant to be an abstract wrapper to different DBF (Data base file ) reading Libraries i'm trying out. The abstract class has an inner class of FieldMapping (table meta data which has a abstract type FieldType which is meant to be a placeholder for the underlying libraries representation of a Data base Field. THe routine getField in the inner class returns a reference of this type.

Following is a concerete implementation of this abstract class: EVEN MORE BACKGROUND

class MyDBFReader( fileName: String, fmap: List[( String, String, Int )]  ) extends DbfReader( fileName )
{
    type DBFDataType = Array[Object]
    type Key = String
    type Value = String

    val dbReader = new jdbf.DBFReader( new java.io.FileInputStream( theFile ) )

    val fieldMappings = addFieldMappings(fmap)(Map())
    case class InnerFieldMapping( acronym: Key, longName: Key, fieldNum: Int) extends FieldMapping
    {
        type FieldType = jdbf.JDBField
        override def getField: jdbf.JDBField = dbReader.getField( fieldNum )
        def getFieldLength = getField.getLength
    }

    def getFieldCount = dbReader.getFieldCount
    def hasRecord = dbReader.hasNextRecord
    def getRecord = dbReader.nextRecord
    def createFieldMapping( fieldAcro: String, fieldLongName: String, fieldPosition: Int ) = InnerFieldMapping( fieldAcro, fieldLongName, fieldPosition )

    def getFieldVal( fieldName: Key )( rowData: DBFDataType ) = {
        if( fieldMappings.keySet.contains( fieldName ) ) stringer( rowData( fieldMappings( fieldName ).fieldNum - 1 ) )
        else
            throw new NoSuchElementException( "Key " + fieldName + " not Found" )
    }

    private def stringer( r: Object ) = r.asInstanceOf[String].trim
}

The trouble I'm running into is when I try to call getField from InnerFieldMapping which extends the abstract Field mapping. I'm trying this in a unit test like this:

WHERE THE PROBLEM HAPPENS

class MyDBFSuite extends FunSuite {
    val fileName = "/Users/Me/api11bdb.dbf"
    val dbf = new MyDBFReader( fileName, DbfReader.SchoolFieldMapping )

    test( "Dbf should have 150 fields" )
    {
        assert( dbf.getFieldCount === 150 )
    }

    test( "Should read a record" )
    {
        assert( dbf.hasRecord === true )
        assert( dbf.getRecord.size === 150 )
    }

    test( "Should Get a Field" )
    {
        println( dbf.fieldMappings.head._2.getField.getType )
        //assert( dbf.fieldMappings.head._2.getField.getType === "S" )
    }

In the last test ( either with the assert enabled or in the println ), whenever I try to access getType which is a routine in DBFField which is what I expect from the inner class InnerFieldMapping routine getField. In the abstract glass the routine specifies a return type of FieldType, which I implement in the concrete class as jdbf.JDBFField

However the compiler says: THE PROBLEM

src/test/scala/ChinaDBFTestSuite.scala:23: value getType is not a member of MyDBFSuite.this.dbf.FieldMapping#FieldType
[error]         println( dbf.fieldMappings.head._2.getField.getType )
[error]                                                     ^

In the other test I call the outer class routine getRecord which in its abstract class returns the abstract type. THe compiler had no problem there. Looking at the error message it seems that it expects a the FieldType to comply with the inner abstract class definition. I mean I would exect it to lookfor a MyDBFSuite.this.dbg.InnerFieldMapping.FieldType. Am I doing something inherently wrong here ?

EDIT: Thanks a lot for the answer. As a follow up, I also have a question about overriding? I notice that in the scala book methods returning abstract types are overriden in the sub-types, however I don't do this here and the compiler doesn't complain about missing implementations when instantiating the subtypes. Why is the override tag needed in the subclass method when the return type of the method is an abstract type ( as defined in the base class )?

1
maybe you need to call a getClass?4lex1v

1 Answers

1
votes

I think this is the problem, that's best illustrated with an example. Your parent class defines this:

def fieldMappings: Map[ Key, FieldMapping ]

And this must be bound, homogeneously to a single type it call during the compiler. For example what if you added a second class above, second "type" and legally start adding it to that map. (Perfectly legal, both are child classes of FieldMapping.) But how then, next, does Scala know what to do later on?

    dbf.fieldMappings.head._2.getField.getType
    dbf.fieldMappings.tail._2.getField.otherFunction

Now consider, the compiler has no idea after .getField which of the 2 classes you loaded in there. Maybe head is of your first child class? Maybe they are both the 2nd class you made that defines "otherFunction"? who knows?

Solutions:

You could either cast it after the fact:

dbf.fieldMappings.head._2.getField match {
  case answer:jdbf.JDBField => answer
  case _ => throw new ClassCastException
}

Or, bind everything from top down using type parameters to begin with - which is probably the most readable and safe way because it can be caught during compile.

abstract class DbfReader[T]( fileName: String )
{
    type DBFDataType <:Any
    type Key <:Any
    type Value <:Any

    abstract class FieldMapping
    {

        def acronym: Key
        def longName: Key 
        def fieldNum: Int
        def getField: T
        def getFieldLength: Int
    }

    def fieldMappings: Map[ Key, FieldMapping ]
    def getFieldCount: Int
    def hasRecord(): Boolean
    def getRecord(): DBFDataType
    def getFieldVal( fieldName: Key )( rowData: DBFDataType ): Value
    protected def createFieldMapping( fieldAcro: Key, 
                                      fieldLongName: Key, 
                                      fieldPosition: Int ): FieldMapping
....
}

To summarize, you can't have a collection of an abstract type that somehow makes calls later to child types without a cast at runtime, even if you have only have one defined it's still the same problem.