0

I've been lately working on the DSL-style library wrapper over Apache POI functionality and faced a challenge which I can't seem to good solution for.

One of the goals of the library is to provide user with ability to build a spreadsheet model as a collection of immutable objects, i.e.

val headerStyle = CellStyle(fillPattern = CellFill.Solid, fillForegroundColor = Color.AquaMarine, font = Font(bold = true))

val italicStyle = CellStyle(font = Font(italic = true))

with the following assumptions:

  • User can optionally specify any parameter (that means, that you can create CellStyle with no parameters as well as with the full list of explicitly specified parameters);
  • If the parameter hasn't been specified explicitly by the user it is considered undefined and the default environment value (default value for the format we're converting to) will be used;

The 2nd point is important, as I want to convert this data model into multiple formats and i.e. the default font in Excel doesn't have to be the same as default font in HTML browser (and if user doesn't define the font family explicitly I'd like him to see the data using those defaults).

To deal with the requirements I've used the variation of the null pattern described here: Pattern for optional-parameters in Scala using null and also suggested here Scala default parameters and null (below a simplified example).

object ModelObject {
  def apply(modelParam : String = null) : ModelObject = ModelObject(
    modelParam = Option(modelParam)
  ) 
}
case class ModelObject private(modelParam : Option[String])

Since null is used only internally in the companion object and very localized I decided to accept the null-sacrifice for the sake of the simplicity of the solution. The pattern works well with all the reference classes.

However for Scala primitive types wrappers null cannot be specified. This is especially a huge problem with Boolean for which I effectively consider 3 states (true, false and undefined). Wanting to provide the interface, where user still be able to write bold = true I decided to reach to Java wrappers which accept nulls.

object ModelObject {
  def apply(boolParam : java.lang.Boolean = null) : ModelObject = ModelObject(
    boolParam = Option(boolParam).map(_.booleanValue)
  )
}
case class ModelObject private(boolParam : Option[Boolean])

This however doesn't right and I've been wondering whether there is a better approach to the problem. I've been thinking about defining the union types (with additional object denoting undefined value): How to define "type disjunction" (union types)?, however since the undefined state shouldn't be used explicitly the parameter type exposed by IDE to the user, it is going to be very confusing (ideally I'd like it to be Boolean).

Is there any better approach to the problem?

Further information:

Community
  • 1
  • 1
Norbert Radyk
  • 2,608
  • 20
  • 24

1 Answers1

3

You can use a variation of the pattern I described here: How to provide helper methods to build a Map

To sum it up, you can use some helper generic class to represent optional arguments (much like an Option).

abstract sealed class OptArg[+T] {
  def toOption: Option[T]
}
object OptArg{
  implicit def autoWrap[T]( value: T  ): OptArg[T] = SomeArg(value)
  implicit def toOption[T]( arg: OptArg[T] ): Option[T] = arg.toOption
}
case class SomeArg[+T]( value: T ) extends OptArg[T] {
  def toOption = Some( value )
}
case object NoArg extends OptArg[Nothing] {
  val toOption = None
}

You can simply use it like this:

scala>case class ModelObject(boolParam: OptArg[Boolean] = NoArg)
defined class ModelObject

scala> ModelObject(true)
res12: ModelObject = ModelObject(SomeArg(true))

scala> ModelObject()
res13: ModelObject = ModelObject(NoArg)    

However as you can see the OptArg now leaks in the ModelObject class itself (boolParam is typed as OptArg[Boolean] instead of Option[Boolean]. Fixing this (if it is important to you) just requires to define a separate factory as you have done yourself:

scala> :paste
// Entering paste mode (ctrl-D to finish)
case class ModelObject private(boolParam: Option[Boolean])
object ModelObject {
  def apply(boolParam: OptArg[Boolean] = NoArg): ModelObject = new ModelObject(
    boolParam = boolParam.toOption
  )
}
// Exiting paste mode, now interpreting.
defined class ModelObject
defined module ModelObject

scala> ModelObject(true)
res22: ModelObject = ModelObject(Some(true))

scala> ModelObject()
res23: ModelObject = ModelObject(None)

UPDATE The advantage of using this pattern, over simply defining several overloaded apply methods as shown by @drexin is that in the latter case the number of overloads grows very fast with the number of arguments(2^N). If ModelObject had 4 parameters, that would mean 16 overloads to write by hand!

Community
  • 1
  • 1
Régis Jean-Gilles
  • 32,541
  • 5
  • 83
  • 97