typelevel / typelevel/cats

Crazy Idea: Eliminate the `syntax` imports

Open
#4,057 10 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Scala
Stars
5.5k
Forks
1.2k
Avg merge
2d 10h
Merged PRs (30d)
5

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No files or tests are named. Start with the Scala 3 universally quantified implicit-conversion experiment described in the issue, then investigate the Scala 2 whitebox-macro approach. Done means determining whether the Scala 3 behavior works and documenting whether the proposed Ops/typeclass changes are viable.

Written by the indexing model from the issue text.

Assessment

Tech stack
scala
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.