什么时候更高的亲属types有用?

我一直在F#开发一段时间,我喜欢它。 但是我知道F#中不​​存在的一个stream行词是高级types。 我已经阅读了更高版本的材料,我想我理解他们的定义。 我只是不知道为什么他们有用。 有人可以提供一些在Scala或Haskell中更高级的types变得容易的例子,需要F#中的解决方法吗? 同样对于这些例子,如果没有更高版本的types(或者F#中的反过来),变通方法是什么? 也许我只是习惯于解决这个问题,我没有注意到这个function的缺失。

(我想)我得到的,而不是myList |> List.map fmyList |> Seq.map f |> Seq.toList更高kindedtypes允许您只需编写myList |> map f ,它会返回一个List 。 这很好(假设它是正确的),但似乎有点小气? (并不能简单地通过允许函数重载?)我通常转换为Seq ,然后我可以转换为任何我想要的事情。 再次,也许我太习惯于解决它了。 但是有没有什么例子可以让更高级的types真正的节省你的击键和types安全?

所以这种types是它的简单types。 例如Int有kind * ,这意味着它是一个基types,可以通过值实例化。 通过对高级types的一些松散的定义(我不确定F#在哪里画线,所以让我们把它包括在内), 多态容器就是一个高级types的好例子。

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

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

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

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

或parsing器

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

两者都有亲切的* -> *


然而,我们可以在Haskell中进一步研究,即使是更高阶的types也是如此。 例如,我们可以寻找一种types(* -> *) -> * 。 一个简单的例子可能是试图填充一个* -> *的容器的Shape

 data Shape f = Shape (f ()) [(), (), ()] :: Shape List 

例如,这对于表征Haskell中的Traversable s非常有用,因为它们总是可以分成它们的形状和内容。

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

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

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

但是我们可以看到分支types包含一Pair Tree a ,所以我们可以从参数types中提取这个块

 data TreeG fa = Branch a (f (TreeG fa)) | Leaf data Pair a = Pair aa type Tree a = TreeG Pair a 

这个TreeGtypes的构造函数有kind (* -> *) -> * -> * 。 我们可以用它来制作像RoseTree这样有趣的其他变体

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

或者像MaybeTree这样的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 。 代数是通过函数载体参数化的。 函数有kind * -> *和载体types*

 data Alg fa = Alg (fa -> a) 

有种(* -> *) -> * -> * 。 因为它与数据types以及在它们上面构build的recursionscheme的关系,所以它们很有用。

 -- | The "single-layer of an expression" functor has kind `(* -> *)` data ExpF x = Lit Int | Add xx | Sub xx | Mult xx -- | 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 fa -> Fix f -> a fold (Alg phi) (Fix f) = phi (fmap (fold (Alg phi)) f) 

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

考虑Haskell中的Functortypes类,其中f是一个更高级的typesvariables:

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

这种types的签名是这样的:fmap将f的types参数从a改为b ,但是保持原样。 所以,如果你在列表上使用fmap ,你会得到一个列表,如果你在parsing器上使用它,你会得到一个parsing器,依此类推。 这些是静态的 ,编译时间的保证。

我不知道F#,但是让我们考虑一下,如果我们试图用Java或C#这样的语言来expressionFunctor抽象,会发生什么,inheritance和generics,但没有更高级的generics。 第一次尝试:

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

第一次尝试的问题是允许接口的实现返回任何实现Functor类。 有人可以写一个FunnyList<A> implements Functor<A>它的map方法返回一个不同types的集合,甚至还有一些不是集合,但仍然是一个Functor 。 另外,在使用map方法时,除非将其转换为实际期望的types,否则不能在结果上调用任何特定于子types的方法。 所以我们有两个问题:

  1. types系统不允许我们表示map方法总是返回与接收方相同的Functor子类的不variables。
  2. 因此,对map的结果没有静态types安全的方式来调用非Functor方法。

还有其他更复杂的方法可以尝试,但其中没有一个真正起作用。 例如,您可以尝试通过定义限制结果types的Functor子types来扩充第一个尝试:

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

这有助于禁止这些较窄接口的实现者从map方法中返回错误types的Functor ,但是由于您可以拥有多less个Functor实现没有限制,所以您将需要多less个更窄的接口。

编辑:注意,这只适用于因为Functor<B>作为结果types出现,所以子接口可以缩小它。所以AFAIK我们不能在以下接口中缩小Monad<B>的使用:

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

在Haskell中,具有更高级别的variables,这是(>>=) :: Monad m => ma -> (a -> mb) -> mb

还有一个尝试是使用recursiongenerics来尝试使接口将子types的结果types限制为子types本身。 玩具例子:

 /** * 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开发人员来说相当神秘,对于普通的开发人员来说也是如此)还是无法expression所需的Functor约束:

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

这里的问题是这并不限制FB具有与FA相同的F ,当你声明一个typesList<A> implements Functor<List<A>, A>map方法仍然可以返回一个NotAList<B> implements Functor<NotAList<B>, B>

在Java中使用原始types(未参数化的容器)进行最后的尝试:

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

这里F将被实例化为像ListMap这样的非参数化types。 这保证了FunctorStrategy<List>只能返回一个List – 但是你放弃了使用typesvariables来跟踪列表的元素types。

这里问题的核心是Java和C#等语言不允许types参数具有参数。 在Java中,如果T是一个typesvariables,则可以写TList<T> ,但不能写T<String> 。 更高级的types消除了这个限制,所以你可以有这样的东西(没有完全想到):

 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更高kindedtypes允许您只需编写myList |> map f ,它会返回一个List 。 这很好(假设它是正确的),但似乎有点小气? (并不能简单地通过允许函数重载?)我通常转换为Seq ,然后我可以转换为任何我想要的事情。

有许多语言以这种方式概括了mapfunction的概念,通过build模,就好像在核心,映射是关于序列。 你的这句话正是本着这种精神:如果你有一个支持Seq转换的types,你可以通过重复使用Seq.map来获得“免费”的map操作。

然而,在Haskell中, Functor这更一般; 它不受序列概念的束缚。 你可以为没有良好映射序列的types实现fmap ,比如IO动作,parsing器组合器,函数等。

 instance Functor IO where fmap f action = do x <- action return (fx) -- This declaration is just to make things easier to read for non-Haskellers newtype Function ab = 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保留这个types的原因,因为只要你得到了产生不同结果types的map操作,就很难做出这样的保证。

我不想在这里重复一些优秀的答案,但是我想补充一点。

你通常不需要更高级的types来实现任何一个特定的monad,函子(或者应用函数,或者箭头,或者…)。 但是这样做大多是没有意义的。

总的来说,我发现当人们没有看到仿函数/单子/凡是的用处时,往往是因为他们一次只想着这些东西。 Functor / monad / etc操作实际上并没有给任何一个实例添加任何东西(而不是调用bind,fmap等,我可以调用我用来实现绑定,fmap等的任何操作)。 你真的想要这些抽象是因为你可以有任何函子/ monad /等一般工作的代码。

在广泛使用这种通用代码的情况下,这意味着每当您编写新的monad实例时,您的types立即就可以访问已经为您编写的大量有用的操作。 这就是在任何地方都能看到monad(和函子,……)的地方。 不是我可以使用bind而不是使用concatmap来实现myFunkyListOperation (本身没有任何好处),但是当我需要myFunkyParserOperationmyFunkyIOOperation我可以重新使用我最初在列表中看到的代码因为它实际上是monad-generic。

但是为了像types安全的monad这样的参数化types进行抽象,需要更高级的types(在这里也可以用其他答案进行解释)。

Monas接口是Haskell中最常用的高级多态的例子。 FunctorApplicative都是用同样的方式更高级的,所以我会给Functor展示一些简洁的东西。

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

现在,检查这个定义,看看如何使用typesvariablesf 。 你会看到f不能代表有价值的types。 您可以识别该types签名中的值,因为它们是函数的参数和结果。 所以typesvariablesab是可以有值的types。 typesexpression式fafb 。 但不是f本身。 f是更高亲和型variables的一个例子。 鉴于*是可以具有值的types, f必须具有types* -> * 。 也就是说,它需要一种可以具有价值的types,因为从以前的考察中我们知道ab必须有价值。 而且我们也知道fafb必须有值,所以它返回一个必须有值的types。

这使得在Functor的定义中使用的f是一个更高的typesvariables。

ApplicativeMonad接口添加更多,但它们是兼容的。 这意味着他们也可以使用* -> *来处理typesvariables。

处理更高级别的types会引入额外的抽象级别 – 您不仅限于创build基本types的抽象。 您也可以通过修改其他types的types创build抽象。

对于更多的.NET特定的angular度来看,我早就写了一篇关于这个的博客文章 。 关键是,更高级的types,你可以重复使用IEnumerablesIObservables之间相同的LINQ块,但是没有更高版本的types,这是不可能的。

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

我后来写了一篇关于Haskell如何简化的文章 。 (一个没有操作,真正限制一个块到某种monad需要代码;启用重用是默认的)。