Skip to content

Instantly share code, notes, and snippets.

@fanf
Last active January 13, 2017 13:57
Show Gist options
  • Save fanf/490c62079c75064d251ddcaa93e57238 to your computer and use it in GitHub Desktop.
Save fanf/490c62079c75064d251ddcaa93e57238 to your computer and use it in GitHub Desktop.
/*
* In our code, we have dozen of class to model our domain.
* Most of them have a name, or an id which happen to be implemented
* by a string.
* As we want the compiler to work for us especially during refactoring,
* we have zillions of things like that:
*/
final case class DirectiveId(value: String)
final case class Directive(
id : DirectiveId
, name : String
, description: String
//etc
)
final case class GroupId(value: String)
final case class Group(
id: GroupId
, //etc
)
/* /////////////////////////////////////////////////////////////////////////
* PROS
* /////////////////////////////////////////////////////////////////////////
* That solution works OK and we were able to do major refactoring
* (like 80% of the code impacted) without major breakage ounce the
* compiler is happy for the domain consistency and tests checking
* algorithms works.
*
* Note that the use case is really very simple, we don't want to
* check any property on the wrapped value, just give it a semantic.
*/
/* /////////////////////////////////////////////////////////////////////////
* CONS 1: construction boilerplate
* /////////////////////////////////////////////////////////////////////////
* The solution is extremelly boilerplate, and we have a ton
* of overhead in construction of values.
* At such a point that we don't use it for anything than
* "identification" members: in the example, name or description
* are String, not "DirectiveName", "DirectiveDescription" etc.
* (not saying it is what we would like to do even if it was
* mostly free to do so, but right now it is not even an option)
*/
val directive = Directive(DirectiveId("directive1"), "foo", "this the foo directive"))
/*
* We know we can help a little on the construction with implicit,
* either implicit def:
* (but it leads to its can of worms and happen to be quite brittle
* when use at large, with a lot of "string to other types" method)
*/
implicit def toDirectiveId(value: String) = DirectiveId(value)
val d = Directive("directive1", "foo", "this the foo directive")
/*
* or implicit class builder + def, for a saner implicit
* management:
*/
implicit class ToDirectiveId(value: String) {
def id = DirectiveId(value)
}
val d = Directive("directive1".id, "foo", "this the foo directive")
/* /////////////////////////////////////////////////////////////////////////
* CONS 2: accessing wrapped string boilerplate
* /////////////////////////////////////////////////////////////////////////
* Their is also a lot of boilerplate accessing the
* wrapped value. The main use case are serialisation
* (even if most framework of 2016 scala correctly handle case
* classes), logs - I can't count the number of time we forgot to
* call a ".value" and have a log with the class name printed (ok,
* the problem is aguably implicit toString, but that one is here
* to remain) - or just wanting to manipulate wrapped value, ex:
*/
final case class TagName(value: String)
final case class TagValue(value: String)
final case class Tag(name: TagName, value: TagValue)
val tags = (1 to 10).map(i => Tag(TagName("tag"+i), TagValue("value"+i)))
val capitalized = tags.map { case Tag(TagName(name), value) = Tag(TageName(name.capitalize), value) } //arg!
logger.info(s"User, your tags are: { capitalized.map( _.name.value ).mkString(";") }")
//most of the time, ".value" forgotten the first time
/*
* Dream:
*/
val tags = (1 to 10).map(i => Tag("tag"+i, "value"+i))
val capitalized = tags.map { case Tag(name, value) = Tag(name.capitalize, value) }
logger.info(s"User, your tags are: { capitalized.map( _.name ).mkString(";") }")
//BUT
val d = Directive(tag.name, ....) //error: tag.name is of type tagName, expecting a DirectiveId
// and yes, perhaps it's not possible to differentiate the two cases
/* /////////////////////////////////////////////////////////////////////////
* CONS 3: GC pressure
* /////////////////////////////////////////////////////////////////////////
* The currently used solution create a ton of very short lived objects with
* 0 runtime value. The type is only interesting for the user. So, bad.
*
*/
/* /////////////////////////////////////////////////////////////////////////
* QUESTION: what is standard answer to that problem in Scala? What is
* possible with it, and what is not?
* I thought I saw solution based on compile-time only macro, 0-runtime-overhead
* phamtom type for a similar used caes, but can't find them. Not sure it was
* for the case, really.
*
* Please scala community, help us delete huge boilerplate and keep it type safe!
*/
@rouazana
Copy link

It seems you are looking for tagged types. It solves most of your concerns but the last one (implicit conversions).
An interesting discussion with other solutions referenced: scalaz/scalaz#693

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment