Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crazy Idea: Eliminate the syntax imports #4057

Open
djspiewak opened this issue Nov 29, 2021 · 10 comments
Open

Crazy Idea: Eliminate the syntax imports #4057

djspiewak opened this issue Nov 29, 2021 · 10 comments

Comments

@djspiewak
Copy link
Member

Okay hear me out…

def foo[F[_]: Functor](fa: F[Int]) = fa.map(_ + 1)

This only works if we have the import cats.syntax.all._ (or one of the variations thereof) in scope. Why is that exactly? It's because we need an implicit conversion F[Int] => FunctorOps[F, Int], and that exists within the FunctorSyntax trait which is folded into AllSyntax, etc etc etc. Getting an implicit conversion into scope requires an implicit value of type Function1 with the appropriately matching types, and that definitely feels like something that's going to require an import one way or another.

But we're overlooking something: we already have an implicit instance in scope, by definition: Functor itself. If we could define Functor[F] <: (F[Int] => FunctorOps[F, Int]), then this would actually solve the problem! The compiler would consider the implicit Function1 value to be a valid implicit conversion, thus surfacing the map syntax and allowing us to proceed forward.

Unfortunately, the way I wrote the types above already hints at the problem: we don't want this for F[Int], but rather F[A] for all A. Scala 3 at least has a syntax for expressing this: Functor[F] <: ([A] ==>> F[A] => FunctorOps[F, A]). Scala 2 has no such syntax, and honestly I'm not sure if Scala 3 is even going to behave correctly in this scenario. We should find out!

So there are two experiments here. First, try this out on Scala 3 and see if it works properly. If it does, then we can probably build some parallel subtyping in the Ops hierarchy to make it possible to refine the inheritance, allowing each typeclass to extend the conversion to its corresponding syntax. This might run into problems with MonadError and a few other cases, but we can probably work around that. If Scala 3 resolves this correctly as a universally-quantified implicit conversion, then we have something that at least is workable in https://github.com/typelevel/spotted-leopards (heads up @mpilquist).

The Scala 2 side of the house will be a lot more challenging. Realistically this is going to require some sort of weird implicit whitebox macro which materializes an implicit Function1 which is specialized to the appropriate call-site type. That feels vaguely doable, but it's going to need some poking and prodding.

As a final note, this obviously only works when being entirely parametric (since this is when you're explicitly putting the implicit value into scope). Cases like 1 |+| 2 are still going to require a syntax import, barring anything really miraculous. Most notably, so will traverse and a few other really common operators. Even still, I think this could be a real step forward.

@mpilquist
Copy link
Member

def foo[F[_]: Functor](fa: F[Int]) = fa.map(_ + 1)

This already works in Scala 3 without an import thanks to this:

The extension method is a member of some given instance that is visible at the point of the reference.

From: https://docs.scala-lang.org/scala3/reference/contextual/extension-methods.html

@joroKr21
Copy link
Member

I think another approach is to put an implicit def Functor[F[_], A](fa: F[A]): FunctorOps[F, A] conversion in package cats so that when you import Functor, you also import the implicit conversion syntactically.

@djspiewak
Copy link
Member Author

I think another approach is to put an implicit def Functor[F[_], A](fa: F[A]): FunctorOps[F, A] conversion in package cats so that when you import Functor, you also import the implicit conversion syntactically.

Doesn't that only work if you import cats._ though?

@joroKr21
Copy link
Member

joroKr21 commented Nov 29, 2021

I think it doesn't work because it causes confusion with object Functor's apply method.
I.e. old usages of Functor[Foo] break

@djspiewak
Copy link
Member Author

I think it doesn't work because it causes confusion with object Functor's apply method

Oh I see you were relying on the name collision. Yeah I think that it straight-up conflicts with the companion object, even ignoring apply.

@johnynek
Copy link
Contributor

I think this is too clever personally...

I think the import isn't such a tax and I don't see a good trade in complicating the typeclasses to enable this hack.

There is also the issue that implicit conversion via function is actually on the way out, and instead we would need to extend Conversion I think, and also, I don't know how (if?) that is going to work with a universally quantified type which we want here.

@djspiewak
Copy link
Member Author

There is also the issue that implicit conversion via function is actually on the way out, and instead we would need to extend Conversion I think, and also, I don't know how (if?) that is going to work with a universally quantified type which we want here.

([A] ==>> Functor[F] => FunctorOps[F, A]) with Conversion :trollface:

@armanbilge
Copy link
Member

In fact, using (not defining) Conversion in Scala 3 requires an import:

import scala.language.implicitConversions

So, um, what's your grand plan for that one? :P

@djspiewak
Copy link
Member Author

([A] ==>> Functor[F] => FunctorOps[F, A]) with Conversion with scala.language.implicitConversions.type

Somebody hold me back, I'm on a roll!

………

Okay this is probably a dumb idea.

@mpilquist
Copy link
Member

FYI more reading here: typelevel/spotted-leopards#9 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants