简体   繁体   中英

Generalizing DivisibleBy in type-level programming with Scala

I'm trying to generalize a divisible-by relation in Scala using type level programming. This is my Nat number definitions:

trait Nat {
  type plus[N<:Nat] <: Nat
}
trait Zero extends Nat {
  type plus[N<:Nat] = N
}
trait Succ[N <: Nat] extends Nat {
  type plus[M<:Nat] = Succ[M#plus[N]]
}

For say number 3 this works:

trait DivBy3[N<:Nat]

object DivByX {
  implicit val DivBy3_0: DivBy3[_0] = new DivBy3[_0]{}
  implicit def DivBy3_n[N <: Nat](implicit witness: DivBy3[N]): DivBy3[Succ[Succ[Succ[N]]]] =
    new DivBy3[Succ[Succ[Succ[N]]]]{}

  implicitly[DivBy3[_0]] // <- compiles, good!
  //implicitly[DivBy3[_1]] <- doesn't compile, good!
  //implicitly[DivBy3[_2]] <- doesn't compile, good!
  implicitly[DivBy3[_3]] // <- compiles, good!
  //implicitly[DivBy3[_4]] <- doesn't compile, good!
  //implicitly[DivBy3[_5]] <- doesn't compile, good!
  implicitly[DivBy3[_6]] // <- compiles, good!
}

When I tried to generalize it with a trait with 2 type parameters it didn't work (something like trait DivByX[X <: Nat, N <: Nat] ). So I tried the following, which didn't work either:

trait Divisors[X <: Nat] {

  trait DivByX[N <: Nat]

  implicit val DivByX_0: DivByX[_0] = new DivByX[_0]{}

  implicit def DivByX_NplusX[N <: Nat](implicit witness: DivByX[N]): DivByX[N#plus[X]] =
    new DivByX[N#plus[X]]{}

}

object Divisors extends Divisors[_3] {

  type DivBy3[N <: Nat] = DivByX[N]

  implicitly[DivBy3[_0]] // <- it compiles, meh!
  //implicitly[DivBy3[_1]] <- doesn't compile, good!
  //implicitly[DivBy3[_2]] <- doesn't compile, good!
  //implicitly[DivBy3[_3]] <- doesn't compile, baaad!
}

What's a correct way to generalize this?

Edit: Followed @TravisBrown 's suggestion but it doesn't compile for 3 | 6 3 | 6 :

trait Nat
trait Zero extends Nat
trait Succ[N <: Nat] extends Nat

trait Adder[N <: Nat, M <: Nat] {
  type Out <: Nat
}

object Adder {
  type Aux[N<:Nat,M<:Nat, Out0 <: Nat] = Adder[N,M] { type Out = Out0 }

  implicit def add0[N<:Nat]: Aux[N,_0,N] = new Adder[N,_0]{
    type Out = N
  }

  implicit def addN[N<:Nat,M<:Nat](implicit adder: Adder[Succ[N],M]): Aux[N,Succ[M], adder.Out] =
    new Adder[N,Succ[M]] {
      type Out = adder.Out
    }

  def apply[N<:Nat, M<: Nat](implicit adder: Adder[N,M]): Aux[N,M, adder.Out] = adder

}

trait DivByX[X <: Nat, N <: Nat]

object DivByX {
  implicit def DivByX_0[X <: Nat]: DivByX[X, _0] = new DivByX[X, _0]{}
  implicit def DivByX_NplusX[X <: Nat, N <: Nat, S <: Nat](
    implicit div: DivByX[X,N], sum: Adder.Aux[N,X,S]): DivByX[X,S] =
    new DivByX[X,S]{}

  import Adder._
  type DivBy3[N <: Nat] = DivByX[_3, N]
  implicitly[DivBy3[_0]]
  implicitly[DivBy3[_3]]
  implicitly[Adder.Aux[_3,_3,_6]]
  val x: DivBy3[_6] = DivByX_NplusX(implicitly[DivBy3[_3]], implicitly[Adder.Aux[_3,_3,_6]])
  //implicitly[DivBy3[_6]] // <- :( doesn't compile, whyyy?
}

This is going to be a little hand-wavy, because to be honest I don't really understand this stuff. I'm not sure it's possible for anyone who isn't closely familiar with scalac's implementation of implicit resolution to understand this stuff, but it is at least possible to build up some intuitions about what the compiler likes and doesn't like, and that's what I'll try to explain here.

Moving unknown stuff to the right

Take your latest implementation:

trait DivByX[X <: Nat, N <: Nat]

object DivByX {
  implicit def DivByX_0[X <: Nat]: DivByX[X, _0] = new DivByX[X, _0]{}
  implicit def DivByX_NplusX[X <: Nat, N <: Nat, S <: Nat](
    implicit div: DivByX[X,N], sum: Adder.Aux[N,X,S]): DivByX[X,S] =
    new DivByX[X,S]{}
}

When you ask for a DivByX[_3, _6] instance, the compiler is going to try to find an implicit that provides it. DivByX_0 is immediately out, since _0 isn't _6 . Next it's going to try DivByX_Nplus , which means it needs to solve for X , N , and S , in that order. The X and S parts are easy—it's N that's the problem—so since we know this version doesn't work, I'd start by rearranging the type parameters to put the unknown one last:

trait DivByX[X <: Nat, N <: Nat]

object DivByX {
  implicit def DivByX_0[X <: Nat]: DivByX[X, _0] = new DivByX[X, _0]{}
  implicit def DivByX_NplusX[X <: Nat, S <: Nat, N <: Nat](
    implicit div: DivByX[X,N], sum: Adder.Aux[N,X,S]): DivByX[X,S] =
    new DivByX[X,S]{}
}

This still doesn't work, unfortunately. If we turn on -Xlog-implicits , we'll see the following (along with some other stuff):

scala> implicitly[DivByX[_3, _6]]
...
<console>:16: this.DivByX.DivByX_NplusX is not a valid implicit value for DivByX[_3,_6] because:
hasMatchingSymbol reported error: could not find implicit value for parameter sum: Adder.Aux[N,_3,_6]
       implicitly[DivByX[_3, _6]]
                 ^
...

So it looks like the problem is that it's not able to solve for N in N + 3 = 6 . Is there a spec'd reason for this? I have no idea. We could sit down with the specification and the scalac source and spend the afternoon trying to figure out whether there's a reason the compiler can't do this, or we could just apply our general rule of moving stuff we don't know to the end.

Minus type class

We can't use our Adder type class to rewrite the N + 3 = 6 equation in such a way that the N comes last, but we can write a similar type class for a new operation that would make that possible:

trait Minus[M <: Nat, S <: Nat] { type Out <: Nat }

object Minus {
  type Aux[M <: Nat, S <: Nat, Out0 <: Nat] = Minus[M, S] { type Out = Out0 }

  def apply[M <: Nat, S <: Nat](implicit m: Minus[M, S]): Aux[M, S, m.Out] = m

  implicit def minus0[M <: Nat]: Aux[M, _0, M] = new Minus[M, _0] {
    type Out = M
  }

  implicit def minusN[M <: Nat, S <: Nat](implicit
    m: Minus[M, S]
  ): Aux[Succ[M], Succ[S], m.Out] = new Minus[Succ[M], Succ[S]] {
    type Out = m.Out
  }
}

We can confirm that it looks like it does what we think it does:

scala> implicitly[Minus.Aux[_6, _1, _5]]
res6: Minus.Aux[_6,_1,_5] = Minus$$anon$2@2d4203b4

scala> implicitly[Minus.Aux[_6, _3, _3]]
res7: Minus.Aux[_6,_3,_3] = Minus$$anon$2@2a071dd

scala> implicitly[Minus.Aux[_6, _3, _2]]
<console>:16: this.Minus.minusN is not a valid implicit value for Minus.Aux[_6,_3,_2] because:
hasMatchingSymbol reported error: type mismatch;
...

Now we can try writing DivByX again, replacing Adder with Minus :

trait DivByX[X <: Nat, N <: Nat]

object DivByX {
  implicit def divByX0[X <: Nat]: DivByX[X, _0] = new DivByX[X, _0] {}
  implicit def DivByXS[X <: Nat, S <: Nat, N <: Nat](implicit
    div: DivByX[X, N],
    minus: Minus.Aux[S, X, N]
  ): DivByX[X, S] = new DivByX[X, S] {}
}

…but it still doesn't work:

scala> implicitly[DivByX[_6, _3]]
<console>:16: error: could not find implicit value for parameter e: DivByX[_6,_3]
       implicitly[DivByX[_6, _3]]
                 ^

Moving unknown stuff to the right again

So we just apply our rule of moving "more unknown" stuff down and to the right again, this time looking at the implicit arguments for divByXS . In the type of div we see both X (which we know) and N (which we don't), while for minus we see two things we know ( S and X ) and one we don't ( N ). So we try switching their order so that the one we know more about comes first:

trait DivByX[X <: Nat, N <: Nat]

object DivByX {
  implicit def divByX0[X <: Nat]: DivByX[X, _0] = new DivByX[X, _0] {}
  implicit def DivByXS[X <: Nat, S <: Nat, N <: Nat](implicit
    minus: Minus.Aux[S, X, N],
    div: DivByX[X, N]
  ): DivByX[X, S] = new DivByX[X, S] {}
}

And then we try again:

scala> implicitly[DivByX[_3, _6]]
res0: DivByX[_3,_6] = DivByX$$anon$4@8d6e389

This seems too good to be true, so let's try some other stuff:

scala> implicitly[DivByX[_3, Succ[Succ[Succ[_6]]]]]
res1: DivByX[_3,Succ[Succ[Succ[_6]]]] = DivByX$$anon$4@4c025f43

scala> implicitly[DivByX[_2, _6]]
res2: DivByX[_2,_6] = DivByX$$anon$4@4d4069f9

scala> implicitly[DivByX[_2, _4]]
res3: DivByX[_2,_4] = DivByX$$anon$4@40dc52e

scala> implicitly[DivByX[_1, _5]]
res4: DivByX[_1,_5] = DivByX$$anon$4@5bd63e47

scala> implicitly[DivByX[_3, _5]]
<console>:16: error: could not find implicit value for parameter e: DivByX[_3,_5]
       implicitly[DivByX[_3, _5]]
                 ^

And yeah, it looks like it works, and even if we don't know why it works, at least we got here in a vaguely principled way.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM