Type a parameter as either a contextual function | Null

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

This is confusing indeed:

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:

  1. A variable v of type A ?=> B when present in code will always look for a A in given scope and have type B
  2. 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))
3 Likes

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.

In that case, maybe overloading is the solution ?


def f()

def f(g: (x: Int) ?=> Int)

def f(g: (x: Int) ?=> Int, h: (y: Int) ?=> Int)

IIRC, in OPs case that would be a lot of boilerplate.

But, TBH, maybe the best solution for their real use case is some form of macro that generates that boilerplate.

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?

I’m not sure ^^’