如何推导出Haskell记录字段的类型?

6

从面向对象编程来看,这段代码对我来说很陌生。

我不理解为什么runIdentity的类型是一个函数:

runIdentity :: Identity a -> a? 我指定它应该是runIdentity :: a

newtype Identity a = Identity {runIdentity :: a} deriving Show

instance Monad Identity where
  return = Identity
  Identity x >>= k = k x

instance Functor Identity where
  fmap  f (Identity x) = Identity (f x)

instance Applicative Identity where
  pure = Identity
  Identity f <*> Identity v = Identity (f v)

wrapNsucc :: Integer -> Identity Integer
wrapNsucc = Identity . succ

调用runIdentity

runIdentity $ wrapNsucc 5 -- 输出6


3
你是否熟悉任何面向对象编程语言?class C {int x;}; C i; 现在 i.x 是一个 int,同样地,如果你有 i :: Identity Int 那么 runIdentity i 就是一个 Int。所以如果 i 是一个 Identity IntrunIdentity i 是一个 Int,那么 runIdentity 是什么? - user253751
3个回答

10
你是对的,runIdentity 只是一个类型为 a 的简单字段。但是,runIdentity 的类型是 Identity a -> a,因为 runIdentity 是从 Identity a 中提取该字段的函数。毕竟,你不能在不提供要获取它的值的情况下获取值的 runIdentity

编辑: 在评论中进一步扩展 OOP 类比,请考虑一个类

class Identity<T> {
    public T runIdentity;
}

这是与OOP代码相似的Identity单子。模板参数T基本上就是你的a;因此,runIdentity的类型就是T。要从对象中获取T,你可能会做一些类似的事情。
Identity<int> foo = new Identity<int>();
int x = foo.runIdentity;

你看到runIdentity的类型是T,但实际上并不是这样。你不能只是这样做:
int x = runIdentity; // Nope!

因为 - 从哪里获取runIdentity函数呢?相反,将其视为执行以下操作:

Identity<int> foo = new Identity<int>();
int x = runIdentity(foo);

这段代码展示了调用成员函数时会发生什么。你有一个函数(runIdentity),需要为其提供一个对象来使用 - 如果我没记错的话,这就是Python使用def func(self)的方式。所以runIdentity不再是简单的T类型,而是取一个Identity<T>作为参数并返回一个T类型。
因此,它的类型为Identity a -> a

但是如果我想让runIdentity成为一个函数,难道不应该写成newtype Identity a = Identity {runIdentity :: Identity a -> a}吗? - Oleg
4
如果您这样编写代码,runIdentity函数的类型将是Identity a -> Identity a -> a。由于您来自面向对象编程(OOP)的背景,可以这样想:您调用一个成员函数foo.bar(),它返回类型为a的结果。您可能会认为函数bar()的类型是a,但这显然不是一个函数。相反,它使用类型为Identity afoo(为方便起见)来返回a。例如,在C语言中,这将是a bar(Identity foo),它明显需要一个类型为Identity的参数:对象本身。 - Phil Kiener

4

另一种看待这个问题的方式是,Haskell中的记录语法基本上只是代数数据类型的语法糖,即在Haskell中不存在真正的记录,只有代数数据类型,可能还带有一些额外的语法美化。因此,在很多面向对象语言中类具有成员的概念,而在Haskell中并没有。

data MyRecord = MyRecord { myInt :: Int, myString :: String }

真正的意思只是

data MyRecord Int String

具备额外功能

myInt :: MyRecord -> Int
myInt (MyRecord x _) = x

myString :: MyRecord -> String
myString (MyRecord _ y) = y

自动定义。

使用常规的代数数据类型,您无法自行完成的唯一事情是记录语法给您提供了一种很好的方法,可以制作仅更改了部分字段的MyRecord副本,并为某些模式指定良好的名称。

copyWithNewInt :: Int -> MyRecord -> MyRecord
copyWithNewInt x r = r { myInt = x }

-- Same thing as myInt, just written differently
extractInt :: MyRecord -> Int
extractInt (MyRecord { myInt = x }) = x

因为这只是普通代数数据类型的语法糖,所以您始终可以回归到通常的方式。

-- This is a more verbose but also valid way of doing things
copyWithNewInt :: Int -> MyRecord -> MyRecord
copyWithNewInt x (MyRecord _ oldString) = MyRecord x oldString

顺便说一下,这就是为什么某些看起来荒谬的限制存在的原因(最显著的是,您不能再次使用记录语法定义另一种类型与myInt相同,否则您将在相同的作用域中创建两个具有相同名称的函数,这是Haskell不允许的)。

因此,

newtype Identity a = Identity {runIdentity :: a} deriving Show

(减去方便的更新语法,当您只有一个字段时并不重要)等价于

newtype Identity a = Identity a deriving Show

runIdentity :: Identity a -> a
runIdentity (Identity x) = x

使用记录语法只是将所有内容压缩到一行中,并且可能更深入地说明为什么runIdentity被命名为动词而不是名词。

3
newtype Identity a = Identity {runIdentity :: a} deriving Show

使用记录语法,这里实际上创建了两个名为 runIdentity 的东西。
其中之一是构造函数 Identity 的字段。您可以使用记录模式语法,例如 case i of Identity { x = runIdentity } -> x,将值 i :: Identity a 与该字段的内容匹配,并将其提取到本地变量 x 中。您还可以使用记录构造或更新语法,例如 Identity { runIdentity = "foo" }i { runIdentity = "bar" }
在所有这些情况下,runIdentity 实际上并不是一个独立的事物。您只是将其用作更大的语法结构的一部分,以说明您正在访问哪个 Identity 字段。使用字段 runIdentity 引用的 Identify a 的“插槽”确实存储类型为 a 的内容。但是,这个 runIdentity 字段不是类型为 a 的值。它甚至根本不是一个值,因为它需要具有关于引用数据类型中特定“插槽”的这些额外属性(值没有的属性)。值是独立的东西,它们存在并且有意义。字段不是;字段内容是,这就是为什么我们使用类型对字段进行分类,但字段本身不是值。1 值可以放置在数据结构中,从函数中返回等。没有办法定义一个值,您可以将其放入数据结构中,取回并使用记录模式、构造或更新语法。
使用记录匹配语法定义的另一件名为 runIdentity 的事物是普通函数。函数是值;您可以将它们传递给其他函数,将它们放入数据结构中等。目的是为您提供一个帮助程序,以获取类型为 Identity a 的值中字段的值。但是,因为您必须指定要从中获取 runIdentity 字段的值的哪个 Identity a 值,所以您必须将 Identity a 传递到函数中。因此,runIdentity 函数是类型为 Identity a -> a 的值,与类型为 a 的非值的 runIdentity 字段不同。
一个简单的方法来区分它们是在你的文件中添加像myRunIdentity = runIdentity这样的定义。这个定义声明了myRunIdentity等于runIdentity,但你只能定义像这样的。确实,myRunIdentity将是一个类型为Identity a -> a的函数,你可以将其应用于类型为Identity a的事物以获取一个a值。但它不能与记录语法一起使用作为字段。在那个定义中,字段runIdentity没有随着值runIdentity而“一起出现”。
这个问题可能是由在ghci中输入:t runIdentity这样的类型引发的,要求它显示类型。它会回答runIdentity :: Identity a -> a。原因是因为:t语法适用于2。你可以在那里输入任何表达式,它都会给你结果的类型。所以:t runIdentity看到的是runIdentity(函数),而不是runIdentity字段。
最后,我一直在强调字段runIdentity :: a和函数runIdentity :: Identity -> a是两个不同的东西。我这样做是因为我认为清晰地将两者分开会帮助那些困惑于“runIdentity的类型是什么”的人。但也可以合理地解释runIdentity是一个单一的东西,当你将字段用作一等值时,它就像一个函数一样运行。这也是人们经常谈论字段的方式。所以如果其他来源坚称只有一个东西,请不要感到困惑;这只是看待相同语言概念的两种不同方式。
1如果你听说过lenses,一个视角是它们是普通值,可以用来给我们所有来自“字段”的语义,而不需要任何特殊的语法。因此,一个假设的语言理论上可能根本不提供任何字段访问的语法,只要我们声明一个新的数据类型就给我们镜头,我们就能够应对。

但Haskell记录语法字段并不是lenses;当作为值使用时,它们只是“getter”函数,这就是为什么有专门的模式匹配、构造和更新语法来使用字段的方式超出了普通值的范围。

2嗯,更准确地说,它适用于表达式,因为它对代码进行类型检查,而不是运行代码,然后查看值以确定其类型(这种方法无法工作,因为GHC系统中的运行时Haskell值没有任何类型信息)。但是您可以模糊界限,将值和表达式称为同一种类型的东西;字段则完全不同。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接