class: center, middle # How to build a functional API `@JulienTruffaut` Code and slides at `julien-truffaut/fp-api` on GitHub ??? --- # Function .center[![function](function.JPG)] --- # Partial function .center[![function2](function_2.jpg)] --- # 1. Do not use partial function ```scala def isAdult(age: Int): Boolean = { require(age >= 0, "age must be positive") age >= 18 } ``` ```scala scala> isAdult(4) res0: Boolean = false scala> isAdult(25) res1: Boolean = true ``` ```scala scala> isAdult(-10) java.lang.IllegalArgumentException: requirement failed: age must be positive at scala.Predef$.require(Predef.scala:277) at .isAdult(
:13) ... 205 elided ``` --- # Increase the output type ```scala def isAdult(age: Int): Either[String, Boolean] = if(age < 0) Left(s"$age must be positive") else Right(age >= 18) ``` ```scala scala> isAdult(4) res3: Either[String,Boolean] = Right(false) scala> isAdult(25) res4: Either[String,Boolean] = Right(true) scala> isAdult(-2) res5: Either[String,Boolean] = Left(-2 must be positive) ``` --- # Reduce the input type ```scala import eu.timepit.refined.api.Refined import eu.timepit.refined.auto._ import eu.timepit.refined.numeric.Positive def isAdult(age: Int Refined Positive): Boolean = age.value >= 18 ``` ```scala scala> isAdult(4) res7: Boolean = false scala> isAdult(25) res8: Boolean = true ``` ```scala scala> isAdult(-10)
:19: error: Predicate failed: (-10 > 0). isAdult(-10) ^ ``` ??? - local reasoning - more predictable results a.k.a. "it compiles, let's ship it" --- # Reduce the input type ```scala import eu.timepit.refined._ val x: Int = 5 val y: Int = 25 val z: Int = -2 ``` ```scala scala> refineV[Positive](x).map(isAdult) res11: scala.util.Either[String,Boolean] = Right(false) scala> refineV[Positive](y).map(isAdult) res12: scala.util.Either[String,Boolean] = Right(true) scala> refineV[Positive](z).map(isAdult) res13: scala.util.Either[String,Boolean] = Left(Predicate failed: (-2 > 0).) ``` --- # 2. Derive most functions ```scala scala> List.empty res14: List[Nothing] = List() scala> List(1,2,3) res15: List[Int] = List(1, 2, 3) ``` ```scala scala> List(1,2) ++ List(3,4) res16: List[Int] = List(1, 2, 3, 4) scala> List(1,2,3,4).reverse res17: List[Int] = List(4, 3, 2, 1) scala> List(List(1,2), List(3,4)).flatten res18: List[Int] = List(1, 2, 3, 4) ``` --- # 2. Derive most functions ```scala def empty[A]: List[A] = List.apply() def ++[A](xs: List[A], ys: List[A]): List[A] = xs.foldRight(ys)(_ :: _) def reverse[A](xs: List[A]): List[A] = xs.foldLeft(List.empty[A])((acc, a) => a :: acc) def flatten[A](xs: List[List[A]]): List[A] = xs.flatMap(identity) ``` --- # Functional Programming in Scala .center[![red-book](red-book.jpeg)] --- class: center, middle # 1. Do not use partial functions # 2. Derive most functions --- # Use case: Parsing excel .center[![workbook](workbook-intro.png)] --- # Use case: Parsing excel .center[![workbook](workbook-with-name.png)] --- # Excel API: Workbook ```scala libraryDependencies += "org.apache.poi" % "poi-ooxml" % "3.15" ``` ```scala import org.apache.poi.ss.usermodel.{Workbook, WorkbookFactory} def load(fileName: String): Workbook = WorkbookFactory.create(getClass.getClassLoader.getResourceAsStream(fileName)) val workbook = load("example.xlsx") ``` --- # Excel API: Name ```scala scala> val oilProduction = workbook.getName("OilProd") oilProduction: org.apache.poi.ss.usermodel.Name = org.apache.poi.xssf.usermodel.XSSFName@5c35c92a scala> oilProduction.getRefersToFormula res24: String = Sheet1!$B$4:$J$4 ``` .center[![workbook](workbook.png)] --- # Excel API: AreaReference ```scala import org.apache.poi.ss.util.AreaReference val formula = workbook.getName("OilProd").getRefersToFormula val area = new AreaReference(formula, workbook.getSpreadsheetVersion) ``` ```scala scala> val cellRefs = area.getAllReferencedCells cellRefs: Array[org.apache.poi.ss.util.CellReference] = Array(org.apache.poi.ss.util.CellReference [Sheet1!$B$4], org.apache.poi.ss.util.CellReference [Sheet1!$C$4], org.apache.poi.ss.util.CellReference [Sheet1!$D$4], org.apache.poi.ss.util.CellReference [Sheet1!$E$4], org.apache.poi.ss.util.CellReference [Sheet1!$F$4], org.apache.poi.ss.util.CellReference [Sheet1!$G$4], org.apache.poi.ss.util.CellReference [Sheet1!$H$4], org.apache.poi.ss.util.CellReference [Sheet1!$I$4], org.apache.poi.ss.util.CellReference [Sheet1!$J$4]) scala> cellRefs.foreach(println) org.apache.poi.ss.util.CellReference [Sheet1!$B$4] org.apache.poi.ss.util.CellReference [Sheet1!$C$4] org.apache.poi.ss.util.CellReference [Sheet1!$D$4] org.apache.poi.ss.util.CellReference [Sheet1!$E$4] org.apache.poi.ss.util.CellReference [Sheet1!$F$4] org.apache.poi.ss.util.CellReference [Sheet1!$G$4] org.apache.poi.ss.util.CellReference [Sheet1!$H$4] org.apache.poi.ss.util.CellReference [Sheet1!$I$4] org.apache.poi.ss.util.CellReference [Sheet1!$J$4] ``` --- # Excel API: Cell ```scala val cellRef = cellRefs.head val cell = workbook. getSheet(cellRef.getSheetName). getRow(cellRef.getRow). getCell(cellRef.getCol) ``` ```scala scala> cell.getNumericCellValue res29: Double = 10.12 ``` --- # Numeric range ```scala def numericRange(workbook: Workbook, name: String): List[Double] = { val formula = workbook.getName(name).getRefersToFormula val area = new AreaReference(formula, workbook.getSpreadsheetVersion) val cells = area.getAllReferencedCells.toList.map(cellRef => workbook .getSheet(cellRef.getSheetName) .getRow(cellRef.getRow) .getCell(cellRef.getCol) ) cells.map(_.getNumericCellValue) } ``` ```scala scala> numericRange(workbook, "OilProd") res30: List[Double] = List(10.12, 12.34, 8.83, 6.23, 9.18, 12.36, 16.28, 18.25, 20.01) ``` --- # Parsing Doubles ```scala def numericRange(workbook: Workbook, name: String): List[Double] = { val formula = workbook.getName(name).getRefersToFormula val area = new AreaReference(formula, workbook.getSpreadsheetVersion) val cells = area.getAllReferencedCells.toList.map(cellRef => workbook .getSheet(cellRef.getSheetName) .getRow(cellRef.getRow) .getCell(cellRef.getCol) ) cells.map(_.getNumericCellValue) } ``` ```scala scala> numericRange(workbook, "PrimaryProduct") java.lang.IllegalStateException: Cannot get a NUMERIC value from a STRING cell at org.apache.poi.xssf.usermodel.XSSFCell.typeMismatch(XSSFCell.java:1050) at org.apache.poi.xssf.usermodel.XSSFCell.getNumericCellValue(XSSFCell.java:316) at .$anonfun$numericRange$2(
:35) at .$anonfun$numericRange$2$adapted(
:35) at scala.collection.immutable.List.map(List.scala:272) at .numericRange(
:35) ... 621 elided ``` --- # Cell error handling ```scala case class ParserError(ref: String, expectedFormat: String, message: String) ``` -- ```scala import cats.syntax.either._ import org.apache.poi.ss.usermodel.Cell case class SafeCell(cell: Cell){ val reference = s"${cell.getSheet.getSheetName}!${cell.getAddress}" def asDouble: Either[ParserError, Double] = Either.catchNonFatal(cell.getNumericCellValue).leftMap(e => ParserError(reference, "Numeric", e.getMessage) ) } ``` --- # Parsing Doubles ```scala def numericRange(workbook: Workbook, name: String): Either[ParserError, List[Double]] = { val formula = workbook.getName(name).getRefersToFormula val area = new AreaReference(formula, workbook.getSpreadsheetVersion) val cells = area.getAllReferencedCells.toList.map(cellRef => workbook .getSheet(cellRef.getSheetName) .getRow(cellRef.getRow) .getCell(cellRef.getCol) ) val safeCells: List[SafeCell] = cells.map(SafeCell) val doubles: List[Either[ParserError, Double]] = safeCells.map(_.asDouble) ??? } ``` --- # Parsing Doubles ```scala import cats.syntax.traverse._ import cats.instances.either._ import cats.instances.list._ def numericRange(workbook: Workbook, name: String): Either[ParserError, List[Double]] = { val formula = workbook.getName(name).getRefersToFormula val area = new AreaReference(formula, workbook.getSpreadsheetVersion) val cells = area.getAllReferencedCells.toList.map(cellRef => workbook .getSheet(cellRef.getSheetName) .getRow(cellRef.getRow) .getCell(cellRef.getCol) ) val safeCells: List[SafeCell] = cells.map(SafeCell) val doubles: List[Either[ParserError, Double]] = safeCells.map(_.asDouble) doubles.sequence } ``` --- # Parsing Doubles ```scala def numericRange(workbook: Workbook, name: String): Either[ParserError, List[Double]] = { val area = new AreaReference(workbook.getName(name).getRefersToFormula, workbook.getSpreadsheetVersion) area.getAllReferencedCells.toList.map(cellRef => workbook .getSheet(cellRef.getSheetName) .getRow(cellRef.getRow) .getCell(cellRef.getCol) ).map(SafeCell).traverse(_.asDouble) } ``` -- ```scala scala> numericRange(workbook, "OilProd") res34: Either[ParserError,List[Double]] = Right(List(10.12, 12.34, 8.83, 6.23, 9.18, 12.36, 16.28, 18.25, 20.01)) scala> numericRange(workbook, "PrimaryProduct").leftMap(_.message) res35: Either[String,List[Double]] = Left(Cannot get a NUMERIC value from a STRING cell) ``` --- # Parsing Doubles ```scala def numericRange(workbook: Workbook, name: String): Either[ParserError, List[Double]] = { val area = new AreaReference(workbook.getName(name).getRefersToFormula, workbook.getSpreadsheetVersion) area.getAllReferencedCells.toList.map(cellRef => workbook .getSheet(cellRef.getSheetName) .getRow(cellRef.getRow) .getCell(cellRef.getCol) ).map(SafeCell).traverse(_.asDouble) } ``` ```scala scala> numericRange(workbook, "foo") java.lang.NullPointerException at .numericRange(
:43) ... 837 elided ``` --- # Parsing Doubles ```scala sealed trait ParserError extends Product with Serializable case class InvalidFormat(ref: String, expectedFormat: String, message: String) extends ParserError case class MissingName(name: String) extends ParserError case class MissingCell(ref: String) extends ParserError ``` ```scala def getArea(workbook: Workbook, name: String): Either[ParserError, AreaReference] = Either.catchNonFatal( new AreaReference(workbook.getName(name).getRefersToFormula, workbook.getSpreadsheetVersion) ).leftMap(_ => MissingName(name)) def getSafeCell(workbook: Workbook, cellRef: CellReference): Either[ParserError, SafeCell] = Either.catchNonFatal(SafeCell( workbook .getSheet(cellRef.getSheetName) .getRow(cellRef.getRow) .getCell(cellRef.getCol) )).leftMap(_ => MissingCell(cellRef.toString)) ``` --- # Parsing doubles ```scala def numericRange(workbook: Workbook, name: String): Either[ParserError, List[Double]] = for { area <- getArea(workbook, name) cells <- area.getAllReferencedCells.toList.traverse(getSafeCell(workbook, _)) doubles <- cells.traverse(_.asDouble) } yield doubles ``` ```scala scala> numericRange(workbook, "OilProd") res39: Either[ParserError,List[Double]] = Right(List(10.12, 12.34, 8.83, 6.23, 9.18, 12.36, 16.28, 18.25, 20.01)) scala> numericRange(workbook, "PrimaryProduct") res40: Either[ParserError,List[Double]] = Left(InvalidFormat(Sheet1!B7,Numeric,Cannot get a NUMERIC value from a STRING cell)) scala> numericRange(workbook, "foo") res41: Either[ParserError,List[Double]] = Left(MissingName(foo)) ``` --- # Parsing API ```scala def numericRange(workbook: Workbook, name: String): Either[ParserError, List[Double]] def intRange (workbook: Workbook, name: String): Either[ParserError, List[Int]] def stringRange (workbook: Workbook, name: String): Either[ParserError, List[String]] def numeric (workbook: Workbook, name: String): Either[ParserError, Double] def int (workbook: Workbook, name: String): Either[ParserError, Int] def string (workbook: Workbook, name: String): Either[ParserError, String] ``` --- # Parsing API ```scala trait Parser[A] { def parse(workbook: Workbook, name: String): Either[ParserError, A] } ``` ```scala val numericRange: Parser[List[Double]] val intRange : Parser[List[Int]] val stringRange : Parser[List[String]] val numeric : Parser[Double] val int : Parser[Int] val string : Parser[String] ``` --- # Numeric ```scala val numeric: Parser[Double] = new Parser[Double] { def parse(workbook: Workbook, name: String): Either[ParserError, Double] = ??? } ``` --- # Numeric ```scala val numeric: Parser[Double] = new Parser[Double] { def parse(workbook: Workbook, name: String): Either[ParserError, Double] = for { area <- getArea(workbook, name) cells <- area.getAllReferencedCells.toList.traverse(getSafeCell(workbook, _)) cell <- cells match { case x :: Nil => Right(x) case _ => Left(InvalidFormat(name, "Single Numeric", "0 or more than 1 value")) } double <- cell.asDouble } yield double } ``` --- # Numeric ```scala val numeric: Parser[Double] = new Parser[Double] { def parse(workbook: Workbook, name: String): Either[ParserError, Double] = numericRange.parse(workbook, name).flatMap{ case x :: Nil => Right(x) case _ => Left(InvalidFormat(name, "Single Numeric", "0 or more than 1 value")) } } ``` ```scala scala> numeric.parse(workbook, "ExplorationFee") res42: Either[ParserError,Double] = Right(1.4) scala> numeric.parse(workbook, "OilProd") res43: Either[ParserError,Double] = Left(InvalidFormat(OilProd,Single Numeric,0 or more than 1 value)) scala> numeric.parse(workbook, "foo") res44: Either[ParserError,Double] = Left(MissingName(foo)) ``` --- # FlatMap ```scala def flatMap[A, B](fa: Either[ParserError, A]) (f: A => Either[ParserError, B]): Either[ParserError, B] = ??? ``` -- ```scala def flatMap[A, B](fa: Parser[A])(f: A => Parser[B]): Parser[B] = new Parser[B]{ def parse(workbook: Workbook, name: String): Either[ParserError, B] = ??? } ``` --- # FlatMap ```scala def flatMap[A, B](fa: Either[ParserError, A]) (f: A => Either[ParserError, B]): Either[ParserError, B] = ??? ``` ```scala def flatMap[A, B](fa: Parser[A])(f: A => Parser[B]): Parser[B] = new Parser[B]{ def parse(workbook: Workbook, name: String): Either[ParserError, B] = fa.parse(workbook, name).flatMap(a => f(a).parse(workbook, name) ) } ``` --- # Numeric ```scala def flatMap[A, B](fa: Parser[A])(f: A => Parser[B]): Parser[B] ``` ```scala val numeric: Parser[Double] = flatMap(numericRange){ case x :: Nil => ??? // Right(x) case _ => ??? // Left(InvalidFormat(name, "Single Numeric", "0 or more than 1 value")) } ``` --- # Success / Failure ```scala def success[A](a: A): Parser[A] = new Parser[A] { def parse(workbook: Workbook, name: String) = Right(a) } def fail[A](error: ParserError): Parser[A] = new Parser[A] { def parse(workbook: Workbook, name: String) = Left(error) } ``` --- # Numeric ```scala val numeric: Parser[Double] = flatMap(numericRange){ case x :: Nil => ??? // Right(x) case _ => ??? // Left(InvalidFormat(name, "Single Numeric", "0 or more than 1 value")) } ``` -- ```scala val numeric: Parser[Double] = flatMap(numericRange){ case x :: Nil => success(x) case _ => fail(InvalidFormat(???, "Single Numeric", "0 or more than 1 value")) } ``` --- # Numeric ```scala def fail[A](error: String => ParserError): Parser[A] = new Parser[A] { def parse(workbook: Workbook, name: String) = Left(error(name)) } ``` -- ```scala val numeric: Parser[Double] = flatMap(numericRange){ case x :: Nil => success(x) case _ => fail(name => InvalidFormat(name, "Single Numeric", "0 or more than 1 value")) } ``` --- # Other building blocks ```scala val intRange: Parser[List[Int]] val int: Parser[Int] val stringRange: Parser[List[String]] val string: Parser[String] ``` -- ```scala val oneToRuleThemAll: Parser[???] ``` --- # Parsing Product ```scala case class Production(oil: List[Double], gas: List[Double]) ``` ```scala val production: Parser[Production] = ??? ``` --- # Parsing Product ```scala case class Production(oil: List[Double], gas: List[Double]) val production: Parser[Production] = new Parser[Production]{ def parse(workbook: Workbook, name: String) = for { oil <- numericRange.parse(workbook, ???) gas <- numericRange.parse(workbook, ???) } yield Production(oil, gas) } ``` --- # Parser / Curried Parser ```scala trait Parser[A] { def parse(workbook: Workbook, name: String): Either[ParserError, A] } val numericRange: Parser[List[Double]] = ??? ``` ```scala trait Parser[A] { def parse(workbook: Workbook): Either[ParserError, A] } def numericRange(name: String): Parser[List[Double]] = ??? ``` --- # Production ```scala val production: Parser[Production] = flatMap(numericRange("OilProd"))(oil => flatMap(numericRange("GasProd"))(gas => success(Production(oil, gas)) ) ) ``` --- # Parsing Product ```scala def product[A, B](pa: Parser[A], pb: Parser[B]): Parser[(A, B)] = ??? ``` -- ```scala def map[A, B](pa: Parser[A])(f: A => B): Parser[B] = ??? ``` -- ```scala val production: Parser[Production] = map( product(numericRange("OilProd"), numericRange("GasProd")) ){ case (oil, gas) => Production(oil, gas) } ``` --- # Product ```scala def product[A, B](pa: Parser[A], pb: Parser[B]): Parser[(A, B)] = new Parser[(A, B)]{ def parse(workbook: Workbook): Either[ParserError, (A, B)] = for { a <- pa.parse(workbook) b <- pb.parse(workbook) } yield (a, b) } ``` -- ```scala val oil = numericRange("OilProd") val gas = numericRange("GasProd") val foo = numericRange("Foo") ``` ```scala scala> product(oil, gas).parse(workbook) res48: Either[ParserError,(List[Double], List[Double])] = Right((List(10.12, 12.34, 8.83, 6.23, 9.18, 12.36, 16.28, 18.25, 20.01),List(0.0, 0.0, 0.0, 3.2, 6.5, 6.38, 5.72, 4.54, 6.99))) scala> product(oil, foo).parse(workbook) res49: Either[ParserError,(List[Double], List[Double])] = Left(MissingName(Foo)) ``` --- # Map ```scala def map[A, B](pa: Parser[A])(f: A => B): Parser[B] = flatMap(pa)(a => success(f(a))) ``` -- ```scala case class OilProduction(value: List[Double]) val oilProduction = map(numericRange("OilProd"))(OilProduction.apply) ``` ```scala scala> oilProduction.parse(workbook) res51: Either[ParserError,OilProduction] = Right(OilProduction(List(10.12, 12.34, 8.83, 6.23, 9.18, 12.36, 16.28, 18.25, 20.01))) ``` --- # Parsing Production ```scala val production: Parser[Production] = map( product(numericRange("OilProd"), numericRange("GasProd")) ){ case (oil, gas) => Production(oil, gas) } ``` ```scala scala> production.parse(workbook) res52: Either[ParserError,Production] = Right(Production(List(10.12, 12.34, 8.83, 6.23, 9.18, 12.36, 16.28, 18.25, 20.01),List(0.0, 0.0, 0.0, 3.2, 6.5, 6.38, 5.72, 4.54, 6.99))) ``` --- # Parser is a Cartesian Functor ```scala trait Functor[F[_]]{ def map[A,B](fa: F[A])(f: A => B): F[B] } trait Cartesian[F[_]]{ def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] } ``` --- # Parser is a Cartesian Functor ```scala implicit val cartesianFunctor: Functor[Parser] with Cartesian[Parser] = ??? ``` ```scala import cats.syntax.all._ val oil = numericRange("OilProd") val gas = numericRange("GasProd") val production = (oil, gas).mapN(Production) ``` -- ```scala scala> (oil, gas, foo).tupled res56: Parser[(List[Double], List[Double], List[Double])] = $anon$1$$anon$3@ef398c1 ``` --- # API Iterations ```scala def numericRange(workbook: Workbook, name: String): List[Double] = ??? ``` -- ```scala def numericRange(workbook: Workbook, name: String): Either[ParserError, List[Double]] = ??? ``` -- ```scala trait Parser[A]{ def parse(workbook: Workbook, name: String): Either[ParserError, A] } ``` -- ```scala trait Parser[A]{ def parse(workbook: Workbook): Either[ParserError, A] } ``` -- ```scala implicit def instances: Functor[Parser] with Cartesian[Parser] = ??? ``` --- # Is it worth it? ```scala val oil: Parser[List[Double]] = numericRange("OilProd") val gas: Parser[List[Double]] = numericRange("GasProd") val production: Parser[Production] = (oil, gas).mapN(Production) ``` --- class: center, middle # Thanks! Code and slides at `julien-truffaut/fp-api` on GitHub ## Questions? --- # Bonus: Parsing Coproduct ```scala sealed trait Fee extends Product with Serializable case class TechnicalFee(technical: Double) extends Fee case class ExplorationFee(exploration: Double, postExploration: Double) extends Fee ``` -- ```scala val technicalFee: Parser[TechnicalFee] = numeric("TechnicalFee").map(TechnicalFee) val explorationFee: Parser[ExplorationFee] = (numeric("ExplorationFee"), numeric("PostExplorationFee")).mapN(ExplorationFee) ``` -- ```scala scala> technicalFee.parse(workbook) res59: Either[ParserError,TechnicalFee] = Left(MissingName(TechnicalFee)) scala> explorationFee.parse(workbook) res60: Either[ParserError,ExplorationFee] = Right(ExplorationFee(1.4,5.8)) ``` ```scala val fee: Parser[Fee] = ??? ``` --- # Combine ```scala def combine[A](p1: Parser[A], p2: Parser[A]): Parser[A] = new Parser[A]{ def parse(workbook: Workbook): Either[ParserError, A] = p1.parse(workbook) orElse p2.parse(workbook) } ``` -- ```scala scala> val fee = combine(technicalFee, explorationFee)
:47: error: type mismatch; found : Parser[TechnicalFee] required: Parser[Fee] Note: TechnicalFee <: Fee, but trait Parser is invariant in type A. You may wish to define A as +A instead. (SLS 4.5) val fee = combine(technicalFee, explorationFee) ^
:47: error: type mismatch; found : Parser[ExplorationFee] required: Parser[Fee] Note: ExplorationFee <: Fee, but trait Parser is invariant in type A. You may wish to define A as +A instead. (SLS 4.5) val fee = combine(technicalFee, explorationFee) ^ ``` --- # Combine ```scala val fee = combine( technicalFee.map(f => f: Fee), explorationFee.map(f => f: Fee) ) ``` -- ```scala val fee = combine( technicalFee.widen[Fee], explorationFee.widen[Fee] ) ``` ```scala scala> fee.parse(workbook) res61: Either[ParserError,Fee] = Right(ExplorationFee(1.4,5.8)) ``` --- # Parser is a SemigroupK ```scala trait Semigroup[A]{ def combine(x: A, y: A): A } trait SemigroupK[F[_]]{ def combineK[A](x: F[A], y: F[A]): F[A] } ``` -- ```scala implicit val parserSemigroupK: SemigroupK[Parser] = ... ``` ```scala val fee = technicalFee.widen[Fee] <+> explorationFee.widen[Fee] ``` ```scala scala> fee.parse(workbook) res62: Either[ParserError,Fee] = Right(ExplorationFee(1.4,5.8)) ``` --- # Bonus: Sequential composition ```scala sealed trait Cost extends Product with Serializable case class Hardcoded(value: List[Double]) extends Cost case class Parametrized(value: Double) extends Cost val cost = boolean("Cost").flatMap{ case true => numericRange("HardcodedCost").map(Hardcoded).widen[Cost] case false => numeric("ParametrizedCost").map(Parametrized).widen[Cost] } ```