Skip to content

Conversation

@FunFunFine
Copy link

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.

@satorg
Copy link
Contributor

satorg commented Oct 28, 2025

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

@FunFunFine
Copy link
Author

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

@FunFunFine
Copy link
Author

it might be better to
name it something like unitOf[F], which would indicate that's not just a Unit value

@satorg
Copy link
Contributor

satorg commented Oct 29, 2025

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.

@satorg
Copy link
Contributor

satorg commented Oct 29, 2025

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.

@FunFunFine
Copy link
Author

FunFunFine commented Oct 29, 2025

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?

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

Successfully merging this pull request may close these issues.

3 participants