Skip to content

Instantly share code, notes, and snippets.

@rentalcustard
Created October 31, 2012 10:15
Show Gist options
  • Save rentalcustard/3986263 to your computer and use it in GitHub Desktop.
Save rentalcustard/3986263 to your computer and use it in GitHub Desktop.
My experience of static typing.
class MasterOfCeremonies { //I hate naming in potted examples
public void handle(Dog dog) {
dog.speak();
}
}
class Dog {
public void speak() {
//something
}
}
//And now I want handle to accept anything that can speak...
class MasterOfCeremonies {
public void handle(Speaker speaker) {
speaker.speak();
}
}
interface Speaker {
void speak();
}
class Dog implements Speaker {
public void speak() {
//something
}
}
class StandupComedian implements Speaker {
public void speak() {
//something else
}
}
class MasterOfCeremonies
def handle(speaker)
speaker.speak #no need to define an interface, anything that responds to speak accepted by this method.
end
end
@timcowlishaw
Copy link

aah ok, yep! I see - much simpler than I thought. I guess the only downside to the ruby version is that it fails at runtime if !speaker.respond_to?(speak), wheras the java version will fail at compile tume. You could write tests for that this doesn't happen under a certain set of circumstances, which may well be sufficient, but if (in an even more hilariously contrived example) thie MasterOfCeremonies class was part of something like an Air-traffic-control system, you might want a compile-time guarantee that that can't happen, so the Java version would be preferable. It's basically a trade-off between safety and expressiveness (or "Safety and usefulness" according to Simon-Peyton Jones) and is a matter of taste.

However, would I be correct in assuming that the problem with the Java class is that, when the Speaker interface is introduced, you need to open the Dog and StandupComedian classes in order to make them implement this interface? (and, is this an open-closed principle violation?). If so, there's a useful idiom in Scala that uses implicit method definitions and view bounds to implement type-class polymorphism, which allows you to achieve polymorphism in a more ad-hoc style, without requiring you to reopen existing classes. For your example, this would look something like:

class MasterOfCeremonies {
  def handle[A](speaker : A <% Speaker[A]) : Unit = {
    speaker.speak()
  }

  trait Speaker[A] {
    def speak : Unit;
 }
  implicit def dogCanSpeak(dog : Dog) : Speaker[Dog] = {
    return new Speaker[Dog] {
       override def speak : Unit = {
          //implement the dog's speak method here
       }
    }
  }

  implicit def standupCanSpeak(standup : StandupComedian) : Speaker[StandupComedian] = {
    return new Speaker[StandupComedian] {
      override def speak : Unit = {
        //implement the standup's speaking related functionality here
     }
    }
  }
}

class Dog {
 //dog-specific implementation
}

class StandupComedian {
 //stand-up comedian specific implementation
}

So, in a nutshell, we have a MasterOfCeremonies, Dog, and StandupComedian class, as before, and a Speaker trait, which is analagous to a Java interface (but actually more like a Ruby mixin because it can contain implementation, but that's not relevant here). The Speaker trait is parameterised by a type A (this is analagous to a Generic in Java)

The fundamental difference is that the MasterOfCeremonies class can accept a Dog or a StandupComedian as an argument to a handle method, and will fail at compile time if passed anything else, but we didn't need to reopen the Dog or StandupComedian classes in order to enforce this.

The sorcery responsible for this is in a couple of places. First look at the type signature of handle: def handle[A](a : A <% Speaker[A]) : Unit. Unit here is the return type, and is analagous to void in Java, we can ignore that. What's interesting is the fact that the method is polymorphic in a type A (handle[A] as opposed to just handle), and the signature of the argument: A <% Speaker [A]. This is what's called a view bound, and in plain english, means that handle can take "Any type A that can be viewed as the type Speaker[A].

This is all well and good, but what does "can be viewed as" mean? Simply, in Scala, methods can be declared with the implicit keyword. When there's an implicit from type A => B in scope (I won't go over the specific scoping rules now, but they're interesting too and pretty flexible), if the compiler comes across a value of type A where it expects one of type B, it silently inserts a call to the implicit method to convert the value, rather than failing with a type error. What "A viewed as B" (or A <% B in a type signature) means, then, is, "some type A for which an implicit conversion to type B is in scope)". This gives us the last piece of our jigsaw, the individual implementations of Speaker for both Dog and StandupComedian, which take the form of implicit conversions from Dog to Speaker[Dog] and StandupComedian to Speaker[StandupComedian], and can live wherever you like as long as you bring them into scope in the MasterOfCeremonies class.

This is pretty verbose compared to the Ruby version, which is a turn-off for some, but the important thing is that you can add new roles and responsiblities to existing classes without reopening them, and also without sacrificing compile-time guarantees of safety. For me then, it's a pretty nice way of reconciling expressiveness with safety. (YMMV as I just generally love Scala, but still!)

@timcowlishaw
Copy link

actually, the scala version is also "pretty verbose" compared to the java version too, so I may have failed in terms of making it appear more expressive (although I'm not conviced expressiveness is correlated with verbosity, but that's another story). However, my scala style could probably be improved, so please don't let this put you off!

@timcowlishaw
Copy link

....also I got the view bound syntax wrong. Try this, which actually compiles:

class MasterOfCeremonies {
  def handle[A <% Speaker[A]](speaker : A) : Unit = {
    speaker.speak
  }

  trait Speaker[A] {
    def speak : Unit;
  }

  implicit def dogCanSpeak(dog : Dog) : Speaker[Dog] = {
    return new Speaker[Dog] {
       override def speak : Unit = {
          //implement the dog's speak method here
       }
    }
  }

  implicit def standupCanSpeak(standup : StandupComedian) : Speaker[StandupComedian] = {
    return new Speaker[StandupComedian] {
      override def speak : Unit = {
        //implement the standup's speaking related functionality here
     }
    }
  }
}

class Dog {
 //dog-specific implementation
}

class StandupComedian {
 //stand-up comedian specific implementation
}

@timcowlishaw
Copy link

Totally don't need the type parameter on Speaker either:

class MasterOfCeremonies {
  def handle[A <% Speaker](speaker : A) : Unit = {
    speaker.speak
  }

  trait Speaker {
    def speak : Unit;
  }

  implicit def dogCanSpeak(dog : Dog) : Speaker = {
    return new Speaker {
       override def speak : Unit = {
          //implement the dog's speak method here
       }
    }
  }

  implicit def standupCanSpeak(standup : StandupComedian) : Speaker = {
    return new Speaker {
      override def speak : Unit = {
        //implement the standup's speaking related functionality here
     }
    }
  }
}

class Dog {
 //dog-specific implementation
}

class StandupComedian {
 //stand-up comedian specific implementation
}

@timcowlishaw
Copy link

Of course, you can also replicate the Ruby-ish duck-typing behaviour, with static checking, using a structural type:

class MasterOfCeremonies {
  def handle(speaker : {def speak : Unit}) : Unit = {
    speaker.speak
  }
}

class Dog {
  def speak : Unit = {
    println("Woof")
  }
}

class StandupComedian {
  def speak : Unit = {
    println("'That Bond villain came in my pub last night. Got drunk and behaved appalingly.' 'Javier Bardem?' 'No, he can come back when he's sober.'")
  }
}

val mc = new MasterOfCeremonies
val dog = new Dog
val standup = new StandupComedian

mc.handle(dog)
mc.handle(standup)

...that way, compilation will fail if handle is not passed an instance of a class that implements speak, but gets around the need to have an actual named interface. However, this still requires you to re-open the classes that you want to implement 'Speak' though, so we still have the same problem as above (I actually just found out there's a name for this).

You could probably have the best of both worlds with an implicit conversion to a structural type, but this doesn't seem to work, sadly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment