函数单子类型参数
问题:为什么会出现(Function monad type parameter)这个问题以及如何解决?
在这个问题中,我们看到了一个类型定义:trait Pure[P[_]] { def pure[A](a: => A): P[A] }
。在方括号中的下划线表示它是一个类型构造器,接受一个类型并返回另一个类型。例如,List
、Option
就是这种类型构造器的例子。给List
一个Int
,一个具体的类型,它会返回List[Int]
,另一个具体的类型。给List
一个String
,它会返回List[String]
。等等。
因此,List
、Option
可以被视为一个接受一个类型并返回另一个类型的类型级函数。我们可以形式化地说,它们的kind是* -> *
,其中星号表示一个类型。
然而,Tuple2[_, _]
是一个接受两个类型并返回一个新类型的类型构造器。因为它们的签名不匹配,所以不能将Tuple2
替换为P
。我们需要做的是对Tuple2
的一个参数进行部分应用,这将给我们一个kind为* -> *
的类型构造器,然后我们可以将它替换为P
。
不幸的是,Scala没有专门的语法来对类型构造器进行部分应用,所以我们不得不使用称为"类型lambda"的怪兽。它们之所以被称为"lambda",是因为它们类似于存在于值级的lambda表达式。
为了解决这个问题,我们可以使用类型成员的技巧,例如:type Partial2[F[_, _], A] = { type Get[B] = F[A, B] }
。这样我们就可以定义一个kind为* -> *
的类型构造器,并将其用作P
的替代。
我们可以通过使用类型lambda和类型成员来解决(Function monad type parameter)这个问题。
(Function monad type parameter)这个问题的出现的原因是想要在一个Tuple2的第二个元素上应用一个函数,并且希望使用Scala的Scalaz库中的Functor来实现这个功能。然而,由于Tuple2的类型参数是固定的,因此无法直接使用Functor。解决方法是使用类型投影和类型参数重定义来实现这个功能。
在给出的示例中,通过定义一个类型别名IntTuple来表示一个具有Int类型作为第一个元素的Tuple2。然后使用Functor[IntTuple]来将一个函数应用于IntTuple的第二个元素。使用类型投影和类型参数重定义,可以通过Functor[(Int, a)]来实现相同的功能。然而,为了简化代码,可以使用Scalaz提供的隐式转换来推断Functor的类型参数,从而避免重复写出类型投影和类型参数重定义。
最后,通过启用IntelliJ的代码折叠功能和Scala插件,可以隐藏类型投影和类型参数重定义的部分,使代码更加简洁可读。
需要注意的是,上述示例中的最后一行代码可能缺少了lambda表达式,应该为`(1, 2).map(a => a + 1)`。
总之,通过使用类型投影和类型参数重定义,结合Scalaz库中的Functor,可以实现在Tuple2的第二个元素上应用函数的功能。而通过启用代码折叠功能和Scala插件,可以使代码更加简洁可读。希望未来的Scala版本能够直接支持这样的语法。
在使用高阶类型时,类型lambda非常重要。
考虑一个简单的例子,定义一个用于Either[A, B]的右投影的monad。monad类型类如下所示:
trait Monad[M[_]] { def point[A](a: A): M[A] def bind[A, B](m: M[A])(f: A => M[B]): M[B] }
现在,Either是一个具有两个参数的类型构造函数,但是为了实现Monad,您需要给它一个具有一个参数的类型构造函数。解决这个问题的方法是使用类型lambda:
class EitherMonad[A] extends Monad[({type λ[α] = Either[A, α]})#λ] { def point[B](b: B): Either[A, B] def bind[B, C](m: Either[A, B])(f: B => Either[A, C]): Either[A, C] }
这是类型系统中柯里化的一个示例-您已经对Either的类型进行了柯里化,这样当您想要创建EitherMonad的实例时,必须指定其中一个类型;当然,另一个类型在调用point或bind时提供。
类型lambda技巧利用了类型位置中的空块创建匿名结构类型的事实。然后,我们使用#语法来获取类型成员。
在某些情况下,您可能需要更复杂的类型lambda,但是内联编写这些类型lambda非常麻烦。以下是我今天代码中的一个示例:
// types X and E are defined in an enclosing scope private[iteratee] class FG[F[_[_], _], G[_]] { type FGA[A] = F[G, A] type IterateeM[A] = IterateeT[X, E, FGA, A] }
这个类的存在仅仅是为了让我能够使用类似FG[F, G]#IterateeM这样的名称来引用专门用于某个转换器版本的第二个monad的IterateeT monad的类型,该转换器版本专门用于某个第三个monad。当您开始叠加时,这些构造变得非常必要。当然,我从不实例化FG;它只是一个让我在类型系统中表达我想要的内容的技巧。
有趣的是,Haskell不直接支持类型级lambda,尽管一些新类型的技巧(例如TypeCompose库)有办法绕过这一点。
我很想看到您如何为EitherMonad类定义bind方法。除此之外,如果我可以引用Adriaan的话,您在那个示例中没有使用高阶类型。在FG中使用了高阶类型,但在EitherMonad中没有。而是使用了类型构造函数,其种类为* => *。这种种类是一阶的,不是"higher"。
我认为类型*是一阶的,但无论如何,Monad的种类是(* => *) => *。此外,您会注意到我指定了"Either[A, B]的右投影" - 实现非常简单(但如果您以前没有这样做过,这是一个很好的练习!)
我猜Daniel的观点,并不将* => *称为"higher",这是有道理的,因为我们不将将非函数(将非函数映射到非函数,换句话说,将普通值映射到普通值)称为高阶函数。
Pierce的TAPL书,第442页:具有类似(*=>*)=>*的种类的类型表达式称为higher-order typeoperators。
Scala 3引入了更简单的类型lambda语法。使用新的语法,上面的EitherMonad定义将变为
EitherMonad[A] extends Monad[[α] =>> Either[A, α]]
。