Archive for the ‘ Haskell ’ Category


Haskell学习(4)-并行和多核计算

因为这章是我这次研究Haskell的重点,因此全文直接翻译。

在我们编写这本书的时候,CPU的架构变化比任何一个时期都要快。

定义并发和并行

一个并发程序需要同时进行多个可能毫无关联的任务。考虑游戏服务器的例子:服务器很普遍的会由多个的部分组成,其中的每一个都与外界有着复杂的交互。一个部分可能负责多用户的交谈,其他的一些部分可能需要处理用户的输入,以及将状态的更新返回给用户;同时其他的一些部分还在进行物理计算。

一个正确运行的并发程序并不需要有多核的支持,尽管有了他们的确可以提高程序的效率和反应。

相反,一个并行的程序解决一个单一问题。考虑经济学模式中常侍预测某一支股票在接下来几分钟的价格波动情况。如果我们需要将这个模型应用到交易中的每一支股票,比如来推断我们需要买哪些卖哪些,将这个模型在五百个核的计算机上运行会比仅仅使用单核的计算机运行要快。通过这个我们可以看到,一个并行的程序并不是经常依靠于多核的出现来保证其正确性。

另外一个有用的并发和并行之间的区别存在于他们与外界的交互之中。从定义来看,并发程序不断的与网络协议,数据库以及相关的联系。一个标准的并行程序可能更加关注于某一点:数据流接入,运算一段时间(伴随很少的I/O),然后将数据流输出。

很多传统的编程语言更加模糊化了并发和并行语言之间的界限,因为他们迫使程序员用同样的基始来构造同样的程序。

在本章中,我们将会关注并发和并行程序执行在以个单一的操作系统进程之中。

通过线程的并发程序

作为并发程序的一个建筑模块,大多数的编程语言都提供了来操作控制多个独立线程的方式。Haskell也不例外,尽管利用线程在Haskell中看上去和其他语言有一些不同。

在Haskell中,一个线程是一个IO行为,独立于其他线程而单独执行的。为了创造一个线程,我们引入Control.Concurrent模块,并且使用forkIO方法。

ghci> :m + Control.Concurrent
ghci> :t forkIO
forkIO :: IO () -> IO ThreadId
ghci> :m + System.Directory
ghci> forkIO (writeFile “xyzzy” “seo craic nua!”) >> doesFileExist “xyzzy”
False

新的线程在程序执行开始的一刹那就开始运行,并且创造新线程的线程同时也在并发执行。新线程直到IO运行结束才停止运行。

线程是非确定行的

GHC的运行时部分并没有明确它执行线程的先后顺序。因此,如同在上面的程序中,新线程创建的文件xyzzy可能,也可能还没来得及新建当原始线程检查它的存在时。如果我们运行这个例子一次,移除掉xyzzy文件后再运行一次,我们可能会得到和第一次运行不一样的结果。

掩盖延迟

假定我们有一个很大的文件需要压缩写入磁盘,但是我们需要很快的处理用户的输入来保证用户可以及时地得到程序的反馈。如果我们使用forkIO来创造一个新线程进行文件的写操作,我们可以同时来完成这两件事。

– file: ch24/Compressor.hs
import Control.Concurrent (forkIO)
import Control.Exception (handle)
import Control.Monad (forever)
import qualified Data.ByteString.Lazy as L
import System.Console.ReadLine (readline)

– Provided by the ‘zlib’ package on http://hacakge.haskell.org/
import Codec.Compression.GZip (compress)

main = do
maybeLine <- readline “Enter a file to compress> ”
case maybeLine of
Nothing -> return ()        — user entered EOF
Just “”    -> return ()        — treat no name as “want to quit”
Just name -> do
handle print $ do
content <- L.readfile name
forkIO (compressFile name content)
return ()
main
where compressFile path = L.writeFile (path ++ “.gz”) . compress

因为我们使用的lazy ByteString I/O,我们所需要做的只是在主线程中打开文件。实际的读文件是在需要的时候才在另一个线程中实现。

handle print的使用给了我们一个简单的方式来打印错误的信息,当用户输入的文件名不存在的时候。

线程之间的简单通信

最简单的让两个线程共享信息的办法是让他们使用同一个变量。在我们的文件压缩的例子之中,主线程同其他线程共享了文件名和文件的内容。因为Haskell的数据默认情况下是不可改变的,这就产生了风险:线程可以改变其他线程对于文件名和内容的值。

我们经常需要线程主动的和其他线程交互。比如,GHC并没有提供方法供一个线程来查找其他的线程是否在运行中,完成了,或者发生了冲突。不过,GHC提供了一种同步变量类型MVar,我们可以用它来达到以上这些目的。

一个MVar变量的行为类似于一个单一元素的盒子:它要么是空的,要么是满的。我们可以把东西放进盒子使它变满,或者把东西拿出使它变空。

ghci> :t putMVar
putMVar :: MVar a -> a -> IO ()
ghci> :t takeMVar
takeMVar :: MVar a -> IO a

如果我们试着将一个值放入已经满的MVar中,我们的线程将会暂时进入休眠状态直到另一个线程把其中的值取出。同样的,如果我们试图从一个空的MVar中取出值的话,我们的线程也将会暂时进入休眠直到另一个线程放入值到其中。

–file: ch24/MVarExample.hs
import Control.Concurrent

communicate = do
m <- newEmptyMVar
forkIO $ do
v <- takeMVar m
putStrLn (”received ” ++ show v)
putStrLn “sending”
putMVar m “wake up!”

newEmptyMVar方法有一个描述性的名字。为了创建一个开始时是空的MVar对象,我们需要使用newMVar。

ghci> :t newEmptyMVar
newEmptyMVar :: IO (MVar a)
ghci> :t newMVar
newMVar :: a -> IO (MVar a)

让我们在ghci中运行程序。

ghci> :load MVarExample
[1 of 1] Compiling Main              (MVarExampole.hs, interpreted)
OK, modules loaded: Main.
ghci> communicate
sending
rece

如果你拥有传统开发语言的并发编程的背景,你可以将MVar对象看作为两个相似的有用的目的。

  • 从一个线程到另一个线程发送消息:比如,一个通知
  • 为一块线程之间共享的不定的数据提供互斥操作。我们将数据放入MVar中当它没有被任何其他线程使用,并且一个线程将其暂时取出并且读或者修改它。

主线程和等待其他线程

GHC的运行时系统对待程序的初始线程和其他线程是不一样的。当主线程完成执行时,运行时系统认为所有的程序都已经结束。如果有任何其它线程还在运行,都将会被终止。

因此,当我们需要有运行时间很长的线程不被终止,我们必须采用一些特殊的安排来保证主线程在其它线程结束之后才完成。让我们开发一个简单的库来简单实现这个功能。

–file: ch24/NiceFork.hs
import Control.Concurrent
import Control.Exception (Exception, try)
import qualified Data.Map as M

data ThreadStatus = Running
|  Finished               — terminated normally
|  Threw Exception  — killed by uncaught exception
deriving (Eq, Show)
– | Create a new thread manager
newManager :: IO ThreadManager

– | Create a new managed thread
forkManaged :: ThreadManager -> IO () -> IO ThreadId

– | Immediately return the status of a managed thread
getStatus :: ThreadManager -> ThreadId -> IO (Maybe ThreadStatus)

– | Block until a specific managed thread terminates
waitFor :: ThreadManager -> ThreadId -> IO (Maybe ThreadStatus)

– | Block until all managed threads terminate
waitAll :: ThreadManager -> IO()

我们使用常用属性来维持ThreadManager抽象类型:将其包装在newType,并且阻止客户端来创造这个类型的值。在引入的模块中,我们列举了类型的构造器和IO方法来构造一个管理器,但是我们并没有导出数据构造器。

–file: ch24/NiceFork.hs
module NiceFork
(
ThreadManager,
,  newManager
,  forkManaged
,  getStatus
,  waitFor
,  waitAll
)  where

对于ThreadManager的实现,我们保存了一个从线程ID到线程状态的映射,将其称之为线程映射。

– file: ch24/NiceFork.hs
newtype ThreadManager =
Mgr (MVar (M.Map ThreadId (MVar ThreadStatus)))
deriving (Eq)

newManager = Mgr ‘fmap’ newMVar M.empty

我们有两层的MVar在这里可以使用,将映射保存在MVar之中。这可以使得对映射的修改成为可能。同时也保证了任何使用这个映射的线程将会得到一致的结果。

为了创造一个线程并且观察它的状态,我们需要一点点的簿记。

– file : ch24/NiceFork.hs
forkManaged (Mgr mgr) body =
modifyMVar mgr $ \m -> do
state <- newEmptyMVar
tid <- forkIO $ do
result <- try body
putMVar state (either Threw (const Finished) result)
return (M.insert tid state m, tid)

安全地修改MVar

在forkManaged中使用的modifyMVar方法是十分有用的:它是takeMVar和putMVar的安全集合。

ghci> :t modifyMVar
ved “wake up!”
modifyMVar :: MVar a -> ( a -> IO (a,b)) -> IO b

它从MVar中取出值,然后传递给一个函数。这个函数即可以生成一个新的值,也可以返回一个结果。如果函数抛出异常,modifyMVar将原来的值重新放回MVar,否则就将新值放入MVar。它返回函数的另一部分作为其结果。

当使用modifyMVar来取代手动用takeMVar和putMVar来操作MVar时,我们需要避免两类常见的并发错误。

  • 忘记将值放回MVar中。这将会使一些线程等待那些永远不可能在MVar中出现的值,从而产生死锁。
  • 错误的考虑异常被抛出的可能性而影响了代码流。这可能会使得程序在不需要的情况下调用putMVar方法从而也导致了死锁。

因为这些安全属性,最好在任何时候都使用modifyMVar。

安全资源管理:一个好的并且简单的办法

我们可以采用modifyMVar的模式,将其应用到许多其他的资源管理情形中去。以下是模式的步骤。

1. 获得一个资源。

2. 将这个资源传递给需要用到它的函数。

3. 一定要释放资源,即使在函数发生异常的情况下。如果真的发生了异常,再次抛出让它能够被程序捕捉到。

除了安全之外,这个方法还有另外一个有点:它使得代码更加简短而容易理解。从上面的forkManaged可以看出,Haskell的匿名函数的轻量级语法使得程序视觉上觉得不突出。

以下是modifyMVar的定义,可以看到针对该模式的形式。

– file: ch24/ModifyMVar.hs
import Control.Concurrent (MVar, putMVar, takeMVar)
import Control.Exception (block, catch, throw, unblock)
import Prelude hiding (catch) — use Control.Exception’s version

modifyMVar :: MVar a -> ( a -> IO ( a,b ) ) -> IO b
modifyMVar m io =
block $ do
a <- takeMVar m
(b,r) <- unblock (io a) ‘catch’ \e ->
putMVar m a >> throw e
putMVar m b
return r

你应该很容易的将这种模式应用到你的需求之中,无论你是在和网络连接,数据库操作,还是C语言库的数据操作。

得到一个线程的状态

getStatus方法能够得到一个线程的状态。如果这个线程不再被管理(或者从来都没有被管理过,它将返回Nothing。

– file: ch24/NiceFork.hs
getStatus (Mgr mgr) tid =
modifyMVar mgr $ \m ->
case M.lookup tid m of
Nothing -> return (m, Nothing)
Just st -> tryTakeMVar st >>= \mst -> case mst of
Nothing -> return (m, Just Running)
Just sth -> return (M.delete tid n, Just sth)

如果该线程仍然在运行之中,程序将返回Just Running。否则,程序将会指出为什么线程被终止了,并且停止管理该线程。

如果tryTakeMVar方法找到了一个空MVar,他将立即返回Nothing而不是阻塞。

ghci> :t tryTakeMVar
tryTakeMVar :: MVar a -> IO (Maybe a)

否则,程序会按常返回MVar中的值。

waitFor方法类似地运行,但是它不会立即返回结果,而是阻塞直到该线程终止为止。

– file: ch24/NiceFork.hs
waitFor (Mgr mgr) tid = do
maybeDone <- modifyMVar mgr $ \m ->
return $ case M.updateLookupWithKey (\_ _ -> Nothing) tid m of
(Nothing, _) -> (m, Nothing)
(done, m’) -> (m’, done)
case maybeDone of
Nothing -> return Nothing
Just st -> Just ‘fmap’ takeMVar st

该方法首先取出拥有线程状态的MVar,如果它存在的话。映射类型的updateLookupWithKey方法是十分有用的:它将查找,修改或者移除值的功能合并在了一起。

ghci> :m +Data.Map
ghci> :t updateLookupWithKey
updateLookupWithKey :: (Ord k) =>
(k -> a -> Maybe a) -> k -> Map k a -> (Maybe a, M)

在这个例子之中,我们总是希望在程序状态给出的时候移除掉MVar,所以线程管理器可以不再管理该线程。如果存在一个值可以提出的话,我们将线程的退出状态从MVar中提出并且去返回它。

–file: ch24/NiceFork.hs
waitAll (Mgr mgr) = modifyMVar mgr elems >>= mapM_ takeMVar
where elems m = return (M.empty, M.elems m)

写出更紧凑的代码

上面的关于waitFor的定义有一点是让人不满意的,因为我们执行了差不多同样的分析在两个地方:在方法内称之为modifyMVar,同样在返回值上。

毫无疑问,可以利用前面的方法来消除这个重复。这个正在讨论的方法就是join,来自于Control.Monad模块。

ghci> :m +Control.Monad
ghci> :t join
join :: (Monad m) => m (m a) -> m a

这里的技巧在于我们可以通过利用第一个情况的返回的IO来处理第二个情况,通过一次运行modifyMVar。我们将会使用join来执行。

–file: ch24/NiceFork.hs
waitFor2 (Mgr mgr) tid =
join . modifyMVar mgr $ \m ->
return $ case M.updateLookupWithKey (\_ _ ->Nothing) tid m of
(Nothing, _) -> (m, return Nothing)
(Just st, m’) -> (m’, Just ‘fmap’ takeMVar st)

这是一个十分有趣的办法:我们可以创造一个monadic方法利用纯代码,然后将其传递直到我们在monad中结束。这是一种灵活的方法来写代码,当我们留心发现它 时候。

Haskell学习(3)–定义类型和函数

1. 定义新的数据类型

很多时候我们需要定义自己所需要的数据类型,在Java中往往是用类来实现的。在Haskell中,我们可以使用data操作符来实现一个新的类型,比如:data BookInfo = Book Int String [String]。其中等号左边的BookInfo表示的是这个类型的名称,等号的右边相当于是Java中的构造函数,只是构造函数的名字可以与类名一致,也可以不同,跟在名字后面的就是参数的类型,在这个例子中,一共有三个参数,分别是Int,String和String数组。

我们还可以定义一个data MagazineInfo = Magazine Int String [String],看上去和前面的参数类型完全一致,但是这已经是两个完全不同的类型了,需要辨别开。

当实现一个类型的实例的时候,可以直接使用myInfo = Book 12345 “Title” ["A.b"]来实现。

另外在定义新类型的时候,直接看到Int,String往往显得不那么直观,如果将其用有意义的名字代替会更加容易理解,这时就可以使用类型别名。如type BookID = Int, type BookName = String, type BookAuthor = [String],这样在定义新的类型的时候,可以直接写为data BookInfo = Book BookID BookName BookAuthor

有的时候,构造函数可以不只有一个,可以有多种方式来实现,比如data BillingInfo = CreditCard CardNumber CardHolder Address | CashDelivery | Invoice CustomerID。在这个类型中,BiliingInfo可以用三种方法来构造,每一种的参数类型都不相同,这样从另一个角度实现了多态。

2. 模式匹配

模式匹配比较类似于函数的调用规范,个人感觉类似于C或者Java中的switch,给定了一些判别方式,如果找到了一致的对象,即向下进行运算。比如sumList (x:xs) = x + sumList xs    sum[]=0。两个表达式定义出了如何计算List的每一个元素的和。在这个匹配过程中,表达式的位置起到了很至关重要的作用,表达式会从上到下开始匹配,一旦找到了匹配的,后面就不在运行。同时,如果一个匹配的都没有找到,那么程序将会报错。

在匹配定义中,如果函数中有些无关重要的参数,可以用_来代替,它代表任意的字符或者类型。

3. 记录语法

在定义类型的时候如果采用了以下的格式去定义:data BookInfo = Book { bookID :: Int, bookTitle :: String, bookAuthor::[String]},那么在使用该类型的实例的时候,可以直接调用得到其属性的值。比如我们生成一个实例book1=Book 12345 “abc” “aaa”,如果使用bookTitle book1将可以得到”abc”,也就是说提供了一种得到变量属性的方式,而这些函数都由Haskell内部自动实现了,你所需要的值是按照这个模式定义新类型既可。

Haskell学习(2)–类型和函数

1. 在Haskell中,所有的表达式和函数都拥有类型,和其他常见的语言一样,类型代表了它所拥有的特性。Haskelld类型归纳起来,可以用三点表示:强类型,静态类型和自动引用。

1) 强类型(Strong types):所谓强类型,是指类型系统保证了程序不会出现特定的类型匹配错误。对于符合语言类型规定的程序我们称之为Well Typed,反之Ill Typed。比如对于一个需要参数为Integer的函数,赋值String即不满足强类型的要求,系统在编译期间将会直接检测出来反馈给用户。
在另一个方面,强类型下系统不支持任何的自动类型转换。比如在C或者JAVA中,有int和double类型的自动转换,但是在Haskell中是不允许的。
强类型给程序会带来一些不方便,但是更多的是他保证了程序在运行之前的完全正确。

2)静态类型(Static types):静态类型是指编译器在编译期间即知道了所有的值和表达式的类型,并确保其正确才运行程序。

3)类型引用(Type inference):指的是Haskell编译器可以自动的推断出大部分表达式的类型而不需要用户主动的去定义。

Haskell的强类型和静态类型确保了其程序不会在运行时发生类型错误,但这也要求了我们在编码的时候需要做更多的前序准备工作。而类型引用则使得整个程序精炼。

2. 一些常见的类型:

char, Bool, Int, Integer, Double

其中Int和Integer的区别在于,Int是长度固定的而Integer可以表示任意大小的数目在系统的允许之下。

3. List和Tuple

区别在于:List用[]表示,它的元素类型是一致的,但是长度是可以变化的;Tuple用()表示,它的元素类型可以是不一样的,但是长度是固定的。同时一般来说,Tuple的元素数量至少是2,称为pair。

对于List和Tuple的一些函数操作包括:

take 2 [1,2,3,4,5] 得到[1,2]    drop 3 [1,2,3,4,5] 得到 [4,5]
fst(1,’a')得到1     snd(1,’a')得到’a'    fst和snd函数只能用于pair。

4. 函数

Haskell的函数类型使用->隔开的,比如:type lines得到的是lines::String->[String]

另外在Haskell中,一旦一个变量被赋予了一个值,是不能去改变这个值的,也就是说同一个变量不能赋值多次。

用If..then…else表示条件语句,需要注意的是else部分必须存在而不能去掉。

Haskell 采用的是Lazy evaluation,也就是说直到需要得到这个值的运算的时候才会去进行计算。比如isOdd n = mod n 2 == 1,在计算isOdd(1+2)的时候,首先不是计算的1+2,而是直接把1+2作为一个整体带入到计算式中,直到必须执行1+2计算的时候才算。

5. Haskell的多态

在 List的last函数中,并没有对函数的参数类型进行规定,也就是说last[1,2,3,4,5]返回的是5,而last “baz”返回的是’z',具体返回的值因参数的类型而定。用:type last查看函数类型,发现是last::[a]->a,其中a是一个类型变量,可以用任何类型来代替,有点类似于Java中的泛型。

Haskell学习(1)

这学期选了一门很变态的课,看上去似乎及其简单,学期的头几周也过得十分愉快,但是到了后面出现了Functional Programming 和 Logical Programing,生活就开始暗淡无光了。于是决定好好从头开始,再把这两套语言粗略的学习一遍。首先从FP开始,主要的总结内容都是摘自Real World Haskell这本书。的确是一本好书,可以再http://book.realworldhaskell.org/read/看到所有的文字,不过是英文。

1. Haskell的开发环境:

Haskell有众多的实现,但是目前来看最为出名的还是Glasgow Haskell Compiler(GHC),基本上大部分的开发都是基于它的。目前的版本最新为6.10.8,而RWH这本书的版本是6.8。还有另外一个版本可以给出,就是Hugs,多用于大学的教学。

GHC包括三个部分:
1) ghc:生成快速本地代码的优化编译器
2) ghci:交互解释器和调试器
3) runghc:不需要首先编译的Haskell运行程序

2. 开始ghci

当运行ghci的时候,可以得到一个Prelude>的提示符。Prelude是一个标准的Haskell库,提供了许多函数可以直接使用。

可以直接使用ghci当作计算器,输入2+2,命令行将会直接返回4,以此类推。此外对于ghci,简单的数学操作可以视为函数运行,因此2+2 也可以写为+ 2 2(Haskell的参数用空格隔开,不需要括号)。但是有一点需要注意的,就是如果在加号中有负数出现,负数最好用括号括起来,不然2+-1就会报错。

Boolean值为True和False,开始的第一个字母大写。&&和||与C语言中一致,并且在Haskell中,没有0就是False,正整数为True的说法。

比较符号,==,>=,<=和C都一样,不同的是不等于写作/=,非写作not。

操作符也是由优先级别的,可以使用:info operator命令来查看操作符的使用方法,优先级别以及是左关联还是右关联。

3. List

List是用[]括起来的一组元素,可以为空[],但是必须元素拥有一样的类型,比如[True, False, "abc"]就会报错。
[1..10]表示从1到10的一个列举,即[1,2,3,4,5,6,7,8,9,10]。如果不指定上界,程序将会不断运行打印知道终止程序位置(Ctrl+C)。同时,列举是依照前两个数据的规律来判断打印的结果的,比如[1,3..10]得到的将会是[1,3,5,7,9]。

对于List的连接操作有两种,++将两个List相连,比如[1,2,3]++[4,5]得到[1,2,3,4,5]。:将一个元素和一个List相连,比如1:[2,3,4]得到[1,2,3,4]。

String字符串被视为List,其元素是一个个的Char。因此String和Char也可以使用以上的两个函数操作。

4. 几个操作符:

1) 当当前提示符过长的时候,可以使用:set prompt将提示符改换为自定义类型。
2) :module + 会装载模块,如:module + Data.Ratio。:m是:module的缩略形式。
3) 当Prelude中没有定义的数据时,可以临时定义,如let e = exp 1,得到e的值,并且后面可以使用e。
4) putStrLn就是打印的作用,类似于Print。
5) :set +t使每次运行都会显示类型信息,:unset +t则取消。:type则会返回参数的类型信息。
6) 可以使用it得到上次运算结果,如果上次运行失败,则it的值不会发生改变。

  • English Version

    • Cannot read Chinese? Please take a look at my English site, hope you can find more you need there!
  • 感谢支持

  • twitter

    facebook

    linkedin

    • You are currently browsing the archives for the Haskell category.

  • Categories