How to make a wrapper class comprehension-compliant?


#1

Hello,

I have the following wrapper class:

sealed trait Val[V] {
    val v: V

    override def toString: String = s"Val($v)"

    override def equals(obj: scala.Any): Boolean = {
      if (obj.isInstanceOf[Val[V]]){
        val other = obj.asInstanceOf[Val[V]]
        v.equals(other.v)
      }
      else
        false
    }
  }

  object Val {
    def apply[T](e:T): Val[T] = new Val[T] {
      override val v:T = e
    }
  }

And I can implicitly wrap and unwrap using these:

  implicit def pack[A](s:A) : Val[A] = new Val[A] {
    override val v: A = s
  }

  implicit def unpack[T](t:Val[_]): Either[String,T] = {
    try {
      val v = t.v.asInstanceOf[T]
      Right(v)
    } catch {
      case e: Exception => Left(e.getMessage)
    }
  }

So currently I can pack/unpack and use Val so:

    val v1: Val[String] = "100.0"
    val v2: Val[Double] = 200.0
    val v3: Val[Int] = 300

    val s1:Either[String,String] = v1
    val s2:Either[String,Double] = v2
    val s3:Either[String,Int] = v3

    val r: Either[String, Double] = for {
      t <- s1
      t1 = t.toDouble
      t2 <- s2
      t3 <- s3
      ts = t1 + t2 + t3
    } yield ts
    r shouldBe 'right
    r.right.get shouldBe 600.0

However I would like to use it so:


    val ri = for {
      v1t:String <- v1
      v1d = v1t.toDouble
      v2d:Double <- v2
      v3d:Int <- v3
      ts = v1d + v2d + v3d
    } yield ts
    ri shouldBe 'right
    ri.right.get shouldBe 600.0

Now this does not compile because Val lacks the withFilter member.
I therefore need to implement the scala.collection.generic.FilterMonadic
interface. I have made several attempts and have also looked at Scala’s
Option in order to use that as a reference. However I have no success.

Can anyone give me hints or point me to an example that
implements these so that the above for-comprehension works
(assuming this is possible)?

    def map[R](f: V => R): R = f(v)
    def flatMap[R](f: V => Val[R]): Val[R] = f(v)
    def filter(p: V => Boolean): ??? = ???
    def withFilter(p: V => Boolean): ??? = ???

TIA


#2

Hmm. This doesn’t look right:

 def map[R](f: V => R): R = f(v)

That should be returning Val[R] instead, IIRC. map() is conceptually unwrap -> process -> rewrap.

(The definition of flatMap looks suspiciously simple, but seems correct at this glance.)

You can and should ignore filter – that’s deprecated – and focus instead on withFilter. That really gets to the problem here, though: I’m not certain that withFilter makes sense on Val.

That FilterMonadic is typically satisfied by returning an instance of WithFilter. (Here’s an example from an obscenely-overcomplicated Monad I wrote a few years back.) You need to fill in the map and flatMap functions there with results that filter out the answers that don’t match the predicate.

But the rub is, you need a concept of “empty” in order to do that, and you don’t yet have an empty Val. That is, when you “filter out” the contents of the Val, what do you have left?

Keep in mind, withFilter is not strictly required – you can write many for comprehensions without it. But as soon as you introduce the concept that this comprehension can result in an empty value – using an if or doing type filtering as you are showing – then you need a concept of the “empty” Monad. (I suspect it’s those type ascriptions on the left-hand side of your for that are introducing the problem. Does it compile without them?)

Basically, I believe you need something conceptually equivalent to None in order to have filtering – some way of expressing the notion that nope, nothing got through here.

This does come up from time to time – some monads just don’t have a clear concept of “empty”. There’s even a compiler plugin that changes the way for works to not use withFilter, so that you can use for with these monads.

I’m not sure what the right answer for you is, but I think you either need a clear concept of an empty Val that can come out of a failed withFilter operation, or you need to stick to a subset of for comprehensions.


#3

Everything @jducoeur said is correct.
But if filtering is meaningless for your datatype and you really want a withFilter method, you could consider

def withFilter(p: V => Boolean): this.type = this

or

def withFilter(p: V => Boolean): this.type = 
  if (p(v)) this else sys.error("predicate failed")

Although I don’t know if it’ll actually help you in the places where you expect to need those type ascriptions.


#4

@jducoeur

Appreciate the feedback.

You are correct. Will change it to return a Val[R].

Ok. Was not aware of this.

You nailed it. When I looked at the Option code filtering makes sense. However I have many Val
(created anonymously), so no None-like class. However, reading @Jasper-M’s response gave me
an idea: I will use a ValErr(String) to represent an Empty.

Going to have a another crack at it.

Thanks.


#5

@Jasper-M

Thanks for the feedback. I think I will use something like your second suggestion.
I will try to use an error Val (see response to @jducoeur)


#6

Hi,

So I made another attempt. I now can do the following:

    val v1: Val[String] = "100.0"
    val v2: Val[Double] = 200.0
    val v3: Val[Int] = 300

    val s1: Either[String, String] = v1
    val s2: Either[String, Double] = v2
    val s3: Either[String, Int] = v3

    val r: Either[String, Double] = for {
      t <- s1
      t1 = t.toDouble
      t2 <- s2
      t3 <- s3
      ts = t1 + t2 + t3
    } yield ts
    r shouldBe 'right
    r.right.get shouldBe 600.0

    val ri: Val[Double] = for {
      v1t:String <- v1
      v1d = v1t.toDouble
      v2d:Double <- v2
      v3d:Int <- v3
      ts: Double = v1d + v2d + v3d
    } yield ts
    ri shouldBe a[Val[_]]
    ri should not be a[Empty[_]]
    ri shouldBe Val(600.0)

    val re: Val[Double] = for {
      v1t:String <- v1
      v1d = v1t.toDouble
      v2d:Double <- v2
      v3d:Double <- v3 // Its an Int, won't compile
      ts: Double = v1d + v2d + v3d
    } yield ts
    re shouldBe a[Val[_]]
    re should not be a[Empty[_]]
    re shouldBe Val(600.0)

However what I need is to do the following:

    val hidden: Val[_] = v3
    val rr: Val[Double] = for {
      v1t:String <- v1
      v1d = v1t.toDouble
      v2d:Double <- v2
      v3d:Double <- hidden 
      ts: Double = v1d + v2d + v3d
    } yield ts
    rr shouldBe a[Val[_]]
    rr should not be a[Empty[_]]
    rr shouldBe Val(600.0)

which fails with (line with the hidden value):

type mismatch;
[error]  found   : Double => (Double, Double)
[error]  required: _$4 => ?
[error]       v3d:Double <- hidden // Its an Int, won't compile

This make sense. So my question is, is there any way to
define the filter so that the type can be unknown (fail
at run-time)? Tried several alternatives such as
making the filter type undefined but I then cannot
set the type I want to the filter.

Here is the code I am using:

  sealed trait Val[V] { self =>
    val v: V

    override def toString: String = s"Val($v)"

    override def equals(obj: scala.Any): Boolean = {
      if (obj.isInstanceOf[Val[V]]){
        val other = obj.asInstanceOf[Val[V]]
        v.equals(other.v)
      }
      else
        false
    }

    def map[R](f: V => R): Val[R] = f(v)
    def flatMap[R](f: V => Val[R]): Val[R] = f(v)
    def filter(p: V => Boolean): Val[V] = if (p(v)) this else Empty[V](v)
    def withFilter(p: V => Boolean): WithFilter = new WithFilter(p)

    class WithFilter(p: V => Boolean) {
      def map[B](f: V => B): Val[B] = self filter p map f
      def flatMap[B](f: V => Val[B]): Val[B] = self filter p flatMap f
      def foreach[U](f: V => U): Unit = self filter p foreach f
      def withFilter(q: V => Boolean): WithFilter = new WithFilter(x => p(x) && q(x))
    }
  }

  case class Empty[A](override val v:A) extends Val[A]

  object Val {
    def apply[T](e:T): Val[T] = new Val[T] {
      override val v:T = e
    }
  }

  implicit def pack[A](s:A) : Val[A] = new Val[A] {
    override val v: A = s
  }

  // No class tag available
  implicit def unpack[T](t:Val[_]): Either[String,T] = {
    try {
      val v = t.v.asInstanceOf[T]
      Right(v)
    } catch {
      // When used via implicits should never reach this
      case e: Exception => Left(e.getMessage)
    }
  }

TIA