0Pricing
Scala for Backend Engineering & Functional Programming · レッスン

変性:共変と反変

ジェネリック型におけるサブタイピング関係を正しく扱うため、共変と反変を使いこなします。

「変性:共変と反変」はCoddyKit上の無料Scala for Backend Engineering & Functional Programmingレッスンです。 これはレッスン2/3です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはScala for Backend Engineering & Functional Programming学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Scala for Backend Engineering & Functional Programmingコースには全3レッスンが含まれています。

このレッスンの一部はまだ翻訳されておらず、英語で表示されています。

What is Type Variance?

In Scala, when you have a type hierarchy (e.g., Cat is a subtype of Animal), how do generic types behave?

Is a List[Cat] considered a subtype of List[Animal]? Not always by default!

Type variance allows us to define how subtyping relationships are preserved (or reversed) for generic types. This is crucial for writing flexible and type-safe code.

Covariance: 'Producer' Types (+T)

Covariance is denoted by placing a + before the type parameter (e.g., trait Box[+T]).

  • If A is a subtype of B, then Box[A] becomes a subtype of Box[B].
  • Think of covariant types as 'producers' of T. They can only appear in output positions (like return types of methods).
  • This means if you expect a Box[Animal], a Box[Cat] can be provided because it 'produces' something more specific (a Cat is an Animal).

Covariance in Action

Try running this example of a covariant Producer trait:

class Animal
class Cat extends Animal

trait Producer[+T] {
  def produce: T // Output position
}

class CatProducer extends Producer[Cat] {
  def produce: Cat = new Cat
}

object Main {
  def main(args: Array[String]): Unit = {
    val catProducer: Producer[Cat] = new CatProducer
    // Because Producer is covariant, Producer[Cat] is a subtype of Producer[Animal]
    val animalProducer: Producer[Animal] = catProducer 
    println("Assigned CatProducer to AnimalProducer.")
    println(s"Produced: ${animalProducer.produce.getClass.getSimpleName}")
  }
}

When to Use Covariance

Covariance is safe when your generic type only 'produces' values of type T, or never accepts T as an argument.

  • Immutable collections like List[+T] are a prime example. You can treat a List[Cat] as a List[Animal] because you only ever 'read' Animals (or their subtypes) from it.
  • You cannot add an arbitrary Animal to a List[Cat] if it's typed as List[Animal], which maintains type safety.

Contravariance: 'Consumer' Types (-T)

Contravariance is denoted by placing a - before the type parameter (e.g., trait Consumer[-T]).

  • If A is a subtype of B, then Consumer[B] becomes a subtype of Consumer[A].
  • Think of contravariant types as 'consumers' of T. They can only appear in input positions (like parameter types of methods).
  • This means if you expect a Consumer[Cat], a Consumer[Animal] can be provided because it 'consumes' something more general (it can handle any Animal, including a Cat).

Contravariance in Action

Try running this example of a contravariant Consumer trait:

class Animal
class Cat extends Animal

trait Consumer[-T] {
  def consume(item: T): Unit // Input position
}

class AnimalConsumer extends Consumer[Animal] {
  def consume(item: Animal): Unit = {
    println(s"Consumed an animal: ${item.getClass.getSimpleName}")
  }
}

object Main {
  def main(args: Array[String]): Unit = {
    val animalConsumer: Consumer[Animal] = new AnimalConsumer
    // Because Consumer is contravariant, Consumer[Animal] is a subtype of Consumer[Cat]
    val catConsumer: Consumer[Cat] = animalConsumer 
    catConsumer.consume(new Cat)
    println("Assigned AnimalConsumer to CatConsumer.")
  }
}

When to Use Contravariance

Contravariance is safe when your generic type only 'consumes' values of type T, or never returns T.

  • A common example is functions, specifically the input parameter type. If a function can process any Animal (Animal => Unit), it can certainly process a Cat. So, (Animal => Unit) is a subtype of (Cat => Unit).
  • This allows for greater flexibility when passing functions as arguments.

Invariance: The Default Behavior

If you don't specify + or -, the type parameter is invariant. This is the default in Scala.

  • Box[A] is only a subtype of Box[B] if A is exactly the same type as B.
  • This is often necessary for mutable collections (e.g., Array[T]) to prevent type safety issues, as you could both read and write different subtypes.
class Food
class Apple extends Food

// Invariant Box
class Box[T](val item: T) {
  def getContent: T = item
}

object Main {
  def main(args: Array[String]): Unit = {
    val appleBox = new Box(new Apple)
    // The following line would cause a compile error:
    // val foodBox: Box[Food] = appleBox 
    println(s"An Apple Box contains: ${appleBox.getContent.getClass.getSimpleName}")
    println("Box[Apple] is NOT a subtype of Box[Food] (invariant).")
    println("The types must match exactly for invariant types.")
  }
}

Functions: Both Covariant & Contravariant

Scala's function types, Function1[-A, +B], elegantly combine both variance types:

  • The input parameter A is contravariant (-A). This means a function that accepts a more general type (e.g., Animal) can be used where a function accepting a more specific type (e.g., Cat) is expected.
  • The return type B is covariant (+B). This means a function that returns a more specific type (e.g., Cat) can be used where a function returning a more general type (e.g., Animal) is expected.
class Vehicle
class Car extends Vehicle

object Main {
  def main(args: Array[String]): Unit = {
    // Contravariance for input: (Vehicle => Unit) is a subtype of (Car => Unit)
    val printVehicle: Vehicle => Unit = (v: Vehicle) => println(s"Printing vehicle: ${v.getClass.getSimpleName}")
    val printCar: Car => Unit = printVehicle // OK: A general printer can print a specific car
    printCar(new Car)

    // Covariance for output: (() => Car) is a subtype of (() => Vehicle)
    val getCar: () => Car = () => new Car
    val getVehicle: () => Vehicle = getCar // OK: A specific producer can fulfill a general request
    println(s"Got vehicle: ${getVehicle().getClass.getSimpleName}")
  }
}

Quick Check: Variance Rules

Consider the following trait:

trait Handler[T] {
  def handle(item: T): Unit
}

To allow Handler[Animal] to be used where a Handler[Cat] is expected (where Cat extends Animal), what variance annotation should T have?

Recap: Variance Mastery

You've mastered variance in Scala! Here's a quick recap:

  • Covariance (+T): Allows Container[Subtype] to be a subtype of Container[Supertype]. Useful for 'producer' types that only return T.
  • Contravariance (-T): Allows Container[Supertype] to be a subtype of Container[Subtype]. Useful for 'consumer' types that only accept T as input.
  • Invariance: The default. Types must match exactly.

Understanding variance helps you create more flexible and type-safe generic code in Scala!

よくある質問

「変性:共変と反変」レッスンは無料ですか?

はい。「変性:共変と反変」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Scala for Backend Engineering & Functional Programmingコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Scala for Backend Engineering & Functional Programmingコースには全3レッスンが含まれています。

「変性:共変と反変」で何を学びますか?

ジェネリック型におけるサブタイピング関係を正しく扱うため、共変と反変を使いこなします。 ブラウザで直接実行するハンズオンコードでScala for Backend Engineering & Functional Programmingを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Scala for Backend Engineering & Functional Programmingを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのScala for Backend Engineering & Functional Programmingは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/3です。

「変性:共変と反変」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このScala for Backend Engineering & Functional Programmingレッスンでコードを書いて実行できますか?

はい。すべてのScala for Backend Engineering & Functional Programmingレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. ジェネリクスと型パラメーター
  2. 変性:共変と反変
  3. 型クラスと暗黙の仕組み
← Scala for Backend Engineering & Functional Programmingに戻る