{"componentChunkName":"component---src-templates-post-tsx","path":"/create-new-types-in-haskell","result":{"data":{"markdownRemark":{"html":"<p>在 Haskell 中，我们有几种方法定义新类型？</p>\n<h2><code class=\"language-text\">data</code> 关键字</h2>\n<p>这是我学 Haskell 后第一个接触的类型定义方法，其类型声明<ruby>体<rp>(</rp><rt>body</rt><rp>)</rp></ruby>有两种写法：<ruby>类型构造器<rp>(</rp><rt>type constructor</rt><rp>)</rp></ruby>和<ruby>记录<rp>(</rp><rt>record</rt><rp>)</rp></ruby>语法。</p>\n<p><ruby>类型构造器<rp>(</rp><rt>type constructor</rt><rp>)</rp></ruby>由构造器名和跟在后面的类型组成，直觉上有点像一个具有名字的<ruby>元组<rp>(</rp><rt>tuple</rt><rp>)</rp></ruby>类型。在没有太多字段的时候，或者字段不需要名字的时候，这种方法很好用。在模式匹配中，我们也可以直接用构造器的语法来<ruby>解构<rp>(</rp><rt>destruct</rt><rp>)</rp></ruby>。</p>\n<div class=\"gatsby-highlight\" data-language=\"haskell\"><pre class=\"language-haskell\"><code class=\"language-haskell\"><span class=\"token keyword\">data</span> <span class=\"token constant\">Vector</span> <span class=\"token hvariable\">a</span> <span class=\"token operator\">=</span> <span class=\"token constant\">Vector</span> <span class=\"token hvariable\">a</span> <span class=\"token hvariable\">a</span>\n\n<span class=\"token builtin\">length</span> <span class=\"token operator\">::</span> <span class=\"token constant\">Floating</span> <span class=\"token hvariable\">a</span> <span class=\"token operator\">=></span> <span class=\"token constant\">Vector</span> <span class=\"token hvariable\">a</span> <span class=\"token operator\">-></span> <span class=\"token hvariable\">a</span>\n<span class=\"token builtin\">length</span> <span class=\"token punctuation\">(</span><span class=\"token constant\">Vector</span> <span class=\"token hvariable\">x</span> <span class=\"token hvariable\">y</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=</span> <span class=\"token builtin\">sqrt</span> <span class=\"token punctuation\">(</span><span class=\"token hvariable\">x</span> <span class=\"token operator\">*</span> <span class=\"token hvariable\">x</span> <span class=\"token operator\">+</span> <span class=\"token hvariable\">y</span> <span class=\"token operator\">*</span> <span class=\"token hvariable\">y</span><span class=\"token punctuation\">)</span></code></pre></div>\n<p><ruby>记录<rp>(</rp><rt>record</rt><rp>)</rp></ruby>语法比较特别，看上去像是在 C 和 C++ 的 <code class=\"language-text\">struct</code> 一样定义结构体。但其中的字段其实并不能用 <code class=\"language-text\">object.property</code> 的形式来访问，字段名实际上是函数。</p>\n<div class=\"gatsby-highlight\" data-language=\"haskell\"><pre class=\"language-haskell\"><code class=\"language-haskell\"><span class=\"token keyword\">data</span> <span class=\"token constant\">Address</span> <span class=\"token operator\">=</span> <span class=\"token constant\">Address</span> <span class=\"token punctuation\">{</span> <span class=\"token hvariable\">city</span> <span class=\"token operator\">::</span> <span class=\"token constant\">String</span><span class=\"token punctuation\">,</span> <span class=\"token hvariable\">street</span> <span class=\"token operator\">::</span> <span class=\"token constant\">String</span> <span class=\"token punctuation\">}</span>\n\n<span class=\"token comment\">-- Run `:t city` and you will get</span>\n<span class=\"token comment\">-- city :: Address -> String</span></code></pre></div>\n<p>最后一点，<code class=\"language-text\">data</code> 关键字可以快速声明<ruby>联合类型<rp>(</rp><rt>tagged union</rt><rp>)</rp></ruby>，或者说<ruby>和类型<rp>(</rp><rt>sum type</rt><rp>)</rp></ruby>。大家常用的 <code class=\"language-text\">Maybe</code> 就是这样声明出来的。</p>\n<div class=\"gatsby-highlight\" data-language=\"haskell\"><pre class=\"language-haskell\"><code class=\"language-haskell\"><span class=\"token keyword\">data</span> <span class=\"token constant\">Maybe</span> <span class=\"token hvariable\">a</span> <span class=\"token operator\">=</span> <span class=\"token constant\">Just</span> <span class=\"token hvariable\">a</span>\n             <span class=\"token operator\">|</span> <span class=\"token constant\">Nothing</span></code></pre></div>\n<h2><code class=\"language-text\">type</code> 关键字</h2>\n<p><code class=\"language-text\">type</code> 关键字的作用是定义类型别名。其用法就像 C 和 C++ 的 <code class=\"language-text\">typedef</code> 一样。但因为可以带有类型参数，在表达能力上更像 <a href=\"https://en.cppreference.com/w/cpp/language/type_alias\" target=\"_blank\" rel=\"noopener noreferrer\">C++ 11 后的 using 关键字</a>。</p>\n<div class=\"gatsby-highlight\" data-language=\"haskell\"><pre class=\"language-haskell\"><code class=\"language-haskell\"><span class=\"token keyword\">type</span> <span class=\"token constant\">Point</span> <span class=\"token hvariable\">a</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span><span class=\"token hvariable\">a</span><span class=\"token punctuation\">,</span> <span class=\"token hvariable\">a</span><span class=\"token punctuation\">)</span>\n\n<span class=\"token hvariable\">origin</span> <span class=\"token operator\">::</span> <span class=\"token constant\">Point</span> <span class=\"token constant\">Float</span>\n<span class=\"token hvariable\">origin</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span><span class=\"token number\">0.0</span><span class=\"token punctuation\">,</span> <span class=\"token number\">0.0</span><span class=\"token punctuation\">)</span></code></pre></div>\n<p>并且新的名字的类型和旧的类型仍然兼容，其可以用在为旧类型所写的函数中，也可以用旧类型的构造器进行解构。</p>\n<div class=\"gatsby-highlight\" data-language=\"haskell\"><pre class=\"language-haskell\"><code class=\"language-haskell\"><span class=\"token hvariable\">zero</span> <span class=\"token operator\">=</span> <span class=\"token builtin\">fst</span> <span class=\"token hvariable\">origin</span>\n\n<span class=\"token comment\">-- Destructing like tuples</span>\n<span class=\"token hvariable\">distance</span> <span class=\"token operator\">::</span> <span class=\"token constant\">Floating</span> <span class=\"token hvariable\">a</span> <span class=\"token operator\">=></span> <span class=\"token constant\">Point</span> <span class=\"token hvariable\">a</span> <span class=\"token operator\">-></span> <span class=\"token constant\">Point</span> <span class=\"token hvariable\">a</span> <span class=\"token operator\">-></span> <span class=\"token hvariable\">a</span>\n<span class=\"token hvariable\">distance</span> <span class=\"token punctuation\">(</span><span class=\"token hvariable\">x1</span><span class=\"token punctuation\">,</span> <span class=\"token hvariable\">y1</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">(</span><span class=\"token hvariable\">x2</span><span class=\"token punctuation\">,</span> <span class=\"token hvariable\">y2</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=</span>\n    <span class=\"token keyword\">let</span> <span class=\"token hvariable\">dx</span> <span class=\"token operator\">=</span> <span class=\"token hvariable\">x1</span> <span class=\"token operator\">-</span> <span class=\"token hvariable\">x2</span>\n        <span class=\"token hvariable\">dy</span> <span class=\"token operator\">=</span> <span class=\"token hvariable\">y1</span> <span class=\"token operator\">-</span> <span class=\"token hvariable\">y2</span>\n    <span class=\"token keyword\">in</span> <span class=\"token builtin\">sqrt</span> <span class=\"token punctuation\">(</span><span class=\"token hvariable\">dx</span> <span class=\"token operator\">*</span> <span class=\"token hvariable\">dx</span> <span class=\"token operator\">+</span> <span class=\"token hvariable\">dy</span> <span class=\"token operator\">*</span> <span class=\"token hvariable\">dy</span><span class=\"token punctuation\">)</span></code></pre></div>\n<h2><code class=\"language-text\">newtype</code> 关键字</h2>\n<p>除了 <code class=\"language-text\">data</code> 和 <code class=\"language-text\">type</code>，Haskell 还有个叫做 <code class=\"language-text\">newtype</code> 的关键字。其用法和 <code class=\"language-text\">data</code> 相同，但有两个限制：</p>\n<ol>\n<li>只能有一个<ruby>构造器<rp>(</rp><rt>constructor</rt><rp>)</rp></ruby>，所以不能声明<ruby>联合类型<rp>(</rp><rt>tagged union</rt><rp>)</rp></ruby>；</li>\n<li>构造器中只能有且仅有一个<ruby>字段<rp>(</rp><rt>field</rt><rp>)</rp></ruby>。</li>\n</ol>\n<p>举个例子，下面两个声明，GHC 就会报错。</p>\n<div class=\"gatsby-highlight\" data-language=\"haskell\"><pre class=\"language-haskell\"><code class=\"language-haskell\"><span class=\"token keyword\">newtype</span> <span class=\"token constant\">Maybe</span> <span class=\"token hvariable\">a</span> <span class=\"token operator\">=</span> <span class=\"token constant\">Just</span> <span class=\"token hvariable\">a</span> <span class=\"token operator\">|</span> <span class=\"token constant\">Nothing</span>\n<span class=\"token keyword\">newtype</span> <span class=\"token constant\">Vector</span> <span class=\"token hvariable\">a</span> <span class=\"token operator\">=</span> <span class=\"token constant\">Vector</span> <span class=\"token hvariable\">a</span> <span class=\"token hvariable\">a</span></code></pre></div>\n<p>换言之，把你代码中的 <code class=\"language-text\">newtype</code> 都替换成 <code class=\"language-text\">data</code> 后代码也可以正常编译，但反过来，如果把 <code class=\"language-text\">data</code> 都替换成 <code class=\"language-text\">newtype</code>，就不能编译了。那么喜欢刨根问底的读者可能就会问了：为什么要设计这个 <code class=\"language-text\">newtype</code> 关键字呢？都用 <code class=\"language-text\">data</code> 关键字不就行了吗？</p>\n<p><code class=\"language-text\">newtype</code> 可以保证这个类型对类型检查器是零开销的。<code class=\"language-text\">newtype</code> 声明的类型构造器在编译时会直接被又划掉。我们知道 Haskell 在默认情况下是<ruby>懒惰求值<rp>(</rp><rt>lazy evaluation</rt><rp>)</rp></ruby>的，所以大量的只有一个字段的类型构造器可能会消耗运行时大量的空间，特别是当这些类型嵌套后，可能会生成很多懒惰执行的代码。所以 Haskell 的<a href=\"https://wiki.haskell.org/Performance/Data_types\" target=\"_blank\" rel=\"noopener noreferrer\">一篇 wiki 文章</a>中也推荐人们把只有一个构造器且构造器中只有一个字段的类型用 <code class=\"language-text\">newtype</code> 来声明。</p>","frontmatter":{"title":"Create New Types in Haskell","date":"March 09, 2018","tags":["haskell","syntax","grammar"],"language":"zh-Hans"}},"site":{"siteMetadata":{"title":"Luyu Cheng"}}},"pageContext":{"id":"258cd97f-4fa5-5672-870e-3bf6263ff003"}},"staticQueryHashes":["3649515864","63159454","684267432"]}