I'm currently trying to wrap my mind around Scala with an intention to use it for my next project that has to deal with DICOM. DICOM has quite broad specification that spans over thousands of pages of the standard. My understanding of DICOM is quite limited, but in short DICOM objects - IODs (Information Object Definition) are composed of Modules, and Modules are a collection of typed name-value attribute pairs. It's further complicated by optionality of some modules and attributes. For example:
SimpleImageIOD: {
PatientModule: {
name: String
dateOfBirth: DateTime
}
StudyModule: {
name: String
date: DateTime (optional)
}
SeriesModule: {
name: String
}
ImageModule: {
height: Integer
width: Integer
pixelSize: Double (optional)
}
EquipmentModule: { (optional)
type: String
}
}
There are tons of modules, they might be composed in various combinations forming different IODs. Scala in turn has lots of modeling power with all the traits, case classes, dynamic classes, etc. How would you model such domain in Scala? I'm quite new to the language but I've been thinking to use immutable case classes to define modules, then aggregate them in various IODs, and use lenses for updating:
case class Patient(name: String, dateOfBirth: DateTime)
case class Study(name: String, date: Option[DateTime])
case class Series(name: String)
case class Image(height: Integer, width: Integer, pixelSize: Option[Double])
case class Equipment(type: String)
case class SimpleImageIOD(patient: Patient, study: Study, series: Series,
image: Image, equipment: Option[Equipment])
object Patient {
val nameL: Lens[Patient, String] = ...
val dateOfBirthL: Lens[Patient, DateTime] = ...
}
object SimpleImageIOD {
val patientL: Lens[SimpleImageIOD, Patient] = ...
val patientNamel = patientL andThen Patient.nameL
}
Etc etc. With respect to lenses it might become a problem to code all the boilerplate - there's going be order of M x N x L
lenses to cover whole domain for M
IODs, N
modules, and L
attributes. Also optionality of some fields complicates the task immensely at least with scalaz-seven
.
What other viable approaches are available there that align with the spirit of Scala?
You can nest lenses, so I don't think you'll have M x N x L
lenses in total. I think that's a perfectly reasonable approach.
An alternate approach is to define your own single-entry update methods like so:
case class Patient(name: String, dateOfBirth: Long) {
def name(f: String => String): Patient = copy(name = f(name))
def dateOfBirth(f: Long => Long): Patient = copy(dateOfBirth = f(dateOfBirth))
}
which you use like so:
scala> Patient("John",0).name(_ + " Smith")
res1: Patient = Patient(John Smith,0)
This is sort of achievable with lenses, but you have to be very careful to avoid having drastically more boilerplate (in both usage and definition). It doesn't have the flexibility of lenses where you can arbitrarily pull apart capabilities (e.g. updating), but it makes up for it by being streamlined.
I do the majority of my deep updating this way:
myImage.patient(_.study(_.date(_ => Some(System.currentTimeMillis))))
is all you need to update the entire tree with a study with the current time (pretending here that DateTime
is actually a Long
), and if you really need to extract this capability of regenerating this paticular tree (which is more of a natural with lenses), you could
val setStudyTime = (t: Long) => myImage.patient(_.study(_.date(_ => Some(t))))
or even
def studyTimeSetter(si: SimpleImageIOD) =
(t: Long) => si.patient(_.study(_.date(_ => Some(t))))
if you need to grab this capability easily at a moment's notice.
If you need everything that lenses offer you, it's more work to rebuild it in an ad-hoc way using this approach, but if you only need a small fraction and mostly just need to cut down on boilerplate for setting, this does a pretty good job.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With