较高的kinded类型何时有用?

时间:2014-01-16 18:58:15

标签: scala haskell types f# higher-kinded-types

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

(我认为)我得到的不是myList |> List.map fmyList |> Seq.map f |> Seq.toList更高级的kinded类型,您只需编写myList |> map f,它就会返回List。这很好(假设它是正确的),但似乎有点小? (并且不能简单地通过允许函数重载来完成?)我通常转换为Seq然后我可以转换为我想要的任何东西。再说一次,也许我只是习惯于解决它。但有没有任何一个例子,高级类型的真的可以在按键或安全类型中保存你?

7 个答案:

答案 0 :(得分:76)

所以类型的类型是它的简单类型。例如,Int具有种类*,这意味着它是基本类型,可以通过值进行实例化。通过对高级类型的一些宽松定义(我不确定F#在哪里绘制线,所以让我们只包括它)多态容器是高级类型的一个很好的例子。

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

类型构造函数List具有类* -> *,这意味着必须传递一个具体类型才能生成具体类型:List Int可以拥有像{{1}这样的居民但[1,2,3]本身不能。

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

* -> *

或解析器

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

两者也有data Parser a = Parser (String -> [(a, String)]) 种。


然而,我们可以在Haskell中进一步采用具有更高阶类型的类型。例如,我们可以查找类型* -> *的类型。一个简单的示例可能是(* -> *) -> *,它会尝试填充Shape种容器。

* -> *

这对于在Haskell中表征data Shape f = Shape (f ()) [(), (), ()] :: Shape List 很有用,因为它们总是可以分为它们的形状和内容。

Traversable

作为另一个例子,让我们考虑一个在它所具有的分支类型上参数化的树。例如,普通树可能是

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

但是我们可以看到分支类型包含data Tree a = Branch (Tree a) a (Tree a) | Leaf Pair个,因此我们可以从参数化类型中提取该片段

Tree a

data TreeG f a = Branch a (f (TreeG f a)) | Leaf data Pair a = Pair a a type Tree a = TreeG Pair a 类型构造函数具有种类TreeG。我们可以使用它来制作有趣的其他变体,例如(* -> *) -> * -> *

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) 这类函数,但我认为你必须深入研究类型序言或依赖类型的文献才能看出类型的复杂程度。

答案 1 :(得分:60)

考虑Haskell中的Functor类型类,其中f是一个更高级的类型变量:

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

此类型签名的含义是fmap会将f的类型参数从a更改为b,但会保留f。因此,如果您在列表上使用fmap,则会获得一个列表,如果您在解析器上使用它,则会获得解析器,依此类推。这些是静态,编译时保证。

我不知道F#,但让我们考虑一下如果我们尝试用Java或C#这样的语言表达Functor抽象,继承和泛型,但没有更高级的泛型。首先尝试:

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方法没有静态类型安全方式。
  3. 您可以尝试其他更复杂的方法,但它们都不起作用。例如,您可以尝试通过定义限制结果类型的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);
    }
    
    // …
    

    这有助于禁止那些较窄接口的实现者从Functor方法返回错误类型的map,但由于对Functor个实现的数量没有限制,你需要多少更窄的接口没有限制。

    编辑:请注意,这仅适用,因为Functor<B>显示为结果类型,因此子接口可以缩小它。所以AFAIK我们不能缩小它的两个用途{@ 1}}位于以下界面中:

    Monad<B>

    在Haskell中,使用更高级别的类型变量,这是interface Monad<A> { <B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f); } 。)

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

    (>>=) :: Monad m => m a -> (a -> m b) -> m b

    但是这种技术(对于你们普通的OOP开发人员来说相当晦涩,对你们普通的功能开发人员来说也是如此)仍然无法表达所需的{{1约束:

    /**
     * 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);
    
    }
    

    此处的问题是,这不会限制Functorinterface Functor<FA extends Functor<FA, A>, A> { <FB extends Functor<FB, B>, B> FB map(Function<A, B> f); } 具有相同的FB - 因此当您声明类型F时,{{1方法可以仍然返回FA

    使用原始类型(非参数化容器)在Java中进行最后一次尝试:

    List<A> implements Functor<List<A>, A>

    此处map将实例化为非参数化类型,例如NotAList<B> implements Functor<NotAList<B>, B>interface FunctorStrategy<F> { F map(Function f, F arg); } 。这可以保证F只能返回List - 但您已放弃使用类型变量来跟踪列表的元素类型。

    问题的核心在于Java和C#等语言不允许类型参数具有参数。在Java中,如果Map是类型变量,则可以编写FunctorStrategy<List>List,但不能编写T。较高级别的类型会删除此限制,因此您可以使用此类内容(未经过充分考虑):

    T

    特别针对这一点:

      

    (我认为)我得到的不是List<T>T<String>更高级的kinded类型,您只需编写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 f。这很好(假设它是正确的),但似乎有点小? (并且不能简单地通过允许函数重载来完成?)我通常转换为myList |> Seq.map f |> Seq.toList然后我可以转换为我想要的任何东西。

    有许多语言通过这种方式概括myList |> map f函数的概念,通过对其进行建模,就像在心脏上映射是关于序列一样。你的这句话就是这样说的:如果你有一个支持List转换的类型,你可以通过重用Seq来免费获得地图操作。

    然而,在Haskell中,map类更普遍;它与序列的概念无关。您可以对没有良好映射到序列的类型实现Seq,例如Seq.map操作,解析器组合器,函数等:

    Functor

    “映射”的概念实际上并不依赖于序列。最好理解仿函数法则:

    fmap

    非正式地说:

    1. 第一条法律规定,使用身份/ noop功能进行映射与无所事事相同。
    2. 第二定律说你可以通过两次映射产生的任何结果,你也可以通过映射产生一次。
    3. 这就是为什么你希望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 个操作,就会变得更加困难,更难以做出这样的保证。< / p>

答案 2 :(得分:26)

我不想在此处的一些优秀答案中重复信息,但我想补充一点。

您通常不需要更高级的类型来实现任何一个特定的monad,或者functor(或applicative functor,或者arrow,或......)。但这样做大多是错过了重点。

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

在广泛使用此类通用代码的上下文中,这意味着只要您编写新的monad实例,您的类型就会立即获得对已经为您编写的大量有用操作的访问那是到处看到monad(和functor,......)的重点;不是这样我可以使用bind而不是concatmap来实现myFunkyListOperation(这本身就没有任何收获),而是当我需要时{ {1}}和myFunkyParserOperation我可以重复使用我最初在列表方面看到的代码,因为它实际上是monad-generic。

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

答案 3 :(得分:14)

对于更具特定于.NET的观点,我前段时间写了一篇blog post。它的关键在于,使用更高级别的类型,您可以在IEnumerablesIObservables之间重复使用相同的LINQ块,但是如果没有更高级的类型,这是不可能的。

你能得到的最接近的(我在发布博客后想出来的)就是制作你自己的IEnumerable<T>IObservable<T>并从IMonad<T>扩展它们。这将允许您重复使用LINQ块,如果它们被标记为IMonad<T>,但它不再是类型安全的,因为它允许您在同一个内混合IObservablesIEnumerables阻止,虽然启动它可能听起来很有趣,但你基本上只是得到一些未定义的行为。

我写了一篇关于Haskell如何简化的later post。 (无操作,真的 - 将块限制为某种monad需要代码;启用重用是默认值。)

答案 4 :(得分:13)

Haskell中最常用的高级类型多态性示例是Monad接口。 FunctorApplicative以同样的方式处于较高的状态,因此我会显示Functor以显示简洁的内容。

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

现在,检查一下这个定义,看看如何使用类型变量f。您会看到f不能代表具有价值的类型。您可以识别该类型签名中的值,因为它们是函数的参数和结果。因此,类型变量ab是可以具有值的类型。类型表达式f af b也是如此。但不是f本身。 f是高级类型变量的示例。鉴于*是可以包含值的类型,f必须具有* -> *种类。也就是说,它采用可以具有值的类型,因为我们从先前的检查中知道ab必须具有值。我们还知道f af b必须包含值,因此它返回一个必须具有值的类型。

这使得f定义中使用的Functor成为更高级别的变量。

ApplicativeMonad界面添加更多,但它们兼容。这意味着它们也可以使用类型* -> *处理类型变量。

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

答案 5 :(得分: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。没有更高级的类型?对于每个函子,您都需要一个自定义版本。这是一个非常简单的示例,但您可能不想每次都重写一些更棘手的示例。

答案 6 :(得分:-4)

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

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