Felix Schlitter, software engineer at DN3010
import Prelude
import Control.Monad.Eff.Console (CONSOLE)
import Control.Monad.Eff.Console as Console
greet :: String -> String
greet name = "Hello, " <> name <> "!"
main :: ∀ eff. Eff (console :: CONSOLE | eff) Unit
main = Console.log (greet "World")
head :: ∀ a. List a -> a
head (Cons x) = x
head Nil = ? -- Oops, no way to implement this!
Type-guided development
main :: forall e. Eff _ Unit
main = Console.log "foobar"
head :: ∀ a. List a -> Maybe a
head (Cons x) = Just x
head Nil = Nothing
head :: ∀ a. Partial => List a -> a
head (Cons x) = x
Partial functions only pass compiler if marked as such
main :: ∀ eff. Eff (console :: CONSOLE | eff) Unit
main = Console.log $ show $
unsafePartial $ head Nil
-- ^ We *MUST* explicitly ask for a crash
Separation of pure from impure code
doEvil :: Eff _ Unit
doEvil = FS.removeSync "/home"
calcSquare :: Int -> Int
calcSquare x =
let _ = doEvil -- does nothing!
in x * x
I/O can only occur in the Eff Monad
There is no officially prescribed or even recommended way to build apps in Purescript.
However, the most popular libraries in terms of uptake are [0]:
[0] since Purescript currently uses bower, all libraries are namespaced with a "purescript-" prefix
We get to use an awesome language on top of proven technology:
JSX is cool, but in Purescript we have no need for it.
render () {
return <div>
<h1 className="title">
Dashboard
</h1>
{ isLoggedIn &&
<button onClick={() => { /* ... */ }}>
Log out
</button>
}
</div>
}
render this = renderIn div do
h1
! className "title"
$ text "Dashboard"
when isLoggedIn do
button
! onClick (\event -> {- ... -})
$ text "Log out"
reducer
render
I/O
The redux-saga [0] project pioneered a novel way of dealing with I/O:
saga :: ∀ env state. Saga env Action state Unit
saga = forever do
take case _ of
LoginRequest { username, password } -> Just do
result <- liftAff $ attempt $ authenticate username password
case result of
Left err -> put $ LoginFailure err
Right token -> put $ LoginSuccess { username, token }
_ -> Nothing
data Action
= LoginRequest { username :: String, password :: String }
| LoginFailure Error
| LoginSuccess { username :: String, token :: String }
A giant, glorified, effectful left-fold over events accumulating into redux state
-- read-only environment, accessible via `MonadAsk` instance
-- / your state container type (reducer)
-- | / the type this saga consumes (i.e. actions)
-- | | / the type of output this saga produces (i.e. actions)
-- | | | / the return value of this Saga
-- | | | | /
newtype Saga' env state input output a = Saga' ...
type MyState = { currentUser :: String }
logUser :: ∀ env input output. Saga' env MyState input output Unit
logUser = void do
{ currentUser } <- getState
liftIO $ Console.log currentUser
The "state" parameter enables us read the current application state.
type MyConfig = { apiUrl :: String }
logApiUrl :: ∀ state input output. Saga' MyConfig state input output Unit
logApiUrl = void do
{ apiUrl } <- ask
liftIO $ Console.log apiUrl
The "env" type parameter enables us to implicitly pass read-only configuration to our Sagas.
getTime :: ∀ env state input output. Saga' env state input output Int
getTime = liftEff $ Date.now
The "return value" parameter enables us to return values from a Saga
data MyAction
= LoginRequest Username Password
| LogoutRequest
loginFlow :: ∀ env state output. Saga' env state MyAction output Unit
loginFlow = forever do
take case _ of
LoginRequest user pw -> do
...
_ -> Nothing -- ignore other actions
The "input" parameter enables us to process inputs (e.g. actions)
data MyAction
= LoginRequest Username Password
| LogoutSuccess Username
| LogoutFailure Error
| LogoutRequest
loginFlow :: ∀ env state input. Saga' env state input MyAction Unit
loginFlow = forever do
take case _ of
LoginRequest user pw -> do
result <- liftAff $ attempt ...
case result of
Left err -> put $ LoginFailure err
Right v -> put $ LoginSuccess user
_ -> Nothing -- ignore other actions
The "output" parameter enables us to dispatch actions to the reducer
As long as the types line up, Sagas can be arbitrarily composed
withGrant
:: ∀ state input output a
. Service
-> (Grant -> Saga' Env state input output a)
-> Saga' Env state input output a
withGrant service action = do
{ authenticator } <- ask
action =<< liftAff (Auth.grant service authenticator)
Example: Ensure authenticated access to APIs
fetchOwnAccount :: ∀ state input output. Saga' Env state input output Unit
fetchOwnAccount = do
{ apiUrl } <- ask
{ currentUser: { id: userId } <- getState
withGrant "edge" $ API.fetchOwnAccount apiUrl userId
type Env =
{ authenticator :: ClientAuthenticator
, currentUser :: { id :: String }
}
Sagas can be forked
postLoginFlow :: ∀ env state input output Unit
postLoginFlow = do
task <- fork $ renewAuthPeriodically
void $ fork $ watchForIncomingCalls
void $ fork $ autoReconnectWS
...
renewAuthPeriodically :: ∀ env state input output Unit
renewAuthPeriodically = forever do
void $ iterateUntil id do
liftAff (attempt $ authenticate ...) >>= case _ of
Left _ -> false <$ do
logInfo "Authentication failed. Retrying in 5 seconds..."
liftAff $ delay $ 5.0 * 1000.0 # Milliseconds
_ -> true <$ do
put $ DidAuthenticate ownId
logInfo "Next authentication renewal scheduled for one hour from now"
liftAff $ delay $ 60.0 * 60.0 * 1000.0 # Milliseconds -- hourly
Channels allow injecting events from arbitrary I/O
data Action
= HangUpRequest
| DidReceiveIncomingCall
forever $ take case _ of
Right HubIncomingCall fromUser ->
...
put DidReceiveIncomingCall -- we still put `Action`
Left HangUpRequest ->
...
data HubEvent
= HubIncomingCall Username
incomingCallsFlow :: ∀ env state. Saga' env state Action Action Unit
incomingCallsFlow = do
void $ channel
(\emit -> do
Hub.onIncomingCall \{ fromUser } ->
emit HubIncomingCall fromUser
) do
Example:
I am still exploring ideas and seeing what's possible