Syntatic Sugar for Option mappings


#1

In my ETL parsing code, I map optional attributes of optional classes (a lot). This often gives me many lines of code where I’m simply mapping or flat-mapping attributes. It would reduce code verbosity if there was syntactic sugar for getting attributes as Options of their element’s type.

// map example
val n: Option[Name] = Some(Name(first:String = "F", last:String = "L"))
val first: Option[String] = n ? first // expands to n.map(_.first)

// flatMap example
val n: Option[Name] = Some(Name(first:Option[String] = "F", last:Option[String] = "L"))
val first: Option[String] = n ?? first // expands to n.flatMap(_.first)

Could this be accomplished through a macro or compiler plugin?

Would you consider adding it to Scala 3?


#2

Interesting idea, but not sure if it is viable with ? symbol. I am pretty sure it is used as a custom operator in some libraries, so it probably would have to be removed from allowed symbols for custom operators in the language.


#3

Hi @dutrow. Can you post an example of the code you don’t like? Maybe we can help you clean it without relying on language changes.


#4

This is my most frequent use case.

// row provides an optional string defined by the schema
// HumanName transforms a free text name into a structured name
val name = row(Schema.userName).flatMap(HumanName.apply)
User(
  userId = Some(id),
  firstName = name.flatMap(_.first),
  middleName = name.flatMap(_.middle),
  lastName = name.flatMap(_.last),
)

#5

What if instead you had case class User(userId: Option[IdType], name: Name) and then you’d have

User(
  userId = Some(id),
  name = name
)

The repetition in your example comes from flattening Name into User, which I’m thinking you could do without.


#6

Fair point, but in my case the data model is fixed.


#7

Then I’m afraid your code is going to be obnoxious to write no matter what.

User(
  userId = Some(id),
  firstName = name??first,
  middleName = name??middle,
  lastName = name??last,
)

is a small improvement for such a large (breaking) language change. There might be some tool in Shapeless that might help you if you’re really desperate, but I don’t know what. Maybe Records.


#8

You could implement this through a macro (although I think it has to be whitebox in this case) if you changed it a little:

val first: Option[String] = n ?? 'first // expanding to n.flatMap(_.first)

This could be achieved by defining ?? as a method in an implicit class that takes the name of the property as a Symbol (as you can’t pass first on it’s own, due to it not resolving to a valid name on its own).

I could maybe take a go at this, but if you have knowledge of macros this should actually be quite simple.

If I may add in on this as a potential language change though, I’d much prefer ?., mimicking Kotlins safe call operator or C#'s null-conditional operator (other languages implement this too). Also, ?: as an alternative to getOrElse would also be useful.

The problem that I see with this, is that we don’t have T?, we have Option[T]. So there is less syntactic connection than there is in Kotlin, Typescript or C# between nullable types and null-safe access.


#9

You can get really close just with aliasing…

Although I still don’t see any benefit to introduce this syntax


#10

I like the Kotlin/Groovy safe call operator also. That must have been where this idea snuck into my mind.
https://kotlinlang.org/docs/reference/null-safety.html#safe-calls

The Scala implicit conversion doesn’t offer much in terms of conciseness http://code.scottshipp.com/2016/07/28/does-scala-have-the-safe-navigation-null-conditional-operator/


#11

it could be implemented using this proposition PRE SIP: ThisFunction | scope injection (similar to kotlin receiver function) (form of this PRESIP will be changed but main idea is there)

implicit class OptionsSuperOps[T](d:Option[T]) {
  def ?[E] (with this c:T => E) = { d.map(c) }
  //def >[E] (with this c:T => E) = { d.map(c) }
  def ??[E] (with this c:T => Option[E]) = { d.flatMap(c) }
  //def >>[E] (with this c:T => Option[E]) = { d.flatMap(c) }
}

User(
  userId = Some(id),
  firstName = name ? first,
  middleName = name ?? middle,
  lastName = name ?? last,
)

//User(
//  userId = Some(id),
//  firstName = name > first,
//  middleName = name >> middle,
//  lastName = name >> last,
//)

but there is rather small chance that this feature’ll land in scala (and no chance that it’ll land in this form).


#12

I think what you need is Option monad + !-notation.

You can use Dsl.scala for !-notation, and cats or scalaz for Option manad.

case class Name(first: Option[String], last: Option[String])

def fullName(optionName: Option[Name]) = Option {
  val name = !optionName
  !name.first + " " + !name.last
}

fullName(Some(Name(Some("John"), Some("Doe")))) // Some("John Doe")

fullName(None) // None

fullName(Some(Name(None, Some("Doe")))) // None

You can try the above example at https://scastie.scala-lang.org/Atry/m8NQ38cvR5yKANEGBttyPQ/1


#13

I implemented the Kotlin / Groovy flavored ? operator for Scala in Dsl.scala yesterday. Unlike the Option monad solution, the ? operator works with null instead of Option. I recommend you use ? with Java libraries, and Option + !-notation with Scala libraries.

You can use ? annotation to represent a nullable value.

import com.thoughtworks.dsl.keywords.NullSafe._

case class Tree(left: Tree @ ? = null, right: Tree @ ? = null, value: String @ ? = null)

val root: Tree @ ? = Tree(
  left = Tree(
    left = Tree(value = "left-left"),
    right = Tree(value = "left-right")
  ),
  right = Tree(value = "right")
)

Normal . is not null safe, when selecting nullable field of left , right or value .

a[NullPointerException] should be thrownBy {
  root.right.left.right.value
}

The above code throws an exception because root.right.left is null . The exception can be avoided by using ? on a nullable value:

root.?.right.?.left.?.value should be(null)

The entire expression is null if one of ? is performed on a null value.

The boundary of a null safe operator ? is the nearest enclosing expression whose type is annotated as ?.

("Hello " + ("world " + root.?.right.?.left.?.value)) should be("Hello world null")
("Hello " + (("world " + root.?.right.?.left.?.value.?): @ ?)) should be("Hello null")
(("Hello " + ("world " + root.?.right.?.left.?.value.?)): @ ?) should be(null)

The related documentation can be found in the Scaladoc page, or you can try it at https://scastie.scala-lang.org/Atry/Jmb1KoAqSbiV2VQo43TejQ/2


#14

awesome