Scala 3 macros for converting multi-parameter functions into functions with named parameters or named-tuple arguments, preserving the original parameter names.
import namedfunctions.syntax.*
def greet(name: String, age: Int): String = s"$name is $age"
// .named — named parameters at call site
val f = greet.named
f(name = "Alice", age = 30) // "Alice is 30"
// .namedTupled — function from named tuple
val g = greet.namedTupled
g((name = "Alice", age = 30)) // "Alice is 30"
// .namedUntupled — reverse of tupling
val h: ((name: String, age: Int)) => String = t => s"${t.name} is ${t.age}"
h.namedUntupled(name = "Alice", age = 30) // "Alice is 30"
// .nameChecked — compile-time name validation, order-independent
val age = 30
val name = "Alice"
greet.nameChecked(age, name) // "Alice is 30" — reordered automatically
// greet.nameChecked(age, x) // compile error: unexpected: x; missing: name
// .applyProduct — apply from case class fields by name
case class Person(age: Int, name: String, email: String)
greet.applyProduct(Person(30, "Alice", "[email protected]")) // "Alice is 30" — extra fields ignoredAvailable on Maven Central as org.polyvariant::named-functions.
In scala-cli:
//> using dep org.polyvariant::named-functions::<version>In sbt:
libraryDependencies += "org.polyvariant" %% "named-functions" % "<version>"In Mill:
ivy"org.polyvariant::named-functions:<version>"import namedfunctions.syntax.*
def foo(entityId: Int, userId: String): Boolean = ???
// Wrap a function so that its parameters are named at the call site
val f = foo.named
f(entityId = 1, userId = "hello")
// Convert a function into a Function1 from a named tuple
val g = foo.namedTupled
g((entityId = 1, userId = "hello"))
// Convert a Function1 from a named tuple back into a named-parameter function
val h: ((entityId: Int, userId: String)) => Boolean = ???
val f2 = h.namedUntupled
f2(entityId = 1, userId = "hello")Methods with multiple parameter lists are supported — the result is a curried function with named parameters at each level:
def bar(entityId: Int)(userId: String): Boolean = ???
val f = bar.named
f(entityId = 1)(userId = "hello")
val g = bar.namedTupled
g((entityId = 1))((userId = "hello"))Converts a multi-parameter function (a: A, b: B, ...) => R into a function with named parameters, preserving the original parameter names.
Like .tupled but the resulting tuple type carries the parameter names from the original method. Converts a multi-parameter function into a Function1 from a named tuple: ((a: A, b: B, ...)) => R.
The reverse of tupled. Converts a Function1 from a named tuple into a multi-parameter function with named parameters.
Compile-time check that argument variable names exactly match the function's parameter names. Arguments are matched by name and automatically reordered, so order doesn't matter — only that the names are correct:
def foo(a: Int, b: String): String = s"$a-$b"
val a = 42
val b = "hello"
foo.nameChecked(a, b) // compiles — "42-hello"
foo.nameChecked(b, a) // also compiles — reordered to "42-hello"
foo.nameChecked(b, x) // compile error: unexpected: x; missing: a
foo.nameChecked("hello") // compile error: requires variable references or field accessesArguments can be plain variable references or field accesses (e.g. obj.field). The last segment of the access is used as the name. Multiple parameter lists are supported — all arguments are passed flat:
def bar(entityId: Int)(userId: String): Boolean = ???
val entityId = 1
val userId = "hello"
bar.nameChecked(entityId, userId)Field accesses work too — the field name is what matters:
case class Source(a: Int, b: String)
val src = Source(42, "hello")
foo.nameChecked(src.a, src.b) // compiles — "42-hello"Applies a function using fields from a case class, matched by name (not position). The case class may have extra fields, but all function parameters must be present:
def foo(a: Int, b: String): String = s"$a-$b"
case class Params(b: String, a: Int)
foo.applyProduct(Params("hello", 42)) // "42-hello" — fields matched by name
case class Extended(a: Int, b: String, extra: Boolean)
foo.applyProduct(Extended(1, "hi", true)) // works — extra fields ignoredMultiple parameter lists are supported:
def bar(entityId: Int)(userId: String): Boolean = ???
case class Params(entityId: Int, userId: String)
bar.applyProduct(Params(1, "hello"))Features can be composed — the output of one transformation is a valid input for another:
def foo(a: Int, b: String): String = s"$a-$b"
case class Params(b: String, a: Int)
foo.named.applyProduct(Params("hello", 42)) // "42-hello"
val a = 42
val b = "hello"
foo.named.nameChecked(a, b) // "42-hello"
// Round-trip through tupling and back
foo.namedTupled.namedUntupled.applyProduct(Params("hello", 42)) // "42-hello"All features require the macro to extract parameter names from the call site's AST. This works with method references (obj.method), eta-expanded methods, and case class constructors (Foo.apply), but not with function values stored in a val:
val f = (a: Int, b: String) => s"$a-$b"
f.named // compile error: Could not extract parameter names
f.nameChecked(a, b) // same
f.applyProduct(p) // sameThe parameter names exist in the lambda at the definition site, but are erased by the time the val reference reaches the macro.