PureScript
PureScript is a small, strongly typed, purely functional programming language that compiles to readable JavaScript, enabling the use of advanced functional programming techniques within the JavaScript ecosystem.[1] Designed by Phil Freeman and first released in late 2013, it is inspired by Haskell and aims to provide a robust environment for large-scale, disciplined functional programming in web development and beyond.[2][3]
The language features a rich type system including algebraic data types, pattern matching, higher-kinded types, row polymorphism for extensible records, type classes with functional dependencies, and higher-rank polymorphism, all of which support expressive and safe code construction.[1] PureScript allows seamless interoperability with existing JavaScript libraries by providing type annotations for foreign functions, facilitating its use in browser-based applications, server-side code via Node.js, and other JavaScript environments.[4] Its compilation process produces efficient, human-readable output, and the ecosystem includes tools like the Spago build system, Pursuit package database, and extensive community libraries for domains such as web UIs, data processing, and backend services.[1] While primarily targeting JavaScript, experimental backends like PureScript Native support compilation to C++11 and Go for broader applicability.[5]
PureScript maintains an active open-source community, with ongoing development hosted on GitHub and resources like the official "PureScript by Example" book to aid learning and adoption.[6] As of November 2025, the latest stable release is version 0.15.15, featuring enhanced command-line options for better tooling integration and continued focus on type safety and developer productivity. Recent updates include the adoption of statically-linked binaries for Linux releases since June 2025 to simplify distribution.[7][8]
Introduction
Overview
PureScript is a strongly typed, purely functional programming language designed for compiling to efficient and readable code in various target environments.[1] It primarily transpiles to JavaScript for web development, with experimental and community backends supporting C++11 (via PureScript Native), Erlang (via Purerl), and Go (via PureScript Native) for other platforms, enabling Haskell-inspired programming paradigms outside traditional Haskell ecosystems.[6][9][10] The language's primary goal is to facilitate the development of robust, type-safe applications, particularly for frontend web applications and server-side logic, by producing optimized output that integrates seamlessly with existing ecosystems like the JavaScript runtime.[4]
Developed by Phil Freeman, PureScript was first released in 2013 as an open-source project.[11] It is licensed under the BSD 3-clause license, promoting broad adoption and contributions from the developer community.[12] The project is hosted on GitHub, where it has fostered a collaborative environment for enhancements and maintenance.[6]
As of November 2025, PureScript remains an active, community-driven initiative, with the latest stable release being version 0.15.15, released in February 2024.[13] This ongoing development supports its use in production environments, backed by tools like the Pursuit package search and a vibrant discourse forum for users and contributors.[14]
Design Philosophy
PureScript's design philosophy draws significant inspiration from Haskell, adopting core principles such as pure functions, which prohibit side effects in function definitions to ensure referential transparency, and immutability, where data structures cannot be modified after creation to promote safer and more predictable code. Unlike Haskell, however, PureScript opts for strict evaluation by default rather than lazy evaluation, prioritizing predictability and performance alignment with its primary compilation target, JavaScript. This choice facilitates easier debugging and avoids the complexities associated with laziness, such as space leaks, while still allowing optional laziness through libraries like Data.Lazy when needed.[4][15][1]
Central to PureScript's philosophy is type-driven development, where an expressive type system—featuring algebraic data types, higher-kinded types, and type classes—enables developers to catch errors at compile time and use types as a guide for implementation. This approach shifts much of the burden of correctness from runtime checks to the compiler, fostering robust software through techniques like property-based testing and equational reasoning enabled by purity. The language's strong, static typing ensures that type inference is powerful yet decidable, supporting refactoring and abstraction without sacrificing usability.[4][1][15]
Interoperability with host environments, particularly JavaScript, is a foundational goal, achieved through direct compilation to readable and efficient JavaScript code that incurs no runtime overhead from the type system. PureScript allows seamless import of JavaScript modules with typed foreign function interfaces, enabling developers to leverage existing ecosystems like npm packages while writing pure functional code for critical logic. This design supports mixed-language projects, where PureScript handles type-safe computations and JavaScript manages imperative or performance-critical sections.[1][4][15]
To balance strict typing and purity—which enable aggressive optimizations and reliable code—the language incorporates flexible features like row polymorphism for extensible records, allowing variant-like behavior without sacrificing type safety. These trade-offs prioritize long-term maintainability and efficiency over immediate expressiveness, aiming to produce output code that is not only performant but also comprehensible to JavaScript developers, thereby facilitating adoption in real-world applications across web, server, and mobile contexts.[1][15][4]
History
Origins and Development
PureScript was created in 2013 by Phil Freeman, motivated by the shortcomings of existing Haskell-to-JavaScript compilers like GHCJS and UHC, which suffered from large runtime overheads, complex build processes, and suboptimal performance for web applications.[16] While working on rewriting a JavaScript application in TypeScript, Freeman sought a stronger type system and more efficient compilation pipeline tailored for browser environments.[16]
The language's initial goals centered on producing a lightweight, strictly evaluated alternative inspired by Haskell, emphasizing clean, readable JavaScript output without Haskell's lazy evaluation or extensive runtime dependencies.[17] This design aimed to enable expressive functional programming with advanced type features, such as row polymorphism and type classes, while ensuring seamless interoperability with JavaScript via straightforward foreign function interfaces (FFI).[17] Freeman implemented the compiler in Haskell itself, using relatively simple language constructs to make it accessible for potential contributors.[16]
Shortly after its inception in October 2013, the project transitioned to open-source under the Apache 2.0 license on GitHub, rapidly attracting community interest and contributions from developers interested in functional programming for the web.[6] An active IRC channel on Freenode further supported early collaboration, leading to the development of supporting libraries and tools.[16]
Among the key early milestones was the first public release toward the end of 2013, which included core features like the type system and FFI support.[3] Soon after, tools such as Pulp were introduced to streamline building and managing PureScript projects, integrating with package managers like Bower for dependency handling.[18] Freeman remained the primary architect through the initial years, guiding the language's evolution until the community increasingly took over maintenance responsibilities.
Major Releases and Milestones
PureScript's first public release occurred in late 2013, marking the language's early availability as a Haskell-inspired compiler targeting JavaScript.
In 2015, version 0.7 introduced row polymorphism, enabling more flexible record types and structural typing for better expressiveness in data handling.[19]
Version 0.13 followed in 2019, featuring internal parser and grammar enhancements that improved overall compiler stability, including refinements to the foreign function interface (FFI) for smoother JavaScript interoperability.[20]
The 0.15 series began with version 0.15.0 in April 2022, adding support for ES modules to enable modern bundling and tree-shaking, alongside a modernized FFI that simplified foreign code integration.[21] Minor releases like 0.13.8 in 2020 and 0.15.14 in 2024 addressed performance challenges through optimizations like reduced memory usage during compilation and faster type inference.[22][23] As of June 2025, future releases after v0.15.15 incorporate statically-linked binaries to facilitate easier distribution across platforms without dependency issues.[8]
Key milestones include the introduction of Pursuit in 2014, a searchable documentation tool that centralized API exploration for packages and replaced fragmented local docs.[24] The Spago package manager launched in December 2018, providing a Dhall-based build system that streamlined dependency management and project setup.[25]
In September 2025, the PureScript Analyzer entered public alpha, offering enhanced IDE support through a dedicated language server for better code analysis and autocompletion.[26]
The project adopted semantic versioning guidelines to manage breaking changes, ensuring compatibility in core libraries and compiler updates.[27] Later releases focused on reducing output bundle sizes via ES module compatibility, aiding deployment efficiency.[21]
Language Fundamentals
Syntax and Semantics
PureScript employs a syntax closely resembling that of Haskell, facilitating familiarity for developers experienced in functional programming languages. The language is indentation-sensitive, where significant whitespace determines the scope of blocks such as function bodies, case expressions, and module declarations, eliminating the need for explicit braces or keywords like "begin" and "end."[28] For instance, a simple function definition might appear as:
double :: Int -> Int
double x = x * 2
double :: Int -> Int
double x = x * 2
Here, the body x * 2 is indented relative to the function name to denote its scope. Data types are declared using the data keyword, supporting algebraic data types with constructors, such as data Maybe a = [Nothing](/page/Nothing) | Just a, which mirrors Haskell's approach to defining sum types.[28] Function definitions leverage pattern matching for destructuring arguments, allowing concise handling of different cases without additional boilerplate.
The module system in PureScript is hierarchical, organizing code into namespaces like Data.List to promote modularity and avoid global namespace pollution. Modules are declared with module ModuleName (export1, export2) where, specifying explicit exports to control visibility, while imports can be unqualified, qualified, or hiding specific names to prevent clashes. Qualified imports, such as import Data.Maybe as Maybe, prefix identifiers with the alias (e.g., Maybe.Just), enabling safe usage of multiple modules with overlapping names.[28] This structure supports large-scale code organization, with the compiler enforcing module boundaries during compilation.
Basic control flow constructs include pattern matching in function definitions or case expressions, guards for conditional branching using |, and do-notation for sequencing operations, particularly useful in monadic contexts. Pattern matching allows exhaustive checks, as in:
head :: List a -> Maybe a
head [Nil](/page/Nil) = [Nothing](/page/Nothing)
head ([Cons](/page/Cons) x _) = Just x
head :: List a -> Maybe a
head [Nil](/page/Nil) = [Nothing](/page/Nothing)
head ([Cons](/page/Cons) x _) = Just x
Guards provide an alternative to if-then-else, evaluated from top to bottom, for example:
bmiTell :: Number -> Number -> String
bmiTell weight height
| weight / sq height <= 18.5 = "Underweight"
| weight / sq height <= 25.0 = "Normal"
| otherwise = "Overweight"
bmiTell :: Number -> Number -> String
bmiTell weight height
| weight / sq height <= 18.5 = "Underweight"
| weight / sq height <= 25.0 = "Normal"
| otherwise = "Overweight"
Do-notation desugars to bind operations, offering a imperative-like syntax for composing computations, such as do x <- action1; y <- action2; pure (x + y).[29]
PureScript adopts strict evaluation semantics, where function arguments are fully evaluated before application, contrasting with Haskell's lazy evaluation and ensuring predictable runtime performance without unexpected thunk buildup.[16] This call-by-value strategy means expressions like f (g ()) first compute g () completely before passing the result to f, aiding in performance tuning for JavaScript targets where laziness must be explicitly implemented via thunks if needed.[30]
By default, PureScript enforces immutability in its core language, treating all bindings as immutable value assignments rather than variables that can be reassigned; there are no mutable references in pure code, and data structures are persistent, with operations returning new instances rather than modifying existing ones.[31] For example, updating a record creates a new record: newBook = book { published = 2014 }, preserving the original unchanged, which aligns with the language's pure functional paradigm and prevents side effects in non-effectful code.[32]
Type System
PureScript employs a strong, static type system that ensures type safety at compile time, with no types present at runtime to maintain performance when targeting JavaScript.[4] The system features full type inference, allowing the compiler to deduce types for expressions without explicit annotations in many cases, though programmers can provide type signatures for clarity, documentation, or to resolve ambiguities.[4] This inference is powered by Hindley-Milner-style algorithms, enabling concise code while catching errors early.[33]
A core element of the type system is support for algebraic data types (ADTs), which facilitate precise data modeling through sum types and product types. Sum types represent choices among variants, such as the Maybe a type with constructors Nothing and Just a for handling optional values, or Either e a for encapsulating success or error cases. Product types combine multiple values, as in records or tuples, allowing structured data representation. These ADTs promote composability and exhaustiveness checking via pattern matching, reducing runtime errors.[34]
Type classes provide ad-hoc polymorphism by defining interfaces with associated functions, constrained to specific types via instances. For example, the Eq a class includes an eq :: a -> a -> Boolean method for equality, with instances for primitives like Boolean and String, enabling polymorphic functions like generic comparison. Similarly, Show a supports string conversion with show :: a -> String. Instances can be derived automatically for ADTs, enhancing code reuse without sacrificing type safety.[35]
Row polymorphism extends the type system to handle extensible records and variants, using row variables to represent unknown additional fields. A function might accept { name :: String | r } where r is a polymorphic row, allowing inputs with extra fields like { name :: String, age :: Int | r } without type errors, provided required fields are present. This feature supports open records, contrasting with closed structural typing in other languages, and is crucial for libraries dealing with dynamic data.[34]
PureScript's type system includes kinded types, where kinds classify types (e.g., * for concrete types like Int, # Type for rows), enabling higher-kinded types for abstraction over type constructors. Higher-kinded types allow type classes like Functor f with map :: (a -> b) -> f a -> f b, where f has kind Type -> Type, supporting patterns such as functors and applicatives for container-like structures. This facilitates type-level programming, where types themselves are manipulated, drawing from Haskell's System Fω extensions.[1][36]
Typed holes support type-driven development by allowing placeholders like ?hole in code, prompting the compiler to report the expected type and suggest fitting values or functions from the environment during compilation errors. This interactive feedback aids exploration, such as inferring a String -> Int -> String implementation, and integrates with editors for rapid prototyping without full type annotations.[37]
Core Features
Functional Programming Constructs
PureScript emphasizes pure functions as the cornerstone of its functional programming paradigm, ensuring that computations produce the same output for the same input without observable side effects, which facilitates reasoning about code and enables optimizations like memoization.[31] Functions are defined with a type signature specifying input and output types, such as add :: Int -> Int -> Int, and implemented using the equals sign followed by the body, promoting referential transparency.[31] Higher-order functions like map, filter, and fold enable composition of pure transformations over data; for instance, map applies a function to each element of an array, returning a new array without modifying the original, as in map (\n -> n * 2) [1, 3, 5] yielding [2, 6, 10].[38]
All functions in PureScript are curried by default, meaning a function declared to take multiple arguments, like subtract :: [Int](/page/INT) -> [Int](/page/INT) -> [Int](/page/INT), actually takes one argument and returns a new function of type [Int](/page/INT) -> [Int](/page/INT), allowing seamless partial application.[31] Partial application creates specialized functions from general ones, such as defining subtractTen :: [Int](/page/INT) -> [Int](/page/INT) as subtract 10, which subtracts 10 from its input.[31] This currying supports point-free programming styles, where functions are composed without explicitly naming arguments, enhancing conciseness and modularity in pure computations.[31]
Lambda expressions, or anonymous functions, provide a way to define short, inline functions using the backslash syntax, such as \x -> x + 1, which can be passed directly to higher-order functions like filter (\n -> n > 0) [-1, 0, 1] to retain positive numbers in a new array [1].[38] Function composition is facilitated by the <<< operator from the Semigroupoid type class, which chains functions such that (f <<< g) x evaluates as f (g x); for example, length <<< trim first trims whitespace from a string and then computes its length, both purely. These constructs allow building complex pipelines of pure transformations, such as foldl (+) 0 <<< map (\x -> x * x) <<< filter even to sum the squares of even numbers in an array.[38]
Recursion serves as the primary mechanism for iteration and control flow in PureScript, replacing imperative loops with functional definitions that process data immutably.[38] Functions like factorial :: Int -> Int are defined recursively as factorial 0 = 1 and factorial n = n * factorial (n - 1), with base cases preventing infinite computation.[38] Pattern matching on algebraic data types (ADTs) destructures inputs for recursive cases, as in a List ADT defined by data List a = Nil | Cons a (List a), where sum :: List Int -> Int matches Nil -> 0 or Cons x xs -> x + sum xs.[34] Guards provide conditional branching in recursive functions using boolean expressions, such as in gcd :: Int -> Int -> Int with gcd n m | n == 0 = m | otherwise = gcd (m mod n) n, ensuring tail-recursive optimization where possible.[34]
Immutable data structures underpin PureScript's pure approach, with records serving as lightweight, named collections like { firstName :: String, lastName :: String }, accessed immutably via dot notation without alteration.[31] Strict linked lists, modeled after Haskell's cons cells, support efficient pure operations like cons (:) to prepend elements, creating new lists as in Cons "Alice" Nil, while arrays from the Data.Array module provide indexed, immutable access for JavaScript interoperability, with functions like foldl accumulating results into new values.[38][39]
In pure contexts, error handling relies on sum types like Either e a from the purescript-either package, which encodes success (Right a) or failure (Left e) explicitly, avoiding exceptions. For example, a division function might return Either String Number with Right (x / y) on success or Left "Division by zero" otherwise, allowing pattern matching to propagate or handle errors purely, such as case divide 10 0 of Left msg -> "Error: " <> msg; Right result -> show result. This approach ensures type safety and composability in functional pipelines.
Effect System and Monads
PureScript enforces purity by default, ensuring that functions without explicit effect annotations cannot perform side effects such as I/O operations or mutable state modifications.[29] Side effects are explicitly isolated within the Effect monad, which represents computations that interact with the external world, such as console output or random number generation, while maintaining type safety and generating efficient JavaScript code.[40] This approach prevents unintended impurities and allows pure functions to be composed freely without risking hidden effects.[29]
Prior to PureScript version 0.12, side effects were managed via the Eff monad, typed as Eff r a, where r was a row type specifying the effects involved, such as { console :: CONSOLE | r } for console interactions, enabling modular tracking of impurities at the type level.[41] In recent versions, the Effect monad (Effect a) replaced Eff, simplifying the system by removing row-based effect tracking, as the original rows proved insufficient for reliably guaranteeing effect composition without excessive complexity.[42] Despite this change, row types remain integral to PureScript's type system for records and polymorphism, and some libraries use row constraints to model effectful behaviors indirectly.[31]
The Effect monad supports standard monadic operations, including do-notation as syntactic sugar for sequencing effectful computations via bind (>>=). For instance, generating and logging a random number can be written as:
purescript
import Effect (Effect)
import Effect.Console (logShow)
import Effect.Random (random)
main :: Effect Unit
main = do
n <- random
logShow n
import Effect (Effect)
import Effect.Console (logShow)
import Effect.Random (random)
main :: Effect Unit
main = do
n <- random
logShow n
This desugars to random >>= logShow, ensuring sequential execution while optimizing away unnecessary closures in the compiled JavaScript.[29] Common monads in PureScript include Maybe for handling optional values and Either for error propagation, both operating in the pure realm, alongside Effect for synchronous external interactions.
For asynchronous operations like promises or callbacks, PureScript provides the Aff monad, which wraps non-blocking effects and supports cancellation via fibers, preventing callback hell while integrating seamlessly with Effect through monad transformers.[43] Monad transformers enable stacking multiple effects, such as combining State for mutable state with Effect for I/O using StateT s Effect a, or Reader for configuration with Writer for logging via ReaderT r (WriterT w Effect) a. This layered approach, exemplified in domain-specific stacks like RWS (Reader-Writer-State) over Effect, allows complex effect compositions while preserving referential transparency outside the base monad.[44] For safe local mutation, the ST monad uses row-polymorphic references (STRef r a) to confine changes to a scoped region, avoiding global effects.[45]
Compilation and Targets
Compiler Architecture
The PureScript compiler, known as purs, is implemented in Haskell and processes source code through a series of stages to produce optimized JavaScript output.[46][6] The pipeline begins with lexing, where the source code is tokenized into lexical elements such as identifiers, keywords, and symbols. This is followed by parsing, which constructs a concrete syntax tree (CST) using a grammar inspired by Haskell's layout-sensitive syntax, handling features like indentation-based blocks and guards. The CST is then transformed into an abstract syntax tree (AST), stripping away concrete syntax details.[47]
Subsequent stages include renaming, which resolves module imports, qualified names, and local bindings to ensure unambiguous references across the codebase. Desugaring then simplifies the AST by expanding high-level constructs, such as do-notation for monadic computations, case expressions, and record updates, into a more primitive form suitable for further analysis. Type checking occurs next, employing bidirectional inference with constraint solving to validate types, infer polymorphic types, and resolve type class instances; this involves generating constraints from unification problems, applying skolemization for existential quantification, and performing AST rewrites to handle higher-rank polymorphism and row polymorphism without relying on ordered contexts.[47][33] The checked AST is desugared further into CoreFn, a small, functional intermediate representation based on System F-omega with extensions for effects and records, facilitating subsequent transformations.[47]
From CoreFn, the compiler applies basic optimizations before JavaScript emission, with advanced optimizations available via external tools like purescript-backend-optimizer.[48] Finally, JavaScript emission generates readable, modular code from the optimized CoreFn, preserving functional idioms where possible.[49][50] The compiler supports incremental compilation by tracking module dependencies and rebuilding only affected modules upon changes, improving development iteration speed in large projects.[51]
For developer tooling, the compiler integrates with editors like VS Code and Vim through the PureScript Language Server Protocol (LSP) implementation, enabling real-time type checking, error reporting, and autocomplete without full recompilation.[52] As an open-source project hosted on GitHub, contributions to the compiler occur via pull requests, with the Haskell codebase allowing for potential self-hosting experiments by compiling equivalent PureScript modules.[6]
PureScript primarily compiles to JavaScript, generating readable code in ES modules format (ECMAScript 2015 and later), which facilitates debugging and integration into modern web projects.[1][53] The output emphasizes minimal overhead, producing self-contained modules without requiring a dedicated runtime for the core language features.[54] This allows PureScript applications to bundle efficiently using tools like Webpack, maintaining compatibility with standard JavaScript ecosystems.[55]
Beyond JavaScript, experimental backends enable compilation to other languages for specialized use cases. The purescript-native project supports C++11 as a target for native performance in systems-level applications and Go for concurrent systems programming. Similarly, the purerl backend targets Erlang, leveraging its strengths in concurrency and fault tolerance for distributed systems.[10] As of 2025, these backends remain experimental, focusing on full interoperability while extending PureScript's applicability beyond the web.[5]
Interoperability with host languages occurs through PureScript's Foreign Function Interface (FFI), which uses foreign import and foreign export declarations to bridge code boundaries. For JavaScript, developers define type signatures in PureScript modules (e.g., .purs files) and implement the corresponding logic in adjacent JavaScript modules (e.g., .js files), enabling direct calls like foreign import encodeURIComponent :: [String](/page/String) -> [String](/page/String).[54] The compiler enforces type safety across this interface by requiring PureScript types to align with JavaScript runtime representations, such as wrapping JavaScript objects as records or handling null values with Maybe types to prevent mismatches.[56]
This type-safe FFI supports practical interop patterns, including gradual integration into JavaScript projects. For instance, PureScript can wrap React components via bindings in the purescript-react package, allowing developers to define PureScript functions that render React elements while exporting them for use in JavaScript code.[57] Such patterns enable hybrid applications where PureScript handles complex logic and UI state management, interoperating seamlessly with existing JavaScript libraries through uncurried functions (FnN types) or asynchronous promises via the Aff monad.[54]
Standard Library
The Standard Library of PureScript comprises a set of core packages maintained by the official PureScript organization, providing essential types, functions, and abstractions for everyday programming needs. These packages form the foundation for most PureScript projects and are designed to be lightweight, emphasizing functional programming principles while avoiding dependencies on external JavaScript libraries.[58]
The Prelude module, imported by default in every PureScript file, serves as the entry point to the standard library and re-exports foundational elements from other core packages. It defines basic types such as Int for integers, String for text, Boolean for truth values, and Number for floating-point numbers, along with fundamental functions like id (identity), const (constant function), and flip (argument reversal). Common operators for equality (==), comparison (compare), and composition (>>>) are also included, enabling concise expression of simple computations. Additionally, the Prelude exposes key type classes such as Eq for equality, Ord for ordering, Semigroup and Monoid for combining values, and higher abstractions like Functor, Apply, Applicative, Bind, and Monad to support composable code patterns.[59]
Data manipulation is handled by dedicated modules for common structures. The Data.Array module provides comprehensive functions for working with JavaScript arrays, including map and filter for transformations, foldl and foldr for reductions, concat for joining, and sortBy for ordering, all while maintaining type safety and efficiency. The Data.Maybe module introduces the Maybe type (Nothing or Just a) for representing optional values, with utilities like maybe (defaulting), fromMaybe (unwrapping), and isJust/isNothing for checks. Similarly, Data.Either defines the Either sum type (Left e for errors or Right a for success), offering functions such as either (pattern matching), hush (extracting success), and note (converting to Maybe). The Data.Tuple module supports pairs with constructors like Tuple and accessors fst and snd, facilitating simple product types without records. These modules emphasize immutability and pure functions, aligning with PureScript's functional paradigm.
Control abstractions are centralized in the Control package, which builds on the Prelude to enable advanced composition. The Control.Applicative module extends applicative functors with pure for injecting values and <*> for applying functions in contexts like Maybe or arrays. The Control.Monad and Control.Bind modules define monadic binding via >>= (bind) and =<< (bind flipped), supporting sequencing of computations with effects abstracted away. Features like ApplicativeDo notation allow do-style syntax for applicatives, improving readability for non-monadic code, as in:
purescript
do
x <- Just 1
y <- Just 2
pure $ x + y
do
x <- Just 1
y <- Just 2
pure $ x + y
This evaluates to Just 3, demonstrating applicative lifting without full monadic structure.
For operations that cannot be total, the standard library includes language-specific modules with explicit warnings. The Partial module defines the Partial type class and functions like fromJust (unsafe Maybe unwrap) and partial folds, allowing developers to mark code that assumes certain inputs exist but requiring explicit imports to signal potential incompleteness. Unsafe operations are segregated in the Unsafe package, such as unsafeCoerce for bypassing the type system and unsafeReference for low-level memory access, both strongly discouraged in favor of safer alternatives to prevent runtime errors. These modules promote disciplined use, with compiler pragmas often flagging partial patterns.
Basic mathematics and utilities round out the essentials. The Math module supplies arithmetic functions like abs, ceil, log, and trigonometric operations (sin, cos, tan), along with constants such as pi and e. Ordering is handled via Data.Ordering with LT, EQ, and GT constructors, integrated into Ord instances for comparable types. Unlike more comprehensive libraries in other languages, PureScript's standard library remains focused and minimal, deferring advanced numerics, strings, or dates to the broader ecosystem while ensuring core utilities are performant and pure.
Spago serves as the primary package manager and build tool for PureScript projects, introduced in 2018 as a modern alternative to earlier tools like Bower and Pulp.[55] It facilitates dependency installation, project building, and integration with documentation platforms by leveraging curated package sets defined in Dhall configurations to ensure compatibility.[55] Spago supports both single-package setups and monorepos, allowing developers to manage multiple related packages within a single repository while handling version pinning and reproducible builds.[55]
The PureScript compiler, invoked as purs (formerly psc), forms the core build tool, compiling source code to JavaScript or other targets with options for optimization and customization.[1] Key flags include --codegen corefn for intermediate representations, --optimizations to enable performance enhancements like dead code elimination, and --target to specify output formats such as ES modules. Spago typically wraps these invocations, streamlining commands like spago build to compile modules while respecting dependency graphs.[55]
For testing, PureScript projects commonly use the purescript-spec framework, which provides a declarative DSL for defining synchronous and asynchronous unit tests inspired by Haskell's HSpec.[60] It supports running tests via runners like purescript-spec-node for Node.js environments or purescript-spec-mocha for browser-based execution. Additionally, interoperability with JavaScript testing tools is available through packages like purescript-jest, enabling PureScript tests to integrate with Jest for broader ecosystem compatibility and parallel execution.
Documentation generation and discovery rely on Pursuit, a searchable database of auto-generated API docs for PureScript packages and modules.[61] Pursuit extracts type signatures, function documentation, and examples directly from source code annotations, allowing queries by name or approximate types to facilitate library exploration.[24] Spago integrates this by offering spago docs to publish project documentation to Pursuit, ensuring up-to-date references for contributors.[55]
Development workflows benefit from editor integrations that provide real-time type checking via the PureScript language server, available as plugins for environments like VS Code and Vim. These tools offer features such as inline error reporting, hover information for types, and autocomplete based on project dependencies. In 2025, the PureScript Analyzer—a Rust-based compiler frontend—emerged as an advanced tool for deeper static analysis, including enhanced diagnostics and refactoring support beyond the standard language server.[62]
Best practices for PureScript projects emphasize using Spago for monorepo structures to centralize dependency management across packages, reducing duplication and improving consistency.[55] For continuous integration, GitHub Actions workflows leverage the setup-purescript action to install the toolchain, run builds, and execute tests efficiently, often caching Spago's package outputs to minimize repetition across runs.
Examples
Basic Programs
PureScript programs are structured as modules, which declare their name, imports, and exports. A minimal module imports the necessary libraries, defines functions or values, and typically includes a main entry point for executable code. The Prelude, a standard library providing common functions and operators, is often imported implicitly or explicitly to simplify basic operations. Programs compile to JavaScript, enabling execution in Node.js or browser environments.[4]
A simple "Hello World" program demonstrates the use of the Effect monad for side effects, such as console output. The following example defines a main function that logs a string to the console:
purescript
module Main where
import Effect.Console (log)
main :: Effect Unit
main = log "Hello, World!"
module Main where
import Effect.Console (log)
main :: Effect Unit
main = log "Hello, World!"
Here, the module is named Main, importing the log function from Effect.Console. The main function has an explicit type signature Effect Unit, indicating it performs an effect but returns no meaningful value; PureScript's type inference could derive this type without the annotation.[4]
To illustrate type inference, consider a basic addition function. Without an explicit signature, the compiler infers add :: Int -> Int -> Int from the use of the + operator on integers:
purescript
add x y = x + y
add x y = x + y
Adding an explicit signature clarifies intent and aids readability, especially in larger codebases:
purescript
add :: Int -> Int -> Int
add x y = x + y
add :: Int -> Int -> Int
add x y = x + y
A curried variant, addFive, infers Int -> Int from partial application:
purescript
addFive = add 5
addFive = add 5
This showcases how inference reduces boilerplate while maintaining type safety.[31]
For handling potential errors like division by zero, PureScript uses the Maybe type to represent optional values, avoiding runtime exceptions. A safe division function employs pattern matching to return Nothing for invalid cases or Just with the result otherwise:
purescript
import Prelude
import Data.Maybe (Maybe(..))
safeDivide :: Int -> Int -> Maybe Int
safeDivide _ 0 = Nothing
safeDivide a b = Just (a / b)
import Prelude
import Data.Maybe (Maybe(..))
safeDivide :: Int -> Int -> Maybe Int
safeDivide _ 0 = Nothing
safeDivide a b = Just (a / b)
To use this, pattern matching extracts the value safely:
purescript
result :: Maybe Int
result = safeDivide 10 2
showResult :: String
showResult = case result of
Nothing -> "Division error"
Just x -> "Result: " <> show x
result :: Maybe Int
result = safeDivide 10 2
showResult :: String
showResult = case result of
Nothing -> "Division error"
Just x -> "Result: " <> show x
This prints "Result: 5", demonstrating how Maybe and pattern matching promote robust data handling.[29]
To compile and run these programs, use Spago, the standard build tool and package manager. Initialize a project with spago init, which sets up a src/Main.purs file. Build with spago build, generating JavaScript in the output/ directory. Execute via spago run, which compiles and invokes the main function in Node.js, or manually with node output/Main/index.js. This workflow integrates compilation to JavaScript and runtime execution seamlessly.[55]
Advanced Usage Patterns
Advanced usage in PureScript often involves integrating multiple language features to handle real-world scenarios, such as asynchronous operations, dynamic data structures, stateful computations, foreign interactions, and performance-critical algorithms. These patterns leverage the type system for safety while composing effects and abstractions effectively.[43][54][44]
One common pattern is asynchronous data fetching using the Aff monad for non-blocking effects, combined with error handling via Either. The purescript-affjax library facilitates HTTP requests within Aff, returning responses wrapped in Either to distinguish success from failure. For instance, a function to retrieve string data from a URL might look like this:
purescript
import Affjax.ResponseFormat as ResponseFormat
import Affjax (get)
import Affjax.Error (printError)
import Data.Either (Either)
import Effect.Aff (Aff)
getUrl :: String -> Aff String
getUrl url = do
result <- get ResponseFormat.string url
pure case result of
Left err -> "Error: " <> printError err
Right response -> response.body
import Affjax.ResponseFormat as ResponseFormat
import Affjax (get)
import Affjax.Error (printError)
import Data.Either (Either)
import Effect.Aff (Aff)
getUrl :: String -> Aff String
getUrl url = do
result <- get ResponseFormat.string url
pure case result of
Left err -> "Error: " <> printError err
Right response -> response.body
This approach ensures type-safe handling of potential network errors without blocking the main thread.[43][63]
Record polymorphism enables functions to operate on dynamic records using row types, allowing extra fields without explicit enumeration. A function like showPerson can process records with required first and last fields, polymorphic over additional row r:
purescript
showPerson :: forall r. { first :: String, last :: String | r } -> String
showPerson rec = rec.last <> ", " <> rec.first
showPerson :: forall r. { first :: String, last :: String | r } -> String
showPerson rec = rec.last <> ", " <> rec.first
This works for minimal records { first: "John", last: "Doe" } or extended ones like { first: "John", last: "Doe", age: 30 }, providing flexibility for varying data shapes while enforcing core fields.[34]
Monad transformers allow stacking effects, such as combining StateT for mutable state with Effect for side effects in a simple state machine. Using the purescript-transformers library, a counter example demonstrates incrementing state within an effectful context:
purescript
import Control.Monad.State.Trans (StateT(..), runStateT, get, put)
import Effect ([Effect](/page/Effect))
import Effect.Console ([log](/page/Log))
import Data.Tuple ([Tuple](/page/Tuple))
counter :: StateT Int [Effect](/page/Effect) Unit
counter = do
n <- get
put (n + 1)
liftEffect $ [log](/page/Log) $ "Count: " <> show (n + 1)
main :: [Effect](/page/Effect) (Tuple Int Unit)
main = runStateT counter 0
import Control.Monad.State.Trans (StateT(..), runStateT, get, put)
import Effect ([Effect](/page/Effect))
import Effect.Console ([log](/page/Log))
import Data.Tuple ([Tuple](/page/Tuple))
counter :: StateT Int [Effect](/page/Effect) Unit
counter = do
n <- get
put (n + 1)
liftEffect $ [log](/page/Log) $ "Count: " <> show (n + 1)
main :: [Effect](/page/Effect) (Tuple Int Unit)
main = runStateT counter 0
Here, liftEffect integrates [Effect](/page/Effect) actions like logging into the StateT computation, enabling a state machine that tracks and outputs increments safely.
Foreign function interface (FFI) provides type-safe access to JavaScript DOM APIs, bridging PureScript's purity with browser interactions. For example, to retrieve a DOM element by ID, declare an import and corresponding JavaScript export:
PureScript:
purescript
import Effect (Effect)
foreign import getElementById :: [String](/page/String) -> Effect [Element](/page/Element)
import Effect (Effect)
foreign import getElementById :: [String](/page/String) -> Effect [Element](/page/Element)
JavaScript (FFI file):
javascript
export function getElementById(id) {
return function() {
return document.getElementById(id);
};
}
export function getElementById(id) {
return function() {
return document.getElementById(id);
};
}
This ensures compile-time checks on the element retrieval, preventing runtime type errors in web applications. Similar patterns apply to other DOM methods, maintaining PureScript's type guarantees.[54][64]
To optimize recursive list processing and prevent stack overflows, PureScript employs tail-recursive functions with accumulators. A tail-recursive array length computation avoids deep recursion by accumulating the count:
purescript
import Data.Array (tail)
import Data.Maybe (fromMaybe)
lengthTailRec :: forall a. Array a -> Int
lengthTailRec arr = go arr 0
where
go :: Array a -> Int -> Int
go xs acc =
case tail xs of
Nothing -> acc
Just xs' -> go xs' (acc + 1)
import Data.Array (tail)
import Data.Maybe (fromMaybe)
lengthTailRec :: forall a. Array a -> Int
lengthTailRec arr = go arr 0
where
go :: Array a -> Int -> Int
go xs acc =
case tail xs of
Nothing -> acc
Just xs' -> go xs' (acc + 1)
This pattern extends to folds and maps on large lists, ensuring constant stack usage regardless of input size.[38]
Community and Ecosystem
Adoption and Use Cases
PureScript is primarily adopted for developing web frontends, leveraging its interoperability with JavaScript frameworks like React through libraries such as purescript-react, which enables seamless integration into existing ecosystems.[1] It also supports server-side applications via Node.js compilation or the Purerl backend for Erlang virtual machines, facilitating full-stack development with shared code between client and server.[65] These capabilities make it suitable for building reliable, type-safe applications in domains requiring complex logic, such as user interfaces and backend services.[6]
Notable projects demonstrate its production viability across industries. Pursuit, the official package documentation search engine for PureScript, is itself implemented in PureScript, showcasing its use for ecosystem tooling.[24] In finance, Juspay, a payments technology company, employs PureScript extensively for mobile app development and UI frameworks like Presto, reporting up to 10x faster development cycles for transactional applications due to its equation-like expressiveness and type safety.[66] Arista Networks, a networking firm, maintains over 100,000 lines of PureScript in production for frontend applications, contributing tools like the purescript-backend-optimizer to enhance JavaScript output performance.[65] Other examples include Lumi's use of PureScript with purescript-react-basic for frontend and a small amount of backend code in packaging software, and id3as's deployment of approximately 100,000 lines via Purerl for Erlang-based production systems over four years.[67][65]
PureScript's adoption remains niche within functional programming communities, though it trails mainstream languages in broader industry uptake.[68] The 2023 community survey indicated that while 56% of former users cited limited large-scale corporate adoption as a deterrent, active production use persists among specialized teams, bolstered by ongoing contributions like backend optimizations.[68][48]
Key benefits include reduced runtime errors through strong static typing and pure functions, leading to more maintainable codebases, as evidenced by Juspay's framework for complex payment flows.[66] However, drawbacks encompass a steep learning curve for developers unfamiliar with functional paradigms and a comparatively smaller ecosystem than JavaScript or TypeScript alternatives.[68] Case studies highlight incremental adoption strategies, such as integrating PureScript modules into existing JavaScript projects module-by-module to leverage type safety without full rewrites, as practiced by teams at CollegeVine for UI components spanning 127,000 lines.[65] Panoramic Software has similarly transitioned all new projects to PureScript, training developers to handle business logic while phasing out Haskell backends.[65]
Resources and Contributions
The official documentation for PureScript includes the "PureScript by Example" interactive book, which provides a structured introduction to the language through practical examples and exercises. The PureScript website offers tutorials covering core concepts, such as modules, types, and effects, aimed at beginners transitioning from other functional languages. Additionally, the Pursuit search engine serves as the primary API reference, indexing packages and documentation from the ecosystem to facilitate code discovery and type checking.
Community discussions occur on the official Discourse forum, where users share questions, announcements, and feedback on language features. The PureScript Discord server provides real-time chat for collaborative problem-solving and quick advice. The subreddit r/purescript hosts informal threads on best practices, library recommendations, and project showcases.
Contributions to PureScript are coordinated through its GitHub repository, which includes detailed guidelines for submitting pull requests, reporting issues via the tracker, and adhering to the code of conduct. Key focus areas encompass compiler optimizations, new FFI bindings for JavaScript libraries, and enhancements to the standard library, with contributors encouraged to tackle labeled issues for newcomers.
Learning paths begin with the online REPL at Try PureScript, allowing immediate experimentation without local setup. For development environments, guides detail integrating PureScript with VS Code using extensions for syntax highlighting, type checking, and debugging support.
In 2025, the PureScript Analyzer reached public alpha in September, introducing features such as auto-completion, hover information, jump to definition, and find all references for enhanced IDE integration.[26] Additionally, June 2025 updates introduced statically-linked binary releases to simplify installation across platforms.[8]
PureScript lacks dedicated annual conferences, but its developers and users participate in functional programming meetups and talks at events like LambdaConf and local Haskell/Purescript groups.