Modeling a binary relationship between two types

2020-07-18 04:30发布

问题:

There are businesses and people. Users can either like or post a comment about a business but the same can not happen with a person. When a user posts something about a business or likes it, that business is called the target of that like or post:

trait TargetingRelation[TargetingType[_],TargetedType]

class Business

class Person

class Post[Target | TargetingRelation[Business,Post] ] {
  def target:Target
}

class Like[Target | TargetingRelation[Business,Like] ] {
  def target:Target
}

Here I'm inventing a T | P[T] notation meaning type parameter T such that it satisfies some property P[T] (or T :|: P[T] if it carries more type-appeal). Somewhere else in the code I want to have declarations like:

object canPostAboutBusiness extends TargetingRelation[Post,Business] 
object canLikeBusiness      extends TargetingRelation[Like,Business]

these objects are evidences actually, something like Haskell type-classes. So this would type check:

val p = new Post[Business]
val l = new Like[Business]

but not this one:

val p = new Post[Person]
val l = new Like[Person]

As far as my knowledge of Scala permits, I can't model this particular state of affairs in a satisfactory way. Now I insist that this is not sub-typing because Business is not a:

class Business extends 
  TargetingRelation[Post,Business] with 
  TargetingRelation[Like,Business]

As a matter of fact it is very desirable that Business remains totally ignorant of the Post. The relationship is actually outside of both Post and Business. Besides, I suppose the above code would not even compile to begin with since Business is inheriting from TargetingRelation twice. Insights are most welcome.

related : Using a context bound in a class type parameter

回答1:

You can do this in scala with something that acts like a typeclass using implicits. For example:

import scala.language.higherKinds

trait TargetingRelation[A[_], B]

class Business
class Person

// Using explicitly declared implicit parameter:
class Post[T](implicit ev: TargetingRelation[Post, T])

// Using a "context bound". The syntax is a little hairy and uses
// a type lambda because TargetingRelation takes multiple type params
class Like[T : ({type S[x] = TargetingRelation[Like, x]})#S]

implicit object canPostAboutBusiness extends TargetingRelation[Post,Business]
implicit object canLikeBusiness      extends TargetingRelation[Like,Business]

Then you can instantiate the classes with Business

scala> val p = new Post[Business]
p: Post[Business] = Post@374c991a

scala> val l = new Like[Business]
l: Like[Business] = Like@1fd348f8

But not with Person

scala> val p1 = new Post[Person]
<console>:15: error: could not find implicit value for parameter ev: TargetingRelation[Post,Person]
       val p1 = new Post[Person]
                ^

scala> val p2 = new Like[Person]
<console>:15: error: could not find implicit value for evidence parameter of type TargetingRelation[Post,Person]
       val p2 = new Like[Person]
                ^

If you search from "scala typeclasses", you'll find plenty of explanations of the details of how this works, but basically, you require the constructor to take an implicit parameter of the type TargetingRelation[TargetingType[_],TargetedType] and then place an implicit of that type in scope when constructing your class (Post or Like). The implicit serves as "evidence" that the TargetedType has an instance of the typeclass (and plays the role of the explicit dictionary of methods that get automatically passed around in other languages typeclass implementations).

In fact, scala has some synatic sugar to help with this, called the Context Bound. This causes methods written as:

def a[A: B] = ???

to be translated to

def a[A](implicit ev: B[A]) = ???

In your particular example, the contexts bounds syntax is a little tricky because there are multiple type parameters, but it can be done as this SO question describes.