Generic module

While kantan.csv goes out of its way to provide default instances for as many types as it can, some are made problematic by my strict rule against runtime reflection. Fortunately, shapeless provides compile time reflection, which makes it possible for the generic module to automatically derive instances for more common types and patterns.

The generic module can be used by adding the following dependency to your build.sbt:

libraryDependencies += "com.nrinaudo" %% "kantan.csv-generic" % "0.7.0"

Let’s first declare the imports we’ll need in the rest of this tutorial:

import kantan.csv._
import kantan.csv.ops._
import kantan.csv.generic._

The rest of this post will be a simple list of supported types.

CellEncoders and CellDecoders

Case classes of arity 1

All case classes of arity 1 have CellDecoder and CellEncoder instances, provided the type of their single field also does.

Let’s declare a (fairly useless) case class (we’ll be making a more useful one in the next section):

case class Wrapper[A](a: A)

We can directly encode from and decode to instances of Wrapper:

val decoded1 = "1, 2, 3\n4, 5, 6".unsafeReadCsv[List, List[Wrapper[Int]]](rfc)
// decoded1: List[List[Wrapper[Int]]] = List(
//   List(Wrapper(a = 1), Wrapper(a = 2), Wrapper(a = 3)),
//   List(Wrapper(a = 4), Wrapper(a = 5), Wrapper(a = 6))
// )

// res0: String = """1,2,3
// 4,5,6
// """

Sum types

We can also get free CellDecoder and CellEncoder instances for sum types where all alternatives have a CellDecoder and CellEncoder. For example:

sealed abstract class Or[+A, +B]
case class Left[A](value: A) extends Or[A, Nothing]
case class Right[B](value: B) extends Or[Nothing, B]

Left is a unary case class and will have a CellDecoder if its type parameter has one, and the same goes for Right. This allows us to write:

val decoded2 = "1,true\nfalse,2".unsafeReadCsv[List, List[Int Or Boolean]](rfc)
// decoded2: List[List[Or[Int, Boolean]]] = List(
//   List(Left(value = 1), Right(value = true)),
//   List(Right(value = false), Left(value = 2))
// )

// res1: String = """1,true
// false,2
// """


Case classes

All case classes have RowEncoder and RowDecoder instances, provided all their fields also do.

Take, for example, a custom Tuple2 implementation (using an actual Tuple2 might not be very convincing, as it’s supported by kantan.csv without needing the generic module):

case class CustomTuple2[A, B](a: A, b: B)

We can encode from and decode to that type for free:

val decoded3 = "1,\n2,false".unsafeReadCsv[List, CustomTuple2[Int, Option[Boolean]]](rfc)
// decoded3: List[CustomTuple2[Int, Option[Boolean]]] = List(
//   CustomTuple2(a = 1, b = None),
//   CustomTuple2(a = 2, b = Some(value = false))
// )

// res2: String = """1,
// 2,false
// """

It is very important to realise that while this is a pretty nice feature, it’s also a very limited one. The only time where you can get your case class codecs derived automatically is when the case class’ fields and the CSV columns are in exactly the same order. Any other scenario and you need to use old fashioned encoders and decoders.

Sum types

As with cells, sum types have RowEncoder and RowDecoder instances provided their all their alternatives also do.

In the following example:

"1,true\nfoobar,".unsafeReadCsv[List, (Int, Boolean) Or CustomTuple2[String, Option[Boolean]]](rfc)
// res3: List[Or[(Int, Boolean), CustomTuple2[String, Option[Boolean]]]] = List(
//   Left(value = (1, true)),
//   Right(value = CustomTuple2(a = "foobar", b = None))
// )

Other tutorials: