首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >Haskell (数据)构造函数构造什么?

Haskell (数据)构造函数构造什么?
EN

Stack Overflow用户
提问于 2018-07-25 02:21:43
回答 3查看 3.6K关注 0票数 18

Haskell允许使用类型构造函数和数据构造函数构造代数数据类型。例如,

代码语言:javascript
复制
data Circle = Circle Float Float Float

我们被告知这个数据构造函数(右圆)是在给定数据时构造一个圆的函数,例如x,y,radius。

代码语言:javascript
复制
Circle :: Float -> Float -> Float -> Circle 

我的问题是:

  1. 具体而言,这个函数实际上是构造什么的?
  2. 我们可以定义构造函数吗?

我见过智能构造函数,但它们似乎只是额外的函数,最终调用了常规的构造函数。

从面向对象的背景来看,构造函数当然有命令式的规范。在Haskell,它们似乎是由系统定义的。

EN

回答 3

Stack Overflow用户

发布于 2018-07-25 03:08:31

在Haskell中,在不考虑底层实现的情况下,数据构造函数创建一个值,本质上是由fiat创建的。“让Circle存在吧,”程序员说,“还有一个Circle。”询问Circle 1 2 3创建了什么类似于询问1或Java中的文字1创建了什么。

髓质构造函数更接近于你通常认为的字面意思。Boolean类型实际上被定义为

代码语言:javascript
复制
data Boolean = True | False

其中TrueFalse是数据构造函数,而不是Haskell语法定义的文字。

数据类型也是构造函数的定义;因为除了构造函数名称及其参数之外,实际上没有任何值,只需说明它就是定义。通过使用3个参数调用数据构造函数Circle来创建一个类型为Circle的值,就是这样。

所谓的“智能构造函数”只是一个调用数据构造函数的函数,也许还有其他一些逻辑来限制可以创建哪些实例。例如,考虑一下围绕Integer的简单包装器

代码语言:javascript
复制
newtype PosInteger = PosInt Integer

构造函数是PosInt;智能构造函数可能类似于

代码语言:javascript
复制
mkPosInt :: Integer -> PosInteger
mkPosInt n | n > 0 = PosInt n
           | otherwise = error "Argument must be positive"

使用mkPosInt,无法使用非正参数创建PosInteger值,因为只有正数才能真正调用数据构造函数。当智能构造函数(而不是数据构造函数)由模块导出时,最有意义,因此典型用户无法创建任意实例(因为数据构造函数不存在于模块之外)。

票数 19
EN

Stack Overflow用户

发布于 2018-07-25 03:31:29

问得好。如你所知,鉴于这一定义:

代码语言:javascript
复制
data Foo = A | B Int

这定义了一个具有( Foo )类型构造函数和两个数据构造函数( AB )的类型。

当完全应用这些数据构造函数时(在A情况下不使用参数,在B情况下使用单个Int参数),则构造一个Foo类型的值。所以,当我写到:

代码语言:javascript
复制
a :: Foo
a = A

b :: Foo
b = B 10

名称ab绑定到Foo类型的两个值。

类型Foo Foo**.**的数据构造函数构造值。

Foo类型的值是什么?嗯,首先,它们不同于任何其他类型的值。第二,它们完全由数据构造函数定义。对于传递给该数据构造函数的一组不同的参数,数据构造函数的每个组合都有一个与Foo类型不同的值,与Foo的所有其他值不同。也就是说,Foo类型的两个值是相同的当且仅当它们是用相同的数据构造函数构造的,给出了相同的参数集。(这里的“完全相同”是指与“相等”不同的东西,它可能不一定是为给定类型的Foo定义的,但我们不要讨论这个问题。)

这也使得数据构造函数不同于Haskell中的函数。如果我有一个功能:

代码语言:javascript
复制
bar :: Int -> Bool

bar 1bar 2的值可能完全相同。例如,如果bar由以下人员定义:

代码语言:javascript
复制
bar n = n > 0

那么很明显,bar 1bar 2 (和bar 3)是相同的True。对于其参数的不同值,bar的值是否相同将取决于函数定义。

相反,如果Bar是一个构造函数:

代码语言:javascript
复制
data BarType = Bar Int

那么,Bar 1Bar 2的价值是相同的,这是不可能的。根据定义,它们将是不同的值(类型为BarType)。

顺便说一句,构造函数只是一种特殊的函数,这是一个普遍的观点。我个人认为这是不准确的,会造成混乱。虽然构造函数通常可以被当作函数来使用(特别是,它们在表达式中使用时的行为非常类似于函数),但我不认为这种观点经得起很大的审视--构造函数在语言的表面语法中表示不同(带有大写标识符),可以在不能使用函数的上下文(如模式匹配)中使用,在编译代码中表示不同,等等。

因此,当您问“我们能否定义构造函数”时,答案是" no ",因为没有构造函数。相反,像ABBarCircle这样的构造函数就是它--它不同于一个函数(有时它的行为像一个具有特殊附加属性的函数),它能够构造数据构造函数所属的任何类型的值。

这使得Haskell构造函数与OO构造函数非常不同,但这并不奇怪,因为Haskell值与OO对象非常不同。在OO语言中,您通常可以提供一个构造函数,在构建对象时进行一些处理,因此在Python中您可以编写:

代码语言:javascript
复制
class Bar:
    def __init__(self, n):
        self.value = n > 0

然后之后:

代码语言:javascript
复制
bar1 = Bar(1)
bar2 = Bar(2)

我们有两个不同的对象bar1bar2 (这将满足bar1 != bar2),它们被配置为相同的字段值,在某种意义上是“相等”的。这有点介于bar 1bar 2创建两个相同的值(即True)和Bar 1Bar 2创建两个不同的值的情况之间,根据定义,这些值在任何意义上都不可能是“相同的”。

在Haskell构造函数中,您永远不会遇到这种情况。与其将Haskell构造函数看作是运行一些底层函数来“构造”一个对象,该对象可能涉及到一些很酷的处理和字段值的派生,相反,您应该将Haskell构造函数看作附加到一个值的被动标记(该值还可能包含零个或多个其他值,这取决于构造函数的性质)。

因此,在您的示例中,Circle 10 20 5不通过运行某些函数来“构造”Circle类型的对象。它直接创建一个标记对象,在内存中,该对象看起来类似于:

代码语言:javascript
复制
<Circle tag>
<Float value 10>
<Float value 20>
<Float value 5>

(或者你至少可以假装这是记忆中的样子)。

在Haskell中,最接近OO构造函数的是使用智能构造函数。正如您注意到的,智能构造函数最终只调用常规构造函数,因为这是创建给定类型值的唯一方法。无论您构建何种奇怪的智能构造函数来创建Circle,它构造的值都需要如下所示:

代码语言:javascript
复制
<Circle tag>
<some Float value>
<another Float value>
<a final Float value>

它需要使用一个普通的旧Circle构造函数调用来构造。智能构造函数不能返回的其他东西仍然是Circle。哈斯克尔就是这样工作的。

这有用吗?

票数 13
EN

Stack Overflow用户

发布于 2018-07-27 00:16:41

我将以一种有点迂回的方式来回答这个问题,我希望用一个例子来说明我的观点,那就是Haskell在一个“类”的概念下,将几个不同的思想耦合在OOP中。理解这一点可以帮助你以较少的难度将你的经验从面向对象转换到Haskell。OOP伪码中的示例:

代码语言:javascript
复制
class Person {

    private int id;
    private String name;

    public Person(int id, String name) {
        if (id == 0)
            throw new InvalidIdException();
        if (name == "")
            throw new InvalidNameException();

        this.name = name;
        this.id = id;
    }

    public int getId() { return this.id; }

    public String getName() { return this.name; }

    public void setName(String name) { this.name = name; }

}

在Haskell:

代码语言:javascript
复制
module Person
  ( Person
  , mkPerson
  , getId
  , getName
  , setName
  ) where

data Person = Person
  { personId :: Int
  , personName :: String
  }

mkPerson :: Int -> String -> Either String Person
mkPerson id name
  | id == 0 = Left "invalid id"
  | name == "" = Left "invalid name"
  | otherwise = Right (Person id name)

getId :: Person -> Int
getId = personId

getName :: Person -> String
getName = personName

setName :: String -> Person -> Either String Person
setName name person = mkPerson (personId person) name

注意:

  • Person类已被转换为一个模块,该模块碰巧以相同的名称导出数据类型--类型(用于域表示和不变量)与模块(用于名称空间和代码组织)解耦。
  • 字段idnameclass定义中被指定为private,它们被转换为data定义中的普通(公共)字段,因为在Haskell中,它们通过从Person模块的导出列表中省略它们而变得私有--定义和可见性是解耦的。
  • 构造函数已经被转换成两部分:一部分( Person数据构造函数)简单地初始化字段,另一部分(mkPerson)执行验证-分配&初始化和验证是解耦的。由于Person类型是导出的,但它的构造函数不是,这是客户端构造Person的唯一方法--它是“抽象数据类型”。
  • 公共接口已被转换为由Person模块导出的函数,而以前更改Person对象的setName函数已成为一个函数,该函数返回一个碰巧共享旧ID的Person数据类型的新实例。 OOP代码有一个bug:它应该包括一个name != ""不变值的签入setName;Haskell代码可以通过使用mkPerson智能构造函数来避免这一点,以确保所有< code >D30值都通过构造有效。因此,状态转换和验证也是解耦的--在构造值时只需要检查不变量,因为此后不能更改。

至于你的实际问题:

  1. 具体而言,这个函数实际上是构造什么的?

数据类型的构造函数为标记和值的字段分配空间,设置用于创建值的构造函数的标记,并将字段初始化为构造函数的参数。您不能覆盖它,因为这个过程是完全机械的,没有理由(在正常的安全代码中)这样做。这是语言和运行时的内部细节。

  1. 我们可以定义构造函数吗?

否-如果您想要执行额外的验证来执行不变量,则应该使用“智能构造函数”函数来调用较低级别的数据构造函数。由于Haskell值在默认情况下是不可变的,所以可以通过构造来使值正确;也就是说,当您没有变异时,您不需要强制所有状态转换都是正确的,只需要正确地构造所有状态本身。通常,您可以安排您的类型,以便智能构造器甚至是不必要的。

唯一可以更改生成的数据构造函数“函数”的是使用GADT对其类型签名进行更严格的限制,以帮助在编译时强制执行更多不变量。另外,GADT还允许您进行存在量化,它允许您在运行时携带封装/类型擦除的信息,就像OOP vtable一样--所以这是另一件在Haskell中解耦但在典型OOP语言中耦合的东西。

长话短说(太晚了),您可以做所有相同的事情,您只需对它们进行不同的安排,因为Haskell在不同的正交语言特性下提供了OOP类的各种特性。

票数 12
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/51509949

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档