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?
As a newcomer to Scala, I have been baffled by the interesting discussion about use and misuse of contextual functions, so I acquainted myself with the documentation about it. Still mystified. However, I am familiar with functional programming (from the 1970s-80s), and it seems as if contextual functions are simply a way to implement dynamic binding for what is a statically typed language. So, a function with a context is a way to express the idiom “with environment e, apply fn”, where e is the context. The restrictions on types within the context are then a result of not being able to close types at runtime. I know this is not exactly equivalent, but could anyone explain contextual functions in a way that appeals to an old-fashioned functional mindset? Or is it a completely different concept?
The point of context functions is to allow abstraction that depends on the call-site context. The context itself may be generated statically or dynamically; it doesn’t matter; the type relationships are static, however, so that they can be checked at compile-time. (They might be parameterized, but it has to eventually resolve to reified types.)
It really is “within environment e, treat f(...) as f(c(e), ...)”. There’s never anything you couldn’t have done explicitly; the types are static (though perhaps generic), the runtime content dynamic. But that simple automatic transformation is incredibly powerful; a lot of the time you really do just want a very stereotypical (possibly complex, type-dependent, but stereotypical) extra input so that f can operate both generically and conveniently.
It is much less of dynamic binding than is, say OOP with vtables. With OOP, context is lost–that in fact is the point, to abstract over context by encapsulating it and then hopefully being able to forget it ever existed.
I view context functions as a kind of opposite: rather than encapsulating and forgetting, you explicitly take the context as a parameter at the definition site, but let the compiler fill in the details at use-site unless you want to intervene. Rather than data-hiding, it is data-exposing, except you’re not burdened by it unless/until you need to be.
given MyType = myInstance
def foo(using bar: MyType) = foo
foo
Behaves exactly the same as
def foo(bar: MyType) = foo(bar)
foo(myInstance)
And this is the case for any usage of implicits, no matter how complex
This is very similar to how List(1, 2) is the same as List[Int](1, 2) (type inference)
Which explains why this feature is sometimes called term inference