Praveen L
Praveen L

Reputation: 987

passing multiple datatypes to Type variable

I am trying to pass either Grass or Rice object. But, it fails in compiling. I tried below Either[] option based on this link using Either. But, its not working.

I want to restrict passing Fish Object like this. I want to pass only Rice or Grass.

(new Cow).eat(new Fish) // I don't want this to happen

Please let me know why Either is not working here.

object AbstractType {
    def main(args: Array[String]): Unit = {
        (new Cow).eat(new Grass) // Error here  -- type mismatch; found : Grass required: scala.util.Either[Grass,Rice]
    }
}

abstract class Animal{
    type FoodType <: Food
    def eat(food : FoodType)
}

class Food{}
class Grass extends Food
class Rice extends Food
class Fish extends Food{}

class Cow extends Animal{
    type FoodType = Either[Grass,Rice]  // instead of Either[], keeping Grass here is compiling successfully as expected.
    def eat(food : FoodType) {
        println("Cow eats")
    }
}

I tried below approach as suggested by slouc. But, even this approach not able to restrict this. (new Cow).eat(Fish()).

object AbstractType {
    def main(args: Array[String]): Unit = {
        (new Cow).eat(Grass())
    }
}
abstract class Animal{
    type FoodType <: Food
    def eat(food : FoodType)
}

sealed trait Food
final case class Grass() extends Food
final case class Rice() extends Food
final case class Fish() extends Food

class Cow extends Animal{
    type FoodType = //fill here 
    def eat(food : FoodType) {
        println("Cow eats")
    }
}

My Question : what will be the better approach to fill in the above code so that, I can pass only Rice or Grass object.(if not with either , how to achieve other way) and restrict Fish object.

Upvotes: 1

Views: 197

Answers (2)

slouc
slouc

Reputation: 9698

This would work:

(new Cow).eat(Left[Grass, Rice](new Grass))

but you have another problem - you defined your abstract class Animal to expect a type FoodType which is a subtype of Food. Types Grass and Rice are both individually valid subtypes of Food, but Either[Grass, Rice] is not.

A bit of underlying theory:

When you work with a type that takes one out of several possible forms, that's called a sum type. As opposed to a product type, which combines all of the given types into one entity (e.g. Person consists of string first name, string last name, integer age etc.), a sum type only takes one materialization out of all the possible ones. This is what you have - your FoodType is either Grass or Rice or Fish.

Your problem here is that you're approaching your sum type with two different constructs which both serve the same purpose of modelling sum types. One approach is having a trait or an abstract class which is then extended by all the possible options:

trait Food
class Grass extends Food
class Rice extends Food
class Fish extends Food

Another approach is using an out-of-the-box sum type such as Either. Clumsy thing with Either is the fact that it only takes two possibilities, so for three you would have to have e.g. Either[Grass, Either[Rice, Fish]]. In some common Scala libraries such as scalaz or cats there are other, more suitable constructs for sum types (also known as "coproducts"), but let's not go into that now.

So, what you need to do is decide whether you want to stick to subtyping or you want to go with Either. For your use case subtyping is completely fine, so just remove the Either and implement type FoodType as e.g. Grass and it will work, as you noted yourself in the comment on the same line.

BTW your Food is a class, but notice how I said "trait or an abstract class". This is the best practice principle; if you're not expecting to ever need an instance of Food itself via new Food (and you're not; you're only going to instantiate its subclasses, e.g. new Grass), then it's better to not allow such instantiation in the first place.

Another hint is to make such trait / abstract class sealed and the subtypes final case class, which means that nobody else can ever provide extra options (that is, introduce some own custom food):

sealed trait Food
final case class Grass extends Food
final case class Rice extends Food
final case class Fish extends Food

Case class (as opposed to standard class) server the purpose of defining some stuff for you out of the box, such as

  • methods like equals(), copy() etc.
  • support for pattern matching (by implementing apply/unapply for you)
  • default companion object, which allows you to use Grass() instead of new Grass()
  • etc.

But OK I'm diverging :) hopefully this helps.

EDIT:

OK, now I realised your actual problem. You need to introduce another sum type. You already have Food, but now you need "cow food". You can easily model it exactly like that, adding a CowFood trait that extends Food and is extended by Grass and Rice.

sealed trait Food

sealed trait CowFood extends Food
sealed trait HorseFood extends Food
sealed trait SealFood extends Food

final case class Grass() extends CowFood with HorseFood
final case class Rice() extends CowFood
final case class Fish() extends SealFood

...

type FoodType = CowFood

(remember that traits are stackable; grass is both cow food and horse food)

I'm not a huge fan of subtyping, but for this particular problem it's a cleaner solution than getting entangled in Eithers and mapping all around the place.

Upvotes: 3

amorfis
amorfis

Reputation: 15770

In the code above, FoodType is defined twice, and it's definition in Cow shadows the one in Animal - this are two different types. You don't need Either in this case. You can define eat method with parameter of type Food, and just pass Grass, Rice or Fish, as all this classes inherit from Food

The example does not compile, because it expects parameter of type Either[Grass, Rice], but parameter of type Grass is passed.

Upvotes: 0

Related Questions