开发者

Scala compiler cannot infer mix-in type for pattern matching

I have a use case for algebraic groups over finite permutation sets. Because I would like to use the group for various permutation classes which are otherwise unrelated, I would like to do this as a mix-in trait. Here's an excerpt of my attempt

trait Permutation[P <: Permutation[P]] { this: P =>
  def +(that: P): P

  //final override def equals(that: Any) = ...
  //final override lazy val hashCode = ...

  // Lots of other stuff
}

object Permutation {
  trait Sum[P <: Permutation[P]] extends Permutation[P] { this: P =>
    val perm1, perm2: P

    // Lots of other stuff
  }

  private object Sum {
    def unapply[P <: Permutation[P]](s: Sum[P]): Some[(P, P)] = Some(s.perm1, s.perm2)
    //def unapply(s: Sum[_ <: Permutation[_]]): Some[(Permutation[_], Permutation[_])] = Some(s.perm1, s.perm2)
  }

  private def simplify[P <: Perm开发者_运维问答utation[P]](p: P): P = {
    p match {
      case Sum(a, Sum(b, c)) => simplify(simplify(a + b) + c)

      // Lots of other rules

      case _ => p
    }
  }
}

At some point in time, I would like to call the simplify method in order to, well, simplify an expression of group operations using the algebraic axioms. Using pattern matching seems to make sense as there are a lot of axioms to be evaluated and the syntax is concise. However, if I compile the code, I get:

error: inferred type arguments [P] do not conform to method unapply's type parameter bounds [P <: Permutation[P]]

I do not understand why the compiler cannot infer the type correctly and I don't know how to help it. Actually, the parameter type of P is irrelevant when pattern matching in this case. If p is any Sum of permutations, the pattern should match. The return type is still a P because the transformation is solely done by calling the + operator on P.

So in a second attempt I swap in the commented out version of unapply. However, then I get an assertion error from the compiler (2.8.2):

assertion failed: Sum((a @ _), (b @ _)) ==> Permutation.Sum.unapply(<unapply-selector>) <unapply> ((a @ _), (b @ _)), pt = Permutation[?>: Nothing <: Any]

Any clues how I can make the compiler accept this?

Thanks in advance!


After breeding over this for two days, I have finally found a solution which compiles without warning and passes my specification test. Following is a compilable excerpt of my code to show what is required. Note however, that the code is a no-op because I have left out the parts to actually perform the permutations:

/**
 * A generic mix-in for permutations.
 * <p>
 * The <code>+</code> operator (and the apply function) is defined as the
 * concatenation of this permutation and another permutation.
 * This operator is called the group operator because it forms an algebraic
 * group on the set of all moves.
 * Note that this group is not abelian, that is the group operator is not
 * commutative.
 * <p>
 * The <code>*</code> operator is the concatenation of a move with itself for
 * <code>n</code> times, where <code>n</code> is an integer.
 * This operator is called the scalar operator because the following subset(!)
 * of the axioms for an algebraic module apply to it:
 * <ul>
 * <li>the operation is associative,
 *     that is (a*x)*y = a*(x*y)
 *     for any move a and any integers x and y.
 * <li>the operation is a group homomorphism from integers to moves,
 *     that is a*(x+y) = a*x + a*y
 *     for any move a and any integers x and y.
 * <li>the operation has one as its neutral element,
 *     that is a*1 = m for any move a.
 * </ul>
 * 
 * @param <P> The target type which represents the permutation resulting from
 *        mixing in this trait.
 * @see Move3Spec for details of the specification.
 */
trait Permutation[P <: Permutation[P]] { this: P =>
  def identity: P

  def *(that: Int): P
  def +(that: P): P
  def unary_- : P

  final def -(that: P) = this + -that
  final def unary_+ = this

  def simplify = this

  /** Succeeds iff `that` is another permutation with an equivalent sequence. */
  /*final*/ override def equals(that: Any): Boolean // = code omitted
  /** Is consistent with equals. */
  /*final*/ override def hashCode: Int // = code omitted

  // Lots of other stuff: The term string, the permutation sequence, the order etc.
}

object Permutation {
  trait Identity[P <: Permutation[P]] extends Permutation[P] { this: P =>
    final override def identity = this

    // Lots of other stuff.
  }

  trait Product[P <: Permutation[P]] extends Permutation[P] { this: P =>
    val perm: P
    val scalar: Int

    final override lazy val simplify = simplifyTop(perm.simplify * scalar)

    // Lots of other stuff.
  }

  trait Sum[P <: Permutation[P]] extends Permutation[P] { this: P =>
    val perm1, perm2: P

    final override lazy val simplify = simplifyTop(perm1.simplify + perm2.simplify)

    // Lots of other stuff.
  }

  trait Inverse[P <: Permutation[P]] extends Permutation[P] { this: P =>
    val perm: P

    final override lazy val simplify = simplifyTop(-perm.simplify)

    // Lots of other stuff.
  }

  private def simplifyTop[P <: Permutation[P]](p: P): P = {
    // This is the prelude required to make the extraction work.
    type Pr = Product[_ <: P]
    type Su = Sum[_ <: P]
    type In = Inverse[_ <: P]
    object Pr { def unapply(p: Pr) = Some(p.perm, p.scalar) }
    object Su { def unapply(s: Su) = Some(s.perm1, s.perm2) }
    object In { def unapply(i: In) = Some(i.perm) }
    import Permutation.{simplifyTop => s}

    // Finally, here comes the pattern matching and the transformation of the
    // composed permutation term.
    // See how expressive and concise the code is - this is where Scala really
    // shines!
    p match {
      case Pr(Pr(a, x), y) => s(a*(x*y))
      case Su(Pr(a, x), Pr(b, y)) if a == b => s(a*(x + y))
      case Su(a, Su(b, c)) => s(s(a + b) + c)
      case In(Pr(a, x)) => s(s(-a)*x)
      case In(a) if a == a.identity => a.identity
      // Lots of other rules

      case _ => p
    }
  } ensuring (_ == p)

  // Lots of other stuff
}

/** Here's a simple application of the mix-in. */
class Foo extends Permutation[Foo] {
  import Foo._

  def identity: Foo = Identity
  def *(that: Int): Foo = new Product(this, that)
  def +(that: Foo): Foo = new Sum(this, that)
  lazy val unary_- : Foo = new Inverse(this)

  // Lots of other stuff
}

object Foo {
  private object Identity
  extends Foo with Permutation.Identity[Foo]

  private class Product(val perm: Foo, val scalar: Int)
  extends Foo with Permutation.Product[Foo]

  private class Sum(val perm1: Foo, val perm2: Foo)
  extends Foo with Permutation.Sum[Foo]

  private class Inverse(val perm: Foo)
  extends Foo with Permutation.Inverse[Foo]

  // Lots of other stuff
}

As you can see, the solution is to define types and extractor objects which are local to the simplifyTop method.

I have also included a little example of how to apply such a mix-in to the class Foo. As you can see, Foo is little more than a factory for composed permutations of its own type. That's a big benefit if you have many classes like this which are otherwise unrelated.

<rant>

However, I cannot resist to say that Scala's type system is insanely complex! I'm a seasoned Java library developer and feel very proficient with Java Generics. Yet it took me two days to figure out the six lines of code with the three type and object definitions! If this were not for educational purposes, I would have ditched the approach.

Right now, I am tempted to oracle that Scala will NOT be the next big thing in terms of programming languages because of this complexity. If you are a Java developer who feels a little uncomfortable about Java generics now (not me), then you would hate Scala's type system because it adds invariants, covariants and contravariants to the concept of Java generics, to say the least.

All-in-all, Scala's type system seems to address more scientists than developers. From a scientific point of view, it's nice to reason about the type safety of a program. From a developers point of view, the time to figure out these details is wasted because it keeps them away from the functional aspects of the program.

Nevermind, I will continue with Scala for sure. The combination of pattern matching, mix-ins and high-order functions is just too powerful to miss. However, I feel Scala would be a much more productive language without it's overly complex type system.

</rant>

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜