Sunday, March 15, 2009

Disjoint Bounded Views - Redux

I cleaned up my previous post on "stuttering-or". The import would now look like this:


package org.okcjug.imports

class DisjointBoundedView[A,B](val a: Option[A], val b: Option[B])

object DisjointBoundedView {
// namespace pollution? Maybe use "||" or "OrType"
type or[A,B] = DisjointBoundedView[A,B]

// convenience of definition functions
private def da[A,B](a: A): or[A,B] = { new DisjointBoundedView(Some(a),None) }
private def db[A,B](b: B): or[A,B] = { new DisjointBoundedView(None,Some(b)) }
private def na[A,B](n: Nothing): or[A,B] = { new DisjointBoundedView(None, None) }

// implicit defs - stuttering-or
implicit def noneToOr2[A,B](n: Nothing): or[A,B] =
{ na(n) }
implicit def aToOr2[A,B](a: A): or[A,B] =
{ da(a) }
implicit def bToOr2[A,B](b: B): or[A,B] =
{ db(b) }
implicit def aToOr3[A,B,C](a: A): or[or[A,B],C] =
{ da(da(a)) }
implicit def bToOr3[A,B,C](b: B): or[or[A,B],C] =
{ da(db(b)) }
implicit def aToOr4[A,B,C,D](a: A): or[or[or[A,B],C],D] =
{ da(da(da(a))) }
implicit def bToOr4[A,B,C,D](b: B): or[or[or[A,B],C],D] =
{ da(da(db(b))) }
implicit def aToOr5[A,B,C,D,E](a: A): or[or[or[or[A,B],C],D],E] =
{ da(da(da(da(a)))) }
implicit def bToOr5[A,B,C,D,E](b: B): or[or[or[or[A,B],C],D],E] =
{ da(da(da(db(b)))) }
// more? ...

}



Calling code would look like this:

package org.okcjug.test

import org.okcjug.imports.DisjointBoundedView._

class Foo {

def bar[T <% Int or String or Double](t: Option[T]) = {
t match {
case Some(x: Int) => println("processing Int: " + x)
case Some(x: String) => println("processing String: " + x)
case Some(x: Double) => println("processing Double: " + x)
case None => println("empty and I don't care the type")
}
}

def baz[T <% String or Int](t: List[T]) = {
for (x <- t) x match {
case x: String => println("String list item: " + x)
case x: Int => println("Int list item: " + x)
}
}
}

object Foo extends Application {

val f = new Foo

f.bar(None)
f.bar(Some(1))
f.bar(Some("blah"))
f.bar(Some(3.45))
// f.bar(Some(Some(2))) // compiler error
// f.bar(Some(Set("x", "y"))) // compiler error

f.baz(List(1,1,2,3,5,8,13))
f.baz(List("boogie", "woogie"))
// f.baz(List(3.4, 3.14)) // compiler error
// f.baz(List(1,"one")) // compiler error
// f.baz(Some(1)) // compiler error
}



The only difference is that we now don't require clarifying the type for a None, and the class name more accurately reflects what it does.

UPDATE: Per Ittay's suggestion below, I've updated the implementation to use Either as the backing class (shown below). This simplifies the code and allows extraction of values from the composite type via pattern matching. Also, it can no longer accurately be described as "stuttering-or".


package org.okcjug.imports

object DisjointBoundedView {
// namespace pollution? Maybe use "||" or "OrType"
type or[A,B] = Either[A,B]

// implicit defs
implicit def l[T](t: T) = Left(t)
implicit def r[T](t: T) = Right(t)
implicit def ll[T](t: T) = Left(Left(t))
implicit def lr[T](t: T) = Left(Right(t))
implicit def lll[T](t: T) = Left(Left(Left(t)))
implicit def llr[T](t: T) = Left(Left(Right(t)))
implicit def llll[T](t: T) = Left(Left(Left(Left(t))))
implicit def lllr[T](t: T) = Left(Left(Left(Right(t))))
// more? ...

}

4 comments:

Ittay Dror said...

Isn't it better to use Either than to define a new class?

Mitch Blevins said...

The motivation was to provide a way to define a method that is overloaded in a way that would normally be disallowed due to erasure. For example, how to provide a method that accepts a Set[Int] or a Set[String] without giving up type-safety by using Set[Object]? Requiring an Either[Set[Int],Set[String]] pushes the conceptual load back onto the users of your method, not to mention the method signature change if you allow an additional type (add Set[Double]?).

Ittay Dror said...

What I meant is that instead of defining the case class DisjointBoundedView, you'd use 'Either'. Then, the object DisjointBoundedView would still provide all the implicit conversions (da is Left, db is Right). type 'or' will be an alias to Either.

The example will work the same

The benefit is that if I want to store the parameter, I can do this using a standard object (Either) instead of DisjointBoundedView.

var someSet = Set[Int or String or Double]()
def bar[T <% Int or String or Double](t: T) {
someSet += t
}

Mitch Blevins said...

I see what you are suggesting now. It really does simplify the code! All the example usages I had considered did not actually call any of the implicit conversion methods, just tricked the type-checker into ensuring that only certain types got passed to the method. Your example actually exercises the conversion code. See post for updated implementation.