Stack Builders logo
Arrow icon Insights

Connecting a Haskell Backend to a PureScript Frontend

In this tutorial we will implement a way to extend the types in the Haskell backend to the PureScript frontend while maintaining consistency and simplifying communication.

Introduction

At Stack Builders we are working on a full-stack app with CollegeVine using Functional Languages. We have a Haskell backend written in Servant that manipulates the database and offers some endpoints to a PureScript frontend, that does all the React-like magic to show a really nice interface on the user's browser. It's great because we have advanced types, purity and all the awesome benefits that the Functional World offers. But not everything is perfect.

Motivation

The problem is that we have two different codebases: one in Haskell and the other in PureScript. The syntax is almost the same, but not exactly the same. For example, in Haskell we do:

{-# LANGUAGE DeriveGeneric #-}

data Blah = Blah
  {
    bFoos :: [Foo]
  } deriving (Generic)

Whereas if we want to get a Generic instance for my Blah type, in PureScript we have to do:

data Blah = Blah
  {
    bFoos :: Array Foo
  }

derive instance genericBlah :: Generic Blah

It's almost the same, but I bet you a penny that Haskell will not accept that derive instance declaration. Also, in PureScript we import typeclasses using import MyModule (class MyClass), and we initialize records with Constructor {field : value}, whereas in Haskell it's import MyModule (MyClass) and Constructor {field = vale}. Tiny, but huge differences.

At the end of the day, that means we can't directly share files. So we have types that describe the entities in the backend, and we have the same types that describe the same entities in the frontend, in different files. And every now and then, the backend team changes something on the backend types, but forgets to change it on the frontend types. Suddenly we start to get the dreaded runtime errors, because the frontend is no longer able to decode the data sent from the backend, because that data no longer conforms to the standard the frontend expects.

Ideally, we would separate the common types to some files, and use these files in the backend and the frontend. Then, the frontend inevitably follows the changes in types from the backend, and refuses to compile if the change is too big and a developer has to look at it. But, again, Haskell code is almost like PureScript code, but not completely compatible. So this is not possible.

The next best thing

Well, if we can't use the same files, we have to look for something not that far from that ideal world. If we can somehow automatically generate PureScript data types from Haskell data types, we could prevent the problem of type difference. We would effectively extend the typesystem from the backend to the frontend.

purescript-bridge to the rescue

Turns out this idea is not new, and Robert Klotzner has already done it for us, which is quite nice. From the docs, purescript-bridge tells us it will write PureScript types from Haskell types, as long as those types conform to some restrictions. But let's not talk about limitations. Instead, let's talk about awesomeness. But, before that, let's review the general architecture.

Simple WebApp

Our app will be split into two parts:

  • A Haskell backend that talks to the database, coordinates people, sends emails and all that awesome stuff backends do.
  • A PureScript frontend that compiles to JavaScript and runs on the browser; showing, in marvellous details using React, all the data that it fetches from the backend.

The two parts have to talk to each other in order to have something useful. We will use REST and JSON. That's it - the frontend will send HTTP requests full of JSON messages to the backend, in order to trigger actions, and the backend will respond to those requests with more JSON full of data, to be shown to the user.

WebApp idea

So we are going to build the next big thing. The website everyone definitely needs in their lives: a scientist browser, where we can browse names, photos and biographies of renowed scientists.

Well, it may not be the next big thing, but you will definitely need it. I promise.

Backend

The backend is going to be simple: trusty Servant is going to provide us with endpoints that talk JSON and are full of scientist biographies. The API will provide (for now) a single endpoint:

  • GET /scientist/ : return a list of scientist biographies
data Scientist = Scientist
  { sId        :: Int
  , sFirstName :: String
  , sLastName  :: String
  } deriving (Eq, Show)

Frontend

The frontend is (somewhat) simple. It's going to be the Pux starter app with small modifications to pull data from our backend. After pulling the data, it's going to show the scientists one at a time, offering buttons to see the next or the previous one.

The Best Thing Since Sliced Bread: the Scientist Viewer!

The Best Thing Since Sliced Bread: the Scientist Viewer!

Changing the app

Did you notice that we have already run into limitations? Some scientists don't just have a name and surname. My friend Gottfried Wilhelm Leibniz - the man behind Calculus - has a middle name. But that is not the only case. Wernher von Braun was a rocket scientist, and he has von in the middle of his name. That's not technically a middle name, but we somehow have to accept it. And Pythagoras, one of the pioneers of Geometry - well, we have no idea of his surname, if he ever had one.

So it's time to change the name format.

 data Scientist = Scientist
  { sId        :: Int
  , sNames     :: [String]
  } deriving (Eq, Show)

$(deriveJSON defaultOptions ''Scientist)

Runtime errors? I hate runtime errors!

Ooops. We need to tweak the frontend to make it accept the new format.

data Scientist = Scientist {
                 id :: Int
               , names :: Array String
               }

instance decodeScientist :: DecodeJson Scientist where
  decodeJson j = case toObject j of
                  Just o -> do
                    id <- o .? "sId"
                    names <- o .? "sNames"
                    pure $ Scientist {
                        id: id,
                        names: names
                      }
                  Nothing -> Left "Noparse"

Yay, rolling again!

Ok, we can do that, but it's kinda silly, isn't it? I'm copying the same code from the backend to the frontend, from data structures to serializer algorithms. I can watch myself getting very annoyed because of this repetition. But, do you imagine what would happen if we happened to have a backend team and a frontend team? Unless the communication is perfect, we are just going to have trouble. And we know it's impossible to have perfect communication, specially as the team grows.

Tutorial

Connecting the types on the backend to the frontend

First of all, let's extend the types from the backend to the frontend. This is no small feat, but it's definitely worth it. We'll do it in several steps.

First we extract the types to be shared to other files for the sake of keeping it all organised:

{-# LANGUAGE TemplateHaskell #-}
module Types where

import Data.Aeson
import Data.Aeson.TH

data Scientist = Scientist
  { sId        :: Int
  , sNames     :: [String]
  } deriving (Eq, Show)

$(deriveJSON defaultOptions ''Scientist)

scientists :: [Scientist]
scientists = [ Scientist 1 ["Isaac", "Newton"]
             , Scientist 2 ["Albert", "Einstein"]
             , Scientist 3 ["Gottfried", "Wilhelm", "Leibniz"]
             , Scientist 4 ["Stephen", "Hawking"]
             , Scientist 5 ["Pythagoras"]
             , Scientist 6 ["Wernher", "von", "Braun"]
             ]

Now we need support for Generics.

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveGeneric #-}
module Types where

import Data.Aeson
import Data.Aeson.TH
import GHC.Generics (Generic)

data Scientist = Scientist
  { sId        :: Int
  , sNames     :: [String]
  } deriving (Eq, Show, Generic)

[...]

And finally we create a Bridge binary and summon purescript-bridge.

module Main where

import Types (Scientist)
import Language.PureScript.Bridge (writePSTypes, buildBridge, defaultBridge, mkSumType)
import Data.Proxy (Proxy(..))

main :: IO ()
main = writePSTypes "../frontend/src" (buildBridge defaultBridge) myTypes
  where
    myTypes = [ mkSumType (Proxy :: Proxy Scientist)
              ]

Now, when we execute the bridge, we get some sweet auto-generated PureScript code.

backend$ stack exec bridge
The following purescript packages are needed by the generated code:

  - purescript-prim

Successfully created your PureScript modules!

The generated code is quite boring, yet exactly what we wanted.

-- File auto generated by purescript-bridge! --
module Types where

import Prim (Array, Int, String)

import Data.Generic (class Generic)


data Scientist =
    Scientist {
      sId :: Int
    , sNames :: Array String
    }

derive instance genericScientist :: Generic Scientist

Ok, it's time to make the frontend use the auto-generated code. It's easy. We are already auto-generating the Scientist datatype, so we are just going to import it.

import Types (Scientist)

Now our app has a lot less repetition.

Using generics to simplify communication

But that's not enough. Although the types are the same, the JSON instances are not the same. And they should be. But having to copy instances from the backend to the frontend is kind of silly. All I want is to copy this backend data to the frontend, where both use an equivalent representation! There has to be a way to do that automatically.

Well, there is a way to do that. Can you see the Generic instances we have introduced somehow? We are going to take advantage of these instances. In fact, I have not invented this - the Argonaut team did it! They made some Generic Argonaut-Aeson codecs which should make the frontend speak Aeson exactly the same way the backend does.

The backend is already using generic encoding thanks to Template Haskell. The code that does the magic is:

$(deriveJSON defaultOptions ''Scientist)

But we have to tweak the frontend to do it.

import Data.Argonaut.Generic.Aeson as Aeson
...
    let result = do
                  scientists <- Aeson.decodeJson request.response
                  pure $ ScientistsLoaded $ Scientist.State {
                                                scientists: scientists
                                                , current: 0
                                              }
...

And we can drop decodeScientist. Now we are rolling!

Changing the app again

You know what? We don't need that much of an ID, but a photo would definitely help here. Let's change the types again.

data Scientist = Scientist
  {
    sNames     :: [String]
  , sPhotoUrl  :: String
  } deriving (Eq, Show, Generic)

...

scientists :: [Scientist]
scientists = [ Scientist ["Isaac", "Newton"] "https://upload.wikimedia.org/wikipedia/commons/3/39/GodfreyKneller-IsaacNewton-1689.jpg"
             , Scientist ["Albert", "Einstein"] "https://upload.wikimedia.org/wikipedia/commons/d/d3/Albert_Einstein_Head.jpg"
             ...
             ]

Now we run the bridge.

backend$ stack exec bridge
The following purescript packages are needed by the generated code:

  - purescript-prim

Successfully created your PureScript modules!
backend$

And now we have the new types on the frontend:

data Scientist =
    Scientist {
      sNames :: Array String
    , sPhotoUrl :: String
    }

derive instance genericScientist :: Generic Scientist

It seems Lady Luck was on our side. The frontend hasn't failed to compile, but that's because we have removed an ID field we weren't using anywhere. If we happened to remove a field we were using, the compiler would definitely refuse to compile.

Now that we have photos on the data type, let's show 'em.

view (State s) =
  ...
    Just (Scientist scientist) ->
      div []
        [
          div [] [
            img [ src scientist.sPhotoUrl, height "400px" ] [],
            h2 [] $ (\x -> text (x <> " ")) <$> scientist.sNames
          ]
        , button [ onClick (const Previous) ] [ text "Prev. Scientist" ]
        , button [ onClick (const Next) ] [ text "Next Scientist" ]
        ]

Now I see you, Mr. Newton

Analysing the result

Thanks to purescript-bridge, we have removed the mental tax on the shared types on the frontend. The backend will generate those types for us, so we no longer have to care about them.

Also, the backend folks can make changes without thinking too much about compatiblity with the frontend, because the tools we built will tell us when something is broken.

Even better, the communication has been simplified a lot. We don't have to care anymore about message format, decoders and encoders, because Aeson and Argonaut, along with purescript-bridge, handles that for us.

And finally, the most awesome of all is that the type system is automatically consistent on the frontend and the backend. We have successfully connected the two worlds; and, as a result, we have gained some extra safety and peace of mind.

Conclusion

I shall thank Robert Klotzner for the awesome package he made. purescript-bridge is incredible in the sense that it helps us extend the wonders of a strong type system across boundaries, such as different subsystems and languages. Definitely purescript-bridge it is worth every bit it costs.

More information

  • purescript-bridge on Hackage: https://hackage.haskell.org/package/purescript-bridge
  • The code in this tutorial: https://github.com/stackbuilders/tutorials/tree/tutorials/tutorials/functional-full-stack/purescript-bridge/code
  • purescript-bridge Github's repository: https://github.com/eskimor/purescript-bridge

Thanks to

  • Mohan Zhang from CollegeVine, for letting us experiment, play and deploy purescript-bridge in order to improve the type safety. BTW, if you want to study in a prestigious university in the USA, the team at CollegeVine knows all the secrets to get you accepted.
  • Wikipedia for providing nice photos of famous scientists, and for the great work they are doing.
Published on: Jul. 20, 2017
Last updated: Dec. 21, 2024

Written by:

User Icon
Javier Casas Velasco

Subscribe to our blog

Join our community and get the latest articles, tips, and insights delivered straight to your inbox. Don’t miss it – subscribe now and be part of the conversation!
We care about your data. Check out our Privacy Policy.