【问题标题】:Howto create a generic type class function using functors如何使用仿函数创建泛型类型类函数
【发布时间】:2017-07-25 08:34:07
【问题描述】:

拥有用于将 Map 转换为案例类的类型类:

/**
* Type class for transforming a map  of values into a case class instance.
*
* @tparam T Wanted case class type.
*/
@implicitNotFound("Missing ToCaseClassMapper implementation for type ${T}")
trait ToCaseClassMapper[T <: Product] {
  def toCaseClass(map: Map[String, Any]): T
}

并且这个函数可以隐式获取正确的映射器

def toCaseClass[T <: Product](map: Map[String, Any])(implicit mapper: ToCaseClassMapper[T]): T = {
  mapper.toCaseClass(map)
}

可以用作toCaseClass[User](aUserMap) // returns User

但我也希望能够将此函数与 Option[Map[]] 或 Future[Map[]] 或 List[Map[]] 一起使用。 所以我使用这样的仿函数实现了一个通用函数:

def toCaseClass[T <: Product, F[_]: cats.Functor](fOfMaps: F[Map[String, Any]])(implicit mapper: ToCaseClassMapper[T]): F[T] = {
  cats.Functor[F].map(fOfMaps)(map => toCaseClass(map)(mapper))
}

但是现在这个函数必须被用作toCaseClass[User,List](listOfUserMaps) // returns List[User]

但是,我希望能够将函数用作

toCaseClass[User](listOfMaps)
toCaseClass[User](futureOfMap)
toCaseClass[User](optionOfMap)

无需指定函子类型。 这有可能吗?
可以用Shapeless的Lazy来解决这个问题吗?

编辑:解决方案
感谢 @Jasper-m 和 @dk14 的回答。 所以解决这个问题的“技巧”是在 Functor 类型之前首先在类中捕获类型“T”。我喜欢 @Jasper-m 使用 'apply' 方法的解决方案,因为这样可以使语法几乎与以前相似。
不过我做了一些调整。由于已经有“ToCaseClassMapper”类也捕获了“T”类型,我决定将它与“ToCaseClass”类结合起来。此外,使用@Jasper-m 的方法,当映射到像Option(value).map(toCaseClass) 这样的某个值时使用“toCaseClass”函数时,toCaseClass 的用法必须在值是 Map 或 List[Map] 时有所不同。

我现在的解决方案如下:

@implicitNotFound("Missing ToCaseClassMapper implementation for type ${T}")
trait ToCaseClassMapper[T <: Product] {
  def toCaseClass(map: Map[String, Any]): T

  import scala.language.higherKinds

  def toCaseClass[F[_]: cats.Functor, A](fOfMaps: F[Map[String, A]]): F[T] = {
    cats.Functor[F].map(fOfMaps)(toCaseClass)
  }
}

由于ToCaseClassMapper 实例在使用toCaseClass 函数的地方已经隐式可用,所以我决定丢弃该函数并用mapper.toCaseClass(_) 替换它。这清理了一些不需要的代码,现在使用映射器的语法是相同的,无论值是 Map 还是 Option、List、Future(或任何其他 Functor)。

【问题讨论】:

    标签: scala functor shapeless scala-cats


    【解决方案1】:

    目前在 Scala 中不可能明确提供一个类型参数并推断同一类型参数列表中的另一个类型参数,目前也不可能为一个方法提供多个类型参数列表。一种解决方法是创建一个辅助类并将您的方法调用拆分为两个阶段:首先创建一个辅助类的实例,然后对该对象调用apply 方法。

    class ToCaseClass[T <: Product] {
      def apply[F[_]: cats.Functor, A](fOfMaps: F[Map[String, A]])(implicit mapper: ToCaseClassMapper[T]): F[T] = {
        cats.Functor[F].map(fOfMaps)(map => toCaseClass(map)(mapper))
      }
    }
    
    def toCaseClass[T <: Product] = new ToCaseClass[T]
    def toCaseClass[T <: Product](map: Map[String, Any])(implicit mapper: ToCaseClassMapper[T]): T = {
      mapper.toCaseClass(map)
    }
    
    toCaseClass[User](listOfMaps)
    toCaseClass[User](futureOfMap)
    toCaseClass[User](optionOfMap)
    

    编辑:正如dk14所指出的,这里仍然存在类型推断问题,其中F被推断为Any。我不知道是什么原因造成的,但我认为这是一个独立的正交问题,与此模式解决的问题不同。

    编辑 2:我想通了。这是因为F 的类型参数是不变的。 F[Map[String, String]] 不是F[Map[String, Any]] 的子类型,因此编译器做了一些奇怪的事情并将F 推断为Any。一种解决方案是使用类型参数A 而不是Any,或者使用存在类型Map[String,_]

    【讨论】:

    • 你是用哪个版本的scala来检查的?我的 2.11.8 给出:could not find implicit value for evidence parameter of type cats.Functor[Any]
    • 并非所有代码都提供,因此测试它并不容易,但现在我尝试了它,我也得到了那个错误。但是在我看来,它与问题是正交的。
    【解决方案2】:

    这行得通:

    class Mapper[T <: Product](implicit val mapper: ToCaseClassMapper[T]){
      def toCaseClass[F[_]: cats.Functor, Z <: Map[String, Any]](fOfMaps: F[Z]): F[T] = {
        cats.Functor[F].map(fOfMaps)(map => mapper.toCaseClass(map))
      }  
    }
    
    object Mapper{
        def apply[T <: Product: ToCaseClassMapper] = new Mapper[T]{}
    }
    
    import cats.implicits._
    
    Mapper[User].toCaseClass(List(Map("aaa" -> 0)))
    

    除了明显的引入类(拆分类型参数)之外,还使用了一些技巧:

    1) 将 mapper 移至构造函数,以便首先解决它(不确定是否有帮助)

    2) 绝对有帮助的是引入Z &lt;: Map[String, Any],否则scala(至少我的旧版本2.11.8)会出于某种原因将F[_] 推断为Any

    附:您也可以使用apply 而不是toCaseClass - 它会缩短语法

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-11
      • 1970-01-01
      相关资源
      最近更新 更多