2

I have a case where I wish to apply modifications to an object based on the presence of (a few, say, 5 to 10) optionals. So basically, if I were to do it imperatively, what I'm aiming for is :

var myObject = ...
if (option.isDefined) {
    myObject = myObject.modify(option.get)   
}
if (option2.isDefined) {
    myObject = myObject.someOtherModification(option2.get)
}

(Please note : maybe my object is mutable, maybe not, that is not the point here.)

I thought it'd look nicer if I tried to implement a fluent way of writing this, such as (pseudo code...) :

myObject.optionally(option, _.modify(_))
        .optionally(option2, _.someOtherModification(_))

So I started with a sample code, which intelliJ does not highlight as an error, but that actually does not build.

class MyObject(content: String) {
   /** Apply a transformation if the optional is present */
  def optionally[A](optional: Option[A], operation: (A, MyObject) => MyObject): MyObject = 
      optional.map(operation(_, this)).getOrElse(this)
   /** Some possible transformation */
  def resized(length : Int): MyObject = new MyObject(content.substring(0, length))
}
object Test {
  val my = new MyObject("test")
  val option = Option(2)

  my.optionally(option, (size, value) => value.resized(size))
}

Now, in my case, the MyObject type is of some external API, so I created an implicit conversion to help, so what it really does look like :

// Out of my control
class MyObject(content: String) {
  def resized(length : Int): MyObject = new MyObject(content.substring(0, length))
}

// What I did : create a rich type over MyObject
class MyRichObject(myObject: MyObject) {
  def optionally[A](optional: Option[A], operation: (A, MyObject) => MyObject): MyObject = optional.map(operation(_, myObject)).getOrElse(myObject)
}
// And an implicit conversion
object MyRichObject {
  implicit def apply(myObject: MyObject): MyRichObject = new MyRichObject(myObject)
} 

And then, I use it this way :

object Test {
  val my = new MyObject("test")
  val option = Option(2)
  import MyRichObject._
  my.optionally(option, (size, value) => value.resized(size))
}

And this time, it fails in IntelliJ and while compiling because the type of the Option is unknown : Error:(8, 26) missing parameter type my.optionally(option, (size, value) => value.resized(size))

To make it work, I can :

  1. Actively specify a type of the size argument : my.optionally(option, (size: Int, value) => value.resized(size))
  2. Rewrite the optionally to a curried-version

None of them is really bad, but if I may ask :

  • Is there a reason that a curried version works, but a multi argument version seems to fail to infer the parametrized type,
  • Could it be written in a way that works without specifying the actual types
  • and as a bonus (although this might be opinion based), how would you write it (some sort of foldLeft on a sequence of optionals come to my mind...) ?
GPI
  • 9,088
  • 2
  • 31
  • 38

2 Answers2

2

One option for your consideration:

// Out of my control
class MyObject(content: String) {
  def resized(length : Int): MyObject = new MyObject(content.substring(0, length))
}

object MyObjectImplicits {

  implicit class OptionalUpdate[A](val optional: Option[A]) extends AnyVal {
    def update(operation: (A, MyObject) => MyObject): MyObject => MyObject =
      (obj: MyObject) => optional.map(a => operation(a, obj)).getOrElse(obj)
  }

}
object Test {
  val my = new MyObject("test")
  val option = Option(2)
  import MyObjectImplicits._
  Seq(
    option.update((size, value) => value.resized(size)),
    // more options...
  ).foldLeft(my)(_)
}

Might as well just use a curried-version of your optionally, like you said.

PH88
  • 1,796
  • 12
  • 12
  • I like the idea of reversing the implicits to the Optional type, instead of MyObject (generalization: making the parametrized type convertible, thus eliminating any parametrized method on the other end). I agree with your conclusion : the curried version looks at least as good. Made me think there may be something to try with witness types (https://stackoverflow.com/questions/8524878/implicit-conversion-vs-type-class?rq=1) – GPI Oct 17 '17 at 15:24
  • Also see http://pchiusano.blogspot.hk/2011/05/making-most-of-scalas-extremely-limited.html about the limitation on type inference you observed – PH88 Oct 17 '17 at 16:12
  • Thanks for digging that. What I don't get, is that in the first example implementation (without implicits), this actually works... – GPI Oct 17 '17 at 16:34
  • But your first example give the same "missing parameter type..." error to me. – PH88 Oct 18 '17 at 01:30
  • OK... Well intelliJ did not highlight it to me, but trying to actually build the JAR results in the error for both samples. Which renders the, like, 80% of the question pointless because I was trying to understand why the compiler was not treating the two examples the same, when it actually does. I'll update the question for future reference. Thanks. – GPI Oct 18 '17 at 07:36
1

A nicer way to think about the need to add the type there is write it this way:

object Test {
  val my = new MyObject("test")
  val option = Some(2)
  my.optionally[Int](option, (size, value) => value.resized(size))
}

Another way, if you only will manage one type since the object creation, is to move the generic to the class creation, but be careful, with this option you only can have one type per instance:

class MyObject[A](content: String) {
  def optionally(optional: Option[A], operation: (A, MyObject[A]) => MyObject[A]): MyObject[A] =
    optional.map(operation(_, this)).getOrElse(this)
  def resized(length : Int): MyObject[A] = new MyObject[A](content.substring(0, length))
}

object Test {
  val my = new MyObject[Int]("test")
  val option = Some(2)
  my.optionally(option, (size, value) => value.resized(size))
}

As you can see, now all the places where the generics was is taken by the Int type, because that is what you wanted in the first place, here is a pretty answer telling why:

(just the part that I think applies here:)

4)When the inferred return type would be more general than you intended, e.g., Any.

Source: In Scala, why does a type annotation must follow for the function parameters ? Why does the compiler not infer the function parameter types?

developer_hatch
  • 15,898
  • 3
  • 42
  • 75
  • Thanks for the first suggestion. Seems obvious in retrospect, and is kind of nicer. Your second proposition is nice too, but has restrictions, plus I can not actually modify MyObject. The rest of your answer does not seem to apply because it does not explain why the inference stops working when implicits come into play... – GPI Oct 17 '17 at 15:18
  • @GPI in this case Rule 4) applies: 4)When the inferred return type would be more general than you intended, e.g., Any. I mean you where returning a general type, when you wanted an specific one, don't you agree? – developer_hatch Oct 17 '17 at 15:24
  • @GPI anyway I deleted the part that not matter to clarify the answer, I have always love to help. – developer_hatch Oct 17 '17 at 15:27
  • Thanks. I do not see the return type being the culprit here. It is the optional argument's type that has an issue, not any result type, and indeed, specifying the argument's type is enough for the compiler. And I do not see how it is too general either, my return type is specified, here, as MyObject, which is as specific as it can get. – GPI Oct 17 '17 at 16:38