天空Blond - Haskell
type: channel
link: https://www.skyblond.info/tag/Haskell/
items: 【代码札记】The Day I Learned Haskell 6
items: 【代码札记】The Day I Learned Haskell 5
items: 【代码札记】The Day I Learned Haskell 4
items: 【代码札记】The Day I Learned Haskell 3
items: 【代码札记】The Day I Learned Haskell 2
items: 【代码札记】The Day I Learned Haskell 1
items: 【歪门邪道】使用Sublime Text 3搭建Haskell IDE

【代码札记】The Day I Learned Haskell 6
type: item
date: 2018-12-23T13:46:00+08:00
description: CIS194 Spring13的Homework 5，有关类型类（typeclass）的作业。你应当提交一个Calc.hs的文件，同时文件中应当包含一个同名的模块（module）。如我们在课上所见，Haskell的类型类提供了特殊的多态性（ad-hoc polymorphism），即能够根据输入的类型决定做什么。 这次作业探讨了在构建特定领域语言（domain-specific languages）时类型类的一个有趣用法。表达式假设有一天你的新工作是一个软件工程师，你被要求为公司的新重磅产品编程：一个计算器。但这不只是一个普通的计算器！广泛的焦点小组（Extensive focus group）分析表明，人们真正想让他们的计算器拥有的功能无非是能够做整数加法和乘法。额外的功能只会让界面变得更杂乱。你的老板已经开始对域（domain）建模了，算术表达式的数据类型如下：data ExprT = Lit Integer | Add ExprT ExprT | Mul ExprT ExprT deriving (Show, Eq)这个类型能够兼容涉及到整数常量、加法、乘法的算术表达式。例如表达式 $(2+3)\times4$ 应该能够表示成：Mul (Add (Lit 2) (Lit 3)) (Lit 4)。你的老板已经在ExprT.hs中定义了ExprT类型，所以和平时一样你只需要在你的文件顶部引用ExprT就可以了。然而，这里就是你的老板卡住的地方。练习 1编写第一个版本的计算器：为ExprT编写一个求值器：eval :: ExprT -> Integer例如：eval (Mul (Add (Lit 2) (Lit 3)) (Lit 4)) == 20我的解答eval :: ExprT -> Integer eval (ExprT.Lit x) = x eval (ExprT.Add a b) = eval a + eval b eval (ExprT.Mul a b) = eval a * eval b解读：简单的模式匹配，这里使用了ExprT.Lit，表明Lit函数是在ExprT模块中定义的，是为了和后面的练习题做出区别，下面还会有定义重名的情况。这个求值器本身就是简单的递归调用和模式匹配。ExprT最基本的结构就是Lit，他的值就是其中包含的整数。而其他的Add和Mul就是对Lit和前述二者的组合，因此对于任意两个ExprT的运算，分别求明白参与运算的两个ExprT的值，再进行运算就是了。练习 2UI部门已经实现了焦点小组的研究，并且已经准备好和你协同了。他们已经开发了前端用户接口：一个用于解析所选语言的文本解析器。他们发给你了一个模组Parser.hs，其中的parseExp函数用于解析。如果你将ExprT的构造函数作为参数传给它，它则将一个字符串以ExprT类型的算术表达式重新表示。例如1：*Calc> parseExp Lit Add Mul "(2+3)*4" Just (Mul (Add (Lit 2) (Lit 3)) (Lit 4)) *Calc> parseExp Lit Add Mul "2+3*4" Just (Add (Lit 2) (Mul (Lit 3) (Lit 4))) *Calc> parseExp Lit Add Mul "2+3*" Nothing利用UI团队的资源实现额外的函数：evalStr :: String -> Maybe Integer该函数对一个字符串类型的算术表达式求值，如果输入的字符串是无效的，那么输出Nothing，如果输入有效，就输出Just n，n是输入所对应的值。我的解答evalStr :: String -> Maybe Integer evalStr str = case parseExp ExprT.Lit ExprT.Add ExprT.Mul str of Nothing -> Nothing Just x -> Just (eval x)这里使用了一个模式匹配来判断由parseExp函数解析的结果是Nothing还是Just x，我知道对于Maybe的判断有更好的函数，例如isNothing和fromMaybe，但是我总觉得对于本题模式匹配更简单吧。如果是Nothing就返回Nothing，否则对其求值并包装一个Just作为返回值。练习 3由于不可抗力—— Win10蓝屏 :( ——后面本来写好的翻译和解答没能保存，因此之后不再进行题目的翻译了。此处推荐使用谷歌翻译，现在谷歌对于长文章已经能翻译的很好了。Good news! Early customer feedback indicates that people really do love the…

【代码札记】The Day I Learned Haskell 5
type: item
date: 2018-12-15T15:22:00+08:00
description: 如期而至的周更，这次是Homework 4. 感觉这作业是越来越费脑子了，不过逐渐适应一种新思维模式的感觉很好，而且解出来的一瞬间也十分的有意思。要求：你最终要提交一个单个的.hs（或.lhs）文件，并且文件必须经过类型检查。1练习 1 全麦编程2用一种在Haskell中更加完美的方式重新实现下面两个函数。实践全麦编程的思想，将每个程序分解为对整个数据结构的增量转换管道3。将你的函数分别命名为fun1'和fun2'。fun1 :: [Integer] -> Integer fun1 [] = 1 fun1 (x:xs) | even x = (x - 2) * fun1 xs | otherwise = fun1 xs fun2 :: Integer -> Integer fun2 1 = 0 fun2 n | even n = n + fun2 (n `div` 2) | otherwise = fun2 (3 * n + 1)提示：对于第二个函数你可能希望使用iterate和takeWhile函数。请查阅Prelude的文档来查看他们是干什么的。（此处就当各位查阅过文档，我就不做额外解释了。）我的解答import Data.List fun1' :: [Integer] -> Integer fun1' = foldl' (*) 1 . map (\x -> x - 2) . filter even fun2' :: Integer -> Integer fun2' = sum . filter even . takeWhile (>0) . iterate helper where helper 1 = 0 helper x = if even x then div x 2 else 3 * x + 1一点解说：关于第一个函数，他做的事情是将给定列表中的所有偶数减去2再相乘。因此按照这个思路，先利用filter函数筛选出所有偶数，再使用map函数对列表中的所有偶数减去2，最后用foldl'（或者foldr也行）将所有的数乘起来。这里需要特别说明map函数用的匿名函数。诸位可能和我一样一开始认为，例如map (+2) xs和map (*2) xs这样的语句中，括号里的的函数应该是Integer -> Integer类型的，而天真的我以为这里的减去2，也应当是类似(-2)这种表达，但是实际上这种表达意味着负2，而非减2，因此编译器会报出类型不匹配的错误。我的解决方案是使用匿名函数，而Haskell中也自带了一个函数来专门应对这种情况，即(subtract 2)，或者为了求省事，直接写成(-2+)也是可以的。关于第二个函数，需要诸位动手写一写画一画了。通过一些简单的代入，我们能够发现一些规律。例如代入一个奇数，那么下一次递归调用的参数一定是偶数（3乘以奇数还是奇数，再加1就是偶数了），因此如果偶数再调用过程中除成了奇数，那么最终还是会变成偶数，直到除成1为止。其中还应该注意到一点，只有偶数被加上去了。因此我这里利用iterate函数生成了符合上述规则的数列。第一个数就是传入的数字n，随后根据其奇偶性做出不同的操作，之后再对上一步的结果同样按照其奇偶性做出不同操作，如此反复，直到除成1。由于iterate函数生成一个无限长度的列表，因此需要使用takeWhile (>0)取出有用的部分。在这里数列最终收敛到0，因此我们遇到0就停止，毕竟后面的求和过程中有没有0都不影响结果的。刚刚我们说了，只有偶数才被求和，因此对取出来的数列过遍筛子，留下偶数，随后一个sum求和，万事大吉。练习 2 折叠树回忆一下二叉树的定义。一个二叉树的高度是从根节点到最深的节点的路径的长度（换句话说就是从根节点到最底下，最多经过了多少层）。例如，一个单独的节点（左右都是叶子）高度是0；一个有两个子树的根节点的高度是1.我们说如果一个二叉树的左右子树的高度相差不超过1，则称这个二叉树平衡，由此他的左子树和右子树也平衡。你应该使用下面的定义来表示二叉树。注意每个节点有一个额外的Integer来表示当前节点的高度。data Tree a = Leaf | Node Integer (Tree a) a (Tree a) deriving (Show, Eq)对于这个练习，写一个函数foldTree :: [a] -> Tree a foldTree = ...它将使用foldr从一个列表产生一个平衡的二叉树。举个例子：foldTree "ABCDEFGHIJ" == Node 3 (Node 2 (Node 0 Leaf 'F' Leaf) 'I' (Node 1 (Node 0 Leaf 'B' Leaf) 'C' Leaf)) 'J' (Node 2 (Nod…

【代码札记】The Day I Learned Haskell 4
type: item
date: 2018-12-09T00:53:00+08:00
description: 这次发的是Homework 3的练习。代码高尔夫！1这次的任务很简单：下面有三个任务。对于每个任务，你都应该按照给定名称和类型标识符提交一个对应的尽可能简短的函数。规则除了代码，你的解答必须包含一些解释性的注释。没有注释将会取得0分。博主注：我将在下面利用说明性的文本代替注释。你的注释应当完全展示出你对你的答案的理解。换句话说，一切都很公平，但是你必须证明你理解你写的代码。如果有疑问，那么就在注释中包含更多细节。注释将不会计入你解答的总长度类型标识不计入解答的总长度import声明不计入解答的总长度。因此你可以随意引用Haskell平台的包。空白不会计入解答的总长度。所以你没必要将你的代码全部挤在一行里。适时地使用空格，但同时保持你的代码尽可能简短。我们鼓励你包含上述要求以外的函数。即：我们鼓励你将你的解答分成若干函数，如果你想这样做的话（确实，有时这样做能够达成更短的解答）。当然，额外的函数同样计入你解答的总长度（他们的类型标识除外）。你最终应当提交一个名为Golf.hs的文件，其中定义的模块是Golf。所以文件首行应该是：module Golf where前三个最短的答案将赋予额外的2分，因此你最多可以获得4分。其他情况下长度不是那么重要，长但是正确的答案将获得对应的解答正确的分数。额外的分数取决于他们代码的风格。提示尽可能多地使用标准库函数。这是这次作业的目的所在。例如使用map要比你自己造轮子方便得多。特别地，尝试使用标准库函数封装递归模式，而非你自己实现递归函数。或许你一开始不需要考虑长度问题，先让代码能够工作。之后再慢慢缩减长度。如果有其他不明白的事情，欢迎到Piazza上弄明白。任务练习 1 跳房子你的第一个任务是写这样一个函数：skips :: [a] -> a这个函数输出一个列表的列表。第一个列表是和输入相同的。第二个列表包含输入的第2、4、6、8...个元素，第n个输出应该包含输入的第n、2n、3n...个元素。例如：skips "ABCD" == ["ABCD", "BD", "C", "D"] skips "hello!" == ["hello!", "el!", "l!", "l", "o", "!"] skips [1] == [[1]] skips [True,False] == [[True,False], [False]] skips [] == []注意输出的列表数量和原输入列表的长度是一致的我的解答skips :: [a] -> a skips xs = if length xs == 0 then [] else map (\n -> map snd $ filter (\(x,y) -> x `mod` n == 0) $ zip [1..length xs] xs) [1..length xs]解释：这里利用了if-then-else语法糖省略了一个模式匹配。即上述函数声明等效如下：skips :: [a] -> a skips [] = [] skips xs = map (\n -> map snd $ filter (\(x,y) -> x `mod` n == 0) $ zip [1..length xs] xs) [1..length xs]若输入为空列表，那么输出自然也是空列表。若不为空，则首先从1开始为输入列表中的每个元素标上号（利用双元组和zip），对于输出的第i项，可以判断标号是否是i的倍数，是就留下，不是就扔掉（利用filter和匿名函数\(x,y) -> mod x n == 02）。而map函数则最终将上述产生每一项输出的函数作用到从1开始到length xs为止的列表的每一个元素上（相当于将每个元素作为参数传给函数，然后保持原有顺序将函数的输出保存为一个新列表）。最终得到的列表即为所求。练习 2 极大值极大值是一个数列中严格大于左右两个邻位的元素。例如[2,3,4,1,5]的唯一极大值是4，因为它比它左右两个邻居都大。而5因为右边没有元素，所以不是极大值。写如下函数：localMaxima :: [Integer] -> [Integer]他的输出是输入数列的极大值列表。例如：localMaxima [2,9,5,6,1] == [9,6] localMaxima [2,3,4,1,5] == [4] localMaxima [1,2,3,4,5] == []我的解答localMaxima :: [Integer] -> [Integer] localMaxima xs = if length xs < 3 then [] else case xs of (x:y:z:xs) -> if x < y && y > z then y :…

【代码札记】The Day I Learned Haskell 3
type: item
date: 2018-11-29T21:37:00+08:00
description: 真是没想到，这个系列还能有3。可惜啊，这事不过三（开玩笑），这次来说说第二周的作业（Homework 2），是个二叉树。话说我们这大学的C语言课程刚讲完结构体，今天讲的链表，二叉树似乎都不讲，结果这宾夕法尼亚大学上来第二周就来二叉树，可见真是硬核。不过在完成这周的作业的过程中，我确确实实体验到了Haskell社区的一句话：If it compiled, it works.另外还有Haskell的一个精髓。就是你不必像C或者Java这些指令式编程的语言，不必考虑细枝末节的数据如何变动，只需要考虑其本身的结构就可以了。且往下看。首先你需要如下文件：Log.hs, error.log, sample.log.提交时文件名字应该是LogAnalysis.hs有些东西出现了一些非常糟糕的问题！解析日志文件我们不确定到底发生了什么，但是我们已经尽最大的可能来恢复日志文件error.log。他看起来是由包含不同日志信息的行组成。每行开头的第一个字符表明了该行包含的日志类型：'I' 表示信息性的日志，'W'表示警告，'E'表示错误。其中错误信息后面跟着一个数字表示错误的严重性。其中100表示史诗级前所未见的错误。日志类型后面都有一个整数表示时间戳，之后直到行末都是日志本身的内容。整个文档看起来乱糟糟的，显然我们需要一个程序来把这些东西排序。我们将定义一些捕捉这些日志格式的结构：data MessageType = Info | Warning | Error Int deriving (Show, Eq) type TimeStamp = Int data LogMessage = LogMessage MessageType TimeStamp String | Unknown String deriving (Show, Eq)注意LogMessage有两个构造函数：一个表示格式正确的日志，另一个表示不符合格式的日志。我们提供了一个模组Log.hs，其中包含了这些数据类型声明，还有一些其他有用的函数。下载这个文件并放在你源文件的所在目录下，并将你的文件重命名为LogAnalysis.hs。LogAnalysis.hs的头几行应该如下：{-# OPTIONS_GHC -Wall #-} module LogAnalysis where import Log这几行能够让你引用Log.hs以便使用其中的数据类型和函数。练习1首先第一步要搞清楚如何解析一条独立的日志。定义如下函数：parseMessage :: String -> LogMessage这个函数将解析日志文件的单行。例如：parseMessage "E 2 562 help help" == LogMessage (Error 2) 562 "help help" parseMessage "I 29 la la la" == LogMessage Info 29 "la la la" parseMessage "This is not in the right format" == Unknown "This is not in the right format"一旦我们能够处理一行日志，那么处理整个日志就不在话下了。定义如下函数：parse :: String -> [LogMessage]这个函数解析整个日志文件并返回一个LogMessage列表。为了测试你的函数，可以使用Log模块中提供的testParse函数，提供你的parse函数，测试的行数以及日志文件。例如在GHCi中加载了你的LogAnalysis.hs之后执行如下命令：testParse parse 10 "error.log"留神别像上周一样重复造轮子！用Prelude库中自带的函数来让你的解答尽量的简洁、高水平、可用。例如将字符串"562"转换成整数，你可以使用read函数。其他你可能用到的函数包括：lines, words, unwords, take, drop 和 (.)。我的解答parseMessage :: String -> LogMessage parseMessage str = case words str of ("E":(x:(y:xs))) -> LogMessage (Error $ read x) (read y) (unwords xs) ("I":(x:xs)) -> LogMessage Info (read x) (unwords xs) ("W":(x:xs)) -> LogMessage Warning (read x) (unwords xs) _ -> Unknown str parse :: String -> [LogMessage] parse str = c…

【代码札记】The Day I Learned Haskell 2
type: item
date: 2018-11-22T19:24:00+08:00
description: 按照上一篇文章中说的，这篇文章将记述 CIS194 Homework1中的第二个练习，三柱和四柱（选做）汉诺塔问题。三柱汉诺塔三柱汉诺塔应当说是入门递归的一个经典问题，简单地说就是：你面前有三根柱子和n个大小不一的碟子，一开始这n个碟子从上到下由小到大依次顺序摞在第一个柱子上，你需要借助第三根柱子把这些碟子移动到第二根柱子上（或者借助第二根柱子移动到第三根柱子上）。其中你需要遵守如下两条规则：每次只能移动一个盘子大盘子不能被放在小盘子上这种问题通常有两种问法，一种是问n个盘子对应最少的操作次数；另一个是操作的顺序。前者很好说，可以从n=1开始列，列出两三个之后就能够找到规律，用数学归纳法得出最小操作数为$2^n - 1$的结论。而后者才算是真正需要用递归完成的。有关于三柱汉诺塔的视频，我推荐3Blue1Brown的Binary, Hanoi and Sierpinski, part 1和Binary, Hanoi, and Sierpinski, part 2。这两个视频虽然不是全部在讲汉诺塔，但是其中有关汉诺塔递归的问题配合视频演示做的非常不错，十分推荐一看。总而言之，要解决n个碟子的三柱汉诺塔问题，应当有如下步骤（此处随题意借助第三根柱子将碟子从第一根挪到第二根）：将第一根柱子上面的$n-1$个碟子借助第二根柱子挪到第三根上将第$n$个碟子从第一根挪到第二根上将先前的$n-1$个碟子借助第一根柱子从第三根挪回第二根在这个练习中，定义如下：type Peg = String type Move = (Peg, Peg) hanoi :: Integer -> Peg -> Peg -> Peg -> [Move]将String类型用Peg代替，类似C语言中的typedef，同理下面的Move本质上则是（String, String）。这样做的目的是方便阅读和书写程序。第三行定义的则是主要的函数。这个函数接收一个整数类型和三个Peg类型的参数，整数代表碟子的数量，后面三个Peg代表柱子的名字。最终函数输出一个Move类型的列表，表示操作的顺序。关于变量的解读，应当作如下解释：假设有代码：hanoi n "a" "b" "c"，则应该是“将n个盘子借助c从a挪到b”。接下来是代码：hanoi3 :: Integer -> Peg -> Peg -> Peg -> [Move] hanoi3 1 a b c = [(a,b)] hanoi3 n a b c = (hanoi3 (n-1) a c b ) ++ [(a,b)] ++ (hanoi3 (n-1) c b a) 这里假设三根柱子分别叫做a，b，c。按照前面说的，首先要将$n-1$个碟子借助b从a移动到c，然后将第$n$个盘子从a移动到b，再将$n-1$个碟子借助a挪回b此时需要考虑递归终止的条件。即$n-1$递归到1，只剩下一个碟子时，无需再借助别的柱子，直接移动即可。在主函数中运行程序：main :: IO() main = putStrLn $ show $ hanoi3 2 "a" "b" "c"输出应当如下：[("a","c"),("a","b"),("c","b")]和Homework1中给的样例没有差异。四柱汉诺塔四柱汉诺塔和三柱汉诺塔其实差不多，无非是多了一根柱子，可以选择哪一部分挪到第三根柱子，余下的挪到第四根柱子（此处假设还是从第一根挪到第二根）。这样，就有了如下的思路（假设先保留$x$个盘子不动）：借助第二、四根柱子将$n-x$个碟子从第一根挪到第三根上借助第四根柱子将余下的$x$个碟子从第一根挪到第二根上再将上面的$n-x$个碟子借助第一、四根挪回到第二根上那么问题来了，这$x$是多少？在指令式编程中可以直接用循环遍历 $\forall x \in [1,n)$ 然后取其中的最小值。同样在Haskell中也可以。定义函数：hanoi4Helper :: Integer -> Peg -> Peg -> Peg -> Peg -> Integer -> [Move] hanoi4Helper 1 a b _ _ _ = [(a,b)] hanoi4Helper n a b c d x = (hanoi4 (n - x) a c b d ) ++ (hanoi3 x a b d) ++ (hanoi4 (n - x) c b a d) main :: IO () main = putStrLn $ show $ minimum $ map (length . hanoi4Helper 15 "a" "b" "c" "d") [1..14]其中hanoi4Helper定义了一个类似三柱汉诺塔的函数（hanoi3 函数是上面写的三柱汉诺塔函数），只是在一个…

【代码札记】The Day I Learned Haskell 1
type: item
date: 2018-11-20T08:45:00+08:00
description: 许久不见，最近我打算重新拾起Haskell这门语言。要问为什么，就是简单的一句话，我想学Haskell。非要说个什么原因的话，就是高一那时候真的没搞懂Haskell，现在都大一了，或许就能学明白呢？总不能让过去限制自己的能力吧。好了，废话少说。这次我选择的教程不是先前买的那本《魔力Haskell》了，那确实是本好书，但是我没时间回家取，可能本周末会回家拿这本书，但是就现在而言，我选择的是宾夕法尼亚大学2013年春季的CIS194。众所周知在2016年Joachim Breitner更新了这份教程，但是我简单地翻看了一下，感觉类似Go语言官方给的教程，搭建一个沙盒然后让你在里面随便耍。个人觉得这样子比较适合零编程基础起步，类似趣学指南一样，并不能简明扼要地涉及到一门语言的思想和底层，但确实能够有效的激发兴趣。总之人各有所向，我选择旧版的教程。写这一系列文章的目的并不是为了把整个教学文档翻译一遍，而是更偏向记录我自己在学习中的一些过程。本文将记录我完成的 Homework 1 中的 Validating Credit Card Numbers。先贴原题：你是否想过网站是如何验证你的信用卡号码的？他们并没有查询一个庞大的数据库，也没有使用魔法。事实上，许多信用卡发卡行采用校验和公式来区分有效的信用卡号和一些随机的数字（亦或是错误的输入）。在这一节，你需要实现验证的一些步骤，所列如下：从右边第二个数字起每隔一个都翻倍。例如[1,3,8,6]变成[2,3,16,6]将所有数字相加。注意是数字不是数。因此[2,3,16,6]相加应当是2+3+1+6+6 = 18将和除以十取余数，若为零则卡号是有效的。接上面的例子，10 % 10 = 8，是无效的号码。再贴提示：首先我们需要找到每个数对应的数字。定义如下函数：toDigits :: Integer -> [Integer] toDigitsRev :: Integer -> [Integer]toDigits应当将正整数转换为数字的列表。对于0和负数输入，应当返回一个空列表。toDigitsRev在上面的基础上输出逆序的列表。例如：toDigits 1234 == [1,2,3,4] toDigitsRev 1234 == [4,3,2,1] toDigits 0 == [] toDigits (-17) == []所有数都正确排列后，我们该将他们中的一些翻倍了。定义函数：doubleEveryOther :: [Integer] -> [Integer]需要注意的是该函数从右边起开始翻倍。所以应有如下输出：doubleEveryOther [8,7,6,5] == [16,7,12,5] doubleEveryOther [1,2,3] == [1,4,3]doubleEveryOther函数的输出中混有一位数和两位数，定义如下函数将他们的数字相加求和：sumDigits :: [Integer] -> Integer例如：sumDigits [16,7,12,5] = 1 + 6 + 7 + 1 + 2 + 5 = 22最后将上述功能组合起来，定义函数：validate :: Integer -> Bool这个函数将接受一个信用卡号码，并输出一个布尔值来表明那是否是有效的信用卡号。例如：validate 4012888888881881 = True validate 4012888888881882 = False最后贴我写的代码：module Main where toDigitsRev :: Integer -> [Integer] toDigitsRev n | n >= 10 = (mod n 10) : toDigitsRev (div n 10) | n < 10 && n > 0 = [n] | n <= 0 = [] revIntegerList :: [Integer] -> [Integer] revIntegerList [] = [] revIntegerList (x : zs) = revIntegerList zs ++ [x] toDigits :: Integer -> [Integer] toDigits n = revIntegerList $ toDigitsRev n doubleEveryOtherHelper :: [Integer] -> [Integer] doubleEveryOtherHelper [] = [] doubleEveryOtherHelper (x : []) = [x] doubleEveryOtherHelper (x : (y : zs)) = x : (2 * y) : doubleEveryOtherHe…

【歪门邪道】使用Sublime Text 3搭建Haskell IDE
type: item
date: 2018-11-17T23:56:00+08:00
description: 本文将在Win10系统上使用Sublime Text 3 搭配插件 SublimeHaskell搭建一个IDE环境。[...]