This library is intended to give a seamless way to manipulate scala structures into one another, mixing the types intelligently as required
For a whistle-stop tour of the underlying algorithm, see here
Releases are available on Maven. Add the following to your build.sbt:
"io.typechecked" %% "alphabet-soup" % "<version>"
for the version of your choice. The project is fully tagged and release versions are available to view online.
The top-level concept is called
Mixer. You give it two types, and if the libary can squeeze the first into the second then it compiles.
At runtime, it turns the supplied value into an instance of the second value.
A quick example:
case class Pet(name: String, age: Int) val pet: Pet = Pet("Jeremy", 5) Mixer[Pet, (Int, String)].mix(pet) == (5, "Jeremy")
It works recursively on much larger examples too.
The base element in the library is an
Mixer stops searching the branch of the tree it's in once it reaches an
Atom - so they form the building blocks of the structures you want to manipulate.
The library provides
Atom evidence for the usual suspects
Making your own is very simple:
implicit val atomT: Atom[T] = Atom[T]
Just make sure it's in scope whenever you create a
Atom can be created by using the macro annotation
In order to use the macro you must add the following compiler plugin to your
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full),
Then creating an
Atom is simple:
@Atomic case class Foo(a: Int) @Atomic trait Foo @Atomic class Foo
Molecule is something beyond a boundary that we can't handle at compile time - for example a
List. The information of how many items are in the source
List is not known to us, so we can't do anything without some extra help or structure.
That takes the form of recursing inside the
Molecule, and treating it as its own closed off world of
Atoms and sub-
Molecules to process. No value from an outer class can make it into a
Molecule is mixed according to the normal rules:
trait A trait B case class AS(as: List[A]) case class BS(bs: List[B])
In the above example,
AS can be mixed into
BS if and only if
A can be mixed into
List is provided for you in the library, for example. In general any
Functor can be implemented very simply as a molecule.
There are two ways of creating defaults in alphabet-soup.
The first is to create a
case class A(i: Int) case class B(i: Int, s: String) implicit val default: DefaultAtom[String] = DefaultAtom("some default string") val mixer = Mixer[A,B] mixer.mix(A(1))
B(1, "some default string")
This can be useful if you want to create default values for types which do not possess much information.
Unit have provided
The second way of creating defaults often comes in handy when you want to mix a class into a bigger type on a one-time basis. To do this, you must manually create your
Mixer using the builder provided, and give it the required defaults. For example:
case class A(i: Int) case class B(i: Int, s: String, b: Boolean) Mixer[A, B] // Does not compile val m = Mixer.from[A].to[B].withDefault("").withDefault(true).build // Does compile. This is an instance of `Mixer[A, B]` m.mix(A(0)) == B(0, "", true)
Here's a more complicated example to show what it can do.
// You should follow the philosophy of every value is a type! @Atomic case class FirstName(value: String) @Atomic case class LastName(value: String) @Atomic case class Address1(value: String) @Atomic case class City(value: String) @Atomic case class Postcode(value: String) @Atomic case class Title(value: String) @Atomic case class Gender(value: String) // This is our data tree case class Address(a1: Address1, c: City, p: Postcode) case class Alias(firstName: FirstName, lastName: LastName, isLegal: Boolean) case class AddressHistory(values: List[Address]) // This is our source data class case class Source( firstName: FirstName, lastName: LastName, addressHistory: AddressHistory, aliases: List[(Title, FirstName, LastName)] ) // This is what we'll be mapping it into case class Target( name: (FirstName, LastName), addresses: List[(Address1, Postcode)], aliases: List[Alias], gender: Gender ) // We're going to map an intance of Source into an instance of Target val source = Source( firstName = FirstName("John"), lastName = LastName("Johnson"), addressHistory = AddressHistory(List( Address(Address1("5 John Street"), City("Johnsville"), Postcode("JOHN")), Address(Address1("5 Jack Street"), City("Jacksville"), Postcode("JACK")) )), aliases = List( (Title("Mr"), FirstName("Johnny"), LastName("Vegas")), (Title("Mr"), FirstName("Jon"), LastName("Snow")) ) ) // We need a Mixer // First attempt: Mixer[Source, Target] // Won't compile because we have no 'Gender' in our Source // Second attempt: Mixer.from[Source].to[Target].withDefault(Gender("male")).build // Won't compile because we have no 'isLegal' in our source aliases // Third attempt: implicit val submixer = Mixer.from[(Title, FirstName, LastName)].to[Alias].withDefault(true) val mixer = Mixer.from[Source].to[Target].withDefault(Gender("male")).build val result = mixer.mix(source) // This test passes! result shouldBe Target( name = (FirstName("John"), LastName("Johnson")), addresses = List( (Address1("5 John Street"), Postcode("JOHN")), (Address1("5 Jack Street"), Postcode("JACK")) ), aliases = List( Alias(FirstName("Johnny"), LastName("Vegas"), true), Alias(FirstName("Johnny"), LastName("Vegas"), true) ), gender = Gender("male") )
This library will be extended to:
- handle functional
Atoms, which contain within them a way to transform one type to another
- have a "strict" version, which will prevent duplicate types in the source of the mixer