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

How to derive the type for Haskell record fields?

来自 OOP,这对我来说就像外星人代码。

我不明白为什么 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 -- gives 6 as output

你说得对,runIdentity 只是一个 a 类型的简单字段。但是 runIdentitytypeIdentity a -> a,因为 runIdentity 是从 Identity a 中提取该字段的函数。毕竟,如果不提供要从中获取的值,就无法从值中获取 runIdentity

编辑: 要在评论中稍微扩展一下 OOP 类比,请考虑 class

class Identity<T> {
    public T runIdentity;
}

这是 Identity monad,松散地翻译成 OOP 代码。模板参数 T 基本上就是你的 a;因此,runIdentityT 类型。要从您的对象中获取 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)并为它提供一个要使用的对象 - IIRC 这就是 Python 对 def func(self) 所做的。因此,runIdentity 不是简单的 T 类型,而是实际上将 Identity<T> 作为 return 和 T 的参数。

因此,它的类型是 Identity a -> a

另一种理解方式是 Haskell 中的记录语法基本上只是代数数据类型的语法糖,即 Haskell 中并不真正存在记录,只有代数数据类型存在,也许一些额外的语法细节。因此,成员的概念与 类 在许多 OO 语言中所采用的方式不同。

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 被命名为那个,即作为动词,而不是名词)。

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 的 "slot" 确实存储了 a 类型的东西。但是这个 runIdentity 字段是 而不是 类型 a 的值。它甚至根本不是一个值,因为它需要具有这些额外的属性(值没有)来引用数据类型中的特定 "slot"。价值观是独立的事物,它们独立存在并有意义。字段不是;字段内容是,这就是为什么我们使用类型class定义字段,但字段本身不是值。1值可以是放置在数据结构中,从函数返回等。无法定义可以放置在数据结构中的值,返回,然后与记录模式、构造或更新语法一起使用。

另一个用记录匹配语法定义的名为 runIdentity 的东西是一个普通函数。函数 值;您可以将它们传递给其他函数,将它们放入数据结构等。目的是为您提供一个帮助程序,以获取类型 Identity a 中字段的值。但是因为你必须指定 which Identity a 值你想从中获取 runIdentity 字段的值,所以你必须将 Identity a 传递给功能。所以 runIdentity function 是类型 Identity a -> a 的值,与 runIdentity field 不同是类型 a.

描述的非值

查看此区别的一种简单方法是将 myRunIdentity = runIdentity 之类的定义添加到您的文件中。该定义声明 myRunIdentity 等于 runIdentity,但您只能这样定义 values。果然 myRunIdentity 将是 Identity a -> a 类型的函数,您可以将其应用于 Identity a 类型的事物以获得 a 值。但它不能与记录语法一起用作字段。字段 runIdentity 没有 "come along with" 该定义中的值 runIdentity

这个问题可能是在 ghci 中输入 :t runIdentity 提示的,要求它显示类型。它会回答 runIdentity :: Identity a -> a。原因是因为 :t 语法适用于 values2。您可以在那里键入任何表达式,它会为您提供结果值的类型。所以 :t runIdentity 看到的是 runIdentity value(函数),而不是 runIdentity 字段。

最后一点,我一直在讨论字段 runIdentity :: a 和函数 runIdentity :: Identity -> a 是如何分开的。我这样做是因为我认为将两者完全分开会帮助人们对为什么 "what is the type of runIdentity" 会有两个不同的答案感到困惑。但说 runIdentity 是一个单一的东西也是一个完全有效的解释,而且当您将一个字段用作第一个 class 值时,它的行为就像一个函数。这就是人们经常谈论领域的方式。因此,如果其他来源坚持只有一件事,请不要感到困惑;这些只是看待相同语言概念的两​​种不同方式。


1 如果您听说过透镜,那么从一个角度来看,它们是普通值,可用于为我们提供我们需要的所有语义"fields",没有任何特殊用途的语法。因此,一种假设的语言在理论上可能根本不提供任何字段访问语法,只是在我们声明新数据类型时给我们镜头,我们就可以凑合了。

但是Haskell 记录语法字段不是镜头;用作值时,它们只是 "getter" 函数,这就是为什么有专门的模式匹配、构造和更新语法来以普通值无法实现的方式使用字段。

2 好吧,它更适合表达式,因为它是对代码进行类型检查,而不是 运行 代码,然后查看值以查看什么键入它是(这无论如何都行不通,因为运行时 Haskell 值在 GHC 系统中没有任何类型信息)。但是您可以模糊界限并将值和表达式称为同一类事物;领域大不相同。