Wednesday, February 19, 2014

Conversions in scala



Table of contents...



Implicit keyword

implicit - keyword means that compiler should try to use function or variable even if it's not explicitly specified in code.
implicit variables and functions are:
  • used automatically by compiler
  • used only when there's no other explicit application available.


  • How to add implicit conversion

    Let say we want to add special debug method to every object in code. That method should return toString of object followed by length of this string.
  • We could just add trait to each class (represented by X here)
    trait HasDebugString {
        def toDebugString = 
            this.toString + " " + this.toString.length
    }
    class X extends HasDebugString
    
    but it's impossible when we use third party code
  • We can extend existing classes and add trait
    class XDebug extends X with HasDebugString
    
    But we don't want to do this for each class in project, right?
  • Also we can wrap instance of class (represented by X here)
    case class X(text : String) 
    class DebugStringWrapper(instance : X) {
        def toDebugString =
            instance.toString + instance.toString.length
    }
    def wrap(instance : X) = new DebugStringWrapper(instance);
    wrap(new X("ABC")).toDebugString
     //X(ABC)6 (result)
  • Again... But we don't want to do this for each class in project, right? We can use generic type parameter... (sponsored by letter T)
    //written only once
    class DebugStringWrapper[T](instance : T) {
        def toDebugString =
            instance.toString + instance.toString.length
    }
    def wrap[T](instance : T) = new DebugStringWrapper(instance);
    
    Other parts of code can use wrapper
    case class X(text : String)
    wrap(new X("ABC")).toDebugString //X(ABC)6 (result)
    wrap(new X("ABC")).toDebugString //X(ABC)6 (result)
    wrap(6).toDebugString //61 (result)
    wrap("ABC").toDebugString //ABC3 (result)
    wrap(List(1,2)).toDebugString //List(1, 2)10 (result)
  • wrap is annoying, can we tell compiler to use wrap every time when we use toDebugString method on object?
    wrap(6).toDebugString == 6.toDebugString
    Actually we can... implicit magic...
    implicit def hiddenwrap[T](instance : T) = new DebugStringWrapper(instance);
    wrap(List(1,2)).toDebugString //List(1, 2)10 (result)
    hiddenwrap(List(1,2)).toDebugString //List(1, 2)10 (result)
    List(1,2).toDebugString //List(1, 2)10 (result)
  • We've declared DebugStringWrapper class and we use it only in 1 place. It can be replaced with anonymous class
    implicit def hiddenwrap[T](instance : T) = new {
        def toDebugString = instance.toString + instance.toString.length
    }
    List(1,2,3).toDebugString //List(1, 2, 3)13 (result)


  • How to add operator to existing class

    In Scala operators are just methods
    Postfix operators are bound to left of operand. They're easy to implement
    implicit def opwrap[T](instance : T) = new {
        def ! = instance.toString + instance.toString.length
    }
    List(1,2,3).! //List(1, 2, 3)13 (result)
    The difference is that you can skip a dot
    List(1,2,3)! //List(1, 2, 3)13 (result)


    How to add prefix operator to existing class

    Prefix operator method name has to start with "unary_"
    implicit def opwrap2[T](instance : List[T]) = new {
        def unary_! = instance.isEmpty
    }
    println(!List(1,2,3)) //() false(result)
    println(!Nil) //() true(result)
    println(List(1,2,3).!) //() List(1, 2, 3)13(result)


    How to add infix operator to existing class

    Infix operators take two operands one from left and one from right.
    Here operator (!) takes two lists with elements of type TR and TL and returns smaller of them
    implicit def opwrap3[TL](instance : List[TL]) = new {
        def ![TR](other : List[TR]) = 
            if (instance.length > other.length) instance else other
    }
    List(1,2,3) ! List("A","B") //List(1, 2, 3) (result)
    List(1,2,3).!(List("A","B")) //List(1, 2, 3) (result)
    opwrap3(List(1,2,3)).!(List("A","B")) //List(1, 2, 3) (result)


    Infix operator names with colons

    If infix operator name ends with colon (":") then operator is bound to right term
    implicit def opwrap4[TL](instance : List[TL]) = new {
        def !:[TR](other : List[TR]) = 
            instance + ".!:(" + other + ")"
    }
    List(1,2,3) !: List("A","B") //List(A, B).!:(List(1, 2, 3)) (result)
    List("A","B").!:(List(1,2,3)) //List(A, B).!:(List(1, 2, 3)) (result)
    opwrap4(List("A","B")).!:(List(1,2,3)) //List(A, B).!:(List(1, 2, 3)) (result)


    Common errors

  • You've declared 2 implicit methods hiddenwrap, hiddenwrap2 and both can be applied
    List(1,2,3).toDebugString
    found : List[Int]
    required: ?{def toDebugString: ?}
    Note that implicit conversions are not applicable
    because they are ambiguous:
     both method hiddenwrap2 of type 
         [T](instance: T)AnyRef{def toDebugString: String}
     and method hiddenwrap of type 
         [T](instance: T)DebugStringWrapper[T]
     are possible conversion functions 
         from List[Int]to ?{def toDebugString: ?}
    
    
  • Implicit conversion does not work with existing methods...
    You cannot use implicit methods to override existing methods. Implicit method is applied only if method does not exists in class
    implicit def happystring[T](instance : T) = new {
    	override def toString = instance.toString + ":)"
    	def withSmiley = instance.toString + ":)"
    }
    //BAD: X has toString method
    println(X("I'm sad").toString) //() X(I'm sad)(result)
    //GOOD: toString is not found in X, so implicit conversion will be applied
    println(X("I'm happy").withSmiley) //() X(I'm happy):)(result)
  • No comments:

    Post a Comment