Как я могу определить, например, методы HKT (поверх объекта) в Scala?

#scala #monads #traits #higher-kinded-types

#scala #монады #Трейты #типы более высокого типа

Вопрос:

Как вы определяете HKT в Scala, чтобы предоставлять методы, такие как map , в качестве методов в экземплярах вместо функций в объектах?

Я знаю, что вы можете

 trait M[F[_]] {
  def map[A, B](f: F[A])(fn: A => B): F[B]
}

object Q extends M[Q] {
  def map[A, B](f: Q[A])(fn: A => B) = new Q(fn(f.e))
}

class Q[A](val e: A)

Q.map(new Q(1))(_.toDouble)
  

Однако вместо этого я хочу использовать map для экземпляров. Я не видел никакой литературы, которая делает это, поэтому я написал следующее.

 trait M[A, F[A]] {
  def map[B](f: A => B): F[B]
}

class Q[A](val e: A) extends M[A, Q] {
  def map[B](f: A => B) = new Q(f(e))
}

new Q(1).map(_.toDouble)
  

Это идиоматический Scala? Делает ли это то, что map должно делать? Существуют ли ограничения по сравнению с версией object выше? (В сторону, с extends M[A, Q[A]] он не будет компилироваться — почему?)

Я видел

 trait M[A] {
  def map[B](f: A => B): M[B]
}
  

но тогда Q ‘s map может возвращать class R[A] extends M[A] , что нежелательно.

Комментарии:

1. Итак, вам нужен typeclass . Идея в том, что существует только один экземпляр класса typeclass для вашего типа, потому что здесь связь является Q _has_ an instance of M вместо Q _is_ an instance of M . Обычно в Scala эти экземпляры делаются неявными, поэтому вы можете использовать их без необходимости передавать их вручную. Кроме того, обычно определяется неявный Ops класс Syntax or (как это сделал Джо К.) , чтобы предоставить функциям класса типов обычную нотацию метода , просто чтобы сделать их использование более естественным.

Ответ №1:

Специально для этого есть библиотека: https://github.com/mpilquist/simulacrum

Вы также можете сделать это самостоятельно. Типичный шаблон заключается в определении соответствующего неявного MOps класса или чего-то еще:

 trait M[F[_]] {
  def map[A, B](f: F[A])(fn: A => B): F[B]
}

implicit class MOps[F[_] : M, A](x: F[A]) {
  def map[B](f: A => B): F[B] = implicitly[M[F]].map(x)(f)
}
  

Затем пример использования:

 case class Box[A](a: A)

implicit val BoxM = new M[Box] {
  def map[A, B](f: Box[A])(fn: A => B): Box[B] = Box(fn(f.a))
}

Box(2).map(_.toString)
  

Но в нем много шаблонов, поэтому simulacrum делает все это за вас (и не только).

Комментарии:

1. итак, вы определяете map в соответствии с версией объекта, а затем используете implicits для предоставления этой функциональности экземпляру?

2. какие последствия имеет этот подход по сравнению с моим?

3. Ваш подход допускает некоторые странные вещи, такие как class Foo[A] extends M[A, Bar] . Это распространенная проблема, я думаю, что это лучшая статья, которую я видел по этому поводу: tpolecat.github.io/2015/04/29/f-bounds.html

4. Кроме того, некоторые методы некоторых классов типов не имеют связанных с ними экземпляров, например, empty метод Monoid. Итак, эти классы типов должны использовать этот стиль представления, и как только у вас появляется несколько таких форм, вам часто требуется наследование ( Monoid[A] extends Semigroup[A] ), и довольно скоро все становится проще делать таким образом