Add `unit[F]` syntax to `cats.syntax.applicative`
I am very surprised it's not been proposed before, but searching for something similar in pull request resulted in nothing.
The motivation is that ().pure[F] is used quite often and it's a lot more typing than a simple unit[F] (three symbols more! that's a lot when there are hundreds of them in a project.)
There's IO.unit, Resource.unit, it's even similar to none[A], so this one tries to replicate it, but without a companion object to call this def from.
Thank you for the PR!
But to be honest, I'm not sure it would be a good idea to start bringing unit to the global scope with any Cats syntax import. It can be quite confusing in some cases. Consider this:
import cats.syntax.all.*
def foo[A](o: Option[A]) = ...
foo(none) // very clear
foo(unit) // ???
The important difference between none and unit here is that none is the same as None, just with better type inference.
Whereas, unit is not the same as Unit – it is a Unit value lifted to some context, which can be omitted in certain cases.
Besides, there are ways already to achieve pretty much the same conciseness:
def bar[F[_]](implicit F: Applicative[F]) = {
...
F.unit // arguably even better than `unit[F]`
}
Here F.unit looks even conciser than unit[F] and closer corresponds to the IO.unit and Resource.unit syntax.
Moreover, on Scala3 it can look even better:
def bar[F[_]: Applicative as F] =
...
F.unit
foo(none) == foo(None), while foo(unit) == foo(Some(())) which is not the same thing at all and the difference is quite obvious, is it not?
as for the F.unit suggestion — at my work place I almost never see any implicit arguments like that, we still use scala 2 and heavily rely on context bounds (Tagless Final approach and all that), so it's not really a plausible way, as nobody's going to want to rewrite all class headers for that.
Yet people at my job still complain about using ().pure[F] and other options as they are quite cumbersome, so I wanted to give them a ready-out-of-the-box solution, no imports or anything
it might be better to
name it something like unitOf[F], which would indicate that's not just a Unit value
Also, for what it's worth, I'd like to note that method F.unit itself has a very little value. It is used for two cases mostly: to conclude a chain of effects making it emiting no value, or to shut one of the branches in some conditional expressions. In both cases there are better combinators available in Cats:
one(a).two(b, c).three(d, e, f) *> F.unit
// VS
one(a).two(b, c).three(d, e, f).void
if (condition) sideFffectWithoutValue else F.unit
// VS
sideFffectWithoutValue.whenA(condition)
Those don't cover all possible cases, of course, but quite helpful in many of them.
... at my work place I almost never see any implicit arguments like that, we still use scala 2 and heavily rely on context bounds (Tagless Final approach and all that), so it's not really a plausible way, as nobody's going to want to rewrite all class headers for that.
It shouldn't be a big issue anyway:
class Foo[F[_]: Applicative](...) {
private val F = Applicative[F] // <-- just add this when necessary
def bar(...) = {
...
F.unit
}
}
Moreover, taking into account this experimental (for now) yet pretty expressive feature: https://docs.scala-lang.org/scala3/reference/experimental/typeclasses.html#better-default-names-for-context-bounds
it would be no exaggeration to say that matching context bound names with their types is becoming a de facto standard.
it might be better to name it something like unitOf[F], which would indicate that's not just a Unit value
Cats usually uses upper case letters appended to function names: whenA, ifM, liftF, mapK, etc.
In a case of the "unit" function it could be unitA, I guess.
That said, I personally not a big fan of one letter "modifiers" since they look cryptic. I don't believe short names should be the ultimate goal, especially if it sacrifices clarity.
unitOf would work, I guess, but it still allows obscure syntax to occur: foo(unitOf), since F can be omitted. I would personally prefer a longer yet clearer name like liftedUnit (or whatever), but this style is not somewhat common in Cats.
Also, for what it's worth, I'd like to note that method F.unit itself has a very little value. I
Actually, we use it a bit differently:
- in stubs/mocks/empty instances of TF-like traits
- in pattern matching expressions
Here's a very simplified example for both cases:
trait SendingService[F[_]] {
def send(something: Something): F[Unit]
}
def implementation[F[_]: Monad](actor: Actor[F], log: Log[F]) = new SendingService[F[_]] {
def send(something: Something): F[Unit] =
actor.ask(something).flatMap {
case State(_) => ().pure[F]
case Response(r) => log.info(r)
case Error(err) => new Error(err).raiseError[F, Unit]
}
}
def empty[F[_]: Applicative] = new SendingService[F[_]] {
def send(something: Something): F[Unit] = ().pure[F]
}
We have traits with dozens of methods that we add def empty to.
It shouldn't be a big issue anyway:
it still requires some additional effort which is just not worth it in the end. Copy-pasting ().pure[F] works with the same amount of effort, however shouldn't the library be nice to use it for people?