Заданне Bank Account (Exercism) на F#
Я заўсёды цікавіўся функцыянальным праграмаваннем. Першы раз я пазнаёміўся з ім ва ўніверсітэце, калі ў адным з апошніх семестраў нам чыталі базавы, але ж разам з тым даволі заблытаны курс. Тэарэтычная частка не мела амаль ніякага дачынення да тэмы і прайшла амаль цалкам міма, а вось на практыцы малады і таленавіты выкладчык пазнаёміў нас з мовай Clojure
. Не магу сказаць, што пад канец семестру я добра валодаў прадметнай базай, але ж нейкае пачатковае разуменне меў. А галоўнае, была цікавасць больш пазнаёміцца з падыходам.
Пасля гэтага было некалькі спроб увайсці ў функцыянальнае праграмаванне - спачатку асновы F#
, потым OCaml
, Haskell
на зусім базавым узроўні, і далей ужо на працягу года ў вольны час я знаёміўся з Erlang
, Elixir
і фантастычнай віртуальнай машынай BEAM
. Зараз я зноў звярнуўся да F#
праз яго неверагодную і даволі моцную статычную сістэму тыпаў і больш-менш знаёмую мне платформу dotnet
, на якой ён працуе. Найлепшы шлях вывучаць новае ў праграмаванні - пачынаць карыстацца і спрабаваць пісаць маленькія праграмы. І вось для гэтага вельмі добра падыходзіць платформа з назвай Exercism, якая дазваляе вывучаць мову праграмаванне з дапамогай простых (і часамі не вельмі) заданняў.
У рамках курса па F#
я сутыкнуўся з адным з такіх заданняў - Bank Account. Нягледзячы на тое, што заданне само па сабе зусім не складанае, але ж у ім ёсць вельмі цікавая ўмова:
Create an account that can be accessed from multiple threads/processes (terminology depends on your programming language).
Доступ да рахунку павінен ажыцяўляцца з некалькіх патокаў/працэсаў (тэрміналогія залежыць ад вашай мовы праграмавання).
“Файна, вось тут трэба глядзець у бок сінхранізацыі патокаў і паралельнага праграмавання” падумаў я. Платформа dotnet
уключае шмат канструкцый для сінхранізацыі патокаў. У якасці прыклада можна прывесці манітор і яго сінтаксічны дапаможнік у выглядзе lock аператара, клас Interlocked, м’ютэкс, семафор і шмат іншых. Сам я з’яўляюся адданым прыхільнікам BEAM
і мадэлі актараў. Нягледзячы на тое, што віртуальная машына BEAM
дакладна не прытрымлівае мадэлі актараў, яна ўсё роўна абапіраецца на падыход абмену паведамленнямі (message-passing) для ажыццяўлення паралельнага праграмавання (як і актары). Амаль адразу я ўспомніў, што ў стандартнай бібліятэцы для F#
была нешта звязанае з актарамі, вось толькі ведаў пра гэта ў мяне не засталося (а можа і не было :). Пошук у сеці інтэрнэт хутка выдаў мне фундаментальны артыкул ад знакамітага гуру F#
Scott Wlaschin. Акрамя тэорыі ў гэтым артыкуле можна знайсці яшчэ і практычную даведку пра актары ў F#
.
Найбольш каштоўнай інфармацыя з артыкула для мяне было тое, што я дазнаўся пра існаванне класа MailboxProcssor, які з’яўляецца нейкай формай актара альбо агента
у F#
. Фактычна, MailboxProcessor
гэта чарга паведамленняў у памяці, якая можа атрымліваць паведамленні з розных патокаў, але апрацоўваць іх будзе ў адным патоку. Відавочна, што аўтары натхняліся Erlang
, але зрабілі нашмат больш простую реэлізацыю.
Каб глыбей пазнаёміцца з тэмай, я прайшоўся па серыі артыкулаў тут, тут і тут. Аказалася, што MailboxProcessor
мае ўсе неабходныя для вырашэння задачы метады: Post
для асінхроннай адпраўкі паведамленняў, PostAndReply
- для адпраўкі паведамленняў з чаканнем адказу.
Тым не менш, галоўным пытаннем для мяне заставалася яшчэ зразумець, якім чынам гэты BankAccount
клас павінен выглядаць у кодзе на F#
, каб ён інкапсуліраваў сваю залежнасць ад чаргі паведамленняў. У Elixir
, напрыклад, стваралі набор функцый (функцыянальны інтэрфейс) вакол GenServer, што значна спрашчала карыстальнікам працу па інтэграцыі з такімі канструкцыямі. Класы у F#
як раз і дапамагаюць стварыць функцыянальны інтэрфейс і схаваць узаемадзеянне з агентамі
. Кожная аперацыя з BankAccount
павінна абапірацца на свой тып паведамлення, а pattern matching і discriminated union у F#
вельмі зручна дапамагаюць апрацоўваць гэтыя паведамленні ў кодзе. Трэба адзначыць, што Elixir
карыстаецца такімі ж падыходамі нягледзячы на тое, што гэта дынамічная мова праграмавання.
Напрыканцы, клас BankAccount
выглядаў неяк так (АСЦЯРОЖНА, СПОЙЛЕР
):
module BankAccount
type Agent<'T> = MailboxProcessor<'T>
type BankAccountOperation =
| Open
| Close
| GetBalance of AsyncReplyChannel<decimal option>
| UpdateBalance of decimal
type BankAccountStatus =
| Active
| Inactive
type BankAccountState =
{ Status: BankAccountStatus
Balance: decimal option }
type BankAccount() =
let mutable state = { Status = Inactive; Balance = None }
let openAccount () =
state <- { Status = Active; Balance = Some 0.0m }
let closeAccount () =
state <- { Status = Inactive; Balance = None }
let getBalance () = state.Balance
let updateBalance amount =
match state.Status with
| Active ->
let balance = getBalance () |> Option.get
let newBalance = balance + amount
state <- { state with Balance = Some newBalance }
| _ -> ()
let agent =
Agent.Start(fun inbox ->
let rec messageLoop () =
async {
let! msg = inbox.Receive()
match msg with
| Open -> openAccount ()
| Close -> closeAccount ()
| GetBalance replyChannel ->
let balance = getBalance ()
replyChannel.Reply balance
| UpdateBalance amount ->
updateBalance amount
return! messageLoop ()
}
messageLoop ())
member _.Open() = agent.Post Open
member _.Close() = agent.Post Close
member _.GetBalance() =
agent.PostAndReply(
(fun replyChannel -> GetBalance replyChannel), 10000)
member _.UpdateBalance amount =
agent.Post(UpdateBalance amount)
let mkBankAccount () = BankAccount()
let openAccount (account: BankAccount) =
account.Open()
account
let closeAccount (account: BankAccount) =
account.Close()
account
let getBalance (account: BankAccount) = account.GetBalance()
let updateBalance (change: decimal) (account: BankAccount) =
account.UpdateBalance change
account