Last active
January 13, 2017 13:57
-
-
Save fanf/490c62079c75064d251ddcaa93e57238 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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! | |
*/ | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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