Skip to content

Add Nullable type to cats data with instances#4823

Open
johnynek wants to merge 4 commits intomainfrom
oscar/20260216-nullable
Open

Add Nullable type to cats data with instances#4823
johnynek wants to merge 4 commits intomainfrom
oscar/20260216-nullable

Conversation

@johnynek
Copy link
Contributor

closes #4822

The idea is to be able to use nullable types from java (or even scala for unboxed optionality) but do so fully safely, so we can't confuse the nullable type with a non-nullable type.

Note: as suspected in #4822 both Monad and Applicative are not lawful. So instances were not added (someone could add them to alleycats, but I'm not motivated to add non-lawful instances).

For those cases, the user should be able to use .fold which is as efficient as a hand written if/else with casting and can express all the operations you want to do.

The functor methods are implemented which could be more efficiently done than with map and we test against the default implementations.

import scala.language.strictEquality
import scala.quoted.*

opaque type Nullable[+A] = A | Null
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could make Nullable[+A] >: A to allow passing an A where a Nullable is being accepted. That said, you would be adding a bunch of no-op if checks to your code by doing so, and I tend to think the cost of calling Nullable(a) for such cases isn't a bad idea.

Maximum convenience isn't the goal here. Maximum correctness with zero-cost is my goal.

Comment on lines +51 to +55
given [A](using A: Arbitrary[A]): Arbitrary[Nullable[A]] =
Arbitrary(Gen.oneOf(Gen.const(Nullable.empty[A]), A.arbitrary.map(Nullable(_))))

given [A](using A: Cogen[A]): Cogen[Nullable[A]] =
Cogen[Option[A]].contramap(_.toOption)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, these instances should be defined in cats-laws module, object cats.laws.discipline.arbitrary.

However, since it is for Scala 3 only, it may require adding some tweaks to the file structure in that module.

}
}

test("option-like Applicative is not lawful for nested Nullable") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hate to break it to you, but Functor[Nullable] seems to be unlawful for nested Nullable too:

// These cases all pass:
checkAll("Option[Option[*]]", FunctorTests[Option].functor[Option[Int], Option[Int], Option[Int]])
checkAll("Option[Nullable[*]]", FunctorTests[Option].functor[Nullable[Int], Nullable[Int], Nullable[Int]])
checkAll("Nullable[Option[*]]", FunctorTests[Nullable].functor[Option[Int], Option[Int], Option[Int]])

// But this one fails:
checkAll("Nullable[Nullable[*]]", FunctorTests[Nullable].functor[Nullable[Int], Nullable[Int], Nullable[Int]])

The latter fails on "invariant composition" and "covariant composition".

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.

A Nullable type for scala3

2 participants