2

I'm trying to implement an Option which doesn't consume extra memory for a wrapper. I create a class. Null stands for None, non-null value stands for Some.

class Maybe[+T](private val nullable: T) extends AnyVal {
  @inline final def isDefined: Boolean = {
    println("isDefined called")
    val res = nullable != null
    println(s"result = $res")
    res
  }
  @inline final def get: T = if (isDefined) nullable else throw new NoSuchElementException
  @inline final def getOrElse[B >: T](default: => B): B = if (isDefined) this.get else default
  @inline final def map[B](f: T => B) : Maybe[B] = if (isDefined) Maybe.some(f(this.get)) else Maybe.none
  @inline final def flatMap[B](f: T => Maybe[B]): Maybe[B] = if (!isDefined) Maybe.none else f(get)
  def toOption : Option[T] = if (isDefined) Some(get) else None
}

object Maybe {
  def some[T](value:T) : Maybe[T] = new Maybe(value)

  final val none : Maybe[Nothing] = {
    println("start initializing none")
    val v = new Maybe(null)
    println("1")
    val res = v.asInstanceOf[Maybe[Nothing]]
    println("2")
    res
  }
}

object MaybeTest extends App {
  println("try to create Maybe.none")
  val myNone = Maybe.none
  println("Initialized")
  println(myNone.isDefined)
  println("Accessed")
}

But when I try to run it I get an NPE:

try to create Maybe.none

start initializing none

1

2

Initialized

Exception in thread "main" java.lang.NullPointerException at com.MaybeTest$.delayedEndpoint$com$MaybeTest$1(Maybe.scala:34) at com.MaybeTest$delayedInit$body.apply(Maybe.scala:30) at scala.Function0$class.apply$mcV$sp(Function0.scala:34) at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12) at scala.App$$anonfun$main$1.apply(App.scala:76) at scala.App$$anonfun$main$1.apply(App.scala:76) at scala.collection.immutable.List.foreach(List.scala:392) at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:35) at scala.App$class.main(App.scala:76) at com.MaybeTest$.main(Maybe.scala:30) at com.MaybeTest.main(Maybe.scala)

If I remove 'extends AnyVal' everything works fine. Could anyone explain such behavior?

Community
  • 1
  • 1
simpadjo
  • 3,947
  • 1
  • 13
  • 38
  • 1
    So... you want to introduce a `null`... which is more of a runtime entity and can not be distinguished at compile time into your "supposedly null-free" ecosystem. And then you are even working hard to do it. Well Done. – sarveshseri Apr 27 '17 at 11:13
  • 2
    Scala represent a `Maybe[Int]` at runtime as an `Int`. Scala's type system mandates that `Nothing` is inhabited by zero values, not even null. Yet it will find a null at runtime to represent your `Maybe[Nothing]`. Strange things will happen. – Jasper-M Apr 27 '17 at 11:31
  • @Jasper-M make sense, thanks. Does a better implementation exist? – simpadjo Apr 27 '17 at 12:14

1 Answers1

3

@Jasper-M's comment explains why your solution can't work, but this is close:

class Maybe[+T] private (private val nullable: Object) extends AnyVal {
  @inline private final def get0 = nullable.asInstanceOf[T]
  @inline final def isDefined: Boolean = nullable != null
  @inline final def get: T = if (isDefined) get0 else throw new NoSuchElementException
  @inline final def getOrElse[B >: T](default: => B): B = if (isDefined) get0 else default
  @inline final def map[B](f: T => B) : Maybe[B] = if (isDefined) Maybe.some(f(get0)) else Maybe.none
  @inline final def flatMap[B](f: T => Maybe[B]): Maybe[B] = if (!isDefined) Maybe.none else f(get0)
  @inline final def toOption: Option[T] = if (isDefined) Some(get0) else None
}

object Maybe {
  def some[A](value: A) : Maybe[A] = {
    val value1 = value.asInstanceOf[Object]
    assert(value1 != null)
    new Maybe[A](value1)
  }

  private val _none = new Maybe[Null](null)
  def none[A]: Maybe[A] = _none.asInstanceOf[Maybe[A]]

  def apply[A](value: A) = new Maybe[A](value.asInstanceOf[Object])
}

But it isn't really "an Option which doesn't consume extra memory for a wrapper": it's just calling null by another name. E.g. Option[Option[A]] works as expected, but Maybe[Maybe[A]] doesn't (and the compiler won't tell you!).

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • Your solution doesn't work for T <: AnyVal. Btw if you want to support only T <: AnyRef there is no need to use unsafe casts: just use Null instead of Nothing in my original solution. – simpadjo Apr 27 '17 at 14:58
  • It seems to work fine to me: e.g. `Maybe.none[Int].isDefined` is false, `Maybe.some(1).get` is `1`. What doesn't work as you expect? If you want `Maybe[Int]` to be represented as an `Int`, then as I said in the beginning, Jasper-M's comment explains why you can't do that without making a value unrepresentable and making it even less like `Option`. – Alexey Romanov Apr 27 '17 at 18:42
  • (Actually, I am not sure you can even get away with a value other than `0` as the unrepresentable one). – Alexey Romanov Apr 27 '17 at 18:50
  • I was wrong, it works even for AnyVal's. Thanks! Probably it's possible to get away from class casts by creating separate implementations for AnyRef and AnyVal cases and providing implicit type evidence. – simpadjo Apr 28 '17 at 08:22