| title | M07 - Jupiter | ||||||
|---|---|---|---|---|---|---|---|
| author | Walker Leite | ||||||
| patat |
|
In this module we'll see Row Types and Records, monad lifting, async computations (Aff) and we'll build a wallet webapp using Halogen.
To run this presentation type (you will need nix):
../../slide README.md- LovelaceAcademy Discord
- StackExchange (:bulb: use the tag lovelace-academy)
- Plutonomicon Discord
- Nix (module 1)
- Nix flakes (module 3)
- PureScript (modules 2-5)
module Main where
import Prelude (($), Unit, show, discard)
import Effect (Effect)
import Effect.Console (log)
-- concrete type
type Name :: Type
type Name = String
-- concrete type
data Dog :: Type
data Dog = Dog Name
-- high kinded type
data Maybe :: Type -> Type
data Maybe a = Nothing | Just a
type MaybeDog :: Type
type MaybeDog = Maybe Dog
mayShowDog :: MaybeDog -> String
mayShowDog Nothing = show "No dog :("
mayShowDog (Just (Dog name)) = show name
main :: Effect Unit
main = do
log $ mayShowDog Nothing
log $ mayShowDog $ Just (Dog "Max")What if we need to have way more dog attributes beyond name?
We could use Sum Types:
module Main where
import Prelude (($), (<>), Unit, show)
import Effect (Effect)
import Effect.Console (log)
type Name = String
type Owner = Name
type Age = Int
data Dog = Dog Name Age Owner
showDog :: Dog -> String
showDog (Dog name age owner) =
show name
<> ", "
<> show age
<> ", "
<> show owner
main :: Effect Unit
main = log $ showDog $ Dog "Max" 2 "John"It's a bit clunky but it works, what would be a better alternative?
data Row :: a -> Type
data Row a = ( label :: a )
type Dog :: Row Type
type Dog = ( name :: String, age :: Int, owner :: String )
-- ( label :: a ) is the type constructor for Row at language level ;)
-- name, age and owner are so called "indexed-labels"
-- !! because `Row Type` we can only have concrete types in labels) !!💡 You can see a
Row Typelike instructions on how to create a dog definition.
Great! But having a type that can't be used to build definitions is not so useful. How can I build a type and a value of Dog Type?
data Record :: Row Type -> TypeRecord constructor: Given a defined Row Type, give me a corresponding concrete Type
type Dog :: Type
type Dog = Record ( name :: String, age :: Int, owner :: String )
-- like Row Type, Record has a syntax suggar constructor { }
type Dog :: Type
type Dog = { name :: String, age :: Int, owner :: String }module Main where
import Prelude (($), (<>), Unit, show)
import Effect (Effect)
import Effect.Console (log)
type Dog = { name :: String, age :: Int, owner :: String }
showDog :: Dog -> String
showDog dog@{ name, owner: ownerName } =
show name
<> ", "
<> show dog.age
<> ", "
<> show ownerName
main :: Effect Unit
main = log $ showDog
{ name: "Max"
, age: 2
, owner: "John"
}What if owner is optional?
main :: Effect Unit
main = do
log $ showDog
{ name: "Max"
, age: 2
, owner: "John"
}
log $ showDog
{ name: "Lily"
, age: 3
} Type of expression lacks required label owner.
while checking that expression { name: "Lily"
, age: 3
}
has type { age :: Int
, name :: String
, owner :: String
}
while applying a function showDog
of type { age :: Int
, name :: String
, owner :: String
}
-> String
to argument { name: "Lily"
, age: 3
}
in value declaration main
What if showOwner needs handle only owner?
type Dog = { name :: String, age :: Int, owner :: String }
type Ownership = { owner :: String }
--...
showOwner :: Ownership -> String
showOwner { owner } = show owner
main :: Effect Unit
main = let
dog = { name: "Max"
, age: 2
, owner: "John"
}
in do
log $ showDog dog
log $ showOwner dog Type of expression contains additional label age.
while checking that type { age :: Int
, name :: String
, owner :: String
}
is at least as general as type { owner :: String
}
while checking that expression dog
has type { owner :: String
}
in value declaration main
Closed Row:
type Dog :: Row Type
type Dog = ( name :: String, age :: Int, owner :: String )Open Row:
type Dog :: Row Type -> Row Type
type Dog a = ( name :: String, age :: Int | a )💡 Dog is a high kinded type, it needs
a, which must have kindRow Type, to define its concreteRow Type.
type Ownership :: Row Type
type Ownership = ( owner :: String )
type Dog :: Row Type -> Row Type
type Dog a = ( name :: String, age :: Int | a )
type DogWithOwner :: Row Type
type DogWithOwner = Dog Ownership💡 Remember that a
Row Typeis the instruction on how to build aTypeusingRecord :: Row Type -> Type.
Closed Record:
type Dog :: Type
type Dog = { name :: String, age :: Int, owner :: String }Open Record:
type Dog :: Row Type -> Type
type Dog a = { name :: String, age :: Int | a }💡 Dog is a high kinded type, it needs
a, which must have kindRow Type, to define its concreteType
type Ownership :: Row Type
type Ownership = ( owner :: String )
type Dog :: Row Type -> Type
type Dog a = { name :: String, age :: Int | a }
type DogWithOwner :: Type
type DogWithOwner = Dog OwnershipDogWithOwner is the concrete Type of a dog, result of applying Ownership to the Dog HKT, which apply the received Ownership to Record :: Row Type -> Type as first argument.
In other words, we use the Ownership Row Type to tell Dog how to build a Type using Record type constructor.
module Main where
import Prelude (($), (<>), Unit, show, discard)
import Effect (Effect)
import Effect.Console (log)
type Ownership :: Row Type
type Ownership = ( owner :: String )
type Dog :: Row Type -> Type
type Dog a = { name :: String, age :: Int | a }
type DogWithOwner :: Type
type DogWithOwner = Dog Ownership
showDog :: forall a. Dog a -> String
showDog { name, age } = show name <> ", " <> show age
showOwner :: forall a. { owner :: String | a } -> String
showOwner { owner } = show owner
main :: Effect Unit
main = let
dog1 = { name: "Max"
, age: 2
, owner: "John"
}
dog2 = { name: "Lily"
, age: 3
}
in do
log $ showDog dog1
log $ showDog dog2
log $ showOwner dog1
-- uncomment to throw "lacks required label owner"
-- log $ showOwner dog2Remember the Contract monad from module 5?
buildTx :: Inputs -> Outputs -> Redeemer -> Validator -> Contract Boolean
data Contract a = Contract aWhat if we need to produce a side-effect in buildTx?
data Contract a = Contract (Effect a)
runContract :: forall a. Contract a -> Effect a
runContract (Contract eff) = log ">running effects" *> effSo we could runContract buildTx, right? Well, we can't:
buildTx :: Contract Boolean
buildTx = do
log ">building tx"
pure trueCould not match type
Effect
with type
Contract
It happens because log returns Effect, not Contract.
module Main where
import Prelude
import Effect (Effect)
import Effect.Class (class MonadEffect, liftEffect)
import Effect.Console (log)
data Contract a = Contract (Effect a)
derive instance Functor Contract
instance Apply Contract where
apply = ap
instance Bind Contract where
--bind :: forall a b. m a -> (a -> m b) -> m b
bind (Contract eff) f = Contract $ eff >>= \v -> case f v of
Contract eff' -> eff'
instance Applicative Contract where
pure value = Contract (pure value)
instance Monad Contract
instance MonadEffect (Contract) where
--liftEffect :: forall m a. MonadEffect m => Effect a -> m a
liftEffect = Contract
c1 :: Contract Boolean
c1 = do
liftEffect $ log ">running contract c1"
pure true
c2 :: Contract Boolean
c2 = do
liftEffect $ log ">running contract c2"
pure false
runContract :: forall a. Contract a -> Effect a
runContract (Contract eff) = log ">running effects" *> eff
main :: Effect Unit
main = do
result1 <- runContract c1
log $ ">result1: " <> show result1
result2 <- runContract c2
log $ ">result2: " <> show result2Aff is used to represent asynchronous effects (similar to Promises, but with more features)
-- The computation may either error with an exception, or produce a result of type a.
data Aff a
To launch it we use launchAff_ :: forall a. Aff a -> Effect Unit
module Main where
import Prelude
import Data.Time.Duration (Milliseconds (Milliseconds))
import Effect (Effect)
import Effect.Aff (launchAff_, delay)
import Effect.Console (log, logShow)
import Effect.Class (liftEffect)
import Fetch (fetch)
main :: Effect Unit
main = do
log "This is an Effect computation (Effect monadic context)"
launchAff_ do
{ text } <- fetch "https://httpbin.org/uuid" {}
uuid <- text
liftEffect $ logShow uuid
launchAff_ do
liftEffect $ log "[other aff] start"
delay $ Milliseconds 1000.0
liftEffect $ log "[other aff] done"
log "Program finished before async stuff ;)"-- Relevant instances, beyond Functor, Apply, Applicative, Bind and Monad
MonadThrow Error Aff
-- so it can throwError
MonadError Error Aff
-- so we can catchError
MonadEffect Aff
-- so we can liftEffect
-- MonadAff, like MonadEffect, allows one yo lift from Aff to m
class (MonadEffect m) <= MonadAff m
liftAff :: Aff ~> mWe need to build simple user interfaces using the knowledge acquired so far.
Halogen is a type-safe library for building user interfaces in PureScript, it rely on both a well known architecture (Elm) and PureScript features like Row Types and Monad Transformers.
Some alternatives:
- react-basic core types and tools for React;
- deku FRP UI framework for games and web apps;
- jelly framework for building web applications based on Signal;
In the module 3 we've created types and functions to help us render HTML, Halogen does the same, in the Halogen.HTML:
module Main where
import Prelude
import Data.Newtype (unwrap)
import Effect.Console (log)
import Halogen.HTML as HH
import Halogen.VDom.DOM.StringRenderer as HVDS
html = HH.h1 [ ] [ HH.text "Hello, world" ]
logRender = log <<< HVDS.render (const mempty) <<< unwrap
main = logRender htmlmodule Main where
import Prelude ((<<<), const, mempty)
import Data.Newtype (unwrap)
import Effect.Console (log)
import Halogen.HTML as HH
import Halogen.HTML.Properties as HP
import Halogen.VDom.DOM.StringRenderer (render)
html :: forall w i. HH.HTML w i
html = HH.div
[ HP.id "root" ]
[ HH.input
[ HP.placeholder "Name" ]
, HH.button
[ HP.classes [ HH.ClassName "btn-primary" ]
, HP.type_ HP.ButtonSubmit
]
[ HH.text "Submit" ]
]
logRender = log <<< render (const mempty) <<< unwrap
main = logRender htmlHalogen HTML is one basic building block of Halogen applications. But pure functions that produce HTML lack many essential features that a real world application needs: state that represents values over time, effects for things like network requests, and the ability to respond to DOM events (for example, when a user clicks a button).
module Main where
import Prelude
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver as HD
data Action = Increment | Decrement
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval H.defaultEval { handleAction = handleAction }
}
where
initialState _ = 0
render state =
HH.div_
[ HH.button [ HE.onClick \_ -> Decrement ] [ HH.text "-" ]
, HH.text (show state)
, HH.button [ HE.onClick \_ -> Increment ] [ HH.text "+" ]
]
handleAction = case _ of
Decrement ->
H.modify_ \state -> state - 1
Increment ->
H.modify_ \state -> state + 1
main = HA.runHalogenAff do
body <- HA.awaitBody
HD.runUI component unit bodyHA.runHalogenAff :: forall x. Aff x -> Effect Unit
HA.awaitBody :: Aff HTMLElement
-- q: query, i: input, o: output
HD.runUI :: forall q i o. Component q i o Aff -> i -> HTMLElement -> Aff (HalogenIO q o Aff)
type HalogenIO q o m =
{ dispose :: m Unit
, messages :: Emitter o
, query :: forall a. q a -> m (Maybe a)
}
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
HD.runUI component unit body-- s: state, q: query, a: action, sl: slot, i: input, o: output, m: monad
mkComponent :: forall s q a sl i o m. ComponentSpec s q a sl i o m -> Component q i o m
type ComponentSpec s q a sl i o m =
{ eval :: (HalogenQ q a i) ~> (HalogenM s a sl o m)
, initialState :: i -> s
, render :: s -> HTML (ComponentSlot sl m a) a
}-- s: state, q: query, a: action, sl: slot, i: input, o: output, m: monad
-- ~> is a Natural Transformation, don't worry about it
-- you can safely read it as a normal arrow (->).
mkEval :: forall s q a sl i o m. EvalSpec s q a sl i o m -> (HalogenQ q a i) ~> (HalogenM s a sl o m)
type EvalSpec s q a s i o m =
{ finalize :: Maybe a
, handleAction :: a -> HalogenM s a sl o m Unit
, handleQuery :: forall a. q a -> HalogenM s a sl o m (Maybe a)
, initialize :: Maybe a
, receive :: i -> Maybe a
}
defaultEval :: forall s q a sl i o m. EvalSpec s q a sl i o m
H.mkComponent
{ initialState
, render
, eval: H.mkEval (H.defaultEval { handleAction = ?handleAction })
}💡 Looking in mkEval source code might help you understand what is happening behind the scene, but you don't need to understand it to use Halogen
-- s: state, q: query, a: action, sl: slot, i: input, o: output, m: monad
newtype HalogenM s a sl o m a = HalogenM (Free (HalogenF s a sl o m) a)
-- In links section there is a long video about Halogen and the Free type
-- not required to use Halogen, but still very interesting
-- Relevant instances, beyond Functor, Apply, Applicative, Bind and Monad
(Monoid a) => Monoid (HalogenM s a sl o m a)
-- so we can append many handleActions/handleQueries
(MonadEffect m) => MonadEffect (HalogenM s a sl o m)
-- so we can liftEffect to perform computations and side-effects
(MonadAff m) => MonadAff (HalogenM s a sl o m)
-- so we can liftAff to perform async computations and side-effects
MonadState s (HalogenM s a sl o m)
-- so we can get/put/modify shandleAction :: forall o m. MonadAff m => Action -> H.HalogenM State Action () o m Unit
handleAction = case _ of
SetUsername username -> do
H.modify_ _ { username = username, result = Nothing }
MakeRequest event -> do
username <- H.gets _.username
H.modify_ _ { loading = true }
response <- H.liftAff $ AX.get AXRF.string ("https://api.github.com/users/" <> username)
H.modify_ _ { loading = false, result = map _.body (hush response) }
render :: forall m. State -> H.ComponentHTML Action () m
render st =
HH.form
[ HE.onSubmit \ev -> MakeRequest ev ]
[ HH.h1_ [ HH.text "Look up GitHub user" ]
, HH.label_
[ HH.div_ [ HH.text "Enter username:" ]
, HH.input
[ HP.value st.username
, HE.onValueInput \str -> SetUsername str
]
]
, HH.button
[ HP.disabled st.loading
, HP.type_ HP.ButtonSubmit
]
[ HH.text "Fetch info" ]
, HH.p_
[ HH.text $ if st.loading then "Working..." else "" ]
, HH.div_
case st.result of
Nothing -> []
Just res ->
[ HH.h2_
[ HH.text "Response:" ]
, HH.pre_
[ HH.code_ [ HH.text res ] ]
]
]User Story: As a cardano user I want to see all my available funds in my browser wallet
nix flake init -t github:LovelaceAcademy/nix-templates#pix
