trait Iso[A, B] {
  def to(a: A): B
  def from(b: B): A
}

trait Derivable[F[_]] {
  type Fields[_]
  def fieldOf[A](name: String, value: F[A]): Fields[A]
  def fieldUnit: Fields[Unit]
  def fieldZip[A, B](left: Fields[A], right: Fields[B]): Fields[(A, B)]
  def fieldMap[A, B](fa: Fields[A])(f: Iso[A, B]): Fields[B]
  
  type Cases[_]
  def caseOf[A](name: String, fields: Fields[A]): Cases[A]
  def caseUnit: Cases[Nothing]
  def caseZip[A, B](left: Cases[A], right: Cases[B]): Cases[Either[A, B]]
  def caseMap[A, B](fa: Cases[A])(f: Iso[A, B]): Cases[B]
  
  def coproduct[A](name: String, cases: Cases[A]): F[A]
  def product[A](name: String, fields: Fields[A]): F[A]
}

trait Show[A] {
  def show(a: A): String
}

val showDerivable: Derivable[Show] = new Derivable[Show] { 
  type Fields[A] = A => List[String]
  
  def fieldOf[A](name: String, value: Show[A]): Fields[A] =
    a => List(name + "=" + value.show(a))
    
  def fieldUnit: Fields[Unit] =
    a => Nil
    
  def fieldZip[A, B](left: Fields[A], right: Fields[B]): Fields[(A, B)] =
    { case (a, b) => left(a) ++ right(b) }
    
  def fieldMap[A, B](fa: Fields[A])(f: Iso[A, B]): Fields[B] =
    b => fa(f.from(b))
    
  type Cases[A] = A => String
  
  def caseOf[A](name: String, fields: Fields[A]): Cases[A] =
    a => name + "(" + fields(a).mkString(", ") + ")"
    
  def caseUnit: Cases[Nothing] =
    a => a
    
  def caseZip[A, B](left: Cases[A], right: Cases[B]): Cases[Either[A, B]] =
    ab => ab.fold(left, right)
    
  def caseMap[A, B](fa: Cases[A])(f: Iso[A, B]): Cases[B] =
    b => fa(f.from(b))
  
  def coproduct[A](name: String, cases: Cases[A]): Show[A] =
    a => cases(a)
    
  def product[A](name: String, fields: Fields[A]): Show[A] =
    a => name + "(" + fields(a).mkString(", ") + ")"
}

implicit val intShow: Show[Int] = a => a.toString
implicit val boolShow: Show[Boolean] = a => a.toString

final case class Foo(i: Int, b: Boolean)

val fooShow = {
  import showDerivable._
  
  val fooIso = new Iso[(Int, Boolean), Foo] {
    def to(p: (Int, Boolean)): Foo = Foo(p._1, p._2)
    def from(p: Foo): (Int, Boolean) = (p.i, p.b)
  }
  
  product("Foo", fieldMap(fieldZip(
    fieldOf("i", implicitly[Show[Int]]),
    fieldOf("b", implicitly[Show[Boolean]])))(fooIso))
}

println(fooShow.show(Foo(1, true)))


///