Haskell简介
Haskell是一种功能强大,快速,类型安全的函数式编程语言。 本书假设您已熟悉Haskell的大部分基础知识。 学习Haskell有两本精彩的书籍,两本都可以在线阅读:
Learn You a Haskell for Great Good!
此外,Haskell学院还有很多很棒的文章。
为了使用Yesod,您至少必须知道Haskell的基础知识。 此外,Yesod使用Haskell的一些功能,这些功能在大多数介绍性文本中都没有介绍。 虽然本书假定读者对Haskell有基本的了解,但本章旨在填补空白。
如果您已经熟悉Haskell,请完全跳过本章。 此外,如果您希望通过使用Yesod弄湿自己的脚,您可以随后回到本章作为参考。
术语
即使对于那些熟悉Haskell语言的人来说,有时也会对术语产生一些混淆。 让我们建立一些我们可以在本书中使用的基本术语。
数据类型
这是像Haskell这样的强类型语言的核心构建块之一。 某些数据类型(如Int)可以视为原始值,而其他数据类型将构建在这些数据类型之上以创建更复杂的值。 例如,您可能代表一个人:
data Person = Person Text Int
复制代码
在这里,文本将给出该人的姓名,而Int将给出该人的年龄。 由于其简单性,这个特定的示例类型将在本书中重复出现。 您可以通过三种方式创建新数据类型:
-
类型声明 例如
type GearCount = Int
仅为现有类型创建同义词。 类型系统无法阻止您在要求GearCount的地方使用Int。 使用它可以使您的代码更加自我记录。 -
newtype声明,例如
newtype Make = Make Text
。 在这种情况下,您不能意外地使用Text
代替Make
; 编译器会阻止你。 newtype包装器在编译期间总是消失,并且不会引入任何开销。 -
数据声明,例如上面的
Person
。 您还可以创建代数数据类型(ADT),例如data Vehicle = Bicycle GearCount | Car Make Model
。
数据的构造函数
在上面的示例中,Person,Make,Bicycle和Car都是数据构造函数。
类型的构造函数
在上面的示例中,Person, Make, and Vehicle都是类型构造函数。
类型变量
考虑数据类型数据也许a = Just a | 没有。 在这种情况下,a是一个类型变量。
在上面的Person和Make数据类型中,我们的数据类型和数据构造函数都共享相同的名称。 在处理具有单个数据构造函数的数据类型时,这是一种常见做法。 但是,没有什么要求遵循这一点; 您始终可以以不同方式命名数据类型和数据构造函数。
工具
自2015年7月以来,Yesod的工具推荐变得非常简单:使用堆栈。 stack是Haskell的完整构建工具,它处理您的编译器(Glasgow Haskell编译器,又名GHC),库(包括Yesod),其他构建工具(如alex和happy)等等。 Haskell中还有其他构建工具,其中大多数都支持Yesod。 但是对于最简单的体验,强烈建议坚持使用堆栈。 Yesod网站提供最新的快速入门指南,其中提供了有关安装堆栈和使用新的脚手架网站的说明。
一旦正确设置了工具链,就需要安装许多Haskell库。 对于本书的绝大多数,以下命令将安装您需要的所有库:
stack build yesod persistent-sqlite yesod-static esqueleto
复制代码
为了从书中运行示例,将其保存在文件中,例如yesod-example.hs,然后运行它:
stack runghc yesod-example.hs
复制代码
编译器
GHC默认运行在非常接近Haskell98模式的地方。 它还附带了大量的语言扩展,允许更强大的类型类,语法更改等。 有多种方法可以告诉GHC打开这些扩展。 对于本书中的大多数代码片段,您将看到语言编译指示,如下所示:
{-# LANGUAGE MyLanguageExtension #-}
复制代码
这些应始终显示在源文件的顶部。 此外,还有另外两种常见方法:
- 在GHC命令行上,传递一个额外的参数-XMyLanguageExtension。
- 在cabal文件中,添加default-extensions块。
我个人从不使用GHC命令行参数方法。 这是个人偏好,但我喜欢在文件中明确说明我的设置。 一般来说,建议避免在您的cabal文件中添加扩展名; 但是,这条规则主要适用于编写公开可用的库。 当您编写一个您和您的团队正在处理的应用程序时,将所有语言扩展定义在一个位置非常有意义。 Yesod scaffolded站点专门使用此方法来避免在每个源文件中指定相同语言编译指示的样板。
我们最终会在本书中使用相当多的语言扩展(在撰写本文时,脚手架使用13)。 我们不会涵盖所有这些的含义。 相反,请参阅GHC文档。
字符串重载
什么是“hello”的类型? 传统上,它是String,定义为type String = [Char]
。 不幸的是,这有很多局限性:
-
这是一种非常低效的文本数据实现。 我们需要为每个cons单元分配额外的内存,加上字符本身每个占用一个完整的机器字。
-
有时我们有类似字符串的数据,实际上不是文本,例如ByteStrings和HTML。
为了解决这些限制,GHC有一个名为OverloadedStrings的语言扩展。 启用时,文字字符串不再具有单形类型字符串; 相反,它们的类型为IsString a⇒a,其中IsString定义为:
class IsString a where
fromString :: String -> a
复制代码
在Haskell中有许多类型可用的IsString实例,例如Text(一种更有效的压缩字符串类型),ByteString和Html。 实际上,本书中的每个示例都会假定此语言扩展已打开。
不幸的是,这个扩展有一个缺点:它有时会混淆GHC的类型检查器。 想象一下,我们有:
{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances #-}
import Data.Text (Text)
class DoSomething a where
something :: a -> IO ()
instance DoSomething String where
something _ = putStrLn "String"
instance DoSomething Text where
something _ = putStrLn "Text"
myFunc :: IO ()
myFunc = something "hello"
复制代码
程序会打印字符串还是文本? 目前尚不清楚。 因此,您需要提供一个显式类型注释,以指定是否应将“hello”视为String或Text。
在某些情况下,您可以通过使用ExtendedDefaultRules语言扩展来克服这些问题,但我们将尝试在本书中明确而不依赖于默认值。
类型族
类型族的基本思想是陈述两种不同类型之间的某种关联。 假设我们想编写一个能安全地获取列表第一个元素的函数。 但我们不希望它只在列表上工作; 我们希望它像处理Word8s一样处理ByteString。 为此,我们需要引入一些关联类型来指定某种类型的内容。
{-# LANGUAGE TypeFamilies, OverloadedStrings #-}
import Data.Word (Word8)
import qualified Data.ByteString as S
import Data.ByteString.Char8 () -- get an orphan IsString instance
class SafeHead a where
type Content a
safeHead :: a -> Maybe (Content a)
instance SafeHead [a] where
type Content [a] = a
safeHead [] = Nothing
safeHead (x:_) = Just x
instance SafeHead S.ByteString where
type Content S.ByteString = Word8
safeHead bs
| S.null bs = Nothing
| otherwise = Just $ S.head bs
main :: IO ()
main = do
print $ safeHead ("" :: String)
print $ safeHead ("hello" :: String)
print $ safeHead ("" :: S.ByteString)
print $ safeHead ("hello" :: S.ByteString)
复制代码
新语法是将类型放在类和实例中的能力。 我们也可以使用数据,这将创建一个新的数据类型,而不是引用现有的数据类型。
还有其他方法可以在类型类的上下文之外使用关联类型。 有关类型系列的更多信息,请参阅Haskell维基页面。
模板Haskell
模板Haskell(TH)是一种代码生成方法。 我们在Yesod的许多地方使用它来减少样板,并确保生成的代码是正确的。 模板Haskell本质上是Haskell,它生成Haskell抽象语法树(AST)。
TH中实际上有更多的功率,因为它实际上可以内省代码。 但是,我们不在Yesod使用这些设施。
编写TH代码可能很棘手,不幸的是,涉及的类型安全性并不高。 您可以轻松编写将生成无法编译的代码的TH。 这只是Yesod开发人员的问题,而不是用户的问题。 在开发过程中,我们使用大量单元测试来确保生成的代码是正确的。 作为用户,您需要做的就是调用这些现有的功能。 例如,要包含外部定义的Hamlet模板,您可以编写:
$(hamletFile "myfile.hamlet")
复制代码
(哈姆雷特在莎士比亚章节中讨论过。)紧跟在括号后面的美元符号告诉GHC接下来是模板Haskell函数。 然后内部代码由编译器运行并生成Haskell AST,然后编译。 是的,甚至可以用这个去做meta。
一个很好的技巧是允许TH代码执行任意IO操作,因此我们可以在外部文件中放置一些输入并在编译时对其进行解析。 一个示例用法是使用编译时检查HTML,CSS和Javascript模板。
如果您的Template Haskell代码用于生成声明,并且被放置在我们文件的顶层,我们可以省略美元符号和括号。 换一种说法:
{-# LANGUAGE TemplateHaskell #-}
-- Normal function declaration, nothing special
myFunction = ...
-- Include some TH code
$(myThCode)
-- Or equivalently
myThCode
复制代码
查看Template Haskell为您生成的代码非常有用。 为此,您应该使用-ddump-splices GHC选项。
这里没有介绍Template Haskell的许多其他功能。 有关更多信息,请参阅Haskell Wiki页面。
模板Haskell引入了一个称为阶段限制的东西,这实质上意味着模板Haskell拼接之前的代码不能引用Template Haskell中的代码,或者后面的代码。 这有时需要您重新排列代码。 同样的限制适用于QuasiQuotes。
虽然开箱即用,Yesod真的适合使用代码生成来避免样板,但是以模板Haskell方式使用Yesod是完全可以接受的。 在“Yesask for Haskellers”一章中有更多相关信息。
QuasiQuotes
QuasiQuotes(QQ)是Template Haskell的一个小扩展,它允许我们在Haskell源文件中嵌入任意内容。 例如,我们之前提到过hamletFile TH函数,它从外部文件中读取模板内容。 我们还有一个名为hamlet的准引号,它将内容内联:
{-# LANGUAGE QuasiQuotes #-}
[hamlet|<p>This is quasi-quoted Hamlet.|]
复制代码
使用方括号和管道来设置语法。 准引脚的名称在开口括号和第一个管道之间给出,并且在管道之间给出内容。
在整本书中,我们经常会使用QQ方法而不是TH驱动的外部文件,因为前者更容易复制和粘贴。 但是,在生产中,除了最短的输入外,建议使用外部文件,因为它可以很好地将非Haskell语法与Haskell代码分离。
API文档
Haskell中的标准API文档程序称为Haddock。标准的Haddock搜索工具叫做Hoogle。我的建议是使用FP Complete的Hoogle搜索及其附带的Haddocks来搜索和浏览文档。这样做的原因是FP Complete Hoogle数据库涵盖了大量的开源Haskell软件包,所提供的文档总是完全生成并且已知可以链接到其他工作的Haddocks。
更常用的来源是Hackage本身,以及haskell.org的Hoogle实例。这些的缺点是 - 基于服务器上的构建问题 - 有时不会生成文档,并且Hoogle搜索默认只搜索可用包的子集。对我们来说最重要的是,Yesod由FP Complete的Hoogle索引,但不是由haskell.org索引。
如果您在阅读本书时遇到了您不理解的类型或功能,请尝试使用FP Complete的Hoogle进行Hoogle搜索以获取更多信息。
概要
你不需要成为Haskell的专家就可以使用Yesod,基本熟悉就足够了。本章希望能够为您提供足够的额外信息,让您在阅读本书的其余部分时感觉更舒服。