Monday, November 24, 2008

Using Scala Implicits to Replace Mindless Delegation

I'm still refactoring my Scala CPU Simulator code, as I keep finding ways to just lop off piles of unneeded code. In this latest example, I was using pretty standard Java style delegation - having my class implement an interface, having a member variable of that interface, and delegating all method calls on that interface to the member variable.

There are several problems with this:

  1. The main problem: if I add a method to the interface I then have to add it to everyone implementing the interface. But, what If I'm not actually writing the implementation of the interface? Client code won't compile. Adding an implicit doesn't remove this problem entirely, but it certainly works for plain old delegates.

  2. It's error prone. Maybe the method was a void, and my IDE added the signature for the method, so the code compiles, but I forgot to actually delegate to the member.
  3. It's just plain wordy and ugly.


To remove the boilerplate, all I needed to do was add an implicit conversion from my class to the interface, using the member variable. I'll show this below.

First, here is an example of the old, more wordy style:

trait PowerSource{
def connect( p: PowerSource ): Unit
def disconnect( p: PowerSource ): Unit
def reconnect( p: PowerSource ): Unit
}

class DelegateToPowerSource( in: PowerSource ) extends PowerSource {
def connect( p: PowerSource ) = in connect p
def disconnect( p: PowerSource ) = in disconnect p
def reconnect( p: PowerSource ) = in reconnect p
}


DelegateToPowerSource has a member, "p", and implements the PowerSource interface by delegating to that member for each of the methods on the PowerSource interface. The more methods that PowerSource has, the longer DelegateToPowerSource gets, and yet the code is just boilerplate. Even if the IDE does this for you, its still wordy and potentially error prone.

Now for the new version:


trait PowerSource{
def connect( p: PowerSource ): Unit
def disconnect( p: PowerSource ): Unit
def reconnect( p: PowerSource ): Unit
}

object DelegateToPowerSource{
implicit def delegateToPowerSource( d: DelegateToPowerSource ) = d.p
}

class DelegateToPowerSource( p: PowerSource )


That's it. Now, any time I add a method to the PowerSource interface (I should probably start calling it trait), DelegateToPowerSource simply has that method; via the conversion. I never have to change DelegateToPowerSource because of a change to PowerSource. DelegateToPowerSource can simply have whatever code in it that it was originally intended to have, obviously augmenting PowerSource in some way.


For completeness, I'll post the actual code where I did exactly this. But, it's pretty much the same, so if you get the point, no need to keep reading.


trait LogicGate extends PowerSource{
val inputA: PowerSource
val inputB: PowerSource
val output: PowerSource
}

abstract class BaseLogicGate(val inputA: PowerSource, inputB: PowerSource) extends LogicGate {

def state = output.state

def -->( p: PowerSource ): PowerSource = output --> p
def <--( p: PowerSource ): PowerSource = output <-- p

def disconnectFrom( p: PowerSource ): PowerSource = output disconnectFrom p
def disconnectedFrom( p: PowerSource ): PowerSource = output disconnectedFrom p

def handleStateChanged( p: PowerSource ) = {}
def notifyConnections = {}
}

class AndGate(a: PowerSource, b: PowerSource)
extends BaseLogicGate(a: PowerSource, b: PowerSource){
val output = new Relay(a, new Relay(b))
}


Of course there were several other LogicGates (or, nor, nand, etc). All of this was condensed down to:


object LogicGate{
implicit def logicGateToPowerSource( lg: LogicGate ): PowerSource = lg.output
}

trait LogicGate{
val inputA: PowerSource
val inputB: PowerSource
val output: PowerSource
}

class AndGate(val inputA: PowerSource, val inputB: PowerSource) extends LogicGate{
val output = new Relay(inputA, new Relay(inputB))
}


BaseLogicGate is now removed entirely. This is great because BaseLogicGate was really weird, it didn't even use its inputs. The only reason it was there was to decouple the delegation logic from the actual trait.

Now things have become MUCH more clear. AndGate is a LogicGate with inputs A and B, and one output, made from Relays.

8 comments:

  1. Any idea why this only work for constructor vals?

    ReplyDelete
  2. Actually, I can't get this to work. It seems extremely fragile, causing compiler error on any change that seems unrelated.

    ReplyDelete
  3. can you please give me an example?

    ReplyDelete
  4. E.g. using a private val instead of a constructor val breaks it on 2.7.4

    ReplyDelete
  5. I mean, can you give me a concrete example? Some actual code that's giving you a problem?

    ReplyDelete
  6. Nevermind, I figured out what went wrong.
    I had
    class DelegateToPowerSource( p: PowerSource ) extends PowerSource

    I didn't notice that your delegate actually doesn't extend PowerSource.

    This is a really weird idiom, I'm not really sure what goes on the in Scala compiler, but it seems to work.

    When DelegateToPowerSource is constructed, isInstanceOf[PowerSource] returns false, unless it's assigned a typed variable of PowerSource. Very odd indeed, but also very cool and useful.

    ReplyDelete
  7. A severe limitation to this approach is the you get rid of the wrapping object (the DelegateToPowerSource) when you call an method that expects a PowerSource. This is probably not what you want. Or is there a solution to this problem that evades me?

    ReplyDelete
  8. Can you show an example?

    I'll still try to give a coherent answer, but I haven't looked at this post in over 3 years. I think it's pretty simple though. DelegateToPowerSource wasn't really providing any additional functionality, it was just acting as a bridge. If DelegateToPowerSource was providing some extra functionality, then of course you wouldn't want to lose that. In that case, your functions would ask for DelegateToPowerSource explicitly (or whatever the name of the type is that adds functionality to your base interface), and callers of that function would have to supply that type explicitly (or via some other implicit conversion).

    Hopefully that's understandable.

    ReplyDelete