Ash*_*ken 13 haskell functional-programming algebraic-data-types
Haskell使用类型构造函数和数据构造函数来构造代数数据类型.例如,
data Circle = Circle Float Float Float
Run Code Online (Sandbox Code Playgroud)
我们被告知这个数据构造函数(右边的圆圈)是一个在给出数据时构造一个圆的函数,例如x,y,radius.
Circle :: Float -> Float -> Float -> Circle
Run Code Online (Sandbox Code Playgroud)
我的问题是:
这个功能实际上是由什么构建的?
我们可以定义构造函数吗?
我见过Smart Constructors,但它们似乎只是额外的函数,最终会调用常规构造函数.
从OO背景来看,构造函数当然具有必要的规范.在Haskell中,它们似乎是系统定义的.
che*_*ner 16
在Haskell中,不考虑底层实现,数据构造函数创建一个值,主要是通过fiat."'让它有一个Circle',程序员说,并且有一个Circle."询问Circle 1 2 3创建的内容类似于询问文字1在Python或Java中创建的内容.
一个无效的构造函数更接近你通常认为的文字.该Boolean类型的字面定义为
data Boolean = True | False
Run Code Online (Sandbox Code Playgroud)
where True和False是数据构造函数,而不是Haskell语法定义的文字.
数据类型也是构造函数的定义; 因为除了构造函数名称及其参数之外没有任何值,只需说明它是定义.您可以Circle通过Circle使用3个参数调用数据构造函数来创建类型的值,就是这样.
所谓的智能构造函数只是一个调用数据构造函数的函数,可能还有一些其他逻辑来限制可以创建哪些实例.例如,考虑一个简单的包装器Integer:
newtype PosInteger = PosInt Integer
Run Code Online (Sandbox Code Playgroud)
构造函数是PosInt; 一个聪明的构造函数可能看起来像
mkPosInt :: Integer -> PosInteger
mkPosInt n | n > 0 = PosInt n
| otherwise = error "Argument must be positive"
Run Code Online (Sandbox Code Playgroud)
有了mkPosInt,没有办法PosInteger用非正参数创建一个值,因为只有正参数才能实际调用数据构造函数.当模块导出而不是数据构造函数时,智能构造函数最有意义,因此典型用户无法创建任意实例(因为模块外部不存在数据构造函数).
K. *_*uhr 13
好问题.如您所知,给定定义:
data Foo = A | B Int
Run Code Online (Sandbox Code Playgroud)
这个定义与(无参)类型构造类型Foo和两个数据构造,A和B.
这些数据构造函数中的每一个在完全应用时(对于A单个Int参数的情况下没有参数B),构造一个类型的值Foo.所以,当我写:
a :: Foo
a = A
b :: Foo
b = B 10
Run Code Online (Sandbox Code Playgroud)
名称a和b绑定到两个类型的值Foo.
因此,类型Foo构造值的数据构造函数类型Foo.
什么是类型的值Foo?嗯,首先,它们与任何其他类型的值不同.其次,它们完全由数据构造函数定义.对于数据构造函数的每个组合以及传递给该数据构造函数的一组不同参数Foo,存在与类型的所有其他值不同的类型的不同值Foo.也就是说,Foo当且仅当它们使用相同的参数集构造相同的数据构造函数时,两个类型的值是相同的.("相同"在这里意味着与"相等"不同,这可能不一定是针对给定类型定义的Foo,但是我们不能进入那个.)
这也是使数据构造函数与Haskell中的函数不同的原因.如果我有一个功能:
bar :: Int -> Bool
Run Code Online (Sandbox Code Playgroud)
这是可能的bar 1和bar 2可能是完全一样的价值.例如,if bar由以下定义:
bar n = n > 0
Run Code Online (Sandbox Code Playgroud)
然后很明显,bar 1和bar 2(和bar 3)是相同的True.bar对于其参数的不同值,值是否相同将取决于函数定义.
相反,如果Bar是构造函数:
data BarType = Bar Int
Run Code Online (Sandbox Code Playgroud)
那么它永远不会是那种情况Bar 1并且Bar 2是相同的价值.根据定义,它们将是不同的值(类型BarType).
顺便说一下,构造函数只是一种特殊函数的想法是一个共同的观点.我个人认为这是不准确的,会引起混淆.虽然构造函数通常可以被用作函数(特别是它们在表达式中使用时它们的行为非常类似),但我认为这种观点在严格审查中不会受到影响 - 构造函数在表面上的表示方式不同语言的语法(带有大写标识符)可用于不能使用函数的上下文(如模式匹配),在编译代码中表示不同等.
所以,当你问"我们可以定义构造函数"时,答案是"不",因为没有构造函数.相反,像构造函数A或B或Bar或Circle为它是什么-不同的东西从一个函数(有时表现如同一些特殊的附加属性的功能),它有能力建造任何类型的数据构造属于价值.
这使得Haskell构造函数与OO构造函数非常不同,但这并不奇怪,因为Haskell值与OO对象非常不同.在OO语言中,您通常可以提供构造函数,在构建对象时执行某些处理,因此在Python中您可以编写:
class Bar:
def __init__(self, n):
self.value = n > 0
Run Code Online (Sandbox Code Playgroud)
之后:
bar1 = Bar(1)
bar2 = Bar(2)
Run Code Online (Sandbox Code Playgroud)
我们有两个不同的对象bar1和bar2(可以满足bar1 != bar2)已经配置了相同的字段值,并在某种意义上"相等".这有点介于上面的情况bar 1和bar 2创建两个相同的值(即True)之间的情况,Bar 1以及Bar 2创建两个不同值的情况,根据定义,它们在任何意义上都不可能是"相同的".
你永远不会遇到Haskell构造函数的这种情况.不要将Haskell构造函数视为运行某些底层函数来"构造"一个可能涉及一些很酷的处理和派生字段值的对象,而应该将Haskell构造函数视为附加到值的被动标记(也可能是包含零个或多个其他值,具体取决于构造函数的arity).
因此,在您的示例中,Circle 10 20 5不Circle通过运行某些函数来"构造"类型的对象.它直接创建一个标记对象,在内存中,它看起来像:
<Circle tag>
<Float value 10>
<Float value 20>
<Float value 5>
Run Code Online (Sandbox Code Playgroud)
(或者你至少可以假装它在内存中的样子).
在Haskell中最接近OO构造函数的是使用智能构造函数.正如您所注意到的,最终智能构造函数只调用常规构造函数,因为这是创建给定类型值的唯一方法.无论你构建什么样的奇怪的智能构造函数来创建它Circle,它构造的值都需要看起来像:
<Circle tag>
<some Float value>
<another Float value>
<a final Float value>
Run Code Online (Sandbox Code Playgroud)
你需要用一个普通的Circle构造函数调用来构造它.智能构造函数无法返回任何其他东西,它仍然是一个Circle.这就是Haskell的工作原理.
这有帮助吗?
我将以一种稍微迂回的方式回答这个问题,我希望通过一个例子说明我的观点,即Haskell 在"类"的概念下解耦了几个在OOP中耦合的不同想法.理解这一点将有助于您将您从OOP到Haskell的体验转化为更少的难度.OOP伪代码中的示例:
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; }
}
Run Code Online (Sandbox Code Playgroud)
在Haskell:
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
Run Code Online (Sandbox Code Playgroud)
注意:
的Person类已翻译成一个模块恰好由相同的名称-导出一个数据类型的类型(对于域表示和不变量)从被解耦模块(对于命名空间和代码组织).
字段id和name被指定为,private在class定义中,被转换为对普通的(公共)字段data定义,因为在Haskell他们从出口清单中移除他们提出私人Person模块- 定义和知名度是分离的.
构造函数已被翻译成两部分:一个(Person数据构造函数)简单地初始化字段,另一个(mkPerson)执行验证 - 分配和初始化和验证是分离的.由于Person类型是导出的,但它的构造函数不是,这是客户端构造Person-it是"抽象数据类型" 的唯一方法.
公共接口已翻译成该功能是通过导出Person模块,以及setName先前功能突变的Person对象已经成为一个返回的新实例的函数Person,恰好共享旧ID数据类型.OOP代码有一个错误:它应该包括setName对name != ""不变量的检入; Haskell代码可以通过使用mkPerson智能构造函数来确保所有Person值都通过构造有效来避免这种情况.所以状态转换和验证也是分离的 - 你只需要在构造一个值时检查不变量,因为它之后不能改变.
至于你的实际问题:
- 这个功能实际上是由什么构建的?
数据类型的构造函数为值和值的字段分配空间,设置用于创建值的构造函数的标记,并将字段初始化为构造函数的参数.你不能覆盖它,因为这个过程完全是机械的,并且没有理由(在正常的安全代码中)这样做.它是语言和运行时的内部细节.
- 我们可以定义构造函数吗?
否 - 如果要执行其他验证以强制执行不变量,则应使用"智能构造函数"函数来调用较低级别的数据构造函数.因为默认情况下Haskell值是不可变的,所以可以通过构造使值正确 ; 也就是说,当您没有变异时,您不需要强制所有状态转换都是正确的,只需要正确构造所有状态本身.通常,您可以安排您的类型,以便甚至不需要智能构造器.
关于生成的数据构造函数"function",你唯一可以改变的是使用GADT 使其类型签名更具限制性,以帮助在编译时强制执行更多的不变量.而作为一个侧面说明,GADTs还让你做存在量词,它可以让你在运行时随身携带封装/类型擦除的信息,正是像一个面向对象的虚函数表,所以这是在Haskell中分离,但加上在典型的面向对象编程语言的另一件事.
长话短说(太晚了),你可以做所有相同的事情,你只是以不同的方式安排它们,因为Haskell在单独的正交语言特性下提供OOP类的各种功能.
| 归档时间: |
|
| 查看次数: |
865 次 |
| 最近记录: |