Простейший пример использования Cloud Haskell

15 января 2014

Итак, у вас было целых два дня на то, чтобы подготовиться к подаче материала про Cloud Haskell, а также составить общее впечатление об отличиях Cloud Haskell от Erlang. Сегодня же мы наконец-то увидим пусть и простую, но саму что ни на есть настоящую программу, использующую сабж. Возможно, кто-то из читателей предпочел бы сначала ознакомиться с детальным описанием API, но я лично всегда больше любил учиться на примерах, так что API отложим до завтра.

Следующая программа создает пять процессов. Один из этих процессов является «сервером». Он хранит свое состояние в Map’е, отражающим строки на целые числа. Три других процесса являются «клиентами». Они ходят в сервер за счетчиком с именем "counter" и если его значение не превышает 1000, говорят серверу записать значение, на единицу больше прочитанного. Здесь имеет место состояние гонки, но в данном примере для нас это не важно. Наконец, последний, пятый, процесс просто запускает сервер и клиентов, после чего дожидается завершения клиентов, используя мониторы, и завершается сам.

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}

import Network.Transport.TCP (createTransport, defaultTCPParameters)
import Control.Distributed.Process
import Control.Distributed.Process.Node
import Control.Concurrent (threadDelay)
import qualified Data.Map.Strict as M
import Control.Monad
import Data.Maybe
import GHC.Generics (Generic)
import Data.Typeable
import Data.Binary
import Text.Printf

data Request
  = Set ProcessId String Int | Get ProcessId String
    deriving (Show, Eq, Typeable, Generic)

instance Binary Request

data Response
  = Ok | Value (Maybe Int)
    deriving (Show, Eq, Typeable, Generic)

instance Binary Response

serverProc :: M.Map String Int -> Process ()
serverProc m = do
  req <- expect :: Process Request
  case req of
    Set p k v -> do
      send p Ok
      serverProc $ M.insert k v m
    Get p k -> do
      let v = M.lookup k m
      send p $ Value v
      serverProc m

clientProc :: ProcessId -> Process ()
clientProc srv = do
  self <- getSelfPid
  send srv $ Get self "counter"
  Value mv <- expect
  let v = fromMaybe 0 mv
  when (v <= 1000) $ do
    say $ printf "counter = %d" v
    send srv $ Set self "counter" (v+1)
    Ok <- expect
    clientProc srv

serverStart :: Process ProcessId
serverStart = do
  say "Starting server"
  spawnLocal $ serverProc M.empty

clientStartMonitor :: ProcessId -> Process ()
clientStartMonitor srv = do
  say "Starting client"
  pid <- spawnLocal $ clientProc srv
  _ <- monitor pid
  return ()

waitClients :: Int -> Process ()
waitClients n =
  when (n > 0) $ do
    ProcessMonitorNotification{} <- expect
    say "Client terminated"
    waitClients (n-1)

main = do
  Right t <- createTransport "127.0.0.1" "4444" defaultTCPParameters
  node <- newLocalNode t initRemoteTable
  runProcess node $ do
    srv <- serverStart
    let n = 3
    replicateM_ n $ clientStartMonitor srv
    waitClients n
    liftIO $ threadDelay 1000000

Основные функции, на которые здесь следует обратить внимание — spawnLocal, getSelfPid, send, expect и monitor. Они обладают довольно очевидными семантикой и типами, которые мы более подробно рассмотрим завтра.

Сообщения в Cloud Haskell должны являться экземплярами класса типов Typeable, с которым мы уже встречались. За счет этого мы можем определять типы сообщений на этапе выполнения. Что в свою очередь позволяет складывать сообщения разных типов в одну очередь сообщений процесса с помощью функции send и определять типы сообщений при извлечении их из очереди с помощью функции expect. Если в очереди нет сообщения с ожидаемым типом, expect блокирует процесс до тех пор, пока такое сообщение не поступит. Если в очереди больше одного сообщения с заданным типом, они извлекаются в порядке FIFO. Функция send никогда не блокирует вызывающий ее процесс.

На самом деле, сообщения в Cloud Haskell должен быть экземплярами класса типов Serializable, который объединяет классы типов Typeable и Binary. Экземпляр первого класса типов можно легко получить при помощи расширения GHC под названием DeriveDataTypeable. Начиная с версии 0.6.3 пакет binary позволяет писать просто instance Binary Foo, если тип Foo является экземпляром класса типов Generic. Экземпляр которого, как и в случае с Typeable, также можно легко получить, подключив расширение DeriveGeneric и сказав deriving Generic.

Как обычно, вводить собственные типы совсем не обязательно. Никто вам не запрещает писать на Haskell, словно на Erlang с прикрученным Dialyzer. Вместо своих типов Request и Response можно использовать уже существующие типы — кортежи, строки, списки, целые числа и так далее. Поскольку они уже являются экземплярами класса типов Serializable, в этом случае мы сможем написать код безо всяких там расширений и объявлений типов, а также перестанем зависеть от модулей GHC.Generics, Data.Typeable и Data.Binary. Если интересно, в качестве домашнего задания можете переписать приведенную выше программу описанным способом.

Функция clientStartMonitor создает новый процесс при помощи spawnLocal, а затем создает монитор на запущенный процесс с помощью функции monitor. В отличие от Erlang, в Cloud Haskell это работает как в случае с мониторами, так и с линками. Если процесс умрет между вызовом spawnLocal и вызовом link, процесс-родитель, вызывающий link, будет завершен, как и ожидается. И снова, в качестве своего домашнего задания можете проверить это утверждение самостоятельно. Как и Erlang, Cloud Haskell предоставляет функции spawnLink и spawnMonitor, но они реализованы просто как вызов spawn, за которым следует вызов link или monitor.

При использовании Cloud Haskell следует учитывать, что стандартные примитивы обмена данными между процессами всегда сериализуют сообщения. Этого требует семантика Cloud Haskell, согласно которой, например, принятое сообщение не может уронить процесс за счет того, что где-то глубоко в нем есть thunk, приводящий к возникновению ошибки. Если вы знаете, что делаете, то для избавления от накладных расходов на сериализацию сообщений, которые не покидают локальной ноды, можете использовать так называемые небезопасные примитивы. Учтите, что эти примитивы, как и, на самом деле, все примитивы в Cloud Haskell, не делают за вас deepseq. Подробности можно найти здесь.

Наконец, последний тонкий момент, который хотелось бы отметить, заключается в том, что большинство функций Cloud Haskell обернуты в монаду Process, что, помимо обеспечения возможности работы с очередями сообщений и так далее, защищает нас от ошибочно вызова какого-нибудь spawnLocal из обычного IO. Монада Process является экземпляром класса типов MonadIO. Это позволяет вызывать из нее обычные IO-функции при помощи liftIO, что мы и делаем в последней строчке приведенного кода.

Все исходники к этой заметке вы найдете в этом архиве. С инструкцией по сборке проектов на Haskell можно ознакомиться здесь.

Дополнение: Памятка по функциям Cloud Haskell

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.