Syntatic Sugar for Option mappings

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?

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.

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.

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),
)

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.

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

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.

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.

You can get really close just with aliasing…

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

1 Like

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/

2 Likes

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).

1 Like

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

1 Like

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

3 Likes

awesome