更高种类的类型什么时候有用?


87

我从事F#开发工作已有一段时间了,我喜欢它。但是我知道在F#中不存在的一个流行词是类型较高的类型。我已经阅读了有关高类型类型的材料,我想我理解它们的定义。我只是不确定为什么它们有用。有人可以提供一些示例,说明在Scala或Haskell中哪种类型更高级的类型更容易,而这需要F#中的解决方法?同样对于这些示例,如果没有更高类型的类型(或者在F#中反之亦然),解决方法将是什么?也许我只是很习惯解决它,以至于我没有注意到该功能的缺失。

(我认为)我得到了替代myList |> List.map fmyList |> Seq.map f |> Seq.toList更高种类的类型,使您可以简单地编写myList |> map f并返回List。很好(假设是正确的),但是看起来有点小?(难道不能简单地通过允许函数重载来完成吗?)我通常Seq无论如何都会转换为,然后我可以转换为以后想要的任何东西。再说一次,也许我只是太习惯了。但是,有没有在那里,kinded更高类型的任何例子真的可以节省你无论是在击键或类型安全?


2
Control.Monad中的许多功能都使用了更高的种类,因此您可能需要在此处查找一些示例。在F#中,必须为每种具体的monad类型重复执行。

1
@Lee但不能你只是做一个接口IMonad<T>,然后将它转换回例如,IEnumerable<int>IObservable<int>当你做了什么?这仅仅是为了避免铸造吗?
龙虾

4
浇铸是不安全的,因此可以回答有关类型安全的问题。另一个问题是如何return工作,因为它实际上属于monad类型,而不是特定实例,因此您根本不想将其放在IMonad接口中。

4
@Lee是的,我只是想您必须在表达式后强制转换最终结果,这没什么大不了的,因为您只是创建了表达式,所以知道类型。但看起来您也必须在每个bindaka SelectMany等内幕中投射同样的内容。这意味着有人可以使用API bindIObservable一个IEnumerable,并相信它会工作,这是啊呸,如果是这样的话,并有周围没有办法。只是不是100%肯定没有办法解决它。
龙虾

5
好问题。我还没有看到该语言功能的一个引人注目的实用示例是有用的IRL。
JD 2015年

Answers:


78

因此,类型的类型为其简单类型。例如,Int具有kind *意味着它是基本类型,并且可以通过值实例化。通过对高类型类型的一些宽松定义(我不确定F#会在哪里划定界线,所以让我们包括在内),多态容器是高类型类型的一个很好的例子。

data List a = Cons a (List a) | Nil

类型构造函数List具有种类* -> *,这意味着必须将其传递给具体类型才能产生具体类型:List Int可以具有类似的居民,[1,2,3]List本身不能。

我将假设多态容器的好处是显而易见的,但是* -> *存在比容器更多的有用类型类型。例如,关系

data Rel a = Rel (a -> a -> Bool)

或解析器

data Parser a = Parser (String -> [(a, String)])

两者也有善良* -> *


但是,我们可以通过在Haskell中使用更高阶的类型来进一步实现这一点。例如,我们可以寻找一个类型为kind的类型(* -> *) -> *。一个简单的例子可能是Shape尝试填充一个种类的容器* -> *

data Shape f = Shape (f ())

[(), (), ()] :: Shape List

Traversable例如,这对于表征Haskell中的s 很有用,因为它们始终可以分为其形状和内容。

split :: Traversable t => t a -> (Shape t, [a])

再举一个例子,让我们考虑一棵根据其具有的分支类型进行参数化的树。例如,正常的树可能是

data Tree a = Branch (Tree a) a (Tree a) | Leaf

但是我们可以看到该分支类型包含PairTree aS和这样我们就可以提取出片的类型的参

data TreeG f a = Branch a (f (TreeG f a)) | Leaf

data Pair a = Pair a a
type Tree a = TreeG Pair a

TreeG类型构造函数具有kind (* -> *) -> * -> *。我们可以用它来制作有趣的其他变化,例如RoseTree

type RoseTree a = TreeG [] a

rose :: RoseTree Int
rose = Branch 3 [Branch 2 [Leaf, Leaf], Leaf, Branch 4 [Branch 4 []]]

或是像 MaybeTree

data Empty a = Empty
type MaybeTree a = TreeG Empty a

nothing :: MaybeTree a
nothing = Leaf

just :: a -> MaybeTree a
just a = Branch a Empty

或一个 TreeTree

type TreeTree a = TreeG Tree a

treetree :: TreeTree Int
treetree = Branch 3 (Branch Leaf (Pair Leaf Leaf))

出现的另一个地方是“函子的代数”。如果我们放弃几层抽象性,最好将其视为折叠,例如sum :: [Int] -> Int。代数在函子载体上被参数化。该函子有样* -> *和载体样的*这么干脆

data Alg f a = Alg (f a -> a)

有善良(* -> *) -> * -> *Alg之所以有用,是因为它与数据类型和基于它们的递归方案的关系。

-- | The "single-layer of an expression" functor has kind `(* -> *)`
data ExpF x = Lit Int
            | Add x x
            | Sub x x
            | Mult x x

-- | The fixed point of a functor has kind `(* -> *) -> *`
data Fix f = Fix (f (Fix f))

type Exp = Fix ExpF

exp :: Exp
exp = Fix (Add (Fix (Lit 3)) (Fix (Lit 4))) -- 3 + 4

fold :: Functor f => Alg f a -> Fix f -> a
fold (Alg phi) (Fix f) = phi (fmap (fold (Alg phi)) f)

最后,虽然他们理论上是可能的,我从来没有看到一个甚至更高kinded类型构造。我们有时会看到诸如此类的函数mask :: ((forall a. IO a -> IO a) -> IO b) -> IO b,但我认为您必须深入研究类型序言或依存类型文献,才能看到类型的复杂性。


3
几分钟后,我将对代码进行类型检查和编辑,我现在正在使用手机。
J. Abrahamson 2014年

12
@ J.Abrahamson +1是个不错的答案,并且耐心在手机上键入内容O_o
Daniel Gratzer 2014年

3
@lobsterism A TreeTree只是病态的,但实际上,这意味着您已经将两种不同类型的树相互交织-进一步推动该想法可以使您获得一些非常强大的类型安全概念,例如静态安全红色/黑树和整洁的静态平衡FingerTree类型。
J. Abrahamson 2014年

3
@JonHarrop一个标准的现实世界示例是对monad进行抽象,例如使用mtl样式的效果堆栈。您可能不同意这在现实世界中的价值。我认为,很明显,没有HKT就能成功存在语言,因此任何示例都将提供某种比其他语言更复杂的抽象。
J. Abrahamson

2
例如,您可以在各种monad中拥有授权效果的子集,并在满足该规范的任何monad中进行抽象。例如,实例化“ teletype”以启用字符级读写的monad可能同时包含IO和管道抽象。您可以抽象各种异步实现作为另一个示例。没有HKT,您将限制由该通用零件组成的任何类型。
J. Abrahamson

62

考虑FunctorHaskell中的类型类,其中f是类型较高的类型变量:

class Functor f where
    fmap :: (a -> b) -> f a -> f b

这种类型签名的意思是fmap将a的类型参数f从更改ab,但保持f原样。因此,如果fmap在列表上使用,则会得到一个列表,如果在解析器上使用,则会得到一个解析器,依此类推。这些是静态的,编译时保证。

我不了解F#,但让我们考虑一下,如果我们尝试使用FunctorJava或C#这样的语言(带有继承和泛型)来表达抽象,而没有更高种类的泛型,那么会发生什么。第一次尝试:

interface Functor<A> {
    Functor<B> map(Function<A, B> f);
}

第一次尝试的问题在于,允许接口的实现返回任何实现的类Functor。有人可以编写一个方法,FunnyList<A> implements Functor<A>map方法的返回一个不同类型的集合,甚至其他根本不是集合但仍然是的东西Functor。同样,当您使用该map方法时,除非将结果转换为您实际期望的类型,否则您将无法对结果调用任何特定于子类型的方法。因此,我们有两个问题:

  1. 类型系统不允许我们表达不变性,即该map方法始终返回与Functor接收方相同的子类。
  2. 因此,没有静态类型安全的方式可以Functor对的结果调用非方法map

您可以尝试其他更复杂的方法,但是没有一种是真正有效的。例如,您可以通过定义Functor限制结果类型的子类型来尝试扩大首次尝试的范围:

interface Collection<A> extends Functor<A> {
    Collection<B> map(Function<A, B> f);
}

interface List<A> extends Collection<A> {
    List<B> map(Function<A, B> f);
}

interface Set<A> extends Collection<A> {
    Set<B> map(Function<A, B> f);
}

interface Parser<A> extends Functor<A> {
    Parser<B> map(Function<A, B> f);
}

// …

这有助于从返回错误类型的禁止那些窄接口的实施者Functormap方法,但由于没有限制多少Functor你可以实现有,没有限制,你会多少较窄的接口需要。

编辑:并且请注意,这仅适用于因为Functor<B>作为结果类型出现,所以子接口可以缩小它的范围。因此AFAIK我们不能Monad<B>在以下接口中同时缩小两种用途:

interface Monad<A> {
    <B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}

在Haskell中,具有较高等级的类型变量是(>>=) :: Monad m => m a -> (a -> m b) -> m b。)

另一种尝试是使用递归泛型来尝试并使接口将子类型的结果类型限制为子类型本身。玩具示例:

/**
 * A semigroup is a type with a binary associative operation.  Law:
 *
 * > x.append(y).append(z) = x.append(y.append(z))
 */
interface Semigroup<T extends Semigroup<T>> {
    T append(T arg);
}

class Foo implements Semigroup<Foo> {
    // Since this implements Semigroup<Foo>, now this method must accept 
    // a Foo argument and return a Foo result. 
    Foo append(Foo arg);
}

class Bar implements Semigroup<Bar> {
    // Any of these is a compilation error:

    Semigroup<Bar> append(Semigroup<Bar> arg);

    Semigroup<Foo> append(Bar arg);

    Semigroup append(Bar arg);

    Foo append(Bar arg);

}

但是,这种技术(对于您的常规OOP开发人员来说是相当神秘的,对于您的常规功能开发人员也是如此)仍然无法表达所需的Functor约束:

interface Functor<FA extends Functor<FA, A>, A> {
    <FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}

这里的问题是,这并不限制FB具有相同FFA-所以,当你声明一个类型List<A> implements Functor<List<A>, A>,该map方法能返回NotAList<B> implements Functor<NotAList<B>, B>

在Java中使用原始类型(未参数化的容器)的最终尝试:

interface FunctorStrategy<F> {
    F map(Function f, F arg);
} 

在这里F将实例化为未参数化的类型,例如just ListMap。这样可以保证a FunctorStrategy<List>只能返回a,List但是您已经放弃使用类型变量来跟踪列表的元素类型。

问题的核心在于,Java和C#等语言不允许类型参数具有参数。在Java中,如果T是类型变量,则可以编写TList<T>,但不能编写T<String>。更高类型的类型消除了此限制,因此您可能会遇到类似这样的事情(尚未完全考虑):

interface Functor<F, A> {
    <B> F<B> map(Function<A, B> f);
}

class List<A> implements Functor<List, A> {

    // Since F := List, F<B> := List<B>
    <B> List<B> map(Function<A, B> f) {
        // ...
    }

}

并特别解决这一点:

(我认为)我得到的是替代类型myList |> List.map fmyList |> Seq.map f |> Seq.toList更高种类的类型,您可以简单地编写myList |> map f并返回List。很好(假设是正确的),但是看起来有点小?(难道不能简单地通过允许函数重载来完成吗?)我通常Seq无论如何都会转换为,然后我可以转换为以后想要的任何东西。

有许多语言通过对map函数进行建模,就好像从本质上来说,映射是关于序列的,因此以这种方式概括了函数的概念。谨以此精神表达您的意见:如果您拥有支持往返转换的类型,则可以Seq通过重用获得“免费”的地图操作Seq.map

但是,在Haskell中,Functor该类比这更笼统。它与序列的概念无关。您可以实现fmap对序列没有良好映射的类型,例如IO动作,解析器组合器,函数等:

instance Functor IO where
    fmap f action =
        do x <- action
           return (f x)

 -- This declaration is just to make things easier to read for non-Haskellers 
newtype Function a b = Function (a -> b)

instance Functor (Function a) where
    fmap f (Function g) = Function (f . g)  -- `.` is function composition

“映射”的概念实际上与序列无关。最好了解函子定律:

(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs

非常非正式地:

  1. 第一条定律说,使用身份/ noop函数的映射等同于不执行任何操作。
  2. 第二定律说,任何可以通过两次映射生成的结果,也可以通过一次映射生成。

这就是为什么要fmap保留类型的原因-因为一旦获得map产生不同结果类型的操作,就很难像这样保证。


所以,我很感兴趣,你的最后一位,为什么是有用的一个fmapFunction a时,它已经有一个.操作?我理解为什么.fmapop 的定义有意义,但是我只是不了解您需要使用fmap而不是的地方.。也许如果您能举个例子说明有用的话,它将对我有所帮助。
龙虾

1
啊,知道了:您可以创建double函子的fn ,在其中double [1, 2, 3]给出[2, 4, 6]double sin给出的fn是罪孽的两倍。我可以看到,如果您开始以这种思维方式思考,那么当您在数组上运行映射时,您会期望返回一个数组,而不仅仅是一个seq,因为,好吧,我们正在这里处理数组。
龙虾

@lobsterism:有些算法/技术依赖于能够抽象出a Functor并让库的客户选择它。J. Abrahamson的答案提供了一个示例:可以使用函子来概括递归折叠。另一个例子是免费的单子。您可以将它们视为一种通用的解释器实现库,在该库中,客户端以任意方式提供“指令集” Functor
路易斯·卡西利亚斯

3
从技术上讲,这是一个合理的答案,但让我想知道为什么有人在实践中会想要这个。我还没有发现自己想买Haskell FunctorSemiGroup。实际程序最在哪里使用此语言功能?
JD 2015年

27

我不想在这里重复一些出色的回答,但是我想补充一个要点。

通常,您不需要种类繁多的类型来实现任何一个特定的monad或functor(或应用性functor或arrow或...)。但是,这样做基本上是没有意义的。

一般来说,我发现,当人们看不到的仿函数/单子/凡是的实用性,这是因为他们在想这些事情往往一次一个。Functor / monad / etc操作实际上不会对任何一个实例添加任何内容(而不是调用bind,fmap等,我可以调用用于实现 bind,fmap等的任何操作)。您真正想要这些抽象的目的是,使您可以拥有可以与任何 functor / monad / etc 通用的代码。

在此类通用代码被广泛使用的上下文中,这意味着每当您编写一个新的monad实例时,您的类型都将立即访问已为您编写的大量有用的操作。那就是到处看到单子(和函子,以及...)的意义。并非如此,我可以使用bind,而不是concatmap实现myFunkyListOperation(其获得我什么本身),而是这样,当我到了需要myFunkyParserOperationmyFunkyIOOperation我行,因为它实际上单子,一般重新使用的代码我本来看到名单的条件。

但是要在具有安全性的monad之的参数化类型之间进行抽象,您需要更高类型的类型(在此处的其他答案中也有很好的解释)。


9
与到目前为止我读过的任何其他答案相比,这更接近是一个有用的答案,但是我仍然希望看到一个实用的应用程序,其中有用的种类更多。
JD 2015年

“您真正想要这些抽象的目的是,使您可以拥有可以与任何functor / monad共同使用的代码”。F#在13年前以计算表达式的形式获得了monad,最初使用seq和async monad。今天,F#享有第三个单子查询。这么少的单子几乎没有共同点,为什么要抽象它们呢?
JD

@JonHarrop您清楚地意识到,其他人已经使用支持Mongod的语言(和函子,箭头等; HKT不仅与Monad有关)编写了代码,并找到了对其进行抽象的用途。显然,您认为该代码没有任何实际用途,并且很好奇为什么其他人会费心编写它。您希望通过对5年前已经发表评论的6岁帖子进行辩论来获得什么样的见解?

“希望通过回来就一个6岁的帖子开始辩论来获得收益”。回顾性的。借助事后的见识,我们现在知道F#对monad的抽象在很大程度上仍未使用。因此,抽象3种不同事物的能力令人信服。
JD

@JonHarrop我的回答的重点是,单个monad(或函子等)确实比没有游牧界面的类似功能真正有用,但是统一了许多不同的东西。我会参考您在F#方面的专业知识,但是如果您说的是它只有3个独立的monad(而不是对可能包含一个,失败,有状态,解析等的所有概念实施monadic接口),那么是的,将这三件事统一不会给您带来什么好处也就不足为奇了。

15

对于特定于.NET的观点,我前不久写了一篇有关此博客的文章。症结在于,对于类型较高的类型,您可能会在IEnumerables和之间重用相同的LINQ块IObservables,但是如果没有类型较高的类型,则这是不可能的。

您能得到的最接近的信息(我在发布博客后才知道)是您自己创建IEnumerable<T>IObservable<T>从扩展它们IMonad<T>。这将允许你,如果他们表示重用你的LINQ块IMonad<T>,但随后不再是类型安全的,因为它允许你混合和匹配IObservablesIEnumerables相同的块,这虽然这听起来耐人寻味启用此,你会内基本上只是得到一些未定义的行为。

后来写了一篇关于Haskell如何简化这一过程的文章。(实际上,无操作-将块限制为某种类型的monad需要代码;默认情况下启用重用)。


2
我将为您提供+1,因为它是唯一提及实用内容的答案,但我认为我从未IObservables在生产代码中使用过。
JD 2015年

5
@JonHarrop这似乎是不正确的。在F#中,所有事件均为IObservable,您可以在自己的书的WinForms章中使用事件。
达克斯·佛尔

1
微软付钱给我写那本书,并要求我涵盖该功能。我不记得在生产代码中使用事件,但我会看一下。
JD

我想IQueryable和IEnumerable之间的重用也是可能的
KolA

四年后,我看完了:我们从生产中剥离了Rx。
JD

13

Monad接口在Haskell中最常用的高类型类型多态性示例。Functor并且Applicative以相同的方式具有较高的种类,因此我将展示Functor以使内容简洁。

class Functor f where
    fmap :: (a -> b) -> f a -> f b

现在,检查该定义,查看如何使用类型变量f。您会看到这f并不意味着有值的类型。您可以在该类型签名中标识值,因为它们是函数的参数和结果。因此,类型变量ab是可以具有值的类型。类型表达式f a和也是如此f b。但f本身不是。 f是种类较高的类型变量的示例。鉴于那*是可以具有值的类型,因此f必须具有kind * -> *。也就是说,它采用可以具有值的类型,因为我们从先前的检查中知道a并且b必须具有值。而我们也知道,f af b 必须具有值,因此它返回必须具有值的类型。

这使得f在定义中使用Functor了更高种类的类型变量。

ApplicativeMonad接口添加更多,但他们不兼容。这意味着它们也可以处理类型为kind的类型变量* -> *

处理更高种类的类型会引入附加的抽象级别-您不仅限于在基本类型上创建抽象。您还可以在修改其他类型的类型上创建抽象。


4
关于更高种类的另一个很好的技术解释,让我想知道它们的用途。您在何处使用了真实代码?
JD 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.