Haskell
Haskell 是一门强大,快速,类型安全的函数式编程语言。本书假定你已经熟悉了Haskell大部分基础知识。这里有两本学Haskell非常棒的书,而且两本都是网上免费阅读的:
另外,这里有许多非常好的文章Scholl of Haskell
为了使用Yesod,你应该至少了解Haskell的基础知识。另外,Yesod使用一些Haskell特性,这些特性在大多入门书籍中没有介绍。所以本书假定读者已经熟悉Haskell的基本语法,本章打算填补这些空缺。
如果你已经能够熟练的使用Haskell,可以轻松的跳过这章。而且如果你在使用Yesod中遇到困难,你仍然可以返回这一章作一个参考。
术语
甚至对于那些已经很熟悉Haskell的程序员来说,有时候仍然会对一些术语感到困惑。这里介绍一些本书要用到的基本术语。
数据类型
数据类型是Haskell这类强类型语言的核心模块之一。 Int
之类的数据类型可以被看做基本数据类型。其他数据类型建立在基本数据类型之上,从而创建出更复杂的数据类型。 举个例子,你可能使用以下代码表示一个person:
data Person = Person Text Int
Text
代表person的名字, Int
代表person的年龄,因为这个例子比较简单,所以会在本书中经常出现,实际上有三种方式来创建一个新的数据类型:
type
声明,例如type GearCount = Int
仅仅是为一个现存的类型创建了一个别名。类型系统允许你在需要GearCount
的地方使用Int
替换。 使用type
能让你的代码更加可读。newtype
声明,例如newtype Make = Make Text
。 这种情况下,编译器不允许你使用Text
替代Make
;newtype包装在编译过程中消失,并不引入额外的开销。data
声明,例如 上面的Person
,你可以创建代数数据类型(ADTs),例如data Vehicle = Bicycle GearCount | Car Make Model
。
数据构造器
在我们上面的例子中, Person
、Make
、Bicycle
和 Car
都是数据构造器。
类型构造器
在我们上面的例子中, Person
、Make
、Vehicle
都是类型构造器。
类型值
思考一下数据类型data Maybe a = Just a | Nothing
。 在这种情况下,a
就是一个类型变量。
Person
和Make
数据类型中,我们的类型构造器和数据构造器使用了同样的名字。在处理单数据构造器的数据类型时,这是一中常见的做法。然而,这样并没有什么硬性的要求;你完全可以给数据构造器和类型构造器取不同的名字。
工具
自从2015年7月,Yesod工具推荐变得非常简单:就是使用stack, stack是一个完整的Haskell构建工具, 它可以处理你的编译器(Glasgow Haskell Compiler, 同 GHC),库(包括Yesod),额外的构建工具(比如alex,happy)等等。这里还有其他Haskell可用的构建工具,其中大多数都对Yesod支持很好。但是作为最简单的方式,强烈建议使用stack。Yesod网页上已经更新到最新了(快速入门指南)[http://www.yesodweb.com/page/quickstart],提供了stack的安装说明和快速开始。
一旦你已经设置好你的工具链,你需要安装许多Haskell库。使用一下命令可以安装本书需要的大部分库:
stack build yesod persistent-sqlite yesod-static esqueleto
运行本书中的例子,你需要把代码保存到一个文件,比如,yesod-example.hs, 然后使用一下命令运行:
stack runghc yesod-example.hs
语言编译指示
GHC运行默认情况下在一些情况非常接近Haskell98。而且附带大量的语言扩展,允许更加强大的类型类,语法变化等等。这里有多种方式告诉GHC打开这些扩展。对于本书中的大部分代码片段,你会看到语言编译指示,就像下方所示:
{-# LAUGUAGE MyLanguageExtension #-}
这些应该出现源文件的开头。另外,这里还有两个其他途径:
- 在GHC命令行中,传入额外的参数
-XMyLanguageExtension
。 - 在你的
cabal
文件,添加一个default-extension
块。
我个人意见是不要使用GHC命令行参数方式。这是个人爱好,但是我比较喜欢把我的设置清晰的写在一个文件中。通常情况下,不建议将扩展写在cabal
文件中; 在写公用的库的时候,这个规则大多数情况下是奏效的。当你和你的团队合作开发的应用时,把所有语言扩展指定到一个特定的位置会有很大的意义。 Yesod网页使用这种途径,来避免每个文件中相同的语言扩展样板代码。
我们会在本书中使用相当多的语言扩展。我们不会覆盖到它们的所有含义,如果需要,请阅读GHC文档
重载字符串
"hello"
是什么类型呢?传统意义上,它是个String
,定义是type String = [Char]
。 不幸的是,这样有一些限制:
- 这是一个很低效的文本数据实现。我们需要为连接每个单元分配额外的内存,加上每个字符本身都会消耗一个完整机器字(machine word)。
- 有时候我们有一些长得像string但不是文本的数据,比如
ByteString
和HTML。
要解决这些限制,GHC有一个语言扩展叫做OverloadedStrings
。当打开这个选项的时候,字面字符串将不再有单一类型String
;替代的,它们将会有这种类型IsString a ⇒ a
,定义如下:
class IsString a where
fromString :: String -> a
在Haskell中,有许多IsString
的可用实例,例如,Text
(一个更加高效的压缩String
类型),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"
程序会打印出什么呢? String
或者Text
?这不清楚。所以,你需要给一个明确的注解,这个"hello"
应该被看做String
或者Text
。
在一些情况中,你可以通过使用
ExtendDefaultRules
语言扩展来解决这些问题,但是我们尝试在书中保持明确,而并不依赖于默认( though we’ll instead try to be explicit in the book and not rely on defaulting.)。
Type Families
Type family的基本概念是说明两种不同类型之间的关联。假定我们想要写一个函数,这个函数安全地取出一个列表中的第一个元素。但是我们不想让它在仅仅在列表中工作;我们可能会把ByteString
看成一个Word8
列表。为了实现这个功能,我们需要引进一些关联类型,来指定一个类型的内容是什么。T
{-# 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)
新语法提供了一种能力,我们可以在class
和instance
中放置一个type
。我们也可以使用 data
替代, 当然,这样会创建一个新类型,而不是为一个存在的类型创建一个别名。
这里有其他方式在类型类上下文之外使用关联类型。其他关于type families,看这里Haskell wiki page. 。
Template Haskell
Template Haskell(TH)是一种代码生成方法。我们在Yesod中大量使用使用,来减少样板代码,和保证生成代码的正确性。Template Haskell本质上是Haskell生成Haskell抽象语法树(AST)。
写 TH 代码可能是很棘手的,而且不幸的是没有很多的类型安全参与其中。 你可以简单地写 TH 生成的代码将无法编译。这是Yesod开发者唯一存在的问题,不是针对Yesod的使用者。整个开发过程, 我们使用大量的单元测试来保证生成的代码的正确性。作为一个使用者,所有你需要做的就是调用这些已经存在的函数。举个例子,包含一个外部定义的Hamlet模板,你可以这样写:
$(hamletFile "myfile.hamlet")
(Hamlet 在Shakespeare章节讨论)。挨着括号的$符号告诉GHC接下来是Haskell模板函数。函数内部的代码会被编译器运行,生成一个Haskell AST,然后再进行编译。 (go meta with this)[http://www.yesodweb.com/blog/2010/09/yo-dawg-template-haskell]。
一个非常好的技巧是 TH 代码允许被任意IO
动作执行,因此,我们可以放置一些外部的文件输入然后在编译时候进行解析。一个使用例子,我们在编译时来检查HTML,CSS,JavaScript模板。
如果你的Template Haskell代码是用来生成声明的,那么它应该被放置在文件的上方,我们可以省去$符号和括号,换句话来说:
{-# LANGUAGE TemplateHaskell #-}
-- Normal function declaration, nothing special
myFunction = ...
-- Include some TH code
$(myThCode)
-- Or equivalently
myThCode
你可以使用-ddump-splices
GHC选项,来观察Template Haskell为你生成的代码,这是非常有用的。
这里有很多其他Template Haskell特性没有涉及到,获得更多信息,请看Haskell wiki page
Template Haskell引进了阶段限制(stage restriction),其本质上意味着Template Haskell代码拼接之前不能引用到Template Haskell代码。这样有时会要求你重新调整一下你的代码位置。QuasiQuotes也有同样的限制。
Yesod 真正面向使用生成代码来避免模板代码,在Yesod中,广泛使用Template Haskell是可以接受的,更多信息请看"Yesod for Haskellers"章节。
QuasiQuotes
QuasiQuotes(QQ)是Template Haskell的一个辅助扩展,它使得我们可以在Haskell源文件中嵌入任意的内容。举个例子,我们先前提到的hamletFile
TH 函数,这个函数功能是从外部文件中读取模板内容。我们也可以使用quasi-quoter来写hamlet
:
{-# LANGUAGE QuasiQuotes #-}
[hamlet|<p>This is quasi-quoted Hamlet.|]
这种语法使用了方括号和管道'|'。quasi-quoter的名字在'['和'|'之间,内容在'|'和']'之间。
在本书中,我们会多次在一个Template Haskell外部文件上,使用QuasiQuotes,因为前者更加易于拷贝粘贴。但是在生产环境中,除了特别短的输入,都建议使用外部文件的方式,因为这样能够很好的隔离那些不是Haskell语法的代码和Haskell代码。
API文档
Haskell 的标准API文档叫做 Haddock 。标准搜索工具叫做 Hoogle。 我得建议是使用FP Complete's Hoogle search和它自带的Haddocks来搜索和浏览文档。原因是,FP Complete's Hoogle search的数据库覆盖了大量的开源Haskell包, 而且文档较全,还能链接到其他Haddocks。
更加常用的资源是Hackage本身,和haskell.org’s Hoogle instance。缺点是在服务器上的构建问题会使得文档不能产生,Hoogle搜索默认只是搜索可用包的子集。最重要的是,Yesod索引是由FP Complete's Hoogle,而不是haskell.org官网。
如果你在读本书的时候,有任何不理解的类型或者函数,尝试使用FP complete's Hoogle进行搜索来获取更多信息。
总结
如果你只是使用Yesod,你不需要成为Haskell专家,对基本语法熟悉就已经足够了。 希望本章能够给你带来足够的额外信息,让你能够更为舒服的读完本书。