TL;DR this could be solvable by abstracting over non-value types.
That seems a very good point. In fact, it is somewhat orthogonal to implicits and applies to the reader monad as well, though it matters less there. I think the current syntax should mean option 2 for consistency (I haven’t checked what’s implemented, I’d call 1 a bug), but there is a consistent way (though I’m not 100% happy with it).
Nowadays we can’t abstract over parts of a method type, because method types are not value types, but type alias only support value types.
We know that f1
and f2
aren’t equivalent operationally because of the function-method distinction, while f2
and f3
are equivalent.
def f1(a: A)(b: B): C = foo
def f2(a: A): B => C = b => foo
def f3(a: A): Reader[B, C] = b => foo
type Reader[S, T] = Function1[S, T]
To abstract over parts of a method type, you might consider something like this, with f4
behaving like f1
, ignoring the concrete syntax:
type ImaginedReader[S, T] = (s: S): T
def f4(a: A): ImaginedReader[B, C] = b => foo
I don’t know the concrete syntax for the body of f4
, what I wrote doesn’t make so much sense (trick questions: is s
in scope in foo
? Why is closure syntax not creating a closure?). But let me ignore syntax bikeshedding for this post—also because this problem doesn’t appear in the implicit case.
I’m not sure how this would look in Haskell syntax happens to support this better (I’m not 100% sure this actually works in Haskell, but it makes sense):
type ImaginedReader s t = s -> t
f4: A -> ImaginedReader B C
f4 a b c = foo
Allowing that is a non-trivial extension, but at least it fits in what we understand — the compiler should just eagerly inline such type aliases.
Note 1: I considered having a separate keyword from type
, but couldn’t pick a decent one—I had picked non-value-type
as a strawman but it was so ugly I didn’t like my own idea.
Note 2: “eagerly inline type aliases” is actually very non-trivial since all type definitions in Scala are mutually recursive, so I expect significant implementation restrictions and I’m not sure such aliases could be exported.
Back to @Ichoran’s concern, it could be solved by
type ImplicitReader[S, T] = (implicit s: S): T
// or maybe it's
// type ImplicitReader[S, T] = implicit (s: S): T
// ? I'm not sure, though `implicit` outside parens makes more sense.
def f5: ImplicitReader[Foo, Bar] = f5body // no abstraction here needed, just like the blog post, so fewer syntax questions
Do we want literally that? More brainstorming needed, please, but I think that’s a start.
And it turns out this goes in a similar direction—if one understands type ImplicitTuple3[A, B, C] = ...
as a non-value type alias to be inlined at each use site (which I think gives the effect you want).
A technical problem is that nowadays a parameter block is not a type at all, just part of the syntax of method types, but changing that sounds conceptually plausible. Alternatively, just abstract over the whole binder.
Another technical problem is that various non-value types (such as method types) have not only free type variables but free term variables. So you can write foo1
and foo2
, but Baz
and foo3
aren’t allowed.
def foo1(ctx: Context)(tree: ctx.Tree): ctx.Tree
type Bar = (ctx: Context)(tree: ctx.Tree): ctx.Tree
def foo2: Bar
type Baz(ctx: Context) = (tree: ctx.Tree): ctx.Tree
def foo3(ctx: Context): Baz(ctx)
But you could generally allow type members to abstract over paths, like it happens in DOT — for non-eager types, the value arguments could be erased during compilation. Right now, any such abstraction can only be done by defining Baz
as a class, and it’s less obvious ctx
can be erased there.