我一直在做F#开发一段时间,我喜欢它 . 然而,我知道在F#中不存在的一个流行语是更高级的类型 . 我已经阅读了关于高等类型的材料,我想我理解他们的定义 . 我只是不确定他们为什么有用 . 有人可以在Scala或Haskell中提供一些高级类型易于使用的示例吗?这需要F#中的变通方法?同样对于这些示例,如果没有更高级的类型(或F#中反之亦然),解决方法是什么?也许我只是习惯于解决它,我没有注意到这个功能的缺席 .
(我认为)我得到的不是 myList |> List.map f
或 myList |> Seq.map f |> Seq.toList
更高的kinded类型允许你简单地写 myList |> map f
并且它将返回 List
. 那个's great (assuming it'是正确的),但似乎有点小气? (并且不能简单地通过允许函数重载来完成?)我通常转换为 Seq
然后我可以转换为我想要的任何东西 . 再说一次,也许我只是习惯于解决它 . 但有没有任何一个例子,高级类型的类型真的可以节省你的击键或类型安全?
5 回答
所以类型的类型是它的简单类型 . 例如
Int
有类*
,这意味着它不确定F#在哪里绘制线,所以让我们只包括它)多态容器是一个更好的类型的一个很好的例子 .类型构造函数
List
具有类型* -> *
,这意味着它必须传递具体类型才能生成具体类型:List Int
可以拥有像[1,2,3]
这样的居民,但List
本身不能 .我将假设多态容器的好处是显而易见的,但存在更多有用的类型
* -> *
类型而不仅仅是容器 . 例如,关系或解析器
两者也有点
* -> *
.然而,我们可以在Haskell中进一步采用具有更高阶类型的类型 . 例如,我们可以查找类型
(* -> *) -> *
的类型 . 一个简单的例子可能是Shape
,它试图填充* -> *
类型的容器 .例如,这对于表征Haskell中的
Traversable
是很有用的,因为它们总是可以分为它们的形状和内容 .作为另一个例子,让我们考虑一个在它所具有的分支类型上参数化的树 . 例如,可能是普通树
但我们可以看到分支类型包含
Pair
的Tree a
s,因此我们可以参数化地从类型中提取该片段这个
TreeG
类型的构造函数有类(* -> *) -> * -> *
. 我们可以使用它来制作有趣的其他变体,如RoseTree
或病态的像
MaybeTree
或
TreeTree
显示的另一个地方是"algebras of functors" . 如果我们放弃几层抽象,这可能更好地被视为折叠,例如
sum :: [Int] -> Int
. 代数是在仿函数和载体上参数化的 . 仿函数* -> *
和载体种类*
完全如此有点
(* -> *) -> * -> *
.Alg
非常有用,因为它与数据类型和在它们之上构建的递归方案有关 .最后,虽然他们从来没有看到过更高级的类型构造函数 . 我们有时会看到这种类型的函数,例如
mask :: ((forall a. IO a -> IO a) -> IO b) -> IO b
,但我认为你必须深入研究类型序言或依赖类型的文献才能看到类型的复杂程度 .考虑Haskell中的
Functor
类型类,其中f
是一个更高级的类型变量:此类型签名所说的是fmap将
f
的类型参数从a
更改为b
,但保留f
. 因此,如果您在列表上使用fmap
,则会获得一个列表,如果您在解析器上使用它,则会获得解析器,依此类推 . 这些是静态的编译时保证 .如果我们试图用Java或C#这样的语言表达
Functor
抽象,继承和泛型,但没有更高级的泛型,我不会考虑会发生什么 . 第一次尝试:第一次尝试的问题是允许接口的实现返回实现
Functor
的任何类 . 有人可以写一个FunnyList<A> implements Functor<A>
,其map
方法返回一个不同类型的集合,甚至是其他根本不是集合但仍然是Functor
的东西 . 此外,当您使用map
方法时,您实际上可以期待't invoke any subtype-specific methods on the result unless you downcast it to the type that you' . 所以我们有两个问题:类型系统不允许我们表达
map
方法总是返回与接收者相同的Functor
子类的不变量 .因此,在
map
的结果上调用非Functor
方法没有静态类型安全的方式 .您可以尝试其他更复杂的方法,但它们都不起作用 . 例如,您可以尝试通过定义限制结果类型的
Functor
的子类型来扩充第一次尝试:这有助于禁止那些较窄接口的实现者从
map
方法返回错误类型的Functor
,但由于您可以拥有多少Functor
实现,因此您需要的接口数量没有限制 .( EDIT: 请注意,这只有效,因为
Functor<B>
显示为结果类型,因此子接口可以缩小它 . 所以AFAIK我们不能缩小以下界面中Monad<B>
的两种用法:在Haskell中,具有更高级别的类型变量,这是
(>>=) :: Monad m => m a -> (a -> m b) -> m b
. )另一种尝试是使用递归泛型来尝试使接口将子类型的结果类型限制为子类型本身 . 玩具示例:
但是这种技术(对你的普通OOP开发人员来说相当骇人听闻,对你们普通的功能开发人员而言)仍然无法表达所需的
Functor
约束:这里的问题是,这并不限制
FB
与FA
具有相同的F
,因此当您声明类型List<A> implements Functor<List<A>, A>
时,map
方法可以 still 返回NotAList<B> implements Functor<NotAList<B>, B>
.Java中的最终尝试,使用原始类型(非参数化容器):
这里
F
将被实例化为非参数类型,例如List
或Map
. 这可以保证FunctorStrategy<List>
只能返回List
- 但是您已经放弃使用类型变量来跟踪列表的元素类型 .这里问题的核心是Java和C#等语言不允许类型参数具有参数 . 在Java中,如果
T
是类型变量,则可以编写T
和List<T>
,但不能编写T<String>
. 较高级别的类型会删除此限制,因此您可以使用此类内容(未完全考虑):并特别解决了这个问题:
有许多语言通过这种方式概括了
map
函数的概念,通过对其进行建模,就像在心脏上映射是关于序列一样 . 你的这句话就是那种精神:如果你有一个支持从Seq
转换的类型,你可以通过重用Seq.map
获得 Map 操作"for free" .然而,在Haskell中,
Functor
类比这更普遍;它与序列的概念无关 . 您可以为没有良好映射到序列的类型实现fmap
,例如IO
actions,parser combinators,functions等:“映射”的概念实际上并不依赖于序列 . 最好理解仿函数法则:
非正式地:
第一定律说用身份/ noop函数进行映射与什么都不做是一样的 .
第二定律说你可以通过映射两次产生的任何结果,你也可以通过映射产生一次 .
这就是为什么你希望
fmap
保留类型的原因 - 因为只要你得到产生不同结果类型的map
操作,就会变得更加困难,这样做很难 .我不想在这里的一些优秀答案中重复信息,但我想补充一点 .
您通常不需要更高级的类型来实现任何一个特定的monad,或functor(或applicative functor,或arrow,或......) . 但这样做大多是错过了重点 .
一般来说,我看到了仿函数/ monads / whatevers的用处,它一次只能想到这些东西 . Functor / monad / etc操作真的没有添加任何一个实例(而不是调用bind,fmap等我可以调用我用来实现bind,fmap等的任何操作) . 你真正想要的这些抽象是因为你可以拥有与 any functor / monad / etc一般工作的代码 .
在这种通用代码被广泛使用的上下文中,这意味着无论何时编写新的monad实例,您的类型都会立即获得对大量有用操作的访问权限 that have already been written for you . 这就是到处看monad(和functor,......)的重点;不是因为我可以使用
bind
而不是concat
和map
来实现myFunkyListOperation
(它本身没有任何收获),而是当我需要myFunkyParserOperation
和myFunkyIOOperation
时,我可以重新使用我最初在列表方面看到的代码因为它实际上是monad-generic .但是要在类型安全的monad之类的参数化类型中进行抽象,你需要更高级的类型(在此处的其他答案中也有解释) .
对于更具特定于.NET的观点,我前段时间写了一篇关于这个问题的文章blog post . 它的关键在于,使用更高级别的类型,您可以在
IEnumerables
和IObservables
之间重复使用相同的LINQ块,但是如果没有更高级的类型,这是不可能的 .你可以得到的最接近的(我发布博客后想出来的)是制作你自己的
IEnumerable<T>
和IObservable<T>
并将它们从IMonad<T>
扩展 . 如果它们被表示为IMonad<T>
,这将允许您重用LINQ块,但它不再是类型安全的,因为它允许您在同一个块中混合并匹配IObservables
和IEnumerables
,虽然它可能听起来很有趣,但是,你基本上只是得到一些未定义的行为 .我写了一篇关于Haskell如何让这个变得容易的later post . (无操作,真的 - 将块限制为某种monad需要代码;启用重用是默认设置) .
Haskell中最常用的高级类型多态性的例子是
Monad
接口 .Functor
和Applicative
以同样的方式进行了更高的交易,因此我将展示Functor
以显示简洁的内容 .现在,检查该定义,查看如何使用类型变量
f
. 你会看到f
可以't mean a type that has value. You can identify values in that type signature because they'重新参数和函数的结果 . 因此类型变量a
和b
是可以具有值的类型 . 类型表达式f a
和f b
也是如此 . 但不是f
本身 .f
是高级类型变量的示例 . 鉴于*
是可以具有值的类型,f
必须具有* -> *
类型 . 也就是说,它需要一个可以具有值的类型,因为我们从之前的检查中知道a
和b
必须具有值 . 并且我们也知道f a
和f b
必须具有值,因此它返回必须具有值的类型 .这使
f
在Functor
的定义中使用了更高的类型变量 .Applicative
和Monad
接口添加更多,但它们兼容 . 这意味着它们也可以使用类型* -> *
处理类型变量 .处理更高级别的类型引入了额外的抽象级别 - 您不仅限于创建基本类型的抽象 . 您还可以在修改其他类型的类型上创建抽象 .