I need to define a function f(a: String, g) where g can either be
(x: Int) ?=> Int
Null
and not(x: Int) ?=> Int | Null. Either the function, or null, not a function that might return null.
such that f("test", x + 5) is valid, and f("test", null) is valid, and f("test") is valid as null is a default argument g, but without those latter two cases being interpreted as a x => null function.
but cannot find a way to express this.
I try
def f(a: String, g: ((x: Int) ?=> Int) | Null = null) = ???
val y = f("test", x + 5) // Not found: x
so putting parentheses seems to break the contextual parameters.
I also tried
type Maybe[T] = T | Null
def f(a: String, g: Maybe[(x: Int) ?=> Int] = null) = ???
val y = f("test", x + 5) // Not found: x
scala> type ContextFn = Int ?=> Int
// defined alias type ContextFn = (Int) ?=> Int
scala> val cfn: ContextFn = summon[Int] + 1
val cfn: ContextFn = Lambda/0x00000fe001564410@74a03bd5
scala> type ContextFnOrNull = ContextFn | Null
// defined alias type ContextFnOrNull = ContextFn | Null
scala> val cfnOrNull: ContextFnOrNull = cfn
-- [E172] Type Error: ----------------------------------------------------------
1 |val cfnOrNull: ContextFnOrNull = cfn
| ^
| No given instance of type Int was found for parameter of ContextFn
| Where ContextFn is an alias of: (Int) ?=> Int
1 error found
scala>
I agree this is confusing, however here is the explanation for why this happens:
A variable v of type A ?=> B when present in code will always look for a A in given scope and have type B
When the expected type is C ?=> D, the expression e is automatically preceded by (using C) => e
Example:
val zeroth: String ?=> Int = 4 // desugars to (s: String) ?=> 4
val first = (String) ?=> 4
val second = first // Looks for String in scope, and returns it, second has type Int
val third: String ?=> Int = first // desugars to the following:
// (s: String) ?=> first(using s)
More information can be found here (Context functions on the reference)
In the case the expected type is (String ?=> Int) | null condition 2 does not apply, leaving only condition 1:
We try to resolve the implicit
My advice would be to use option instead:
def f(a: String, g: Option[(x: Int) ?=> Int] = None) = ???
val y = f("test", Some(x + 5))
Generally this means that all unions break context functions? That’s unfortunate. I see no issue with a function whose x may either be a context parameter or an integer, for example.
The Option approach won’t be acceptable as this is a DSL striving for ergonomics and needing to type Some( hundreds of times in a project will make people not want to use the library to begin with (myself included).
f(
g = Some(x + 1),
h = Some(y + 2)
k = Some(z + 3 - 4)
// ...dozens more
)
(in the use case, there are dozens of these parameters per function and dozens of these functions, likely thousands total in a large project.). I did try an implicit conversion into Some, but it looks like that breaks in the same way. It only works if you literally type Some() on the outside.
I had considered this, I’m fine with boilerplate that users never see. But in my use case, there are at least 10 parameters, all optional and independent, which means I would need to implement 1024 overloads to account for all combinations. I could write a script to write them I guess, but could the compiler and LSP handle that many overloads well?