1 of 26

Product :: RubyOnRails → Haskell

Alexander Granin

graninas@gmail.com, Twitter: @graninas

2 of 26

Plan

  • History of the solution
  • Design and architecture
  • Haskell in production

3 of 26

History of the solution

(Poland)

Restaumatic

https://www.restaumatic.com/pl/

Michal Kozakiewicz

https://github.com/kozak

Maciej Bielecki

https://github.com/zyla

Kamil Figiela

https://github.com/kfigiela

Adam Szlachta

https://github.com/dktn

… and others

4 of 26

History of the solution

  • Ruby on Rails (5 years)

5 of 26

History of the solution

  • Ruby on Rails (5 years)
    • Big codebase, deep Object Oriented Programming

6 of 26

History of the solution

  • Ruby on Rails (5 years)
    • Big codebase, deep Object Oriented Programming
    • Monolith

7 of 26

History of the solution

  • Ruby on Rails (5 years)
    • Big codebase, deep Object Oriented Programming
    • Monolith
    • Bugs (dynamic types)

8 of 26

History of the solution

  • Ruby on Rails (5 years)
    • Big codebase, deep Object Oriented Programming
    • Monolith
    • Bugs (dynamic types)
    • Bad maintainability

9 of 26

History of the solution

  • Ruby on Rails (5 years)
    • Big codebase, deep Object Oriented Programming
    • Monolith
    • Bugs (dynamic types)
    • Bad maintainability
    • Lack of guarantees (can blow at any moment)

10 of 26

History of the solution

  • Ruby on Rails (5 years)
    • Big codebase, deep Object Oriented Programming
    • Monolith
    • Bugs (dynamic types)
    • Bad maintainability
    • Lack of guarantees (can blow at any moment)
    • Strange default practices

11 of 26

History of the solution

  • Ruby on Rails (5 years)
    • Big codebase, deep Object Oriented Programming
    • Monolith
    • Bugs (dynamic types)
    • Bad maintainability
    • Lack of guarantees (can blow at any moment)
    • Strange default practices
  • Haskell (Backend) + PureScript (Frontend) (1 year)

12 of 26

History of the solution

  • Ruby on Rails (5 years)
    • Big codebase, deep Object Oriented Programming
    • Monolith
    • Bugs (dynamic types)
    • Bad maintainability
    • Lack of guarantees (can blow at any moment)
    • Strange default practices
  • Haskell (Backend) + PureScript (Frontend) (1 year)
    • Powerful static type system

13 of 26

History of the solution

  • Ruby on Rails (5 years)
    • Big codebase, deep Object Oriented Programming
    • Monolith
    • Bugs (dynamic types)
    • Bad maintainability
    • Lack of guarantees (can blow at any moment)
    • Strange default practices
  • Haskell (Backend) + PureScript (Frontend) (1 year)
    • Powerful static type system
    • Significant reduction of bugs (especially financial bugs)

14 of 26

History of the solution

  • Ruby on Rails (5 years)
    • Big codebase, deep Object Oriented Programming
    • Monolith
    • Bugs (dynamic types)
    • Bad maintainability
    • Lack of guarantees (can blow at any moment)
    • Strange default practices
  • Haskell (Backend) + PureScript (Frontend) (1 year)
    • Powerful static type system
    • Significant reduction of bugs (especially financial bugs)
    • Less strange code, more maintainable

15 of 26

History of the solution

  • Ruby on Rails (5 years)
    • Big codebase, deep Object Oriented Programming
    • Monolith
    • Bugs (dynamic types)
    • Bad maintainability
    • Lack of guarantees (can blow at any moment)
    • Strange default practices
  • Haskell (Backend) + PureScript (Frontend) (1 year)
    • Powerful static type system
    • Significant reduction of bugs (especially financial bugs)
    • Less strange code, more maintainable
    • No need in ceremonies and OOP boilerplate

16 of 26

Architecture and Design

Frontend, Backend

Frontend

PureScript

Specular (FRP)

Backend

Haskell

Servant (HTTP server)

Opaleye (DB layer)

ADTs (Data Model)

Ruby On Rails

PostgreSQL

JavaScript

Records (DB Model)

JavaScript

Migrations

Freer Monad

REST

JSON RPC

Embedding

aeson (serialization)

17 of 26

Data Model: Algebraic Data Types (ADTs)

  • Definition in types
  • Type gen for PureScript
  • Lenses

data Invoice = Invoice� { _companyId :: CompanyId , _invoiceNumber :: Text

, _totalAmount :: Money� , _paidAmount :: Money� , _invoiceDate :: Day� }� deriving (Eq, Show, Ord, Generic)��type Invoices = [Invoice]�

data Company = Company� { _companyId :: CompanyId� , _name :: Text

, _iban :: IBAN� , _email :: EMail� }� deriving (Eq, Show, Ord, Generic)�

type Companies = [Company]

18 of 26

JSON serialization: aeson

  • JSON
  • Manual or automatic
  • Applicative parsers
  • Lens-compatible

data Invoice = Invoice� { _companyId :: CompanyId� , _invoiceNumber :: Text

, _totalAmount :: Money� , _paidAmount :: Money� , _invoiceDate :: Day� }� deriving (Eq, Show, Ord, Generic)��instance FromJSON Invoice where parseJSON� = genericParseJSON stripLensPrefix��instance ToJSON Invoice where toJSON� = genericToJSON stripLensPrefix

19 of 26

DB Model: Records

  • eDSL (type Level)
  • vinyl records + custom solution
  • Automatic id, timestamps
  • Automatic data mapping to tables
  • Mind-blowing

Alternatives:

  • ???
  • ad-hoc solutions

type instance HsFields InvoicesTable ='[ "company_id" :-> CompanyId� , "invoice_number" :-> Text� , "total_amount" :-> Money� , "paid_amount" :-> Money� , "invoice_date" :-> UTCTime� , "created_at" :-> UTCTime� , "updated_at" :-> UTCTime� ]��fromDBInvoice :: DBInvoice -> InvoicefromDBInvoice dbInvoice = Invoice� { _payment = #company_id dbInvoice� , _totalAmount = #invoice_number dbInvoice� , _totalAmount = #total_amount dbInvoice� , _paidAmount = #paid_amount dbInvoice� , _invoiceDate = #invoice_date dbInvoice� }

20 of 26

DB Layer: Opaleye

  • eDSL (type Level + value Level)
  • Many different features
  • Extremely mind-blowing

Alternatives:

  • Persistent
  • Esqueleto
  • Beam

findInvoices� :: ( DBQueryEff effs )� => Text-> Eff effs Invoices�findInvoices number = do� invoices <- query (\inv -> #invoice_number .== number)� pure $ fromDBInvoice <$> invoices��saveInvoice� :: ( Member TimeL effs, DBUpdateEff effs )� => Invoice-> Eff effs InvoiceId�saveInvoice invoice = dolet rec = #id =: Nothing� <+> #company_id =: (invoice ^. companyId)� <+> #invoice_number =: (invoice ^. invoiceNumber)� <+> #total_amount =: (invoice ^. totalAmount)� <+> #paid_amount =: (invoice ^. paidAmount)� <+> #invoice_date =: (invoice ^. invoiceDate)� insertWithTimestamp @InvoiceTable rec

21 of 26

HTTP Server: Servant

  • eDSL (type level)
  • Many features
  • Can be hard on the start

Alternatives:

  • Snap Server
  • Warp
  • Happstack

type InvoicesAPI =� ("api" :> "v1" :> "billing" :> "invoices":> ( QueryParam "companyId" CompanyId:> Throws NotAuthorized:> Throws NotFound:> Get '[JSON] Invoices:<|> ReqBody '[JSON] Invoice:> Throws NotAuthorized:> Throws NotFound:> PostCreated '[JSON] NoContent� )� )��invoicesServer

:: ServerEff effs

=> ServerT InvoicesAPI (Eff effs)�invoicesServer = runDomain ... getInvoices� :<|> runDomain ... createInvoice

22 of 26

Business Logic: Freer Monad & Effects

  • Inversion of Control
  • “Onion architecture”
  • Separate eDSLs for specific tasks
  • Easy testing, easy mocking
  • Maintaining of effects is hard

Alternatives:

  • Final tagless / mtl
  • Free Monads
  • Service Handle Pattern
  • ReaderT Pattern

data InvoiceL a whereCreateInvoice :: InvoiceData -> InvoiceL (Result Invoice)� GetInvoice :: InvoiceId -> InvoiceL (Result Invoice)� UpdateInvoice :: InvoicePatch -> InvoiceL (Result Invoice)� DeleteInvoice :: InvoiceId -> InvoiceL (Result ())���assignInvoice� :: ( Member InvoiceL effs, Member CompanyL effs )� => CompanyId -> InvoiceId -> Eff effs Company�assignInvoice companyId invoiceId = do� ensureCompanyExists companyId� invoice <- withSuccess $ getInvoice invoiceId� updateCompany $ nilCompanyPatch

{ _companyId = companyId, _invoices = Add invoice }

23 of 26

Web UI: Specular FRP

  • PureScript: eager Haskell
  • PureScript → JavaScript
  • Specular: reflex-dom port

invoicesTable� :: forall m� . MonadWidget m� => Invoices-> m (Dynamic (Array InvoiceMark))�invoicesTable invoices = do� table $ do� caption $ h3 $ text "Invoices"� thead $ do� tr $ do� th $ text "Mark to pay"� th $ text "Number"� th $ text "Invoice date"� th $ text "Total amount"� th $ text "Unpaid amount"� tbody $ traverse (makeInvoiceRow config) invoices

24 of 26

Haskell in production

25 of 26

Haskell in production

  • Significant reducing of bugs, very reliable code, simple refactoring
  • Testability and maintainability is a matter of a chosen design solutions
  • Too many complex solutions and libraries; software design discipline is not yet developed
  • Effect systems are hard to maintain, type level magic is hard to maintain
  • Simple solutions are never bad
  • Records can be significantly better (see recent proposal #282 Record Dot Syntax)
  • Free Monads are a bit slow, Church-Encoded Free Monads are as fast as FT / mtl
  • There is no difference between Free & Church Free from the usage perspective
  • A lot of libraries for server side applications
  • STM is a bless, multithreading and concurrency is the best in class

26 of 26

Thank you for watching!

Alexander Granin

graninas@gmail.com, Twitter: @graninas