理解 狀態(tài) Monad


本文為翻譯,個(gè)人學(xué)習(xí)之用,原地址


程序狀態(tài)

如果你以前有其他語言的編程經(jīng)驗(yàn)塔沃,你可能寫過一些函數(shù)或者方法來控制程序的狀態(tài)。概念化的話阳谍,狀態(tài)就是當(dāng)執(zhí)行一些計(jì)算過程的時(shí)候蛀柴,所必須的一個(gè)或者多個(gè)變量螃概,而且這些變量跟函數(shù)的參數(shù)沒有關(guān)聯(lián)。面向?qū)ο蟮恼Z言鸽疾,比如C++吊洼,就以成員變量的形式廣泛地使用了狀態(tài)變量。C語言中制肮,我們在函數(shù)的作用域外聲明變量來保持狀態(tài)冒窍。

在Haskell中,這種技術(shù)就沒那么容易直接應(yīng)用了豺鼻。因?yàn)楸仨氁褂米兞孔垡海馕吨瘮?shù)會隱藏掉一些依賴。這與Haskell的純函數(shù)(參數(shù)相同儒飒,函數(shù)每次調(diào)用結(jié)果一致)相違背谬莹。

幸運(yùn)的是,在大多數(shù)情況下桩了,我們可以避免這種額外的計(jì)算附帽,然后使用純函數(shù)的方式來跟蹤程序狀態(tài)。我們通過傳遞狀態(tài)信息來做到這點(diǎn)井誉,這樣那些隱藏的依賴就會變得明確了士葫。

State類型就是一個(gè)精心設(shè)計(jì),讓這個(gè)過程更加便利的工具送悔。在這章中,我們會從一個(gè)典型的問題(產(chǎn)生偽隨機(jī)數(shù))中引進(jìn)state爪模,來展示它是怎樣幫助我們的欠啤。

偽隨機(jī)數(shù)

生成真正的隨機(jī)數(shù)是很困難的。計(jì)算機(jī)程序大多數(shù)情況下會使用偽隨機(jī)數(shù)來代替屋灌。之所以稱之為“偽”洁段,是因?yàn)樗鼈儾⒉皇钦嬲碾S機(jī)。它們使用算法(偽隨機(jī)數(shù)生成器)生成共郭,這種算法需要一個(gè)初始狀態(tài)(通常叫做種子)祠丝,然后根據(jù)這個(gè)狀態(tài)來產(chǎn)生一系列貌似隨機(jī)的數(shù)字。每次產(chǎn)生隨機(jī)數(shù)的時(shí)候除嘹,這個(gè)狀態(tài)就必須要更新写半,偽隨機(jī)數(shù)生成器就會刷新,如果知道了初始種子和生成算法尉咕,不同的偽隨機(jī)數(shù)序列是可以重現(xiàn)的叠蝇。

在Haskell中的實(shí)現(xiàn)

在大多數(shù)編程語言中,生成偽隨機(jī)數(shù)非常簡單:在庫中就已經(jīng)提供了生成偽隨機(jī)數(shù)的函數(shù)年缎。(甚至是真正的隨機(jī)數(shù)悔捶,取決于其實(shí)現(xiàn))铃慷。Haskell也不例外,在random包中的System.Random模塊中也有這個(gè)方法:

GHCi> :m System.Random
GHCi> :t randomIO
randomIO :: Random a => IO a
GHCi> randomIO
-1557093684
GHCi> randomIO
1342278538

randomIO是一個(gè)IO action蜕该。它也不例外犁柜,使用一個(gè)可變的狀態(tài)。因?yàn)檫@個(gè)隱藏的依賴堂淡,這個(gè)函數(shù)生成的偽隨機(jī)數(shù)每次都是不同的馋缅。

例子:擲骰子

假定我們在寫一個(gè)擲骰子的游戲,我們創(chuàng)建一個(gè)擲骰子的方法淤齐。我們使用IO函數(shù)randomRIO股囊,它允許我們指定一個(gè)隨機(jī)數(shù)的范圍。對于六面篩子更啄,randomRIO (1, 6).

import Control.Monad
import System.Random

rollDiceIO :: IO (Int, Int)
rollDiceIO = liftM2 (,) (randomRIO (1,6)) (randomRIO (1,6))

這個(gè)方法會擲骰子兩次稚疹,(,)是個(gè)函數(shù),接受兩個(gè)參數(shù)祭务,返回一個(gè)二維元組内狗,liftM2(,)函數(shù)能夠接受monadic參數(shù)。這樣义锥,這個(gè)函數(shù)會返回一個(gè)IO元組柳沙。

練習(xí)
實(shí)現(xiàn)一個(gè)函數(shù)rollNDiceIO :: Int -> IO [Int],需要一個(gè)整型數(shù)(擲骰子次數(shù))拌倍,返回一個(gè)隨機(jī)數(shù)列表赂鲤,范圍1-6

去掉IO

randomRIO 一個(gè)缺點(diǎn)是,它必須使用IO柱恤,將我們的狀態(tài)保存在程序外数初,我們并不能控制它。我們盡量只在必須要與外部世界交互的時(shí)候使用I/O梗顺。

為了避免使用IO泡孩,我們創(chuàng)建一個(gè)本地的生成器。在System.Random包中的 randommkStdGen函數(shù)允許我們生成一個(gè)元組寺谤,其中包含偽隨機(jī)數(shù)和一個(gè)更新過的生成器仑鸥,以備下次這個(gè)函數(shù)下次調(diào)用。

GHCi> :m System.Random
GHCi> let generator = mkStdGen 0           -- "0" is our seed
GHCi> :t generator
generator :: StdGen
GHCi> generator
1 1
GHCi> :t random
random :: (RandomGen g, Random a) => g -> (a, g)
GHCi> random generator :: (Int, StdGen)
(2092838931,1601120196 1655838864)

注意
random generator :: (Int, StdGen)中变屁,我們使用::來引進(jìn)類型注解眼俊,本質(zhì)上就是一個(gè)類型簽名。我們可以認(rèn)為random generator(Int, StdGen)類型粟关。random可以產(chǎn)生不同類型的值泵琳,如果我們想要Int類型,我們最好通過類型簽名指定。

我們設(shè)法避免IO获列,但這里又有一個(gè)新問題谷市。首先,如果我們要使用generator獲取一個(gè)隨機(jī)數(shù)击孩,明顯我們定義...

GHCi> let randInt = fst . random $ generator :: Int
GHCi> randInt
2092838931

...是毫無用處的迫悠。它總是返回相同的值,2092838931巩梢,因?yàn)槊看味际窍嗤纳善髟谙嗤臓顟B(tài)创泄。為了解決這個(gè)問題,我們可以使用元組的第二個(gè)成員(就是那個(gè)生成器)括蝠,然后傳遞給新調(diào)用的random

GHCi> let (randInt, generator') = random generator :: (Int, StdGen)
GHCi> randInt                            -- Same value
2092838931
GHCi> random generator' :: (Int, StdGen) -- Using new generator' returned from “random generator”
(-2143208520,439883729 1872071452)

這樣看起來好笨鞠抑,而且相當(dāng)啰嗦。

不使用IO的擲骰子

我們使用一種新方法來擲骰子忌警,randomR函數(shù):

GHCi> randomR (1,6) (mkStdGen 0)
(6, 40014 40692)

結(jié)果包含了一次擲骰子的結(jié)果和一個(gè)新的生成器搁拙。擲兩次骰子的實(shí)現(xiàn):

clumsyRollDice :: (Int, Int)
clumsyRollDice = (n, m)
        where
        (n, g) = randomR (1,6) (mkStdGen 0)
        (m, _) = randomR (1,6) g

練習(xí)
實(shí)現(xiàn)rollDice :: StdGen -> ((Int, Int), StdGen)函數(shù),接受一個(gè)生成器法绵,返回一個(gè)包含兩個(gè)隨機(jī)數(shù)的元組和一個(gè)生成器箕速。

clumsyRollDice 執(zhí)行一次就能得到我們想要的結(jié)果,但是朋譬,我們必須手動傳入生成器g盐茎。它會隨著我們的程序的復(fù)雜變得越來越笨重。而且非常容易出錯(cuò):如果我們將中間的生成器傳入到一個(gè)錯(cuò)誤的where從句中了呢徙赢?

我們真正需要的是一種能自動地提取元組的第二個(gè)參數(shù)字柠,然后傳遞給新一次的random調(diào)用中。所以State 來了狡赐。

State 介紹

注意
在本章中募谎,我們使用Control.Monad.Trans.State模塊transformers包中的state monad。通過廣泛閱讀Haskell代碼阴汇,你會遇到Control.Monad.State,一個(gè)和mtl包密切相關(guān)的模塊节槐。這兩個(gè)模塊之間的區(qū)別現(xiàn)在可以不用關(guān)心:我們討論的都是應(yīng)用在mtl的變種搀庶。

Haskell類型State 描述一個(gè)函數(shù),這個(gè)函數(shù)消費(fèi)一個(gè)state铜异,產(chǎn)出一個(gè)二維元組哥倔,包含結(jié)果和一個(gè)更新過的state。

這個(gè)狀態(tài)函數(shù)封裝了一個(gè)定義了runState訪問器的數(shù)據(jù)類型揍庄。這樣就不用使用模式匹配了咆蒿。對于當(dāng)前目的,這個(gè)State類型應(yīng)該定義成:

newtype State s a = State { runState :: s -> (a, s) }

這里,s是state的類型沃测,a是產(chǎn)出結(jié)果的類型缭黔。把它叫做State類型可能有些不恰當(dāng),因?yàn)榉庋b的值并不是state本身蒂破,而是一個(gè)state處理器馏谨。

newtype

注意,我們使用newtype關(guān)鍵字定義這個(gè)數(shù)據(jù)類型附迷,而不是data惧互。newtype只能有一個(gè)數(shù)據(jù)構(gòu)造器和一個(gè)字段。這樣保證了可以使用編譯器來做封裝和解封這個(gè)單個(gè)字段的工作喇伯。出于這個(gè)原因喊儡,我們通常使用newtype來定義State。使用type來定義一個(gè)別名能否滿足需求呢稻据?答案是否定的艾猜,因?yàn)?code>type不允許我們?yōu)橐粋€(gè)新數(shù)據(jù)類型定義實(shí)例,這與我們的目的背道而馳攀甚。

State構(gòu)造器在哪箩朴?

當(dāng)你開始使用Control.Monad.Trans.State,你很快會注意到并沒有可用的State構(gòu)造器秋度。這就是我們在前幾段介紹這個(gè)類型時(shí)炸庞,“我們當(dāng)前目的” 警告的原因。transformers包以一種不大相同的方式實(shí)現(xiàn)了State類型荚斯。這些差別并不影響我們使用和理解State埠居;除了這個(gè),Control.Monad.Trans.State導(dǎo)出了state函數(shù)事期,來替代State構(gòu)造器滥壕,做了同樣的工作。

state :: (s -> (a, s)) -> State s a

至于實(shí)現(xiàn)為什么不顯而易見兽泣,我們接下來介紹绎橘。

實(shí)例化Monad

到目前為止,我們所做的僅僅是包裝了一個(gè)方法唠倦,然后給它命名称鳞。還有另外一個(gè)任務(wù),State是一個(gè)monad稠鼻,提供給我們很便利的方式去使用它冈止。不像我們以前見過的FunctorMonad實(shí)例,State有兩個(gè)類型參數(shù)候齿。因?yàn)轭愋皖愔辉试S一個(gè)類型參數(shù)熙暴,因此我們要指出另外一個(gè)闺属,s

instance Monad (State s) where

這意味著有很多不同的State monad周霉,每種類型都有可能成為state- State String掂器,State IntState SomeLargeDataStructure诗眨,等等唉匾。當(dāng)然,我們需要實(shí)現(xiàn)return(>>=)方法匠楚;這些方法能夠處理所有s取值的情況巍膘。

return函數(shù)實(shí)現(xiàn):

return :: a -> State s a
return x = state ( \ st -> (x, st) )

給定一個(gè)值 (x)到return得到一個(gè)函數(shù),這個(gè)函數(shù)接受一個(gè)state (st)芋簿,并將其和一個(gè)我們想要返回的值一起返回峡懈。最后一步,這個(gè)函數(shù)被一個(gè)state函數(shù)包裹起來与斤。

綁定函數(shù)有一些繞:

(>>=) :: State s a -> (a -> State s b) -> State s b
pr >>= k = state $ \ st ->
   let (x, st') = runState pr st -- Running the first processor on st.
   in runState (k x) st'       -- Running the second processor on st'.

(>>=) 需要兩個(gè)參數(shù)肪康,一個(gè)state處理器和一個(gè)根據(jù)第一個(gè)結(jié)果創(chuàng)建出另外一個(gè)處理器的函數(shù)k。兩個(gè)處理器在一個(gè)接受初始化state (st)并返回第二個(gè)結(jié)果和第三個(gè)state的函數(shù)中結(jié)合(這句話可能有問題)撩穿,總之磷支,(>>=)允許我們依次運(yùn)行兩個(gè)state 處理器,第一階段的結(jié)果會影響到第二個(gè)階段的結(jié)果食寡。

一個(gè)實(shí)現(xiàn)的細(xì)節(jié)是runStateState的包裹下是怎樣使用的雾狈,我們深入到這個(gè)應(yīng)用到state上的函數(shù)中去。runState pr抵皱,是s -> (a, s)的實(shí)例善榛。

設(shè)置和訪問State

monad實(shí)例讓我們能夠操作各種state 處理器,但是在此時(shí)呻畸,你可能很好奇移盆,那個(gè)最原始的state是來自哪的碾牌。這個(gè)問題是由put函數(shù)處理:

put newState = state $ \_ -> ((), newState)

給定一個(gè)state(我們想要引進(jìn)的那個(gè))仪吧,put生成了一個(gè)state處理器,忽略它接收到的任何state干发,然后發(fā)揮返回提供給put的state绞愚。由于我們不關(guān)心這個(gè)處理器的結(jié)果(我們要做的是替換這個(gè)state)叙甸,元組的第一個(gè)元素會是(),一個(gè)占位的值爽醋。

get = state $ \st -> (st, st)

結(jié)果state處理器返回state st,會被作為一個(gè)結(jié)果同時(shí)也作為一個(gè)state返回便脊。這就意味著這個(gè)state將不會改變蚂四,并且提供一個(gè)副本供我們操作。

獲取值和State

我們已經(jīng)實(shí)現(xiàn)了(>>=)runState解包State a b遂赠,來獲取真正的state處理函數(shù)久妆,這個(gè)函數(shù)接著會應(yīng)用于一些初始state。還有其他相似用途的函數(shù)比如evalStateexecState跷睦。提供一個(gè)State a b和一個(gè)初始state筷弦,evalState會只返回state處理后的結(jié)果,execState只返回新的state抑诸。

evalState :: State s a -> s -> a
evalState pr st = fst (runState pr st)

execState :: State s a -> s -> s
execState pr st = snd (runState pr st)

擲骰子和state

是時(shí)候?qū)?code>State monad應(yīng)用到我們擲骰子的例子上了烂琴。

import Control.Monad.Trans.State
import System.Random

我們希望通過類型StdGen的偽隨機(jī)發(fā)生器產(chǎn)生擲骰子的結(jié)果。因此state 處理器的類型是State StdGen Int蜕乡,跟包裝后的StdGen -> (Int, StdGen)一致奸绷。

現(xiàn)在我們可以實(shí)現(xiàn)的處理器,給定一個(gè)StdGen發(fā)生器层玲,產(chǎn)生1和6之間的一個(gè)數(shù)号醉。randomR的類型是:

-- The StdGen type we are using is an instance of RandomGen.
randomR :: (Random a, RandomGen g) => (a, a) -> g -> (a, g)

看起來熟悉嗎?如果我們把a看成Int辛块,g看成StdGen畔派,就會變成:

randomR (1, 6) :: StdGen -> (Int, StdGen)

我們已經(jīng)有了一個(gè)state處理函數(shù)!現(xiàn)在缺少的是把它包裝進(jìn)state

rollDie :: State StdGen Int
rollDie = state $ randomR (1, 6)

出于便于理解的目的润绵,我們可以使用get线椰,put和do語法來寫rollDie,這樣就能很清晰的展示出每一步的處理過程:

rollDie :: State StdGen Int
rollDie = do generator <- get
             let (value, newGenerator) = randomR (1,6) generator
             put newGenerator
             return value

我們來過一遍每一個(gè)步驟:

  1. 第一步授药,我們使用<-士嚎,從一個(gè)monadic 上下文中取出偽隨機(jī)數(shù)生成器,供后邊使用悔叽。
  2. 然后莱衩,我們使用randomR函數(shù)和上步產(chǎn)生的生成器產(chǎn)生一個(gè)1-6的整數(shù)。我們把randomR返回的新生成的生成器存儲起來娇澎。
  3. 我們接下來使用put把新的newGenerator設(shè)置到state笨蚁,以至于以后的randomR或者(>>=)鏈能使用一個(gè)不同的隨機(jī)數(shù)生成器。
  4. 最后趟庄,我們使用return將結(jié)果注入到State StdGen monad中括细。

最終我們可以使用我們的monadic骰子了。在此之前戚啥,初始state生成器本身是由mkStdGen函數(shù)生成奋单。

GHCi> evalState rollDie (mkStdGen 0)
6

為什么我們要把monad摻和進(jìn)來,創(chuàng)建了一個(gè)錯(cuò)綜復(fù)雜的框架僅僅是為了實(shí)現(xiàn)這個(gè)fst $ randomR (1,6)猫十?好吧览濒,思考下以下函數(shù):

rollDice :: State StdGen (Int, Int)
rollDice = liftM2 (,) rollDie rollDie

我們得到一個(gè)可以產(chǎn)生一個(gè)包含兩個(gè)偽隨機(jī)數(shù)的二維元組的函數(shù)呆盖。注意一下他們的與眾不同之處:

GHCi> evalState rollDice (mkStdGen 666)
 (6,1)

在其內(nèi)部,state是通過(>>=)從一個(gè)rollDie傳遞給其他的贷笛。而我們原本使用randomR (1, 6)的做法十分笨重应又,因?yàn)槲覀儽仨毷謩拥膫鬟fstate。現(xiàn)在乏苦,monad實(shí)例幫助我們做這些事情株扛。假定我們知道怎樣使用lifting函數(shù),構(gòu)造復(fù)雜的隨機(jī)數(shù)組合(元組汇荐,列表或者其他)會突然變得很簡單洞就。

State_Monad_Bind.svg.png-71.7kB
State_Monad_Bind.svg.png-71.7kB

不同類型的隨機(jī)數(shù)

到目前為止,我們僅僅通過偽隨機(jī)數(shù)生成器產(chǎn)出了Int類型的值拢驾。但是從randomR的類型并不僅限于Int奖磁。它可以生成System.RandomRandom類中的任何類型的值。已經(jīng)實(shí)現(xiàn)了Int繁疤,Char咖为,IntegerBool稠腊,DoubleFloat躁染,所以你可以生成它們其中的任何一個(gè)。

因?yàn)?code>State StdGen并不知曉關(guān)于生成的偽隨機(jī)數(shù)的類型架忌,所以我們可以寫一個(gè)相似的函數(shù)來提供一個(gè)并不指定類型的偽隨機(jī)數(shù):

getRandom :: Random a => State StdGen a
getRandom = state random

rollDie相比吞彤,這個(gè)函數(shù)在聲明中并不指定Int類型,然后使用random代替randomR叹放;否則的話它們是相同的饰恕。getRandom可以用在任何Random實(shí)例上。

GHCi> evalState getRandom (mkStdGen 0) :: Bool
True
GHCi> evalState getRandom (mkStdGen 0) :: Char
'\64685'
GHCi> evalState getRandom (mkStdGen 0) :: Double
0.9872770354820595
GHCi> evalState getRandom (mkStdGen 0) :: Integer
2092838931
someTypes :: State StdGen (Int, Float, Char)
someTypes = liftM3 (,,) getRandom getRandom getRandom

allTypes :: State StdGen (Int, Float, Char, Integer, Double, Bool, Int)
allTypes = liftM (,,,,,,) getRandom
                     `ap` getRandom
                     `ap` getRandom
                     `ap` getRandom
                     `ap` getRandom
                     `ap` getRandom
                     `ap` getRandom

對于allTypes井仰,因?yàn)闆]有liftM7(標(biāo)準(zhǔn)庫中只到liftM5)我們使用Control.Monad中的 ap函數(shù)替代埋嵌。ap適合多次計(jì)算成多參數(shù)函數(shù)的應(yīng)用。理解ap函數(shù)俱恶,看一下它聲明:

ap :: (Monad m) => m (a -> b) -> m a -> m b

謹(jǐn)記雹嗦,在Haskell中,a類型變量可以被函數(shù)類型替換合是,跟這個(gè)比較:

GHCi>:t liftM (,,,,,,) getRandom
liftM (,,,,,) getRandom :: (Random a1) =>
                           State StdGen (b -> c -> d -> e -> f -> g
                               -> (a1, b, c, d, e, f, g))

很明顯了罪,monad m變成了State StdGenap的第一個(gè)參數(shù)是一個(gè)函數(shù)b -> c -> d -> e -> f -> g -> (a1, b, c, d, e, f, g)聪全。重復(fù)應(yīng)用ap泊藕,我們最后得到一個(gè)元組,而不是一個(gè)函數(shù)难礼。把它們加起來娃圆,ap把一個(gè)在monad中的函數(shù)應(yīng)用于一個(gè)monadic值(而liftM/fmap汽久,是把一個(gè)不在monad中的函數(shù)應(yīng)用于一個(gè)monadic值)。

GHCi> evalState allTypes (mkStdGen 0)
GHCi>(2092838931,9.953678e-4,'\825586',-868192881,0.4188001483955421,False,316817438)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末踊餐,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子臀稚,更是在濱河造成了極大的恐慌吝岭,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吧寺,死亡現(xiàn)場離奇詭異窜管,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)稚机,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門幕帆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人赖条,你說我怎么就攤上這事失乾。” “怎么了纬乍?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵碱茁,是天一觀的道長。 經(jīng)常有香客問我仿贬,道長纽竣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任茧泪,我火速辦了婚禮蜓氨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘队伟。我一直安慰自己穴吹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布缰泡。 她就那樣靜靜地躺著刀荒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棘钞。 梳的紋絲不亂的頭發(fā)上缠借,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機(jī)與錄音宜猜,去河邊找鬼泼返。 笑死,一個(gè)胖子當(dāng)著我的面吹牛姨拥,可吹牛的內(nèi)容都是我干的绅喉。 我是一名探鬼主播渠鸽,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼柴罐!你這毒婦竟也來了徽缚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤革屠,失蹤者是張志新(化名)和其女友劉穎凿试,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體似芝,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡那婉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了党瓮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片详炬。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖寞奸,靈堂內(nèi)的尸體忽然破棺而出呛谜,到底是詐尸還是另有隱情,我是刑警寧澤枪萄,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布呻率,位于F島的核電站,受9級特大地震影響呻引,放射性物質(zhì)發(fā)生泄漏礼仗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一逻悠、第九天 我趴在偏房一處隱蔽的房頂上張望元践。 院中可真熱鬧,春花似錦童谒、人聲如沸单旁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽象浑。三九已至,卻和暖如春琅豆,著一層夾襖步出監(jiān)牢的瞬間愉豺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工茫因, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚪拦,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像驰贷,于是被迫代替她去往敵國和親盛嘿。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理括袒,服務(wù)發(fā)現(xiàn)次兆,斷路器,智...
    卡卡羅2017閱讀 134,672評論 18 139
  • 前言 近期又開始折騰起Haskell锹锰,掉進(jìn)這個(gè)深坑恐怕很難再爬上來了类垦。在不斷深入了解Haskell的各種概念以及使...
    Tangentw閱讀 2,146評論 0 9
  • 一個(gè)單子(Monad)說白了不過就是自函子范疇上的一個(gè)幺半群而已,這有什么難以理解的城须?* 之前了解了下Monad,...
    純白色固體閱讀 1,221評論 1 1
  • 你不知道JS:異步 第四章:生成器(Generators) 在第二章米苹,我們明確了采用回調(diào)表示異步流的兩個(gè)關(guān)鍵缺點(diǎn):...
    purple_force閱讀 960評論 0 2
  • 前言 人生苦多糕伐,快來 Kotlin ,快速學(xué)習(xí)Kotlin蘸嘶! 什么是Kotlin良瞧? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,215評論 9 118