Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Shapeless: a real world use case
Juliano Alves
Juliano Alves

Posted on • Originally published atjuliano-alves.com

     

Shapeless: a real world use case

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))}
Enter fullscreen modeExit fullscreen mode

In case you are not familiar with ZIO (you should, it's awesome), what you need to know is:

  • every get requests returns aTask ofList
  • theget 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(_))}}
Enter fullscreen modeExit fullscreen mode

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)
Enter fullscreen modeExit fullscreen mode

The Problem

Using the client (viaget helper) is trivial, except for one detail:

importHttpClient.getdeforganisations(request:OrganisationRequest):get[Organisation]("/organisations",???)
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

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))
Enter fullscreen modeExit fullscreen mode

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))
Enter fullscreen modeExit fullscreen mode

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 theOptions 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}}
Enter fullscreen modeExit fullscreen mode

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 parametertoMap 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)
Enter fullscreen modeExit fullscreen mode

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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

An open-source enthusiast, firm believer that the future belongs to polyglot and functional programming!
  • Location
    London
  • Work
    CTO at Broad
  • Joined

More fromJuliano Alves

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp