An exploration of generic (aka polytypic) programming in Scala derived from implementing scrap your boilerplate and higher rank polymorphism patterns
—
The Lift module in shapeless provides utilities for lifting ordinary functions of arbitrary arity into various contexts, such as Option. This enables functional programming patterns where operations can safely handle absent values.
object Lift {
/**
* Lifts a function of arbitrary arity into Option context
* All arguments must be Some for function to be applied
*/
def liftO[InF, InL <: HList, R, OInL <: HList, OutF](f: InF)
(implicit
hlister: FnHListerAux[InF, InL => R],
mapper: MapperAux[get.type, OInL, InL],
folder: MapFolder[OInL, Boolean, isDefined.type],
unhlister: FnUnHListerAux[OInL => Option[R], OutF]
): OutF
}The liftO function transforms regular functions to work with Option types, automatically handling the case where any argument is None.
Usage Examples:
import shapeless._
// Lift binary function
val add: (Int, Int) => Int = _ + _
val addOption = Lift.liftO(add) // (Option[Int], Option[Int]) => Option[Int]
val result1 = addOption(Some(5), Some(3)) // Some(8)
val result2 = addOption(Some(5), None) // None
val result3 = addOption(None, Some(3)) // None
// Lift ternary function
val combine: (String, Int, Boolean) => String =
(s, i, b) => s"$s-$i-$b"
val combineOption = Lift.liftO(combine)
// Type: (Option[String], Option[Int], Option[Boolean]) => Option[String]
val combined1 = combineOption(Some("hello"), Some(42), Some(true))
// Some("hello-42-true")
val combined2 = combineOption(Some("hello"), None, Some(true))
// NoneThe liftO function works with functions of any arity from 0 to 22:
import shapeless._
val constant: () => String = () => "fixed"
val constantOption = Lift.liftO(constant) // () => Option[String]
val result = constantOption() // Some("fixed")import shapeless._
val double: Int => Int = _ * 2
val doubleOption = Lift.liftO(double) // Option[Int] => Option[Int]
val doubled1 = doubleOption(Some(21)) // Some(42)
val doubled2 = doubleOption(None) // Noneimport shapeless._
// Quaternary function
val process4: (String, Int, Double, Boolean) => String =
(s, i, d, b) => s"$s:$i:$d:$b"
val process4Option = Lift.liftO(process4)
// Type: (Option[String], Option[Int], Option[Double], Option[Boolean]) => Option[String]
val result = process4Option(Some("test"), Some(1), Some(2.5), Some(false))
// Some("test:1:2.5:false")
// Missing any argument results in None
val resultNone = process4Option(Some("test"), None, Some(2.5), Some(false))
// Noneimport shapeless._
// Complex business logic function
case class User(name: String, age: Int)
case class Product(name: String, price: Double)
case class Order(user: User, product: Product, quantity: Int)
val createOrder: (User, Product, Int) => Order = Order.apply
val createOrderOption = Lift.liftO(createOrder)
// Type: (Option[User], Option[Product], Option[Int]) => Option[Order]
val user = Some(User("Alice", 30))
val product = Some(Product("Widget", 19.99))
val quantity = Some(2)
val order = createOrderOption(user, product, quantity)
// Some(Order(User("Alice", 30), Product("Widget", 19.99), 2))
// Missing user information
val failedOrder = createOrderOption(None, product, quantity)
// Noneimport shapeless._
// Chain lifted functions
val parseInput: String => Int = Integer.parseInt
val validatePositive: Int => Int = i => if (i > 0) i else throw new Exception("negative")
val doubleValue: Int => Int = _ * 2
val formatOutput: Int => String = i => s"Result: $i"
// Lift each function
val parseInputOption = Lift.liftO(parseInput) // Option[String] => Option[Int]
val validatePositiveOption = Lift.liftO(validatePositive) // Option[Int] => Option[Int]
val doubleValueOption = Lift.liftO(doubleValue) // Option[Int] => Option[Int]
val formatOutputOption = Lift.liftO(formatOutput) // Option[Int] => Option[String]
// Create pipeline (in practice you'd handle exceptions properly)
def safePipeline(input: Option[String]): Option[String] = {
val parsed = parseInputOption(input)
val validated = parsed.flatMap(i => try { Some(validatePositive(i)) } catch { case _ => None })
val doubled = doubleValueOption(validated)
formatOutputOption(doubled)
}
val result1 = safePipeline(Some("21")) // Some("Result: 42")
val result2 = safePipeline(Some("-5")) // None (validation fails)
val result3 = safePipeline(Some("abc")) // None (parsing fails)
val result4 = safePipeline(None) // None (no input)import shapeless._
case class Point3D(x: Double, y: Double, z: Double)
case class Vector3D(x: Double, y: Double, z: Double)
val createPoint: (Double, Double, Double) => Point3D = Point3D.apply
val createVector: (Double, Double, Double) => Vector3D = Vector3D.apply
val createPointOption = Lift.liftO(createPoint)
val createVectorOption = Lift.liftO(createVector)
// Safe construction from potentially missing coordinates
def safePoint(x: Option[Double], y: Option[Double], z: Option[Double]): Option[Point3D] =
createPointOption(x, y, z)
def safeVector(x: Option[Double], y: Option[Double], z: Option[Double]): Option[Vector3D] =
createVectorOption(x, y, z)
val point = safePoint(Some(1.0), Some(2.0), Some(3.0)) // Some(Point3D(1.0, 2.0, 3.0))
val invalidPoint = safePoint(Some(1.0), None, Some(3.0)) // None
// Vector operations
val addVectors: (Vector3D, Vector3D) => Vector3D =
(v1, v2) => Vector3D(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z)
val addVectorsOption = Lift.liftO(addVectors)
val v1 = Some(Vector3D(1, 2, 3))
val v2 = Some(Vector3D(4, 5, 6))
val sum = addVectorsOption(v1, v2) // Some(Vector3D(5.0, 7.0, 9.0))import shapeless._
// Validation functions
def validateEmail(email: String): Option[String] =
if (email.contains("@")) Some(email) else None
def validateAge(age: Int): Option[Int] =
if (age >= 0 && age <= 120) Some(age) else None
def validateName(name: String): Option[String] =
if (name.nonEmpty) Some(name) else None
// User creation function
case class ValidUser(name: String, email: String, age: Int)
val createUser: (String, String, Int) => ValidUser = ValidUser.apply
val createUserOption = Lift.liftO(createUser)
// Safe user creation with validation
def createValidUser(name: String, email: String, age: Int): Option[ValidUser] = {
val validName = validateName(name)
val validEmail = validateEmail(email)
val validAge = validateAge(age)
createUserOption(validName, validEmail, validAge)
}
val user1 = createValidUser("Alice", "alice@example.com", 30) // Some(ValidUser(...))
val user2 = createValidUser("", "alice@example.com", 30) // None (empty name)
val user3 = createValidUser("Alice", "invalid-email", 30) // None (bad email)
val user4 = createValidUser("Alice", "alice@example.com", -5) // None (invalid age)The liftO function uses several type classes internally:
get over Option HList to extract valuesisDefined to check all are presentThis enables the lifting to work generically across functions of any arity while maintaining type safety.
Function lifting provides a powerful way to work with potentially absent values in a compositional manner, enabling robust and safe functional programming patterns.
Install with Tessl CLI
npx tessl i tessl/maven-com-chuusai--shapeless-2-11