Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala Type-Inference For Type Constructor

I have a question regarding type-inferencing on Scala's type-constructors. I'm running Scala 2.9.1...

Suppose I defined Tree:

 sealed trait Tree[C[_], A]
 case class Leaf[C[_], A](a: A) extends Tree[C, A]
 case class Node[C[_], A](a: A, c: C[Tree[C, A]]) extends Tree[C, A]

And defined a BinaryTree based upon my Tree definition:

 type Pair[A] = (A, A)
 type BinaryTree[A] = Tree[Pair, A]

I can now define a BinaryTree of integers as such:

 val tree: BinaryTree[Int] = Node[Pair, Int](1, (Leaf(2), Leaf(3)))

The problem with this is that I have to supply the type parameters whenever I instantiate Node.

So if do this:

 val tree: BinaryTree[Int] = Node(1, (Leaf(2), Leaf(3)))

I get the error:

error: no type parameters for method apply: (a: A, c: C[Tree[C,A]])Node[C,A] in 
object Node exist so that it can be applied to arguments (Int, (Leaf[Pair,Int], Leaf[Pair,Int]))
 --- because ---
 argument expression's type is not compatible with formal parameter type;
 found   : (Leaf[Pair,Int], Leaf[Pair,Int])
 required: ?C[Tree[?C,?A]]
   val tree: BinaryTree[Int] = Node(1, (Leaf(2), Leaf(3)))
                               ^

Is there any way I can coerce the type-checker so that I don't have to explicitly supply the types of Node?

Thanks!



Revised After didierd's Comments

If I'm understanding correctly, the statement

 type Pair[A] = (A, A)

in my original question doesn't work since this Pair declaration is just syntactic sugar for a Tuple2 type-constructor (which requires two type-parameters). This causes the type-inferencer to fail.

If I declare my own Pair class (as didierd suggests in his answer), I'm successful in getting the Tree to work correctly.

// Assume same Tree/Leaf/Node definition given above
case class MyPair[A](_1: A, _2: A)
type BinaryTree[A] = Tree[MyPair, A]

Then I can do this...

scala> val t: BinaryTree[Int] = Leaf(3)
t: BinaryTree[Int] = Leaf(3)

scala> val t2: BinaryTree[Int] = Node(1, MyPair(Leaf(2), Leaf(3)))
t2: BinaryTree[Int] = Node(1,MyPair(Leaf(2),Leaf(3)))

I know didierd mentioned this solution in passing, but this seems to behave the way I want. Please let me know what you think!

like image 218
shj Avatar asked Dec 03 '11 18:12

shj


People also ask

Does Scala support type inference?

The Scala compiler can infer the types of expressions automatically from contextual information. Therefore, we need not declare the types explicitly. This feature is commonly referred to as type inference. It helps reduce the verbosity of our code, making it more concise and readable.

What would be the type inferred by Scala compiler for variable?

Scala compiler can automatically infer types of each variable declared. If the value of a variable is declared in double-quotes it will automatically be inferred as String. Also, the compiler can infer any value in a single quote is inferred as Char. The compiler, by default it infer any number without decimal as Int.

What is .type in Scala?

Type declaration is a Scala feature that enables us to declare our own types. In this short tutorial, we'll learn how to do type declaration in Scala using the type keyword. First, we'll learn to use it as a type alias. Then, we'll learn to declare an abstract type member and implement it.

How does Scala determine types when they are not specified?

For example, a type constructor does not directly specify a type of values. However, when a type constructor is applied to the correct type arguments, it yields a first-order type, which may be a value type. Non-value types are expressed indirectly in Scala.


2 Answers

There is a problem to start with with inferring C[X] = (X,X). Suppose you pass (String, String) somewhere the compiler expects C[String] and must infer C, C[X] could be (X, X), (X, String), (String, X) or even (String, String) with X phantom.

Having declared an alias Pair does not help. I believe you will have to declare case class Pair[A](_1: A, _2: A) - granted, inferring C[X] = Pair[String] and X phantom would still be possible, but fortunately, the compiler does not do that.

Still, when you write Tree(1, Pair(Leaf(2), Leaf(3)), it will not infer the C in Leaves. I am not very sure why. But anyway, there is no way it could infer it when you simply write val l = Leaf(2).

I think you can get to something by making everything covariant

sealed trait Tree[+C[+X], +A]
case class Leaf[+A](a: A) extends Tree[Nothing, A]
case class Node[+C[+X], +A](a: A, c: C[Tree[C,A]]) extends Tree[C,A]

with covariance, you remove C from Leaf, so it need not be inferred

case class Pair[+A](left: A, right: A)

val tree = Node(1, Pair(Leaf(2), Node(3, Pair(Leaf(3), Leaf(4)))))
tree: Node[Pair,Int] = Node(1,Pair(Leaf(2),Node(3,Pair(Leaf(3),Leaf(4)))))

A side remark, would it not make more sense to have

case object Empty extends Tree[Nothing, Nothing]

rather than Leaf. With Leaf, there are shapes of binary tree that you cannot get.


Update regarding your comments:

First do not bother too much with the phantom type, I should not have mentioned it. If you define a type T[X] and X does not appear otherwise in the definition of T, it is called a phantom type. Clever code may be written with that to ensure that some properties are proved at compile time, but this is not the point here.

As a matter of fact, when the scala compiler is given some types T and X, and must infer C, such that C[X] is (a supertype of) T - in my example, that was T = (String, String) and X = String - it will work only if T (or a supertype) is an parametrization of a type with exactly one type parameter. More generally, as many type parameters as the type parameter C has. As C has one and Tuple2 has two (your having defined an alias Pair does not count), it cannot work.

What I tried to point was that, without such a rule, the compiler would have many choices for C. If I know (String, Int) is a C[String], and that I must guess what C is, then I would think C[X] is (X, Int). When you write if passed (String, Int), wouldn't if infer (Any, Any), it does not make sense, given that it is trying to infer a type constructor. The answer must be C[X] = something with X (except for the possibility that X is phantom). Completely different would be having Pair[X] = (String, Int) and having to infer X. Then indeed, it would infer X = Any. So given C[String] = (String, String), C[X] = (X, String) is as much a solution as C[X] = (X,X) is.

On your second comment regarding List, it is the same problem that also exists with Pair once you have defined case class Pair (third paragraph in the answer above), namely that it will not infer what C is when you write Leaf(2), where C does not appear. This is where covariance kicks in, dispensing with the parameter C in Leaf, and so the need to infer it.

like image 155
Didier Dupont Avatar answered Oct 17 '22 12:10

Didier Dupont


The only variation that occurred to me was to annotate the Pair argument instead. Other people I'm sure will have other ideas.

object BinaryTreeTest {
  sealed trait Tree[C[_], A]
  case class Leaf[C[_], A](a: A) extends Tree[C, A]
  case class Node[C[_], A](a: A, c: C[Tree[C, A]]) extends Tree[C, A]

  type Pair[A] = (A, A)
  type BinaryTree[A] = Tree[Pair, A]

  val p: Pair[Tree[Pair, Int]] = (Leaf(2), Leaf(3))    
  val t: BinaryTree[Int] = Node(1, p)    
}
like image 44
Dave L. Avatar answered Oct 17 '22 13:10

Dave L.