PureScript Cheat Sheet
Syntax, type classes, Effect/Aff for side effects, and Halogen basics for a strongly-typed functional language that compiles to JavaScript.
Core Syntax
Bindings, type signatures, records, and ADTs.
module Main whereimport Prelude-- Type signature + bindinggreet :: String -> Stringgreet name = "Hello, " <> name-- Recordstype User = { name :: String, age :: Int }alice :: Useralice = { name: "Alice", age: 30 }older :: User -> Userolder u = u { age = u.age + 1 }-- Algebraic data typesdata Status = Loading | Success String | Failure Stringdescribe :: Status -> Stringdescribe = case _ of Loading -> "loading..." Success msg -> "ok: " <> msg Failure err -> "error: " <> err
Type Classes
PureScript's answer to ad-hoc polymorphism, with laws unlike TS interfaces.
class Describable a where describe :: a -> Stringinstance describableInt :: Describable Int where describe n = "Int: " <> show n-- Functor / Applicative / Monad for a custom typedata Box a = Box ainstance functorBox :: Functor Box where map f (Box a) = Box (f a)instance applyBox :: Apply Box where apply (Box f) (Box a) = Box (f a)instance applicativeBox :: Applicative Box where pure = Box
Effect & Aff
Effect models synchronous side effects; Aff models async, replacing callback hell.
import Effect (Effect)import Effect.Console (log)import Effect.Aff (Aff, launchAff_, delay)import Effect.Class (liftEffect)import Data.Time.Duration (Milliseconds(..))main :: Effect Unitmain = log "hello"asyncTask :: Aff UnitasyncTask = do liftEffect (log "starting") delay (Milliseconds 1000.0) liftEffect (log "done after 1s")runIt :: Effect UnitrunIt = launchAff_ asyncTask
Halogen Component (sketch)
A minimal Halogen component shape: State, Action, render, handleAction.
component :: forall q i o m. H.Component q i o mcomponent = H.mkComponent { initialState: \_ -> 0 , render , eval: H.mkEval H.defaultEval { handleAction = handleAction } } where 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 Increment -> H.modify_ (_ + 1) Decrement -> H.modify_ (_ - 1)
spago & PureScript Tooling
Common commands with spago, the standard build tool.
- spago init- scaffold a new project
- spago build- compile the project
- spago run- build and run the Main module
- spago test- run the test suite
- spago bundle-app- produce a single bundled JS file
- spago repl- interactive PSCi shell
- spago install <pkg>- add a dependency from the package set
- purs ide- IDE server used by editor plugins for type info
Row Polymorphism & Extensible Records
Functions can accept "a record with at least these fields" without committing to the exact shape, unlike Elm or TS structural typing quirks.
-- "r" is a row-type variable: any additional fields are allowedgreet :: forall r. { name :: String | r } -> Stringgreet person = "Hello, " <> person.namegreet { name: "Alice", age: 30 } -- fine, extra 'age' field ignoredgreet { name: "Bob" } -- fine, no extra fields-- Record update/delete with row operations (via Record module)import Record (delete, insert)import Data.Symbol (SProxy(..))withoutAge :: forall r. { name :: String, age :: Int | r } -> { name :: String | r }withoutAge = delete (SProxy :: SProxy "age")
Monad Transformers (ReaderT/StateT/ExceptT)
Stack effects explicitly instead of relying on a single opaque Effect/Aff -- common in real app architectures.
import Control.Monad.Reader (ReaderT, runReaderT, ask)import Control.Monad.Except (ExceptT, runExceptT, throwError)import Effect.Aff (Aff)import Effect.Class (liftEffect)type AppConfig = { apiUrl :: String }-- App = "has access to config, can fail with String, runs in Aff"type App a = ReaderT AppConfig (ExceptT String Aff) afetchUser :: Int -> App UserfetchUser userId = do config <- ask when (userId < 0) (throwError "invalid id") -- ... use config.apiUrl to make the request pure defaultUserrunApp :: AppConfig -> App a -> Aff (Either String a)runApp config app = runExceptT (runReaderT app config)
Phantom Types & Kind Signatures
Encode invariants (units, validation state) purely at the type level with zero runtime cost.
-- Phantom type parameter "validated" never appears in the runtime valuenewtype Email (validated :: Boolean) = Email Stringunvalidated :: String -> Email falseunvalidated s = Email svalidate :: Email false -> Maybe (Email true)validate (Email s) = if contains (Pattern "@") s then Just (Email s) else Nothing-- Only an Email true can be sent -- the compiler rejects unvalidated onessendEmail :: Email true -> Effect UnitsendEmail (Email s) = log ("sending to " <> s)-- Kind signature restricting a type parameter to a specific setdata Direction = Up | Down | Left | Right
Common Type Class Hierarchy in Practice
How Semigroup/Monoid/Foldable/Traversable compose for real data processing, beyond Functor/Applicative/Monad.
import Data.Foldable (foldMap, sum)import Data.Traversable (traverse)import Data.Maybe (Maybe)-- Monoid: combine values with an identity elementnewtype Sum = Sum Intinstance semigroupSum :: Semigroup Sum where append (Sum a) (Sum b) = Sum (a + b)instance monoidSum :: Monoid Sum where mempty = Sum 0total :: Array Int -> Inttotal xs = case foldMap Sum xs of Sum n -> n-- Traversable: run an effect over a structure, short-circuit on failurevalidateAll :: Array String -> Maybe (Array Email)validateAll names = traverse parseEmail names-- Foldable works uniformly over Array, Maybe, Map, custom trees, etc.
Advanced Concepts Glossary
Terms you'll hit reading real PureScript codebases and library docs beyond the basics.
- Newtype deriving- `derive newtype instance` reuses the wrapped type's instances (e.g. Eq, Ord) for a newtype with zero boilerplate
- Type classes with functional dependencies- `class Foo a b | a -> b` lets the compiler infer `b` from `a`, avoiding ambiguous instance resolution
- PureScript Record.Builder- efficiently builds up large records field-by-field instead of repeated `{ r | field = x }` copies
- Free monads- `Free f a` lets you build an interpreter-agnostic DSL, then supply different interpreters (pure test vs Effect production)
- Variant / Data.Variant- open-union alternative to sum types, useful for extensible error types across module boundaries
- unsafeCoerce / unsafePerformEffect- escape hatches from `purescript-unsafe-coerce`; last resort, bypass the type system entirely
- PSCi `:type` / `:kind`- REPL commands to inspect an expression's inferred type or a type constructor's kind
Reach for newtype over type alias when you want compile-time distinction between semantically different values with the same runtime representation (e.g. UserId vs Int) — newtypes are erased at runtime so there's zero performance cost.