92

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

(我认为)我明白了,而不是myList |> List.map fmyList |> Seq.map f |> Seq.toList更高种类的类型允许您简单地编写myList |> map f它,它会返回一个List. 这很棒(假设它是正确的),但似乎有点小?(难道不能简单地通过允许函数重载来完成吗?)我通常转换为Seq无论如何,然后我可以转换为我想要的任何东西。同样,也许我只是太习惯于解决它。但是,有没有任何例子可以说明更高种类的类型真的可以在击键或类型安全方面为您节省时间?

4

7 回答 7

82

所以类型的类型是它的简单类型。例如Int有 kind*这意味着它是一个基本类型并且可以通过值来实例化。通过对高级类型的一些松散定义(我不确定 F# 的界限在哪里,所以我们只包含它)多态容器是高级类型的一个很好的例子。

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

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

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

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

或解析器

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

两者也有亲切* -> *


然而,我们可以在 Haskell 中更进一步,通过具有更高阶类型的类型。例如,我们可以使用 kind 查找类型(* -> *) -> *。一个简单的例子可能是Shape试图填充 kind 的容器* -> *

data Shape f = Shape (f ())

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

例如,这对于在 Haskell 中表征Traversables 很有用,因为它们总是可以分为它们的形状和内容。

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

作为另一个例子,让我们考虑一棵根据它拥有的分支类型参数化的树。例如,一棵普通的树可能是

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

但是我们可以看到分支类型包含 a Pairof Tree 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)

最后,尽管它们在理论上是可能的,但我从未见过更高种类的类型构造函数。我们有时会看到这种类型的函数,例如mask :: ((forall a. IO a -> IO a) -> IO b) -> IO b,但我认为您必须深入研究类型序言或依赖类型的文献才能看到类型的复杂程度。

于 2014-01-16T19:15:03.103 回答
65

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

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

这个类型签名的意思是 fmap 改变了ffrom ato的类型参数b,但保持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);
}

这里的问题是 this 不限FB于与 - 相同FFA因此当您声明 type 时List<A> implements Functor<List<A>, A>,该map方法仍然可以返回 a NotAList<B> implements Functor<NotAList<B>, B>

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

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

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

这里问题的核心是 Java 和 C# 等语言不允许类型参数有参数。在Java中,ifT是一个类型变量,你可以写Tand List<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这种方式概括函数的概念,通过对其进行建模,就好像在本质上,映射是关于序列的。您的这句话就是本着这种精神:如果您有一个支持SeqSeq.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产生不同结果类型的操作,做出这样的保证就变得非常非常困难。

于 2014-01-16T22:51:33.993 回答
30

我不想在这里重复一些优秀答案中的信息,但我想补充一个关键点。

您通常不需要更高种类的类型来实现任何一个特定的 monad 或 functor(或 applicative functor,或箭头,或......)。但这样做大多没有抓住重点。

总的来说,我发现当人们看不到 functors/monads/whatevers 的用处时,通常是因为他们一次只考虑这些东西。Functor/monad/etc 操作实际上不会向任何一个实例添加任何内容(而不是调用 bind、fmap 等,我可以调用我用来实现bind、fmap 等的任何操作)。您真正想要这些抽象的目的是使您可以拥有与任何仿函数/单子/等通用的代码。

在这种泛型代码被广泛使用的上下文中,这意味着任何时候您编写一个新的 monad 实例,您的类型都会立即获得对已经为您编写的大量有用操作的访问权限。这就是到处都能看到 monad(和 functors,以及……)的意义所在;不是为了让我可以使用bind而不是实现(这本身concat并没有给我带来任何好处),而是这样当我需要时,我可以重用我最初在列表方面看到的代码,因为它实际上是单子通用的.mapmyFunkyListOperationmyFunkyParserOperationmyFunkyIOOperation

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

于 2014-01-30T02:24:30.327 回答
15

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

您可以获得的最接近(我在发布博客后发现)是自己制作IEnumerable<T>IObservable<T>IMonad<T>. 如果它们被表示,这将允许您重用您的 LINQ 块IMonad<T>,但是它不再是类型安全的,因为它允许您在同一个块中混合和匹配IObservablesIEnumerables虽然启用它可能听起来很有趣,但您会基本上只是得到一些未定义的行为。

后来写了一篇关于 Haskell 如何让这变得简单的文章。(无操作,真的——将块限制为某种 monad 需要代码;启用重用是默认设置)。

于 2014-01-29T23:42:35.290 回答
14

Haskell 中最常用的高级类型多态性示例是Monad接口。Functor并且Applicative以相同的方式具有更高的种类,所以我将展示Functor以展示简洁的东西。

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

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

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

和接口增加了更多ApplicativeMonad但它们是兼容的。这意味着它们也可以处理带有 kind 的类型变量* -> *

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

于 2014-01-16T19:16:47.387 回答
0

为什么你会关心Applicative?因为穿越。

class (Functor t, Foldable t) => Traversable t where
  traverse :: Applicative f => (a -> f b) -> t a -> f (t b)

type Traversal s t a b = forall f. Applicative f => (a -> f b) -> s -> f t

一旦你编写了一个Traversable实例,或者Traversal为某种类型编写了一个,你就可以将它用于一个任意的Applicative.

为什么你会关心Monad?原因之一是流系统,如pipesconduitstreaming. 这些是用于处理有效流的完全不平凡的系统。有了这个Monad类,我们可以重用所有我们喜欢的机器,而不必每次都从头开始重写。

你为什么还要关心Monad?单子变压器。我们可以分层 monad 转换器,但是我们喜欢表达不同的想法。的一致性Monad是使这一切正常工作的原因。

还有哪些其他有趣的高级类型?比方说Coyoneda…… 想要快速进行重复映射?利用

data Coyoneda f a = forall x. Coyoneda (x -> a) (f x)

f这有效或传递给它的任何函子。没有更高级的类型?对于每个仿函数,您都需要一个自定义版本。这是一个非常简单的示例,但是您可能不想每次都重写一些更棘手的示例。

于 2021-03-25T04:42:23.133 回答
-4

最近表示学习了一些关于更高种类的类型。尽管这是一个有趣的想法,但除了库开发人员之外,能够拥有一个需要另一个泛型的泛型,我在任何实际应用程序中都看不到任何实际用途。我在商业应用程序中使用 scala,我还看到并研究了一些设计精美的 sgstems 和库的代码,如 kafka、akka 和一些金融应用程序。我在任何地方都没有发现任何更高种类的类型在使用。

看起来它们对学术界或类似的东西很好,但市场不需要它,或者还没有达到 HKT 有任何实际用途或被证明比其他现有技术更好的地步。对我来说,你可以用它来打动别人或写博客文章,但仅此而已。这就像多元宇宙或弦理论。在纸上看起来不错,给你几个小时的时间来谈论,但没有什么真实的(对不起,如果你对理论物理没有任何兴趣)。一个证明是,上面的所有答案,他们都出色地描述了机制,但没有引用一个我们需要它的真实案例,尽管事实上自从 OP 发布它已经 6 年多了。

于 2021-03-25T03:50:32.207 回答