
Shapeless is a library for generic programming in Scala, largelypresent in the ecosystem, but mostly behind the scenes; it is likely shapeless powers some of the libraries in your project, even though you don't use it directly.
Trying to solve an everyday problem, I've found an use case which I could solve using shapeless. This post doesn't intend to explain how shapeless work (there is awhole book about it here), but to provide a taste of it instead.
The Challenge
We need to write a REST API consumer usingZIO andhttp4s. Here is the service definition:
importio.circe.Decoderimportzio.{Has,RIO,Task}objectHttpClient{typeHttpClient=Has[Service]traitService{protectedfinalvalrootUrl="http://localhost:8080"defget[T](uri:String,parameters:Map[String,String])(implicitd:Decoder[T]):Task[List[T]]}defget[T](resource:String,parameters:Map[String,String])(implicitd:Decoder[T]):RIO[HttpClient,List[T]]=RIO.accessM[HttpClient](_.get.get[T](resource,parameters))}
In case you are not familiar with ZIO (you should, it's awesome), what you need to know is:
- every get requests returns a
Task
ofList
- the
get
function outside theService
is just a helper to access the environment of the effect (ZIO stuff)
Learn more about ZIO modules and layers here.
This is theHttpClient.Service
implementation, usinghttp4s:
importio.circe.Decoderimportorg.http4s.Uriimportorg.http4s.circe.CirceEntityCodec.circeEntityDecoderimportorg.http4s.client.Clientimportorg.http4s.client.dsl.Http4sClientDslimportzio._importzio.interop.catz._classHttp4sClient(client:Client[Task])extendsHttpClient.ServicewithHttp4sClientDsl[Task]{defget[T](resource:String,parameters:Map[String,String])(implicitd:Decoder[T]):Task[List[T]]={valuri=Uri(path=rootUrl+resource).withQueryParams(parameters)client.expect[List[T]](uri.toString()).foldM(IO.fail(_),ZIO.succeed(_))}}
Http4sClient.get
addsresource
to theuri
and theparameters
are the query string. Now, to represent the request call, we have acase class
calledOrganisationRequest
:
caseclassOrganisationRequest(code:Option[String],description:Option[String],page:Integer=1)
The Problem
Using the client (viaget
helper) is trivial, except for one detail:
importHttpClient.getdeforganisations(request:OrganisationRequest):get[Organisation]("/organisations",???)
We need to transform therequest
intoMap[String, String]
, what is an easy task. However, there are many "request" objects, and writingtoMap
methods to every single one of them is a Java-ish solution. Here is the challenge: how can we build this generic transformation?
Spoiler: with shapeless.
A bit of shapeless
This section is a grasp of how shapeless works, so the solution will make more sense when we get there. Shapeless can create anHeterogenous List (orHList
) as a generic representation of case classes. Let's do it usingGeneric
:
scala>importshapeless._scala>valorg=OrganisationRequest(Some("acme"),None,5)org:OrganisationRequest=OrganisationRequest(Some(org),None,5)scala>valgen=Generic[OrganisationRequest]gen:shapeless.Generic[OrganisationRequest]{typeRepr=Option[String]::Option[String]::Integer::shapeless.HNil}=anon$macro$4$1@48f146f2scala>gen.to(org)res8:gen.Repr=Some(acme)::None::5::HNil
The generic representation ofOrganisationRequest
is anHList
of typeOption[String] :: Option[String] :: Int :: HNil
. We have the values, but we need the names of the fields for ourMap
. We needLabelledGeneric
instead ofGeneric
:
scala>vallgen=LabelledGeneric[OrganisationRequest]lgen:shapeless.LabelledGeneric[OrganisationRequest]{typeRepr=Option[String]withshapeless.labelled.KeyTag[Symbolwithshapeless.tag.Tagged[String("code")],Option[String]]::Option[String]withshapeless.labelled.KeyTag[Symbolwithshapeless.tag.Tagged[String("description")],Option[String]]::Integerwithshapeless.labelled.KeyTag[Symbolwithshapeless.tag.Tagged[String("page")],Integer]::shapeless.HNil}=shapeless.LabelledGeneric$$anon$1@55f78c67
As you can see, withLabelledGeneric
it's possible to retain the information about the field names as well.
The Solution
Luckily, we don't need to manipulateLabelledGeneric
ourselves, shapeless provides us with plenty of useful type classes that can be found in theshapless.ops
package. We will build our solution usingToMap
:
scala>importshapeless.ops.product.ToMapscala>valtoMap=ToMap[OrganisationRequest]toMap:shapeless.ops.product.ToMap[OrganisationRequest]{typeK=Symbolwithshapeless.tag.Tagged[_>:String("page")withString("description")withString("code")<:String];typeV=java.io.Serializable}=shapeless.ops.product$ToMap$$anon$5@3bccd311scala>valmap=toMap(org)map:toMap.Out=Map('page->5,'description->None,'code->Some(acme))
We can make it even nicer using shapeless syntax:
scala>importshapeless.syntax.std.product._scala>valmap=org.toMap[Symbol,Any]map:Map[Symbol,Any]=Map('page->5,'description->None,'code->Some(acme))
For the final solution, let's create animplicit class
in order to add aparameters
method to ourrequest
class. Besides, we should remove every entry withnull
orNone
values, flatten theOption
s and turn keys and values intoString
:
importshapeless.ops.product.ToMapimportshapeless.syntax.std.product._implicitclassRequestOps[A<:Product](vala:A){defparameters(implicittoMap:ToMap.Aux[A,Symbol,Any]):Map[String,String]=a.toMap[Symbol,Any].filter{case(_,v:Option[Any])=>v.isDefinedcase(_,v)=>v!=null}.map{case(k,v:Option[Any])=>k.name->v.get.toStringcase(k,v)=>k.name->v.toString}}
A few comments here:
A <: Product
needs to be in place so we can useshapeless.ops.product
.All case classes implement Product, it's just a matter of adding the constrain for implicit resolution;- the implicit parameter
toMap
is aToMap.Aux
instead of justToMap
. Long story short, shapeless defines theAux
alias in order to make some of its internal complexity more readable and usable. Just trust me here ;)
Finally, this brings us to an elegant solution:
importHttpClient.getimportRequestOpsdeforganisations(request:OrganisationRequest):get[Organisation]("/organisations",request.parameters)
Conclusion
Even though shapeless looks almost magical at the first glance, after dedicating myself to understand it better, I've figured it can be very useful in practical terms. Shapeless provides a broad range of typeclasses that can be used in all sort of ways, and spending time learning about how they work is a very interesting exercise, improving skills related to typeclasses, derivations and bringing clarity about how some popular libraries that use shapeless work, likecirce.
I've heard that adding too much shapeless can properly affect the project's compile time. I'd like to hear more about it, if you have experience using shapeless directly, please share in the comments.
Originally published athttps://juliano-alves.com on April 6, 2020.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse