如何导出 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
类型的简单字段。但是 runIdentity
的 type 是 Identity a -> a
,因为 runIdentity
是从 Identity a
中提取该字段的函数。毕竟,如果不提供要从中获取的值,就无法从值中获取 runIdentity
。
编辑:
要在评论中稍微扩展一下 OOP 类比,请考虑 class
class Identity<T> {
public T runIdentity;
}
这是 Identity
monad,松散地翻译成 OOP 代码。模板参数 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
)并为它提供一个要使用的对象 - 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 系统中没有任何类型信息)。但是您可以模糊界限并将值和表达式称为同一类事物;领域大不相同。
来自 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
类型的简单字段。但是 runIdentity
的 type 是 Identity a -> a
,因为 runIdentity
是从 Identity a
中提取该字段的函数。毕竟,如果不提供要从中获取的值,就无法从值中获取 runIdentity
。
编辑: 要在评论中稍微扩展一下 OOP 类比,请考虑 class
class Identity<T> {
public T runIdentity;
}
这是 Identity
monad,松散地翻译成 OOP 代码。模板参数 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
)并为它提供一个要使用的对象 - 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 系统中没有任何类型信息)。但是您可以模糊界限并将值和表达式称为同一类事物;领域大不相同。