Code Examples for

Programming in Scala, Fifth Edition

Return to chapter index

23 Typeclasses

Sample run of chapter's interpreter examples

23.1 Why typeclasses?


// In file typeclasses/Misc.scala def -(x: Double): Double def -(x: Float): Float def -(x: Long): Long def -(x: Int): Int def -(x: Char): Int def -(x: Short): Int def -(x: Byte): Int
// In file typeclasses/rainbow-color.scala sealed trait RainbowColor class Red extends RainbowColor class Orange extends RainbowColor class Yellow extends RainbowColor class Green extends RainbowColor class Blue extends RainbowColor class Indigo extends RainbowColor class Violet extends RainbowColor
// In file typeclasses/Misc.scala def paint(rc: RainbowColor): Unit
class Hope[+T <: Ordered[T]] extends Ordered[Hope[T]] 1 |class Hope[+T <: Ordered[T]] extends Ordered[Hope[T]] |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |covariant type T occurs in invariant position in type |Object with Ordered[Hope[T]] {...} of class Hope
// In file typeclasses/hope-utils.scala import org.stairwaybook.enums_and_adts.hope.Hope object HopeUtils: given hopeOrdering[T](using ord: Ordering[T]): Ordering[Hope[T]] with def compare(lh: Hope[T], rh: Hope[T]): Int = import Hope.{Glad, Sad} (lh, rh) match case (Sad, Sad) => 0 case (Sad, _) => -1 case (_, Sad) => +1 case (Glad(lhv), Glad(rhv)) => ord.compare(lhv, rhv)

23.2 Context bounds


// In file typeclasses/MaxList1.scala def maxList[T](elements: List[T]) (using ordering: Ordering[T]): T = elements match case List() => throw new IllegalArgumentException("empty list!") case List(x) => x case x :: rest => val maxRest = maxList(rest)(using ordering) if ordering.gt(x, maxRest) then x else maxRest
// In file typeclasses/MaxList2a.scala def maxList[T](elements: List[T]) (using ordering: Ordering[T]): T = elements match case List() => throw new IllegalArgumentException("empty list!") case List(x) => x case x :: rest => val maxRest = maxList(rest) // Uses the given. if ordering.gt(x, maxRest) then x // This ordering is else maxRest // still explicit.
def summon[T](using t: T) = t
def maxList[T](elements: List[T]) (using comparator: Ordering[T]): T = // same body...
// In file typeclasses/MaxList2.scala def maxList[T](elements: List[T]) (using ordering: Ordering[T]): T = elements match case List() => throw new IllegalArgumentException("empty list!") case List(x) => x case x :: rest => val maxRest = maxList(rest) if summon[Ordering[T]].gt(x, maxRest) then x else maxRest
// In file typeclasses/MaxList2c.scala def maxList[T](elements: List[T]) (using iceCream: Ordering[T]): T = ??? // same body...
// In file typeclasses/MaxListContextBound.scala def maxList[T : Ordering](elements: List[T]): T = elements match case List() => throw new IllegalArgumentException("empty list!") case List(x) => x case x :: rest => val maxRest = maxList(rest) if summon[Ordering[T]].gt(x, maxRest) then x else maxRest

23.3 Main methods


// In file typeclasses/echoargs.scala // In file echoargs.scala @main def echo(args: String*) = println(args.mkString(" "))
$ scala echoargs.scala Running as a script Running as a script
$ scalac echoargs.scala $ scala echo Running as an application Running as an application
// In file typeclasses/repeat.scala // In file repeat.scala @main def repeat(word: String, count: Int) = val msg = if count > 0 then val words = List.fill(count)(word) words.mkString(" ") else "Please enter a word and a positive integer count." println(msg)
$ scalac repeat.scala $ scala repeat hello 3 hello hello hello
trait FromString[T]: def fromString(s: String): T
// In file typeclasses/moody.scala // In file moody.scala enum Mood: case Surprised, Angry, Neutral
// In file typeclasses/moody.scala // In file moody.scala val errmsg = "Please enter a word, a positive integer count, and\n" + "a mood (one of 'angry', 'surprised', or 'neutral')" @main def repeat(word: String, count: Int, mood: Mood) = val msg = if count > 0 then val words = List.fill(count)(word.trim) val punc = mood match case Mood.Angry => "!" case Mood.Surprised => "?" case Mood.Neutral => "" val sep = punc + " " words.mkString(sep) + punc else errmsg println(msg)
// In file typeclasses/moody.scala // In file moody.scala object Mood: import scala.util.CommandLineParser.FromString given moodFromString: FromString[Mood] with def fromString(s: String): Mood = s.trim.toLowerCase match case "angry" => Mood.Angry case "surprised" => Mood.Surprised case "neutral" => Mood.Neutral case _ => throw new IllegalArgumentException(errmsg)
$ scalac moody.scala $ scala repeat hello 3 neutral hello hello hello $ scala repeat hello 3 surprised hello? hello? hello? $ scala repeat hello 3 angry hello! hello! hello!

23.4 Multiversal equality


scala> "hello" == Option("hello") // (in Scala 2) val res0: Boolean = false
scala> "hello" == Option("hello") // (in Scala 3) 1 |"hello" == Option("hello") |^^^^^^^^^^^^^^^^^^^^^^^^^^ |Values of types String and Option[String] cannot be | compared with == or !=
// On class Any: final def ==(that: Any): Boolean final def !=(that: Any): Boolean
sealed trait CanEqual[-L, -R]
object CanEqual: object derived extends CanEqual[Any, Any]
// In file typeclasses/apple-orange-example1.scala case class Apple(size: Int)
// In file typeclasses/apple-orange-example1.scala val appleTwo = Apple(2) val appleTwoToo = Apple(2) appleTwo == appleTwoToo // true
// In file typeclasses/apple-orange-example1.scala case class Orange(size: Int) val orangeTwo = Orange(2) appleTwo == orangeTwo // false
import scala.language.strictEquality
scala> appleTwo == orangeTwo 1 |appleTwo == orangeTwo |^^^^^^^^^^^^^^^^^^^^^ |Values of types Apple and Orange cannot be | compared with == or !=
scala> appleTwo == appleTwoToo 1 |appleTwo == appleTwoToo |^^^^^^^^^^^^^^^^^^^^^^^ |Values of types Apple and Apple cannot be | compared with == or !=
// In file typeclasses/apple-orange-example2.scala case class Apple(size: Int) object Apple: given canEq: CanEqual[Apple, Apple] = CanEqual.derived
// In file typeclasses/apple-orange-example3.scala case class Apple(size: Int) derives CanEqual // idiomatic
appleTwo == appleTwoToo // true too

23.5 Implicit conversions


// In file typeclasses/implicit-conversion.scala case class Street(value: String)
// In file typeclasses/implicit-conversion.scala val street = Street("123 Main St")
scala> val streetStr: String = street 1 |val streetStr: String = street | ^^^^^^ | Found: (street : Street) | Required: String
// In file typeclasses/implicit-conversion.scala val streetStr: String = street.value // 123 Main St
abstract class Conversion[-T, +U] extends (T => U): def apply(x: T): U
// In file typeclasses/implicit-conversion.scala given streetToString: Conversion[Street, String] = _.value
// In file typeclasses/implicit-conversion.scala import scala.language.implicitConversions
// In file typeclasses/implicit-conversion.scala val streetStr: String = street
val streetStr: String = streetToString(street)

23.6 Typeclass case study: JSON serialization


{ "style": "tennis", "size": 10, "inStock": true, "colors": ["beige", "white", "blue"], "humor": null }
trait JsonSerializer[T]: def serialize(o: T): String
// In file typeclasses/JsonSerializer.scala trait JsonSerializer[T]: def serialize(o: T): String extension (a: T) def toJson: String = serialize(a)
// In file typeclasses/JsonSerializer.scala object JsonSerializer: given stringSerializer: JsonSerializer[String] with def serialize(s: String) = s"\"$s\"" given intSerializer: JsonSerializer[Int] with def serialize(n: Int) = n.toString given longSerializer: JsonSerializer[Long] with def serialize(n: Long) = n.toString given booleanSerializer: JsonSerializer[Boolean] with def serialize(b: Boolean) = b.toString
// In file typeclasses/ToJsonMethods.scala object ToJsonMethods: extension [T](a: T)(using jser: JsonSerializer[T]) def toJson: String = jser.serialize(a)
// In file typeclasses/json1.scala import ToJsonMethods.* "tennis".toJson // "tennis" 10.toJson // 10 true.toJson // true
// In file typeclasses/json-models.scala case class Address( street: String, city: String, state: String, zip: Int ) case class Phone( countryCode: Int, phoneNumber: Long ) case class Contact( name: String, addresses: List[Address], phones: List[Phone] ) case class AddressBook(contacts: List[Contact])
// In file typeclasses/json-models.scala object Address: given addressSerializer: JsonSerializer[Address] with def serialize(a: Address) = import ToJsonMethods.{toJson as asJson} s"""|{ | "street": ${a.street.asJson}, | "city": ${a.city.asJson}, | "state": ${a.state.asJson}, | "zip": ${a.zip.asJson} |}""".stripMargin object Phone: given phoneSerializer: JsonSerializer[Phone] with def serialize(p: Phone) = import ToJsonMethods.{toJson as asJson} s"""|{ | "countryCode": ${p.countryCode.asJson}, | "phoneNumber": ${p.phoneNumber.asJson} |}""".stripMargin
object JsonSerializer: // givens for strings, ints, and booleans... given listSerializer[T](using JsonSerializer[T]): JsonSerializer[List[T]] with def serialize(ts: List[T]) = s"[${ts.map(t => t.toJson).mkString(", ")}]"
// In file typeclasses/json2.scala import ToJsonMethods.* List(1, 2, 3).toJson // [1, 2, 3]
scala> List(1.0, 2.0, 3.0).toJson 1 |List(1.0, 2.0, 3.0).toJson |^^^^^^^^^^^^^^^^^^^^^^^^^^ |value toJson is not a member of List[Double]. |An extension method was tried, but could not be fully |constructed: | | ToJsonMethods.toJson[List[Double]]( | List.apply[Double]([1.0d,2.0d,3.0d : Double]*) | )(JsonSerializer.listSerializer[T]( | /* missing */summon[JsonSerializer[Double]])) | failed with | | no implicit argument of type JsonSerializer[List[Double]] | was found for parameter json of method toJson in | object ToJsonMethods. | I found: | | JsonSerializer.listSerializer[T]( | /* missing */summon[JsonSerializer[Double]]) | | But no implicit values were found that match type | JsonSerializer[Double].
// In file typeclasses/json-models.scala object Contact: given contactSerializer: JsonSerializer[Contact] with def serialize(c: Contact) = import ToJsonMethods.{toJson as asJson} s"""|{ | "name": ${c.name.asJson}, | "addresses": ${c.addresses.asJson}, | "phones": ${c.phones.asJson} |}""".stripMargin object AddressBook: given addressBookSerializer: JsonSerializer[AddressBook] with def serialize(a: AddressBook) = import ToJsonMethods.{toJson as asJson} s"""|{ | "contacts": ${a.contacts.asJson} |}""".stripMargin
// In file typeclasses/json2.scala addressBook.toJson
// In file typeclasses/json2.scala val addressBook = AddressBook( List( Contact( "Bob Smith", List( Address( "12345 Main Street", "San Francisco", "CA", 94105 ), Address( "500 State Street", "Los Angeles", "CA", 90007 ) ), List( Phone( 1, 5558881234 ), Phone( 49, 5558413323 ) ) ) ) )
{ "contacts": [{ "name": "Bob Smith", "addresses": [{ "street": "12345 Main Street", "city": "San Francisco", "state": "CA", "zip": 94105 }, { "street": "500 State Street", "city": "Los Angeles", "state": "CA", "zip": 90007 }], "phones": [{ "countryCode": 1, "phoneNumber": 5558881234 }, { "countryCode": 49, "phoneNumber": 5558413323 }] }] }

23.7 Conclusion

For more information about Programming in Scala, Fifth Edition (the "Stairway Book"), please visit:

http://www.artima.com/shop/programming_in_scala_5ed

and:

http://booksites.artima.com/programming_in_scala_5ed

Copyright © 2007-2020 Artima, Inc. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.