True and false-because

I have a general question about a particular coding style. Sometimes I’d like to write code which expresses what I expect, but when the expectation fails, THEN I wish the code was in the form of describing what I DONT expect. I.e the code describes the SUCCESS case, not the FAIL case.

I have an example of this coding style here declarative coding style

The problem is that when the code fails, I don’t know which actual values were discovered which violate the assumption. This is because the assertion is very far (code-wise) from the boolean expression which discovered the issue.

In the Scastie example, I have code which decides whether a particular set is a monoid, or is a semigroup, or is a group. And then I bombard the decision procedure with some sets and discover something that is not a group. But this tells me nothing about WHY it is not a group.

This coding style is very similar to property based testing, but I’m not trying to test code for correctness, I’m trying to do something more general.

The workhorse functions currently return Boolean true or false, but I think what I’d rather do is return a different kind of Boolean, which would be true or false because something, i.e. imbed additional information into the false case. But such an object would not work with forall and would not work with &&.

Is there a way to do this without converting my code to use negative logic?

If I define a monad TrueOrFalseBecause, is there some hope of making gen().forall(p) or some cats cousin thereof iterate until the p returns FalseBecause. I could write my own forall function but it would not be a method on LazyList[Int].

The code currently looks like this:

    gen().forall { a =>
      gen().forall { b =>
        member(op(a, b))
      }
    }

Creating a custom data type (and integrating it with whatever eco system - stdlib collections, Cats,…) certainly is a viable option. However, depending on your requirements, piggy-backing on what’s already there might go a long way. E.g. using Either:

type TrueOrFalseBecause[T] = Either[String, T]

object TrueOrFalseBecause:
  def cond[T](
      pred: T => Boolean, because: T => String)(
      v: T
  ): TrueOrFalseBecause[T] =
    Either.cond(pred(v), v, because(v))

def even(i: Int): TrueOrFalseBecause[Int] = ???
def greaterTen(i: Int): TrueOrFalseBecause[Int] = ???
def evenAndGreaterTen(i: Int): TrueOrFalseBecause[Int] = 
  even(i).flatMap(greaterTen)

List(2, 3, 12).map(evenAndGreaterTen).foreach(println)
// Left(2 is not greater than 10)
// Left(3 is not even)
// Right(12)

Then, bringing in Cats:

import cats.syntax.all.*

def forAll[T](
    pred: T => TrueOrFalseBecause[T])(
    vs: List[T]
): TrueOrFalseBecause[Unit] =
  vs.traverse_(pred)

println(forAll(evenAndGreaterTen)(List(2, 3, 12)))
// Left(2 is not greater than 10)

println(forAll(evenAndGreaterTen)(List(12, 20, 100)))
// Right(())

thanks, this is moving the the right direction. But how does it extend to something like
gen().forall{a => gen().forall{b => a*b > 10}}

Instead of putting the value itself back in the either, you can use an Either[ErrorType, Boolean], which allows you to use the forallM method from cats.Traverse (basically forall, but taking a function A => G[Boolean] and returning G[Boolean], where G is a Monad). With that, you can nest exactly like with normal forall:

gen().forallM(a =>
  gen().forallM(b =>
    if (a * b > 10) Right(true)
    else Left("product too small")
  )
)

Edit: You can even use Either[String, true], so that there can be no false without an error message, but at least in Scala 2 this requires explicitly setting forallM’s G[_] parameter:

2 Likes

Many ways to skin a cat…

Naively using my draft as is with nested raw #traverse_() (#forAll() as written is too restrictive):

def productGreaterTen(p: (Int, Int)): TrueOrFalseBecause[(Int, Int)] =
  TrueOrFalseBecause.cond[(Int, Int)](
    p => p(0) * p(1) > 10,
    p => s"${p(0)} * ${p(1)} not > 10")(
    p
)

def allProductGreaterTen(
    as: List[Int],
    bs: List[Int]
): TrueOrFalseBecause[Unit] =
  as.traverse_(i =>
    bs.traverse_(j => productGreaterTen(i -> j))
  )

println(allProductGreaterTen(List(3, 4, 5), List(3, 4, 5)))
// Left(3 * 3 not > 10)

println(allProductGreaterTen(List(3, 4, 5), List(4, 5, 6)))
// Right(())

…or use the applicative/monadic nature of your generator type and generate pairs to start with:

def allProductGreaterTen2(
    as: List[Int],
    bs: List[Int]
): TrueOrFalseBecause[Unit] =
  forAll(productGreaterTen)((as, bs).mapN(_ -> _))

println(allProductGreaterTen2(List(3, 4, 5), List(3, 4, 5)))
// Left(3 * 3 not > 10)

println(allProductGreaterTen2(List(3, 4, 5), List(4, 5, 6)))
// Right(())

…or what @crater2150 says.

…or something else. :slight_smile:

This is a good move forward. Is there something special about Either or could I implement my own TrueOrFalseBecause, with its own flatMap, and still use forallM?

What about situations like. isClosed() && isSemiGroup() && isAbelian() when those functions return an Either or a TrueOrFalseBecause ?

to implement isClosed() && isSemiGroup() && isAbelian(), I see that Either has a method orElse method but I don’t see an andAlso method or andThen method.

If I try to use Either[String,true] I get a compile error I don’t understand.

  def isClosed(): Either[String,true] = {
    gen().forallM { a =>
      gen().forallM { b =>
        member(op(a, b)) match {
          case Right(true) => Right(true)
          case Left(str) => Left(s"not closed because op($a,$b) $str")
        }
      }
    }
  }

The compiler claims:

type mismatch;
 found   : scala.util.Either[String,Boolean]
 required: Either[String,true]
    gen().forallM { a =>

Is this my fault, or a compiler limitation?

I used Either for the example, because it already has a monad instance, but you can of course create your own monad. forallM just requires functions to return any G[Boolean] with an implicit cats.Monad[G]. It is defined on the Traverse typeclass

What would andThen do? If you just want to join several methods returning Either together and get the first error or true, you can use flatMap (or a for comprehension) and just ignore the function’s parameter.

That’s what I meant by

The 2.13 compiler doesn’t seem to be able to infer forallM’s type parameter itself when using a literal type (true), so you have to use

type EitherString[A] = Either[String, A]
gen().forallM[EitherString] { a =>
  ...

(or the equivalent kind-projector type lambda). In Scala 3, type inference is improved enough that it works without that.

  def andM(a:Either[String,true],
           b:Either[String,true]):Either[String,true] = {
    a.flatMap(_ => b)
  }

seems to me like if Either has orElse it ought to have the dual method andThen. Oh well.

I’m not quite seeing yet.
my attempt
I tried to apply what you suggested, but even giving forallM the type parameter (as I understand it),
it still fails to compile.

type mismatch;
 found   : scala.util.Either[String,Boolean]
 required: Either[String,true]
    gen().forallM { a =>
  def isClosed(): TrueOrFalseBecause = {
    gen().forallM[EitherStr] { a =>
      gen().forallM[EitherStr] { b =>
        member(op(a, b)) match {
          case Right(true) => Right(true)
          case Left(str) => Left(s"not closed because op($a,$b), $str")
        }
      }
    }
  }

Ah, I see the problem. Passing the explicit type widens the result of forallM to Either[String, Boolean], so it isn’t a TrueOrFalseBecause anymore, as it could contain Right(false). This also happens with Scala 3 when it infers the type. I’m afraid my idea of using the literal type to disallow Right(false) won’t work with forallM.

Maybe you can solve it with a custom monad that doesn’t contain a value for it’s variant of Right. I’ll have to think about, if that is possible without it being unlawful.

I think seeing a correct TrueOrFalseBecause implementation would be enlightening for me.

From the call-site it would certainly express the intent better than Right and Left.

The idea works in general.
Here is the code, factoring away the Cats stuff.
I’ve basically written my own foreachM, and andM and assertM functions.
functional approach

The code fails on an assert, and I can hover the mouse over the error (in scastie) and see the explanation with the embedded counter-examples.

A new thing I realized while working on this code a couple of days is that if I add a datum to the True case also, then I can implement logical inversion. If True is atomic, then the inverse of the inverse cannot recuperate the datum. So it basically needs to be something isomorphic to Either[String,String].

One very nice side consequence is that I can implement existsM as a wrapper around forallM, which is pretty elegant. link on gitlab

  def forallM[T](items: LazyList[T], p: T => TrueOrFalseBecause): TrueOrFalseBecause = {
    def loop(acc: TrueOrFalseBecause, tail: LazyList[T]): TrueOrFalseBecause = {
      if (tail.isEmpty)
        acc
      else acc match {
        case False(_) => acc ++ s"example ${tail.head}"
        case True(_) =>
          loop(p(tail.head), tail.tail)
      }
    }

    loop(True(""), items)
  }

  def existsM[T](items: LazyList[T], p: T => TrueOrFalseBecause): TrueOrFalseBecause = {
    !(forallM[T](items, x => !(p(x))))
  }

If the right side doesn’t contain any more information than “right is ok” I would go with Either[Error, Unit]. Then you simply do result.isRight to test for truth.

Yes, the two types are isomorphic. Do you think Left and Right and swap and isRight express the intent of False, True, ! and toBoolean ?

To me, using the fact that the types are isomorphic and therefore using unintuitive names makes the code cryptic. Maybe that’s just me?

I while back I had a project where I needed to represent true, false and don't-know, so I used Optional[Boolean]. This choice made some code elegant because I can use all the methods already defined in Option; however, some things are ugly, such as obj.contains(false), obj.map( x => !x), obj.isEmpty which do not at all express the intent in language the human reader understands.

#swap() on an Either[String, Unit] will yield an Either[Unit, String], an entirely different type. As for Either[String, String], I’d be confused about e.g. the semantics of Right("5 is not greater than 10").

Either[String, T] would represent the result of a (validation) computation that was either successful and yields the validated value, or a validation failure. It seems that you are rather looking for a truth value data type that somehow extends Boolean and is independent of its origin. I’m not sure whether Either is a good match for this.

You can always add extension methods.

type HeisenBool = Option[Boolean]

extension (hb: HeisenBool)
  def negated: HeisenBool = hb.map(!_)
  def isDefinitely(b: Boolean): Boolean = hb.contains(b)
  def force(default: Boolean): Boolean = hb.getOrElse(default)
  // ...

…but what API do you need for such a type, other than the (extended) boolean algebra? Do you really need Option-like functionality at all? I’d be tempted to create a custom enum for this use case.

I agree that forcing the use of Right and Left into the code just because it could be made to work, is probably not such a great idea.

On the other hand, I don’t think it would be confusing if you asked: does there exists an x such that x is not greater than 10. Depends on the question being asked.
However, I do think that using Right and Left to mean Works vs Failed is confusing.