CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-chuusai--shapeless-2-11

An exploration of generic (aka polytypic) programming in Scala derived from implementing scrap your boilerplate and higher rank polymorphism patterns

Pending
Overview
Eval results
Files

lift.mddocs/

Function Lifting

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.

Core Functionality

LiftO - Lifting into Option

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))
// None

Arity Support

The liftO function works with functions of any arity from 0 to 22:

Nullary Functions

import shapeless._

val constant: () => String = () => "fixed"
val constantOption = Lift.liftO(constant)  // () => Option[String]

val result = constantOption()  // Some("fixed")

Unary Functions

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)      // None

Higher Arity Functions

import 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))
// None

Advanced Usage Patterns

Lifting Complex Operations

import 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)
// None

Pipeline Operations

import 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)

Working with Case Classes

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))

Integration with Validation

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)

Implementation Details

The liftO function uses several type classes internally:

  • FnHLister: Converts function to HList function
  • Mapper: Maps get over Option HList to extract values
  • MapFolder: Folds over Option HList with isDefined to check all are present
  • FnUnHLister: Converts HList function back to regular function

This 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

docs

conversions.md

generic.md

hlist.md

hmap.md

index.md

lift.md

nat.md

poly.md

records.md

sized.md

sybclass.md

typeable.md

typeoperators.md

tile.json