0

In Scala, suppose I would like a class that is just like List[String] except for a few methods, for example toString.

What I have now is

    case class FQN(val names : List[String]) extends LinearSeq[String] {

        def this( params : String* ) { this( List(params : _* )) }

        override def toString() : String = {
              names.reduce((a,b) => b++"."++a)
        }

        override def apply( i : Int ) = names.apply( i ) 

        override def length() = names.length

        override def tail = new FQN( names.tail )  

        override def head = names.head 

        override def isEmpty = names.isEmpty

        def ::( str : String) = new FQN( str :: names ) 

    }

However, what I would like is for fqn0 ++ fqn1 to give me an FQN, just as list0 ++ list1 gives a List'; it currently gives aLinearSeq[String]`. So something is not as I would like.

Also I made FQN a case class in order to get equals and hashCode to be the way I want, but I'm not sure whether this is the best way to do it.

What is the best way to make a class that is just like List, but implements certain methods differently?

Theodore Norvell
  • 15,366
  • 6
  • 31
  • 45
  • 1
    Probably easier to define an implicit class on List that adds your custom function. – Ryan Dec 09 '14 at 22:55
  • I'm not sure I understand how implicit classes help. I tried `implicit class FQN( names : List[String] ) { override def toString() : String = { names.reduce((a,b) => b++"."++a) } }` But then `fqn0 ++ fqn1` gives an error that `++` is not a member of FQN. – Theodore Norvell Dec 09 '14 at 23:41
  • I'm saying you don't use FQN at all, you just write functions in your implicit class which will return either your result type (e.g., `String`) or another `List`. Implementing your own Scala collections is likely to be painful. – Ryan Dec 10 '14 at 00:06
  • @TheodoreNorvell Do you want to override (redefine) existing methods or add new methods to the interface of `List`? Implicit classes are a great way to enrich an interface with new methods, but are probably not good if you want to, e.g., override the `toString` method. Is adding a second `toString`-method, e.g., `fqnToString` an option? – Kulu Limpa Dec 10 '14 at 00:31
  • @Ryan I think I see what you are saying now. See my answer. – Theodore Norvell Dec 10 '14 at 01:54
  • @KuluLimpa I would prefer to use toString. – Theodore Norvell Dec 10 '14 at 01:55

2 Answers2

1

My guts tell me that you are probably better off not implementing a specialized FQN collection, but hide the collection as part of the internal implementation of FQN, effectivly removing the corresponding operations from the interface of FQN. What I mean is, you should ask yourself whether the world must really see FQN as a Seq and be able use all corresponding operations, or whether it is enough to maybe add solely the operation ++ with a dispatch to the underyling list's ++ operation to FQN's interface.

However, the Scala guys apparently did a great job in generalizing their collections and at second glance, implementing your own collection doesn't seem like a huge pain. The documentation I posted in a comment explains how to reuse code when implementing your own collection and I recommend you read it.

In short, you'd want to mix-in traits with a name of the form xxxLike; in your specific case LinearSeqLike[String, FQN] and override the method newBuilder: mutable.Builder[String, FQN] to make methods like drop return a FQN.

The builder alone however isn't powerful enough on its own: Methods such as map (and to my surprise ++) may map the elements contained in the collection to a different type that is not necessary supported by your collection. In your example, FQN("foo").map(_.length) is not a legal FQN, so the result cannot be a FQN, however FQN("Foo").map(_.toLowercase) is legal (at least regarding the types). This problem is solved by bringing an implicit CanBuildFrom value to the scope.

The final implementation could look like this:

final class FQN private (underlying: List[String]) extends LinearSeq[String] with LinearSeqLike[String, FQN] {
  def apply(i: Int): String = underlying.apply(i)

  def length = underlying.length

  /** From the documentation of {LinearSeqLike}:
   *  Linear sequences are defined in terms of three abstract methods, which are assumed
   *  to have efficient implementations. These are:
   *  {{{
   *     def isEmpty: Boolean
   *     def head: A
   *     def tail: Repr
   *  }}}
   */
  override def isEmpty: Boolean = underlying.isEmpty

  override def head: String = underlying.head

  override def tail: FQN = FQN.fromSeq(underlying.tail)

  override def newBuilder: mutable.Builder[String, FQN] = FQN.newBuilder

  def ::(str: String) = new FQN(str :: underlying)

  override def toString(): String = {
    underlying.mkString(".")
  }
}

object FQN {

  implicit def canBuildFrom: CanBuildFrom[FQN, String, FQN] =
    new CanBuildFrom[FQN, String, FQN] {
      def apply(): mutable.Builder[String, FQN] = newBuilder
      def apply(from: FQN): mutable.Builder[String, FQN] = newBuilder
    }

  def newBuilder: mutable.Builder[String, FQN] =
    new ArrayBuffer mapResult fromSeq

  def fromSeq(s: Seq[String]): FQN = new FQN(s.toList)

  def apply(params: String*): FQN = fromSeq(params)

}
Kulu Limpa
  • 3,501
  • 1
  • 21
  • 31
0

Based on comments from @Ryan and @KuluLimpa (or at any rate my understanding of them), here is an approach

type FQN = List[String] 

implicit class FQNWithFQNToString( names : List[String] ) {
    def fqnToString() : String = {
        names.reduce((a,b) => b++"."++a)
    }
}

Now everything you can do with List[String], you can do with FQN. For example fqn1++fqn2 gives an FQN. In order to get the string I want, I can use fqn.fqnToString.

I find this answer less than satisfactory for two reasons

  • Weak type checking. There is no error if any old list of Strings is treated as an FQN. In particular I can't define a type QN (for qualified name) and have a compile time error if I use a QN where a FQN is needed.
  • There are now two ways to convert an FQN to a string. I have to be sure to use the right one in each case. If I create another name QN for List[String], I'll then have three ways to convert an object to a string. This is starting to feel very non-object-oriented.
Theodore Norvell
  • 15,366
  • 6
  • 31
  • 45
  • This probably feels non-object-oriented because it's rather functional programming: This implementation is very similar to adding the function `fqnToString(names: List[String])` with the slight difference that the implicit class makes the function part of the interface of `List`. This isn't necessary a bad thing, but in your specific case, I agree that having two `toString` methods around isn't ideal. The problem is, that you don't want to add new features to the interface, but change existing behavior, hence a functional way won't yield a satisfying solution. – Kulu Limpa Dec 10 '14 at 04:52
  • The only satisfying solution I can think of is to follow your initial implementation and override all methods you need redefined. This is especially painful as you are dealing with the fairly complex Scala collections. Maybe [this documentation](http://www.scala-lang.org/docu/files/collections-api/collections-impl_0.html) can help? However, you should probably rethink why `FQN` needs to implement the interface in the first place! Does `FQN` need to be used polymorphically where `LinearSeq[String]` is expected? Can't you simply hide the collection as part of the implementation? – Kulu Limpa Dec 10 '14 at 04:57
  • @KuluLimpa I will read the The Architecture of Scala Collections in detail. I don't think, though, that it is a question of functional style vs. object-oriented style. In Haskell, for example, I could create a `newtype` based on `[String]` and then make my new type derive `Monad` but not `Show`. – Theodore Norvell Dec 10 '14 at 14:55