diff --git a/app/Main.hs b/app/Main.hs index c8182678..c564a601 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -17,6 +17,23 @@ main = getEnvironment >>= (hakyllWith configuration . rules) sitePort :: Int sitePort = 4000 +supportedLanguages :: [String] +supportedLanguages = [ -- Name here the directories of the programming languages to be supported + "haskell" + , "functional-full-stack" -- Haskell backend + PureScript frontend + ] + +markdownPattern :: Pattern +markdownPattern = filePattern "md" + +imagePattern :: Pattern +imagePattern = filePattern "png" + +filePattern :: String -> Pattern +filePattern extension = foldl (.||.) (head patterns) (tail patterns) + where + patterns = fmap (\dir -> fromGlob $ "tutorials/" ++ dir ++ "/*/*." ++ extension) supportedLanguages + configuration :: Configuration configuration = defaultConfiguration @@ -52,7 +69,7 @@ rules env = do create ["archive.html"] $ do route idRoute compile $ do - tutorials <- recentFirst =<< loadAll "tutorials/haskell/*/*.md" + tutorials <- recentFirst =<< loadAll markdownPattern let archiveContext = listField "tutorials" tutorialCtx (return tutorials) @@ -68,7 +85,7 @@ rules env = do match "tutorials/index.html" $ do route idRoute compile $ do - tutorials <- recentFirst =<< loadAll "tutorials/haskell/*/*.md" + tutorials <- recentFirst =<< loadAll markdownPattern let indexContext = listField "tutorials" tutorialCtx (return tutorials) @@ -87,7 +104,7 @@ rules env = do match "templates/*" (compile templateCompiler) - match "tutorials/haskell/*/*.md" $ do + match markdownPattern $ do let tutorialRoute i = takeDirectory p "index.html" where p = toFilePath i @@ -99,14 +116,14 @@ rules env = do >>= relativizeUrls >>= cleanIndexUrls - match "tutorials/haskell/*/*.png" $ do + match imagePattern $ do route idRoute compile copyFileCompiler create ["tutorials/sitemap.xml"] $ do route idRoute compile $ do - posts <- recentFirst =<< loadAll "tutorials/haskell/*/*.md" + posts <- recentFirst =<< loadAll markdownPattern let allPosts = return posts let sitemapCtx = listField "entries" tutorialCtx allPosts @@ -115,7 +132,7 @@ rules env = do >>= cleanIndexHtmls let pumpFeedPosts = - fmap (take 10) . recentFirst =<< loadAll "tutorials/haskell/*/*.md" + fmap (take 10) . recentFirst =<< loadAll markdownPattern create ["tutorials/atom.xml"] $ do route idRoute diff --git a/tutorials/functional-full-stack/purescript-bridge/code/.gitignore b/tutorials/functional-full-stack/purescript-bridge/code/.gitignore new file mode 100644 index 00000000..1d0e1013 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/.gitignore @@ -0,0 +1 @@ +backend/.stack-work/ diff --git a/tutorials/functional-full-stack/purescript-bridge/code/README.md b/tutorials/functional-full-stack/purescript-bridge/code/README.md new file mode 100644 index 00000000..712f9a1d --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/README.md @@ -0,0 +1,2 @@ +# purescript-bridge Tutorial +This tutorial shows how to use purescript-bridge diff --git a/tutorials/functional-full-stack/purescript-bridge/code/backend/LICENSE b/tutorials/functional-full-stack/purescript-bridge/code/backend/LICENSE new file mode 100644 index 00000000..d09601e3 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/backend/LICENSE @@ -0,0 +1,30 @@ +Copyright Author Stack Builders (c) 2017 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Stack Builders nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tutorials/functional-full-stack/purescript-bridge/code/backend/Setup.hs b/tutorials/functional-full-stack/purescript-bridge/code/backend/Setup.hs new file mode 100644 index 00000000..9a994af6 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/backend/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/tutorials/functional-full-stack/purescript-bridge/code/backend/app/Main.hs b/tutorials/functional-full-stack/purescript-bridge/code/backend/app/Main.hs new file mode 100644 index 00000000..f66a415f --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/backend/app/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Lib + +main :: IO () +main = startApp diff --git a/tutorials/functional-full-stack/purescript-bridge/code/backend/backend.cabal b/tutorials/functional-full-stack/purescript-bridge/code/backend/backend.cabal new file mode 100644 index 00000000..52184406 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/backend/backend.cabal @@ -0,0 +1,56 @@ +name: backend +version: 0.1.0.0 +synopsis: Initial project template from stack +description: Please see README.md +homepage: https://github.com/githubuser/backend#readme +license: BSD3 +license-file: LICENSE +author: Author name here +maintainer: example@example.com +copyright: 2017 Author name here +category: Web +build-type: Simple +extra-source-files: README.md +cabal-version: >=1.10 + +library + hs-source-dirs: src + exposed-modules: Lib + , Types + build-depends: base >= 4.7 && < 5 + , aeson + , servant-server + , wai + , wai-cors + , warp + default-language: Haskell2010 + +executable backend-exe + hs-source-dirs: app + main-is: Main.hs + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: base + , backend + default-language: Haskell2010 + +executable bridge + hs-source-dirs: bridge + main-is: Main.hs + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: base + , backend + , purescript-bridge + default-language: Haskell2010 + +test-suite backend-test + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Spec.hs + build-depends: base + , backend + ghc-options: -threaded -rtsopts -with-rtsopts=-N + default-language: Haskell2010 + +source-repository head + type: git + location: https://github.com/githubuser/backend diff --git a/tutorials/functional-full-stack/purescript-bridge/code/backend/bridge/Main.hs b/tutorials/functional-full-stack/purescript-bridge/code/backend/bridge/Main.hs new file mode 100644 index 00000000..883f8735 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/backend/bridge/Main.hs @@ -0,0 +1,11 @@ +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) + ] diff --git a/tutorials/functional-full-stack/purescript-bridge/code/backend/src/Lib.hs b/tutorials/functional-full-stack/purescript-bridge/code/backend/src/Lib.hs new file mode 100644 index 00000000..f419fead --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/backend/src/Lib.hs @@ -0,0 +1,31 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeOperators #-} +module Lib + ( startApp + ) where + +import Data.Aeson +import Data.Aeson.TH +import Network.Wai +import Network.Wai.Handler.Warp +import Servant +import Network.Wai.Middleware.Cors +import Types (Scientist, scientists) + +type API = "scientist" :> Get '[JSON] [Scientist] + +startApp :: IO () +startApp = do + putStrLn "Starting" + run 8080 app + putStrLn "Finished" + +app :: Application +app = simpleCors (serve api server) + +api :: Proxy API +api = Proxy + +server :: Server API +server = return scientists diff --git a/tutorials/functional-full-stack/purescript-bridge/code/backend/src/Types.hs b/tutorials/functional-full-stack/purescript-bridge/code/backend/src/Types.hs new file mode 100644 index 00000000..2b2fd6d4 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/backend/src/Types.hs @@ -0,0 +1,24 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE DeriveGeneric #-} +module Types where + +import Data.Aeson +import Data.Aeson.TH +import GHC.Generics (Generic) + +data Scientist = Scientist + { + sNames :: [String] + , sPhotoUrl :: String + } deriving (Eq, Show, Generic) + +$(deriveJSON defaultOptions ''Scientist) + +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" + , Scientist ["Gottfried", "Wilhelm", "Leibniz"] "http://apusepress.booktype.pro/early-readings-in-the-philosophy-of-science/gottfried-wilhelm-leibniz-1646-1716/static/Leibniz.jpg" + , Scientist ["Stephen", "Hawking"] "https://upload.wikimedia.org/wikipedia/commons/e/eb/Stephen_Hawking.StarChild.jpg" + , Scientist ["Pythagoras"] "http://www.thefamouspeople.com/profiles/images/pythagoras-4.jpg" + , Scientist ["Wernher", "von", "Braun"] "https://upload.wikimedia.org/wikipedia/commons/5/56/Wernher_von_Braun_1960.jpg" + ] diff --git a/tutorials/functional-full-stack/purescript-bridge/code/backend/stack.yaml b/tutorials/functional-full-stack/purescript-bridge/code/backend/stack.yaml new file mode 100644 index 00000000..a6dea46d --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/backend/stack.yaml @@ -0,0 +1,6 @@ +resolver: lts-7.17 +packages: +- '.' +extra-deps: [] +flags: {} +extra-package-dbs: [] diff --git a/tutorials/functional-full-stack/purescript-bridge/code/backend/test/Spec.hs b/tutorials/functional-full-stack/purescript-bridge/code/backend/test/Spec.hs new file mode 100644 index 00000000..cd4753fc --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/backend/test/Spec.hs @@ -0,0 +1,2 @@ +main :: IO () +main = putStrLn "Test suite not yet implemented" diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/.gitignore b/tutorials/functional-full-stack/purescript-bridge/code/frontend/.gitignore new file mode 100644 index 00000000..6b2e5752 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +bower_components/ +output/ +dist/ +static/dist +.psci_modules +npm-debug.log +**DS_Store diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/LICENSE b/tutorials/functional-full-stack/purescript-bridge/code/frontend/LICENSE new file mode 100644 index 00000000..69da0fed --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2016, Alexander C. Mingoia +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/README.md b/tutorials/functional-full-stack/purescript-bridge/code/frontend/README.md new file mode 100644 index 00000000..a0bf4371 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/README.md @@ -0,0 +1,6 @@ +# Basic Proof of Concept for purescript-bridge + +This is a basic Proof-of-concept frontend for a functional fullstack app. +Based on [Pux-starter-app](https://github.com/alexmingoia/pux-starter-app). + +See the [Tutorial](https://www.stackbuilders.com/tutorials/functional-full-stack/purescript-bridge/) for more context on how to use this. diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/bower.json b/tutorials/functional-full-stack/purescript-bridge/code/frontend/bower.json new file mode 100644 index 00000000..e1afaf84 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/bower.json @@ -0,0 +1,17 @@ +{ + "name": "pux-starter-app", + "homepage": "https://github.com/alexmingoia/pux-starter-app", + "authors": [ + "Alex Mingoia " + ], + "description": "Starter Pux application using webpack with hot-reloading.", + "main": "support/index.js", + "license": "BSD3", + "dependencies": { + "purescript-pux": "^7.0.0", + "purescript-pux-devtool": "^4.1.0", + "purescript-affjax": "^3.0.2", + "purescript-argonaut-codecs": "^2.1.0", + "purescript-argonaut-generic-codecs": "5.2.0" + } +} diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/package.json b/tutorials/functional-full-stack/purescript-bridge/code/frontend/package.json new file mode 100644 index 00000000..7056554d --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/package.json @@ -0,0 +1,49 @@ +{ + "name": "pux-starter-app", + "version": "9.0.0", + "description": "Starter Pux application using webpack with hot-reloading.", + "main": "support/index.js", + "keywords": [ + "pux", + "purescript-pux", + "boilerplate", + "starter-app" + ], + "scripts": { + "postinstall": "bower cache clean && bower install", + "clean": "rimraf static/dist && rimraf dist && rimraf output", + "build": "npm run clean && webpack --config ./webpack.production.config.js --progress --profile --colors", + "watch": "npm run clean && node ./webpack.config.js", + "serve": "http-server static --cors -p 3000", + "start": "npm run watch", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git://github.com/alexmingoia/pux-starter-app.git" + }, + "author": "Alexander C. Mingoia", + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/alexmingoia/pux-starter-app/issues" + }, + "dependencies": { + "bower": "^1.7.9", + "connect-history-api-fallback": "^1.2.0", + "express": "^4.13.4", + "html-webpack-plugin": "^2.15.0", + "http-server": "^0.9.0", + "purescript": "^0.10.1", + "purescript-psa": "^0.3.9", + "purs-loader": "^2.0.0", + "react": "^15.0.0", + "react-dom": "^15.0.0", + "rimraf": "^2.5.2", + "webpack": "^2.1.0-beta.25" + }, + "devDependencies": { + "source-map-loader": "^0.1.5", + "webpack-dev-middleware": "^1.8.3", + "webpack-hot-middleware": "^2.12.2" + } +} diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Counter.purs b/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Counter.purs new file mode 100644 index 00000000..3fd31e2f --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Counter.purs @@ -0,0 +1,25 @@ +module App.Counter where + +import Prelude ((+), (-), const, show) +import Pux.Html (Html, div, span, button, text) +import Pux.Html.Events (onClick) + +data Action = Increment | Decrement + +type State = Int + +init :: State +init = 0 + +update :: Action -> State -> State +update Increment state = state + 1 +update Decrement state = state - 1 + +view :: State -> Html Action +view state = + div + [] + [ button [ onClick (const Increment) ] [ text "Increment" ] + , span [] [ text (show state) ] + , button [ onClick (const Decrement) ] [ text "Decrement" ] + ] diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Layout.purs b/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Layout.purs new file mode 100644 index 00000000..52a4e177 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Layout.purs @@ -0,0 +1,39 @@ +module App.Layout where + +import App.Counter as Counter +import App.Scientist as Scientist +import App.NotFound as NotFound +import App.Routes (Route(Home, NotFound)) +import Prelude (($), map) +import Pux.Html (Html, div, h1, p, text) + +data Action + = ScientistAction (Scientist.Action) + | ScientistsLoaded (Scientist.State) + | PageView Route + | Nop + +type State = + { route :: Route + , sScientists :: Scientist.State } + +init :: State +init = + { route: Home + , sScientists: Scientist.init } + +update :: Action -> State -> State +update (PageView route) state = state { route = route } +update (ScientistAction action) state = state { sScientists = Scientist.update action state.sScientists } +update (ScientistsLoaded s) state = state { sScientists = s } +update Nop state = state + +view :: State -> Html Action +view state = + div + [] + [ h1 [] [ text "Scientist Viewer" ] + , case state.route of + Home -> map ScientistAction $ Scientist.view state.sScientists + NotFound -> NotFound.view state + ] diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Main.purs b/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Main.purs new file mode 100644 index 00000000..a107d390 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Main.purs @@ -0,0 +1,76 @@ +module Main where + +import App.Routes (match) +import App.Scientist as Scientist +import App.Layout (Action(PageView, ScientistsLoaded, Nop), State, view, update) +import Control.Bind ((=<<)) +import Control.Monad.Eff (Eff) +import DOM (DOM) +import Prelude (bind, pure, ($), show, unit) +import Pux (App, Config, CoreEffects, fromSimple, renderToDOM, start) +import Pux.Devtool (Action, start) as Pux.Devtool +import Pux.Router (sampleUrl) +import Signal ((~>)) + +import Control.Monad.Eff.Class (liftEff) +import Control.Monad.Aff (runAff) +import Control.Monad.Eff.Exception.Unsafe (unsafeThrow) +import Control.Monad.Eff.Console (CONSOLE, log) +import Data.Either (Either(..)) +import Data.HTTP.Method (Method(..)) +import Network.HTTP.Affjax (affjax, defaultRequest, AJAX) +import Signal.Channel (channel, subscribe, CHANNEL, send) +import Data.Argonaut.Decode.Class (decodeJson) +import Data.Argonaut.Generic.Aeson as Aeson + + +type AppEffects = (dom :: DOM, console :: CONSOLE, ajax :: AJAX) + +-- | App configuration +config :: forall eff. State -> Eff (ajax :: AJAX, dom :: DOM, channel :: CHANNEL, console :: CONSOLE | eff) (Config State Action AppEffects) +config state = do + -- | Create a signal of URL changes. + urlSignal <- sampleUrl + + -- | Map a signal of URL changes to PageView actions. + let routeSignal = urlSignal ~> \r -> PageView (match r) + + chan <- channel Nop + let signal = subscribe chan + let dataArrivedSignal = signal ~> \a -> a + let logError e = do + log e + unsafeThrow e + runAff (\e -> liftEff $ logError $ show e) (\s -> pure unit) do + request <- affjax $ defaultRequest { url = "http://localhost:8080/scientist", method = Left GET } + let result = do + scientists <- Aeson.decodeJson request.response + pure $ ScientistsLoaded $ Scientist.State { + scientists: scientists + , current: 0 + } + case result of + Left error -> liftEff $ logError error + Right r -> liftEff $ send chan r + + pure + { initialState: state + , update: fromSimple update + , view: view + , inputs: [routeSignal, dataArrivedSignal] } + +-- | Entry point for the browser. +main :: State -> Eff (CoreEffects AppEffects) (App State Action) +main state = do + app <- start =<< config state + renderToDOM "#app" app.html + -- | Used by hot-reloading code in support/index.js + pure app + +-- | Entry point for the browser with pux-devtool injected. +debug :: State -> Eff (CoreEffects AppEffects) (App State (Pux.Devtool.Action Action)) +debug state = do + app <- Pux.Devtool.start =<< config state + renderToDOM "#app" app.html + -- | Used by hot-reloading code in support/index.js + pure app diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/NotFound.purs b/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/NotFound.purs new file mode 100644 index 00000000..d22d26d1 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/NotFound.purs @@ -0,0 +1,8 @@ +module App.NotFound where + +import Pux.Html (Html, (#), div, h2, text) + +view :: forall state action. state -> Html action +view state = + div # do + h2 # text "404 Not Found" diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Routes.purs b/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Routes.purs new file mode 100644 index 00000000..7970b81c --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Routes.purs @@ -0,0 +1,12 @@ +module App.Routes where + +import Data.Functor ((<$)) +import Data.Maybe (fromMaybe) +import Prelude (($)) +import Pux.Router (end, router) + +data Route = Home | NotFound + +match :: String -> Route +match url = fromMaybe NotFound $ router url $ + Home <$ end diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Scientist.purs b/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Scientist.purs new file mode 100644 index 00000000..13e9dd40 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Scientist.purs @@ -0,0 +1,63 @@ +module App.Scientist where + +import Prelude ((+), (-), const, show, ($), (<=), (>=), bind, pure, (<>), (<$>)) +import Pux.Html (Html, div, span, button, text, h2, img) +import Pux.Html.Attributes (src, height) +import Pux.Html.Events (onClick) +import Data.Array (length, (!!)) +import Data.Maybe (Maybe(..)) + +import Data.Argonaut.Decode.Class (class DecodeJson, decodeJson) +import Data.Argonaut.Decode ((.?)) +import Data.Argonaut.Core (toObject) +import Data.Either (Either(..)) +import Types (Scientist(..)) + +data Action = Next | Previous + +data State = State { + scientists :: Array Scientist +, current :: Int +} + +init :: State +init = State { + scientists : [] +, current : 0 +} + +update :: Action -> State -> State +update Previous (State s) = State $ s {current = bounded_current} + where + ncurrent = s.current - 1 + bounded_current = if ncurrent <= 0 + then 0 + else ncurrent +update Next (State s) = State $ s {current=bounded_current} + where + ncurrent = s.current + 1 + max_current = length s.scientists - 1 + bounded_current = if ncurrent >= max_current + then max_current + else ncurrent + +view :: State -> Html Action +view (State {scientists:[]}) = + div [] + [ + span [] [text "Loading scientists..."] + ] +view (State s) = + let mScientist = s.scientists !! s.current in + case mScientist of + Nothing -> view (State {scientists: [], current: 0}) + 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" ] + ] diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Types.purs b/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Types.purs new file mode 100644 index 00000000..263a9405 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/src/Types.purs @@ -0,0 +1,16 @@ +-- File auto generated by purescript-bridge! -- +module Types where + +import Prim (Array, String) + +import Data.Generic (class Generic) + + +data Scientist = + Scientist { + sNames :: Array String + , sPhotoUrl :: String + } + +derive instance genericScientist :: Generic Scientist + diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/static/app.css b/tutorials/functional-full-stack/purescript-bridge/code/frontend/static/app.css new file mode 100644 index 00000000..efef88d3 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/static/app.css @@ -0,0 +1,7 @@ +body { + font-family: 'Source Sans Pro', 'Trebuchet MS', 'Lucida Grande', 'Helvetica Neue', sans-serif; + text-rendering: optimizeLegibility; + font-size: 14px; + letter-spacing: .2px; + text-size-adjust: 100 +} diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/support/index.html b/tutorials/functional-full-stack/purescript-bridge/code/frontend/support/index.html new file mode 100644 index 00000000..d7b7fd26 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/support/index.html @@ -0,0 +1,13 @@ + + + + + + + Pux Starter App + + + +
+ + diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/support/index.js b/tutorials/functional-full-stack/purescript-bridge/code/frontend/support/index.js new file mode 100644 index 00000000..3ab610ee --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/support/index.js @@ -0,0 +1,13 @@ +var Main = require('../src/Main.purs'); +var initialState = require('../src/Layout.purs').init; +var debug = process.env.NODE_ENV === 'development' + +if (module.hot) { + var app = Main[debug ? 'debug' : 'main'](window.puxLastState || initialState)(); + app.state.subscribe(function (state) { + window.puxLastState = state; + }); + module.hot.accept(); +} else { + Main[debug ? 'debug' : 'main'](initialState)(); +} diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/support/pux-starter-app.gif b/tutorials/functional-full-stack/purescript-bridge/code/frontend/support/pux-starter-app.gif new file mode 100644 index 00000000..8c84f69e Binary files /dev/null and b/tutorials/functional-full-stack/purescript-bridge/code/frontend/support/pux-starter-app.gif differ diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/webpack.config.js b/tutorials/functional-full-stack/purescript-bridge/code/frontend/webpack.config.js new file mode 100644 index 00000000..e513518d --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/webpack.config.js @@ -0,0 +1,102 @@ +var path = require('path'); +var webpack = require('webpack'); +var HtmlWebpackPlugin = require('html-webpack-plugin'); + +var port = process.env.PORT || 3000; + +var config = { + entry: [ + 'webpack-hot-middleware/client?reload=true', + path.join(__dirname, 'support/index.js'), + ], + devtool: 'cheap-module-eval-source-map', + output: { + path: path.resolve('./static/dist'), + filename: '[name].js', + publicPath: '/' + }, + module: { + loaders: [ + { test: /\.js$/, loader: 'source-map-loader', exclude: /node_modules|bower_components/ }, + { + test: /\.purs$/, + loader: 'purs-loader', + exclude: /node_modules/, + query: { + psc: 'psa', + pscArgs: { + sourceMaps: true + } + } + } + ], + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('development') + }), + new webpack.optimize.OccurrenceOrderPlugin(true), + new webpack.LoaderOptionsPlugin({ + debug: true + }), + new webpack.SourceMapDevToolPlugin({ + filename: '[file].map', + moduleFilenameTemplate: '[absolute-resource-path]', + fallbackModuleFilenameTemplate: '[absolute-resource-path]' + }), + new HtmlWebpackPlugin({ + template: 'support/index.html', + inject: 'body', + filename: 'index.html' + }), + new webpack.HotModuleReplacementPlugin(), + new webpack.NoErrorsPlugin(), + ], + resolveLoader: { + modules: [ + path.join(__dirname, 'node_modules') + ] + }, + resolve: { + modules: [ + 'node_modules', + 'bower_components' + ], + extensions: ['.js', '.purs'] + }, +}; + +// If this file is directly run with node, start the development server +// instead of exporting the webpack config. +if (require.main === module) { + var compiler = webpack(config); + var express = require('express'); + var app = express(); + + // Use webpack-dev-middleware and webpack-hot-middleware instead of + // webpack-dev-server, because webpack-hot-middleware provides more reliable + // HMR behavior, and an in-browser overlay that displays build errors + app + .use(express.static('./static')) + .use(require('connect-history-api-fallback')()) + .use(require("webpack-dev-middleware")(compiler, { + publicPath: config.output.publicPath, + stats: { + hash: false, + timings: false, + version: false, + assets: false, + errors: true, + colors: false, + chunks: false, + children: false, + cached: false, + modules: false, + chunkModules: false, + }, + })) + .use(require("webpack-hot-middleware")(compiler)) + .listen(port); +} else { + module.exports = config; +} diff --git a/tutorials/functional-full-stack/purescript-bridge/code/frontend/webpack.production.config.js b/tutorials/functional-full-stack/purescript-bridge/code/frontend/webpack.production.config.js new file mode 100644 index 00000000..00486d75 --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/code/frontend/webpack.production.config.js @@ -0,0 +1,53 @@ +var path = require('path'); +var webpack = require('webpack'); +var HtmlWebpackPlugin = require('html-webpack-plugin'); + +module.exports = { + entry: [ path.join(__dirname, 'support/index.js') ], + output: { + path: path.resolve('./static/dist'), + filename: '[name]-[hash].min.js', + publicPath: '/dist/' + }, + module: { + loaders: [ + { + test: /\.purs$/, + loader: 'purs-loader', + exclude: /node_modules/, + query: { + psc: 'psa', + bundle: true, + warnings: false + } + } + ], + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('production') + }), + new webpack.optimize.OccurrenceOrderPlugin(true), + new webpack.LoaderOptionsPlugin({ + minimize: true, + debug: false + }), + new HtmlWebpackPlugin({ + template: 'support/index.html', + inject: 'body', + filename: 'index.html' + }), + ], + resolveLoader: { + modules: [ + path.join(__dirname, 'node_modules') + ] + }, + resolve: { + modules: [ + 'node_modules', + 'bower_components' + ], + extensions: ['.js', '.purs'] + } +}; diff --git a/tutorials/functional-full-stack/purescript-bridge/error_on_frontend_2.png b/tutorials/functional-full-stack/purescript-bridge/error_on_frontend_2.png new file mode 100644 index 00000000..6f0b8c30 Binary files /dev/null and b/tutorials/functional-full-stack/purescript-bridge/error_on_frontend_2.png differ diff --git a/tutorials/functional-full-stack/purescript-bridge/frontend_with_pictures.png b/tutorials/functional-full-stack/purescript-bridge/frontend_with_pictures.png new file mode 100644 index 00000000..14758f92 Binary files /dev/null and b/tutorials/functional-full-stack/purescript-bridge/frontend_with_pictures.png differ diff --git a/tutorials/functional-full-stack/purescript-bridge/frontend_works_again_2.png b/tutorials/functional-full-stack/purescript-bridge/frontend_works_again_2.png new file mode 100644 index 00000000..8d6c1a47 Binary files /dev/null and b/tutorials/functional-full-stack/purescript-bridge/frontend_works_again_2.png differ diff --git a/tutorials/functional-full-stack/purescript-bridge/scientist_viewer_1.png b/tutorials/functional-full-stack/purescript-bridge/scientist_viewer_1.png new file mode 100644 index 00000000..327bba11 Binary files /dev/null and b/tutorials/functional-full-stack/purescript-bridge/scientist_viewer_1.png differ diff --git a/tutorials/functional-full-stack/purescript-bridge/tutorial.md b/tutorials/functional-full-stack/purescript-bridge/tutorial.md new file mode 100644 index 00000000..3e69ed6a --- /dev/null +++ b/tutorials/functional-full-stack/purescript-bridge/tutorial.md @@ -0,0 +1,386 @@ +--- +title: Connecting a Haskell Backend to a PureScript Frontend +published: 2017-07-17 +ghc: 7.10.3 +purs: 0.10.5 +lts: 7.17 +libraries: purescript-bridge +language: functional-full-stack +author-name: Javier Casas Velasco +description: 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. +--- +# Connecting a Haskell Backend to a PureScript Frontend +## Introduction +At [Stack Builders](https://www.stackbuilders.com) we are working on a full-stack app with [CollegeVine](https://www.collegevine.com/) using Functional Languages. +We have a [Haskell](https://www.haskell.org) backend written in [Servant](http://haskell-servant.readthedocs.io) that manipulates the database and offers some endpoints to a [PureScript](http://www.purescript.org) 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: +```Haskell +{-# 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: + +```Haskell +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`](https://hackage.haskell.org/package/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 + +```haskell +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](https://github.com/alexmingoia/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!](scientist_viewer_1.png "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](https://en.wikipedia.org/wiki/Gottfried_Wilhelm_Leibniz) - the man behind Calculus - has a middle name. But that is not the only case. +[Wernher von Braun](https://en.wikipedia.org/wiki/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](https://en.wikipedia.org/wiki/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. + +```haskell + data Scientist = Scientist + { sId :: Int + , sNames :: [String] + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''Scientist) +``` + +![](error_on_frontend_2.png "Runtime errors? I hate runtime errors!") + +_Runtime errors? I hate runtime errors!_ + +Ooops. We need to tweak the frontend to make it accept the new format. + +```haskell +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" +``` + +![](frontend_works_again_2.png "Yay, rolling again!") + +_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: + +```haskell +{-# 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. + +```haskell +{-# 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`. + +```Haskell +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. + +```shell +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. + +```haskell +-- 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. + +```haskell +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](https://github.com/purescript-contrib/purescript-argonaut) team did it! +They made some [Generic Argonaut-Aeson codecs](https://pursuit.purescript.org/packages/purescript-argonaut-generic-codecs/6.0.3/docs/Data.Argonaut.Generic.Aeson) which should make the frontend speak [Aeson](https://hackage.haskell.org/package/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: +```Haskell +$(deriveJSON defaultOptions ''Scientist) +``` + +But we have to tweak the frontend to do it. +```Haskell +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. + +```Haskell +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. + +```bash +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: + +```Haskell +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. + +```Haskell +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" ] + ] +``` + +![](frontend_with_pictures.png "Now I see you, Mr. Newton") + +_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](http://www.mohanzhang.com/) from [CollegeVine](https://www.collegevine.com), 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](https://www.collegevine.com) knows all the secrets to get you accepted. +* [Wikipedia](https://www.wikipedia.org/) for providing nice photos of famous scientists, and for the great work they are doing.