1

Is there a way to map a natural transformation (e.g. a Option ~> Either[String, *]) over a KList (e.g. a HList with a UnaryTCConstraint)? That would seem to be the natural thing to do with a KList.

Specifically I was trying to do the following:

object myNaturalTransformation extends (Option ~> Either[String, *]) {
  def apply[T](a: Option[T]): Either[String, T] = a.toRight("oh noe!")
}

def doStuff[KList <: HList: *->*[Option]#λ](klist: KList) = {
  klist.map(myNaturalTransformation)
}

I understand that the missing piece is the Mapper required to perform the .map and that Shapeless isn't able to generate one from myNaturalTransformations cases and the UnaryTCConstraint. Is it possible to obtain one some other way? Or is there another approach to map over a KList that I'm overlooking (apart from passing a Mapper to the doStuff-function)?

I was able to write my own version of UnaryTCConstraint that includes a

def mapper[G[_], HF <: ~>[TC, G]](hf: HF): Mapper[hf.type, L]

to explicitly generate a mapper for a given natural transformation. However I am curious if it's possible to do that with Shapeless' implementation of UnaryTCConstraint.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
mrArkwright
  • 254
  • 2
  • 10

1 Answers1

2

UnaryTCConstraint (*->*) is not for mapping, it's a constraint (common for HLists, Coproducts, case classes and sealed traits). For mapping there are type classes NatTRel, Mapped, Comapped, Mapper etc. (separate for HLists and Coproducts).

Try both constraint and type class

def doStuff[KList <: HList: *->*[Option]#λ, L <: HList](klist: KList)(implicit 
  natTRel: NatTRel[KList, Option, L, Either[String, *]]
): L = natTRel.map(myNaturalTransformation, klist)

or just type class

def doStuff[KList <: HList, L <: HList](klist: KList)(implicit 
  natTRel: NatTRel[KList, Option, L, Either[String, *]]
): L = natTRel.map(myNaturalTransformation, klist)

or hiding type parameter L via PartiallyApplied pattern

def doStuff[KList <: HList] = new PartiallyAppliedDoStuff[KList]

class PartiallyAppliedDoStuff[KList <: HList] {
  def apply[L <: HList](klist: KList)(implicit 
    natTRel: NatTRel[KList, Option, L, Either[String, *]]
  ): L = natTRel.map(myNaturalTransformation, klist)
}

or hiding type parameter L via existential (but then return type is not precise)

def doStuff[KList <: HList](klist: KList)(implicit 
  natTRel: NatTRel[KList, Option, _, Either[String, *]]
) = natTRel.map(myNaturalTransformation, klist)

or using extension method

implicit class NatTRelOps[KList <: HList](val klist: KList) extends AnyVal {
  def map[F[_], G[_], L <: HList](f: F ~> G)(implicit 
    natTRel: NatTRel[KList, F, L, G]
  ): L = natTRel.map(f, klist)
} 

def doStuff[KList <: HList, L <: HList](klist: KList)(implicit 
  natTRel: NatTRel[KList, Option, L, Either[String, *]]
): L = klist.map(myNaturalTransformation)

Testing:

doStuff(Option(1) :: Option("a") :: HNil)           // compiles
//doStuff(Option(1) :: Option("a") :: true :: HNil) // doesn't compile
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Are there any resources to learn about Shapeless constraints? If UnaryTCConstraint is not for mapping, what is is's purpose? Also I could only find constraints for HLists in Shapeless 2.3.3, ant not also for Coproducts, case classes and sealed traits as you metioned. – mrArkwright Nov 05 '20 at 09:33
  • 1
    @mrArkwright Well, Shapeless documentation is not perfect. There are a [book](https://underscore.io/books/shapeless-guide/), [wiki](https://github.com/milessabin/shapeless/wiki/Feature-overview:-shapeless-2.0.0), [examples](https://github.com/milessabin/shapeless/tree/master/examples/src/main/scala/shapeless/examples) and [tests](https://github.com/milessabin/shapeless/tree/master/core/src/test/scala/shapeless). For example tests for [constraints](https://github.com/milessabin/shapeless/blob/master/core/src/test/scala/shapeless/constraints.scala). – Dmytro Mitin Nov 05 '20 at 10:09
  • @mrArkwright If you look at `UnaryTCConstraint` [sources](https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/constraints.scala), it's pretty obvious that it's a type class such that for a type `A` either there is an instance `UnaryTCConstraint[A]` or not. `*->*` is for a context bound. There are no type members or methods in the type class. So there is no way to transform the type `A` into some `B` or a value `a: A` into some `b: B`. And there defined instances for `HNil`, `CNil`, `H :: T`, `H :+: T`, and with `Generic`. – Dmytro Mitin Nov 05 '20 at 10:10
  • @mrArkwright Its purpose is being a constraint, not mapping. I.e. verifying that a type parameter `L` of a method or type class is of form `TC[A] :: TC[B] :: ... :: HNil` or `TC[A] :+: TC[B] :+: ... :+: CNil` or `SomeCaseClass(tca: TC[A], tcb: TC[B], ...)`. – Dmytro Mitin Nov 05 '20 at 10:16
  • 1
    Up until 2.3.3 `UnaryTCConstraint` seems to be limited to `HList`. From 2.4.0-M1 on it works on `Coproduct` and `Generic` as well. So that explains my confusion about that. – mrArkwright Nov 05 '20 at 13:43
  • I've read through all of these resources. I'm still not clear about what can be achieved with a `UnaryTCConstraint`. Maybe I should have phrased my question differently: What is the purpose of a constraint, in which scenarios can it be applied and what can be achieved by them? Only restricting a function call to a certain property but then not being able to do anything with that information seems counterintuitive. Or is that exactly what constraints in Shapeless achieve? – mrArkwright Nov 05 '20 at 13:48
  • @mrArkwright You're right about versions. I haven't noticed. https://github.com/milessabin/shapeless/blob/shapeless-2.3.3/core/src/main/scala/shapeless/hlistconstraints.scala Regarding use cases you can search for `UnaryTCConstraint` https://stackoverflow.com/search?q=UnaryTCConstraint – Dmytro Mitin Nov 05 '20 at 14:21
  • @mrArkwright *"but then not being able to do anything with that information seems counterintuitive."* Well, I guess you can do something, just not with the type class (or with another type class). Let's say can it be useful to check that all fields of a class are optional? – Dmytro Mitin Nov 05 '20 at 14:28
  • @mrArkwright Not all operations are type-level (that can be done at compile-time). There are many operations that can be done only at runtime. – Dmytro Mitin Nov 05 '20 at 14:31