Preface

This repo is my way of using the Feynman Technique to learn Purescript and its ecosystem.

Feynman used simple language, storytelling, comprehensively wrote down everything he knew about the topic, then attempted to teach it such that a child could understand it. Without jargon but with brevity, he identified what he didn't know to the audience and had his content organized.

Intended Audience

The intended reader is one who has some background in programming, but no background in the Functional Programming paradigm. A reader should consult the summarized version of the Table of Contents below before determining what and how much to read.

If you want to understand why you should care about PureScript, read through the Why Learn PureScript page and Philosophical Foundations section, starting with Composition Everywhere.

If you want to learn PureScript, read the entire work from start to finish.

Overview and Scope of the Work

All code in this work uses PureScript 0.15.4

This work was created so a reader can understand PureScript and how to use it properly from a deep foundational understanding. Most other resources will get you started quickly, but then you will get confused at some point along the way. This resource takes longer to get started, but you will either not be confused or be less confused when we get to more advanced topics (e.g. monad transformers, type-level programming, etc.)

This work does not cover how to use PureScript to do web-development. In other words, things like the following:

  • how to use a PureScript single-page application (SPA) framework to build a frontend
  • how to use a web server framework to build a backend
  • how to do bundling and/or code-splitting effectively
  • how to use HTML and CSS correctly, etc.

None of the above things need to be known to learn PureScript, but one will need to learn the above things outside of this work before they can build a great application via PureScript.

How to Read This Work

This work is intended to be read in the following order:

  1. Getting Started
  2. FP Philosophical Foundations
  3. Building Tools
  4. Syntax
  5. Hello World

The "Design Patterns" section should be read alongside of the "Syntax" and "Hello World" folders.

Check the issue tracker for any unresolved issues via the bug label.

Summarized Table of Contents

There are currently 8 parts to this book. I summarize what is in each section below by showing the kinds of questions the section answers:

  • 00-Getting-Started:
    • Why learn/use PureScript?
    • How do I set up an editor (using Atom)?
    • How do I use the REPL?
    • What other things should I know before starting my learning journey?
  • 01-Philosophical Foundations:
    • What are some foundational ideas I need to understand before FP makes more sense?
    • What is the "big idea" behind using FP languages?
    • What are the drawbacks of using FP languages?
  • 02-Build-Tools:
    • Which tools do I use to compile and build my libraries/applications?
    • What are the workflows behind using those tools?
    • What other optional tools help me be more productive?
  • 11-Syntax:
    • How do I learn PureScript's syntax easily?
    • What other compiler features exist syntactically?
    • How do I read/write type-level programming?
    • How does do notation and ado notation work?
    • How does rebinding do notation and rebinding ado notation work?
  • 21-Hello-World:
    • How do I write a simple program?
    • How do I debug a program?
    • How do I write a complex program using modern FP architecture?
    • How do I test a program?
    • How do I benchmark a program or function within a program?
    • What are some examples of simple and complex real-world projects?
  • 31-Design Patterns:
    • What are commonly-used patterns or idioms to solve problems in FP languages?
    • What are other FP principles or concepts not explained in the "Hello World" part of this work?

Contributing

Feel free to open a new issue for:

  • Clarification on something you don't understand. If I don't know it yet and I'm interested, it'll force me to learn it
  • A link to something you'd like me to research more. If I'm interested or see the value, I'll look into it and try to document it or explain the idea in a clear way
  • Corrections for any mistakes or typos I've made
  • Improvements to anything I've written thus far

License

Unless stated otherwise in a specific folder or file, this project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International license: (Human-readable version), (Actual License)

Creative Commons Licence

Versioning Policy

The below versioning policy was created to abide by the following principles:

Principles

  • Indicate PS version:
    • The release should indicate which major PureScript version is being used for the library. This helps one know whether the work is still up-to-date.
  • Provide "stable" versions...:
    • Readers of a given version should be able to read and bookmark files without worrying about those files/links breaking due to changes in its name (via renaming/reordering files, headers in files, etc.)
    • Older versions should be available via git tag.
  • ...without restricting developer creativity:
    • I should be able to continue writing new content and re-ordering things without concern
  • Load the latest release:
    • This repo should show the latest release version of this project, not the one on which I'm working. In other words, the default branch should coincide with the last release.
  • Lessen maintenance as much as possible:
    • There should only be two branches, latestRelease and development since a branch name like master is overloaded with connotations. Those who want to read older versions can checkout a tag.
    • I currently will not hyperlink to other files within this project until either a 1.0.0 release is made or I find a way to automate that.

Release Syntax and Explanation

ps-[purescript's major release]-v[Major].[Minor].[Patch] where

  • purescript's major release means
    • Normally, this would be 1.x.x, but we don't yet have a 1.0 release yet. Thus, it is currently 0.13.x
    • x is a placeholder for the latest minor/patch release.
  • major change means
    • a file/folder name has changed, so that bookmarks or links to that file/folder are now broken
    • files/folders have been modified, so that one is recommended to re-read the modified parts
    • a dependency (e.g. PureScript, Spago, etc.) was updated to a breaking change release
  • minor change means
    • a file's contents have been modified/updated to such a degree that one is recommended to re-read the modified parts- Read through these links about learning:
    • a file's header name has changed, so that bookmarks or links to that header/section are now broken
    • Spago was updated to a minor release
  • patch means
    • additional files/folders have been added without breaking links
    • a file's contents have been modified/updated to a minor degree that one could re-read the modified parts but is not likely to benefit much from it.
    • a file's contents have been slightly updated (typos, markdown rendering issues, etc.)

Getting Started

This folder will cover the following topics:

  • Why learn PureScript
  • How to install Purescript
  • Getting familiar with the REPL
  • Other info you should know before working through the other folders in this project

Why Learn PureScript?

All languages make tradeoffs in various areas and on various spectrums:

  • learning curve
  • abstractions
  • syntax
  • errors
  • type systems
  • etc.

The question is "Which combination of tradeoffs provides the most benefits in prioritized areas?" "Good" languages happen to select specific tradeoffs that make the language well-suited for specific problems. For example, Python is well-suited for creating dirty one-time-run scripts to do tedious work on a computer. While Python can be used to create financial or medical applications that need to be extremely fast and secure, it would be better to use a different language that is better suited for such a task, such as Rust.

PureScript has chosen tradeoffs that its developers think are the best for creating simple to complex front-end applications that "just work;" that are easy to refactor, debug, and test; and help make developers more productive rather than less.

It can be said that other front-end languages buy "popularity" at the cost of "power and productivity." PureScript buys "power and productivity" at the cost of "popularity."

To fully answer "Why learn PureScript?" we must answer three other questions:

  • Why one should use Javascript to build programs...
  • ...but not write Javascript to build it...
  • ...and write Purescript instead of alternatives

Why one should use Javascript to build programs...

...but not write Javascript to build it...

Some other ideas that are relevant:

  • dynamic typing leads to errors that do not appear until after you have already shipped the code to your customers
  • a linter is just a basic static type checker
  • it is sometimes easier to write, read, and understand a 'safer language' that compiles to efficient Javascript than to write, read, and understand JavaScript itself (as the above articles show)

...and write Purescript instead of alternatives

TL;DR

Language Comparisons

For a full list of possible alternatives to JavaScript, see CoffeeScript's wiki's list of 'Languages that compile to JavaScript'

Note: the below comparisons are still a WIP. To fully support this claim, it would help to compare each languages' various "overall rating" on various aspects. Unfortunately, since I'm not familiar with every other language mentioned, it's very difficult for me to do that. If you are familiar with such languages, consider opening an issue on this repo and discussing it with me.

In short, the below comparison will be biased towards PureScript and will not yet fairly represent the corresponding side in some situations. Consider this a starting point for your own research.

PureScript vs TypeScript

One of the main issues with JavaScript is a poor type system. Many errors aren't discovered until a person, usually a customer, runs the program. Many of these same errors could be detected and fixed before shipping code if one used a language with a better type system.

TypeScript seems to address this type safety issue. Just consider its name! However, a few people who are using PureScript now have said this about TypeScript: "You might as well be writing Javascript." TypeScript does not provide any real guarantees; it only pretends. PureScript does provide such guarantees.

PureScript vs Elm

Since Elm is founded on the similar philosophical foundations as PureScript, one can use Elm and gain many of the same benefits as PureScript due to its type safety. However, there is a ceiling on the abstractions one can express in Elm. PureScript's ceiling is much higher than Elm's because it has type classes.

Elm

  • ... sacrifices the following features ...
    • type classes, which
      • reduce boilerplate code since the compiler can write code for you
      • enable one to define and uphold constraints about their program (e.g. this sequence of commands must be executed in the correct order)
  • ... to gain the following ...
    • clear actionable error messages because there are less ambiguous cases to deal with in the type system

Elm and PureScript can both be used to build a complex website. However, one will need to write more lines of code in Elm than they would in PureScript.

PureScript vs OCaml / Reason

This section has not yet been written.

PureScript vs GHCJS

Haskell, which heavily influenced PureScript, has an option for compiling Haskell to JavaScript via GHCJS. However, that comes with its own tradeoffs. PureScript was developed partly because those tradeoffs were too costly.

See PS or ghcjs for Frontend with Haskell backend for my summary of the main issues at play here.

The Strengths of PureScript

In this file, I'll cover what some of the tradeoffs PureScript makes are and why they are good. These ideas will be further explained in the "FP Philosophical Foundations" folder that appears later in this repository.

Strongly Adheres to the Functional Programming Paradigm

  • A Secret Weapon for Startups -- Functional Programming?
  • Paradigm shifts, such as the one demonstrated by this video using C++, are what enable programs with less problems: Logging a function's name each time it is called: migrating an "object-oriented paradigm" solution to an "functional paradigm" solution. As will be explained later, this is what is known as the "Writer Monad."
  • Object-oriented "design patterns" in FP languages are often just functions in disguise. Rather than learning the 20 different design patterns, one can learn how functions work and can be used to create really beautiful concepts and solutions.
  • Functional Architecture: The Pits of Success. To summarize this video, FP languages force you to structure your code in a way that makes it:
    • easy to test in an unbiased way (Can I prove that the logic/algorithm that solves the business problem is correct and works according to the specification despite any programmer's laziness or lack of foresight in thinking of a possible scenario where the code could fail?)
    • easy to add/change/remove a "backend" to account for trends, new insights, or faster code (Without introducing a new bug or deleting a current feature, can I switch from Company A's database to Company B's database without rewriting more than 30 lines of code?)
    • unconcerning to allow a new developer to work on the code, knowing that he/she cannot screw up anything major (Can the Lead/Senior Developer take the weekend off and return, knowing that it's extraordinarily difficult for developers with little experience to break something?)

Powerful Static Type System

  • This video explains how a type system with algebraic data types comes with a number of benefits (note: it uses a different syntax than PureScript: Domain Modeling Made Functional. To summarize it, algebraic data types
    • allow you to model a domain at a 1-to-1 ratio
    • make impossible states impossible
    • become your always-up-to-date UML diagrams
    • make it easy for new developers to learn how the code is structured
    • guide how business logic should be implemented
  • The PureScript compiler infers most of your types for you. For those who are curious and want to understand how that works, see this video: Type Inference From Scratch
  • The compiler (via its warning and error messages) is your friend, not your enemy. It
    • prevents you from releasing bug-filled code to a customer. (Can I guarantee that the code "just works" or cannot be built at all?)
    • forces you to handle most errors correctly the first time rather than permit you to throw them under the rug because you are lazy (Can I guarantee all possible errors will not create future problems that lead to short-term hard-to-understand code that rarely gets cleaned up and ultimately costs the company more time to fix than if it had just been written correctly the first time?)
    • helps you figure out how to implement functions correctly via "Typed Holes" (explained later in the Syntax folder)
  • This video explains how a type system with type classes allow one to re-use "dumb old data structures" (i.e. algebraic data types) rather than create many new data structures that differ only one slight way: Type Classes vs the World. To summarize it, type classes
    • allow you to write declarative code ("this is what will be true") rather than imperative code ("this is how to make truth true (hopefully, you got it right)")
    • enables the compiler to infer runtime code

Immutable Persistent Data Structures by Default

In PureScript, immutable data structures are the default rather than being "opt-in." In most other languages, mutable data structures are the default with immutable ones being "opt-in."

Immutable data structures are

  • easier to reason about because the value never changes
  • are always thread-safe, preventing many typical issues with concurrency
  • can be as performant as mutable data structures in most cases

Multiple Backends with Easy Foreign Function Interface

Most languages have their own backend.

  • Javascript is compiled and run via a Javascript engine.
  • Java compiles to bytecode that can be run on a Java Virtual Machine.
  • Python gets compiled into bytecode that is then interpreted.

PureScript does not have a backend. Rather, it's source code can be compiled to other languages. While JavaScript is the focus, PureScript compiles to other languages besides JavaScript. Thus, writing one library in PureScript can work in multiple languages, and one can choose the backend (or a combination of them) that best solves their problem.

Caveat: PureScript's support for non-Javascript backends is still a work-in-progress. In future releases, they will be getting first-class support.

This backend-independent nature of PureScript makes "Foreign Function Interface" very clean. At various times, Language X needs to use code from another language, Language Y. For code written in one language to use code written in another language, there needs to be a "Foreign Function Interface" or FFI.

Many languages' FFI can be difficult to work with. Language X made various language tradeoff decisions that are different than Language Y. Getting two languages to work together is difficult to say the least. However, PureScript's FFI is very easy because PureScript already compiles to that language.

If you are compiling PureScript to Javascript, you can still write JavaScript as FFI for PureScript. This makes it possible to wrap Javascript libraries on which you heavily depend. It also enables one to easily migrate from some other language or framework (e.g. TypeScript, Angular, etc.) to PureScript in a modular, piece-by-piece fashion

FAQs on PureScript

I'll answer a few possible questions the below audiences may have:

  • A developer who is already competent/productive in Javascript or a compile-to-Javascript language (e.g. Coffeescript, Typescript, etc.)
  • A developer who is only starting to learn web technologies, but has heard that Javascript is horrible and is investigating other compile-to-Javascript languages.
  • A business person who knows very little about programming or programming languages but who wants to know more about what options are available and what their pros/cons are.

These are the kinds of questions the above people might be asking:

  • Cost-Benefit Issue: Is the price of the steep learning curve worth the benefits of using PureScript in code?
  • Job Prospects Issue: If I learn PureScript, can I get a good developer job?
  • When-to-Learn Issue: Should I learn PureScript now or wait until sometime later?
  • Time-to-Productivity Issue: How long will it take me before I can write idiomatic code and be productive in PureScript?
  • Opportunity Cost Issue: If I choose to learn PureScript, will I later regret not having spent that same time learning a different compile-to-Javascript language (e.g. TypeScript, CoffeeScript, etc.) or a "compile to WebAssembly"-capable language (e.g. Rust) instead?
  • Ecosystem Issue: How mature is the Ecosystem? Will I need to initially spend time writing/improving/documenting libraries for this language or can I immediately use libraries that are stable and mature?
  • Foreign Function Interface Issue: How hard it is to use another language's libraries via bindings?
  • Program Tools Experience: How easy/pleasant is it to use the language's build tools (e.g. compiler, linter/type checker, dependency manager, etc.) and text editor tools (e.g. ease of setup, refactoring support, pop-up documentation, etc.)?
  • Community Friendliness Issue: How friendly, helpful, responsive, inspiring, determined, and collaborative are the people who use and contribute to this language and its ecosystem?
  • Code Migration Issue: What problems do developer teams typically encounter when migrating from Language X to PureScript and how hard are these to overcome?

Let's answer these one at a time. Each answer is my opinion and could be backed up with better arguments and explanations in some areas..

Is the price of the steep learning curve worth the benefits of using PureScript in code?

Yes. Most mainstream languages force you to depend on the IDE, linters, and other tools outside of the language to help you write correct code. Such languages often lack the features within the language itself to help you express certain ideas and constraints within your program. PureScript's powerful language features allow you to express what other languages cannot.

Even if one ultimately decides not to use PureScript, the language itself can be a helpful environment for training your mind to think more precisely about how to write code.

If I learn PureScript, can I get a good developer job?

Should I learn PureScript now or wait until sometime later?

You might want to learn it now for these reasons:

  • PureScript is actually quite mature and close to a 1.0. Many are already using it in production code.
  • PureScript is syntactically and conceptually very similar to Haskell. If you learn PureScript, you've basically learned Haskell, too. I believe that PureScript provides a better environment for learning Functional Paradigm concepts than Haskell since it's easier to install, build, and experiment with.
    • No need to use any language extensions (e.g. OverloadedStrings)
    • Better record syntax
    • More granular type class hierarchy
  • This project covers enough items that you should be able to learn PureScript relatively quickly. Still, while this project's code compiles and runs, its accuracy has not been verified by an "expert in the language" per say.

You might want to learn it later for these reasons:

  • PureScript's documentation could be improved in a number of ways:
    • Documentation for libraries are good in some areas and lacking in others.

How long will it take me before I can write idiomatic code and be productive in PureScript?

The average time for learning FP languages in general is usually 3-6 months due to the below reasons. This repository hopes to speed that process up, but, as always, people learn at different paces:

  • Many tutorials/guides assume their readers already know foundational principles. New learners who read them often do not know, nor are even aware of, those foundational principles.
    • This project's Hello World/FP Philosophical Foundations folder exists to counter this issue
  • No one really explains what the "big picture" that FP programming is all about. Thus, it's hard to see how some concept fits in the larger scheme of things, much less why that concept is so fundamental to everything.
    • See this project's Hello World/FP Philosophical Foundations/07-FP--The-Big-Picture.md file
  • People (wrongly) believe that they must know a very abstract mathematics called "Category Theory" in order to use/write PureScript or another FP language. Due to its very abstract nature, Category Theory can be difficult to grasp and scares people off.
    • This "myth" is false. Most FP developers do not understand Category Theory and yet they already have an intuition for some of its ideas.
  • The syntax for FP languages are paradigmatically different than the syntax with which most developers are familiar (C/Java/Python). It takes a while to get used to a "different" syntax family before it feels normal. Until it feels normal, reading through code examples will be harder.
    • This project's Syntax folder exists to counter the above issue.
  • Related to the above, FP languages often use symbol-based aliases to refer to functions that are well-known to FP Programmers instead of those functions' literal names (e.g. <$> instead of map, <$ instead of voidRight, $> instead of voidLeft). It's more concise and similarities between these symbol-based aliases add meaning to them, such as their "direction." Since new learners do not already know that to which function a symbol refers, it can be hard to know what that function even does.
  • Due to their powerful type systems, FP languages trade errors that occur when running the program (runtime errors) with errors that occur when attempting to build the program via the compiler (compile-time errors). To understand how to debug these compile-time "your program would not work if it was built" errors, one must have a strong understanding of how the compiler and its type system works.
    • This project's Syntax folder (and more specifically, the Syntax/Type-Level Programming Syntax folder) explain enough to help one understand why some (but not all) problems arise.
    • The Error Documentation sometimes explains what the error is and how to fix it (example) and other times is simply left unexplained (example).
    • The PureScript Discord server is active and often helps people troubleshoot the error messages.
  • Related to the above point, the powerful type system enables one to model some abstract ideas in a very precise way using well-defined types or things called type classes. When these features start to stack, a new learner can become overwhelmed.
    • If one reads this work in order, they are unlikely to be overwhelmed.
    • Most of the "cool type things" one can do are helpful but not always necessary. Consider the Haskell Pyramid. "Monads" are an important and fundamental FP concept, but new learners do not need to learn what they are or how to use them right away.
  • Many people try to re-explain something that another has already explained well and they write a poor re-explanation. It's hard to determine which explanations are accurate and correct and which are vague and mistaken until after you have already read it and/or know better.
    • I've been you. This work is my attempt to sift through the noise and present things in the best and simplest way possible. In various cases, I summarize and/or link to other posts that I believe to be credible that also explain a concept clearly. My sources include Haskell Weekly, the PureScript Discord server, a number of books I've read on FP programming, a number of papers I've read on FP programming, and various videos I've watched regarding FP programming.
  • There are few sites or locations that "centralize" a lot of high-quality FP guides/explanations. Thus, it's hard for new learners to find them.
    • This project exists partly because of this issue and hopes to resolve some of these problems.
    • For other "centralized" locations, see Hello World/ReadMe.md#other-learning-resources.
  • Many ideas are explained in papers that are not written for new learners but for academics. Understanding these papers' contents sometimes requires an understanding of high-level math, notation for specific concepts, etc., making the entry barrier higher
    • In various situations, I link to such papers and only in one situation do I walk a read through such a paper. In other words, this problem is still at large.

If I choose to learn PureScript, will I later regret not having spent that same time learning a different compile-to-Javascript language (e.g. TypeScript, CoffeeScript, etc.) or a "compile to WebAssembly"-capable language (e.g. Rust) instead?

You might regret it if you are not being honest or thoughtful about the purpose you are trying to achieve. Not being aware of your expectations, nor having realistic ones, will almost always end in having those expectatiosn broken, leaving you angry, disappointed, or frustrated.

Some facts:

  • WebAssembly holds promise, but it is still being developed.
  • Languages that are popular or backed by companies with many resources are not necessarily the best languages to use for your particular purposes
  • While PureScript offers more guarantees than most other languages, it unfortunately might not be the best language to use/learn if
    • you need mature libraries for a particular need that hasn't yet been written in PureScript. This is one benefit of TypeScript/Javascript.
    • you find that Elm's tradeoffs are "good enough" for your purposes.
  • A few people who are using PureScript now have said this about TypeScript: "You might as well be writing Javascript"

How mature is the Ecosystem? Will I need to initially spend time writing/improving/documenting libraries for this language or can I immediately use libraries that are stable and mature?

It's primarily good for front-end work and not so much (yet) for back-end work. When it is lacking, one will likely need to use FFI to utilize JS libraries. See awesome-purescript and the documentation site, Pursuit.

Also, attempting to port over Haskell libraries to this language are harder at times and have unexpected performance. Why? Because Haskell is a lazily-evaluated language, but PureScript is a strictly-evaluated language.

How hard it is to use another language's libraries via bindings?

Writing bindings is simple. See the Syntax/Foreign Function Interface folder for examples of how simple bindings are and things related to this.

However, using FFI via bindings can introduce runtime errors. Whenever one uses a library via FFI, you don't know whether a function will throw an exception or not. This can produce unexpected runtime errors even though you've written your code in a type-safe language. On stable mature well-tested libraries, this shouldn't be a big problem.

Lastly, writing bindings is tedious. PureScript uses algebraic data types (ADTs), but most libraries will define one function that can take multiple sets of arguments. For example, one might call the function, foo, with any of these sets of arguments:

  • foo("apple") (the first String argument, apple, is required)
  • foo("apple", ["banana", "orange"]) (the Array of Strings argument is optional)
  • foo("apple", ["banana", "orange"], true) (the boolean flag is optional, too)
  • foo("apple", {first:"banana", second:"orange"}, true) (the array can be passed in as a record/map/dictionary/object, too)
  • foo("apple", {first:"banana", second:"orange", optional: true}, true) (the second argument can have optional fields in addition to its required ones)

In reality, foo is at least 5 different functions that are all using the same name. Thus, writing bindings for foo is tedious to do in a language like PureScript due to PureScript's type-safe nature. However, there is a library for handling these kind of situations.

Others have also worked on writing code-generators that, for example, can look at the code of a library written in TypeScript and generate the corresponding PureScript bindings for that code. Such a tool is still a work-in-progress.

How easy/pleasant is it to use the language's build tools (e.g. compiler, linter/type checker, dependency manager, etc.) and text editor tools (e.g. ease of setup, refactoring support, pop-up documentation, etc.)?

The build tools are pretty good. One will typically use a spago-based workflow. These are explained in Build Tools/Tool Comparisons/Dependency Managers.md.

See the Build Tools/ folder for more up-to-date information. Likewise, see Editor and Tool Support for other editor-related configurations.

How friendly, helpful, responsive, inspiring, determined, and collaborative are the people who use and contribute to this language and its ecosystem?

People usually get help from some of the core contributors or other well-informed people via the #purescript and #purescript-beginners. No question is too stupid. For longer threads, some post on the PureScript Discorse Forum.

The language's development is currently slow because each core contributor have full-time jobs and contribute in their spare time, not because they don't want to.

What problems do developer teams typically encounter when migrating from Language X to PureScript and how hard are these to overcome?

Install Guide

Getting Additional Help

Throughout your learning process, it will be helpful to ask others for help. The two places this is often done is on PureScript's Discourse forum and its Discord server.

  • Register for an account on PureScript's Discord server
    • Note: I'll refer to this as PureScript's chatroom throughout this work.
  • Register for the Purescript ML forum here

Setting up Purescript for the First Time

Overview

We'll show how to install the following programs:

  • purescript - the PureScript language & compiler
  • spago - a dependency manager and build tool for PureScript
  • (optional) a formatter for PureScript
  • parcel - a build tool for bundling a PureScript application into a multiple JS backends (node, browser, electron)

Installation

Installing NPM

We can install everything using npm. However, getting npm is it's own problem. We can either install it manually by downloading node and installing that. Or we can use nvm (Node Version Manager) to install it for us and continue from there.

Manual Install

Justin Woo explains how to set up one's environment for the 0.12.x release but has not been updated for two things. First, the PureScript release at the time was 0.12.0 but now 0.13.8 is out. Second, the instructions use pulp and psc-package, a different build tool workflow than the one we'll use here.

If you just want to get things set up ASAP, follow the below summary of his article's instructions (using spago instead of the other tools). If you want to understand why you should do these commands, read his article here:

  1. Install Node 14 or greater: https://nodejs.org/en/download/
  2. Set your npm prefix: npm set prefix ~/.npm
    • Note: this prevents having to use sudo when using NPM to install things since it's default prefix is in a place that requires admin privileges
  3. Set your PATH: export PATH="$HOME/.npm/bin:$PATH"
NVM Install
  1. Install nvm using their installation instructions
  2. Verify that the installation was successful via command -v nvm
  3. Install node via nvm. To get the latest node version, use the command, nvm install node.

Unlike the manual install, nvm properly handles the npm prefix for you. So, you don't need to set it yourself.

Once you have installed npm, we can use it to install everything in one command:

npm i -g purescript@0.15.4 spago@0.20.9 esbuild@0.15.7

If you want to install a PureScript formatter, refer to their instructions. The history behind these tools will be covered in the Build Tools folder:

  • purs-tidy - A self-contained formatter written in PureScript
  • pose - A plugin written in PureScript for the Prettier formatter

If you want to produce optimized JavaScript for your production environment (rather than a developer environment), install purs-backend-es:

npm i -g purs-backend-es

Versions Used in this Project

The following commands should now work:

purs --version        # 0.15.4
spago --version       # 0.20.9
esbuild --version     # 0.15.7

Building This Project

Once the above has been verified, you can build this project.

First of all, if you haven't yet cloned this project locally, then do so now:

git clone https://github.com/JordanMartinez/purescript-jordans-reference

Then execute the script below which will install, build, and test every folder in this project:

source .ci/install-build-test-all.sh

Whenever I make a new release with breaking changes, this script will remove any outdated dependencies, reinstall the correct ones, and rebuild all of the folders' code.

Setting up your editor

The following are instructions for setting up the Atom editor. For Emacs, Vim, or Visual Studio, consult Justin Woo's post on the matter and the respective page in the documentation repo

Atom setup instructions:

  1. Install Atom: sudo apt-get install atom
  2. Launch Atom and install the following packages:
    • ide-purescript
    • atom-ide-ui
    • language-purescript
  3. Configure ide-purescript

The Atom package, ide-purescript, is configured to Bower, but we'll be using spago as our dependency manager for this project. Follow these instructions

  1. Open Atom's settings dialog (CTRL+,)
  2. Click on the Packages tab
  3. Search for ide-purescript
  4. Click on the Settings button in the entry that appears
  5. Check the Add spago sources checkbox
  6. Change the build command to: spago build -u --json-errors

Getting IDE support (autocomplete, documentation-on-hover, etc.) in Atom

While this repository's contents are useful for learning various lessons, IDE support (autocomplete, documentation, etc.) will only work if you open this repository's contents in a specific way when using Atom. Follow the instructions below:

  1. Click "File" and click "Open Folder..." (shortcut: CTRL+O)
  2. In the folder chooser, choose one of this repo's project folders (i.e. a folder with a spago.dhall file and src folder)
  3. Click on "Packages" and click on "PureScript" and then on "Build". The IDE server will start running and rebuild just that project.*
    • Autocomplete, importing, and documentation will now work.
  • This is a command you will use frequently, so consider adding a keyboard shortcut for it.
  1. Open the Atom settings dialog (CTRL+,)
  2. Click on the "KeyBindings" tab
  3. Click on the "your keymap file" hyperlink that appears before the bindings
  4. Follow the instructions for adding your personal shortcut for the ide-purescript:build command.

For mine, I did:

'.platform-linux atom-workspace atom-text-editor:not([mini])':
  'ctrl-shift-b': 'ide-purescript:build'

Dealing with IDE Server issues in Atom

Sometimes when editing a file, the IDE server will go out-of-sync. For example, you might change the definition of a type and the IDE doesn't realize that occured, so it will tell you that you have used a type incorrectly. In such cases, rebuild the project using Step 3 (or your keyboard shortcut) above and things should correct themselves from there.

In situations where I have used the same names for things, the autocomplete might actually import a function or type with the same name as the one you want but from a different module. So, if you have weird compiler errors, check the imports to insure the IDE server didn't accidentally import something incorrect or from the wrong location.

Setting up Module Linker

When you're browsing through code on GitHub, the browser extenstion, Module Linker, can greatly help: https://github.com/fiatjaf/module-linker

The REPL

REPL stands for Read, Evaluate, Print, Loop.

Starting the REPL

Use spago repl. The REPL should print something like the following:

$ spago repl
PSCi, version 0.13.8
Type :? for help

import Prelude

> |

Let's walk through each part:

  • PSCi means "PureScript Compiler interactive". It's similar to GHCi, the Haskell language's REPL.
  • version prints the PureScript version you are using.
  • :? indicates how to print a list of commands with their description. These are described below in this file.

After this, you may see zero or more import <ModuleName> lines. Spago will read the .purs-repl file to get this list and import the modules automatically. The .purs-repl file is covered at the end of this file.

Note: if you do not see import Prelude appear above, expressions like 5 + 5 will produce an error. To fix that, you should import the Prelude module by typing import Prelude followed by pressing Enter.

Using the REPL

In general, there are five things you can do in the REPL:

  1. See the result of an expression by typing it into the REPL (e.g. 3 + 3) and hitting Enter.
  2. Define a binding to some variable or function using the binding = <expression> syntax. For example...
    • x = 3
    • function = (\x -> x + 1)
  3. Input multi-line expressions using the :paste command (followed by CTRL+D)
  4. Use other commands to explore a module's functions, types, and kinds
  5. Use other commands to interact with the REPL's current state (e.g. clearing out bindings and/or imported modules, showing which modules have been imported, etc.)

Possible Outputted REPL Errors

Sometimes, the REPL will output errors. These errors may not be immediately understandable for new learners, so the table below will help you understand them and know what to do.

The ErrorIts MeaningWhat to do
"No type class instance was found for Data.Show.Show [Type]"An expression cannot be turned into a String. For example, a function's implementation ((\x -> x + 1)) cannot be turned into a String whereas a value (5) or expression (10 + 10) can be (5 and 20, respectively).If it's possible for you to define one, define an instance of the Show type class. If not, then ignore it and move on.
"Multiple value declarations exist for [binding]."You defined the binding twice, which you cannot doSee the Reload command section for what your options are
"Unknown operator (+)"The + function was not imported because the Prelude module was importedImport the Prelude module by typing import Prelude followed by pressing Enter.

A Quick Overview of Some of the REPL Commands

The REPL offers a few commands. You can see the entire list by typing either :help or :? and pressing Enter.

These commands are listed in the same order as what the :? outputs.

Note: the commands can be shortened to their first unique letters. So, rather than entering :type, one can enter :t. Likewise, rather than entering :paste or :print, one can enter :pa or :pr, respectively.

Help

Displays the REPL commands via :help/:?.

Quit

Exits the REPL, returning control to your shell.

Reload

The Problem

You can only define a binding once. Defining it again with a different expression will output an error

x = 5 -- first time
x = 6 -- second time raises error
-- REPL's outputs error: "Multiple value declarations exist for x."

You need to clear the x binding name to be able to reuse it for other bindings.

For example, let's say you wrote two functions and the second uses the first. However, you wrote the wrong implementation for the second and need to rewrite it:

add1 = (\x -> x + 1)
times2 = (\x -> x * 3) -- "3" should be "2"

The Solutions

Ideally, you could just clear the second function's binding and rewrite it. Unfortunately, you cannot do that. You can either:

  1. use the :reload command to clear out both functions' bindings, redefine the first one, and then define the second one with the correct implementation
  2. define a new binding for the correct implementation:
-- 1st option
add1 = (\x -> x + 1)
times2 = (\x -> x * 3) -- Whoops! "3" should be "2"
:reload
add1 = (\x -> x + 1) -- define the "add1" binding again
times2 = (\x -> x * 2) -- define "times2" again but with correct implmentation.

-- 2nd option
add1 = (\x -> x + 1)
times2 = (\x -> x * 3) -- Whoops! "3" should be "2"
times2_fix = (\x -> x * 2) -- define new function with correct implementation
  1. define your code in a file (as a module) and import that module into your REPL session. Any edits made to this file are picked-up upon a REPL reload.

Create a file containing your REPL script:

-- MyModule.file
module MyModule where

import Prelude

add1 = (\x -> x + 1)
times2 = (\x -> x * 3) -- This typo will be fixed later

Load script into the REPL:

> import MyModule
> times2 4
12

Make any edits to this file. For example, change to times2 = (\x -> x * 2). Save file, then reload in existing REPL session. The MyModule import will be remembered.

> :reload
> times2 4
8

Clear

Use :cl rather than :c to distinguish between this command and :complete. This works the same as :reload except that all imported modules are also removed. If you do this, you will need to reimport any modules you wish to use. For example, you will likely need to reimport Prelude (import Prelude), so that you can use number operations (i.e. +, -, /, *) and the == function again.

Browse

See all the functions, types, and type classes that a module exports and which you can use

Type

This displays the type signature of a value, a function or a type-class. One should be able to determine what the body of the function does based on the type signature, so that body is not shown:

> :type x + 1
Int
> :type (\x -> x + 1)
Int -> Int

Kind

Displays the kind of a type. Kinds will be explained more in the Syntax folder:

> :kind Int
Type
> :kind (Int -> Int)
Type
> :kind Array
Type -> Type

Show

There are two commands in this one:

  • show loaded/:s loaded - Shows all modules that the REPL session knows about. Some may or may not have been imported. (Before the REPL session starts, the PureScript compiler will compile all PureScript files based on the source globs given to it. All modules in those globs are then known to the REPL session, but you might not want to use them all in a given session.)
  • show import/:s import - Shows which modules you currently have imported into the REPL session

Print

Changes how a value is printed to the console after an expression is evaluated. By default, it uses PSCI.Support.eval.

New learners can ignore this command for now. Those who are familiar with the language can change it to a different one by calling :print Path.To.Module.functionName.

Regardless, to reset it to the default, one can call :print PSCI.Support.eval.

Paste

The REPL only accepts single-line Purescript code. If anything requires you to write multi-line expressions, you must use the :paste command.

The workflow goes something like this:

  1. Type in the paste command: :paste
  2. Do one of the following
    • input multi-line expressions (e.g. a type class and its function, a data type and its values, a function's type signature and its implementation, etc.).
    • paste some external code into the REPL
  3. Type CTRL+D/CMD+D to indicate that you are finished.

The REPL will then parse and all of the code, enabling you to use it from that point forward.

Complete

The REPL already supports tab-completion. So, this command isn't meant to be used by humans. Rather, it's for tools that need a way to get tab-completion. For context, see Harry's comment.

The .purs-repl File

If you ever want to automatically import a list of modules, modify the .purs-repl file. By default, it will only display the following content:

import Prelude

You can add more modules there so you don't have to type them in later:

import Prelude
import Data.Maybe
import Data.Either

Unfortunately, defining variables in the file will not automatically create them before the REPL starts. Let's say you update .purs-repl to the below content

import Prelude

x = 5

When you run spago repl, it will produce the following error:

$ spago repl
PSCi, version 0.13.8
Type :? for help

Unexpected or mismatched indentation at line 3, column 1

Other Important Info

  • Functional Programming Made Easier is a more recent work that literally walks you through every possible thought process, mistake, compiler error, and issue you would need to make to learn PureScript and build a web application in one book. I would recommend reading this book over the PureScript by Example book below.
  • Purescript By Example is the official up-to-date book that teaches Purescript.
  • PureScript Cookbook is an unofficial cookbook that shows "How to do X" in PureScript.
  • "Not Yet Awesome" PureScript is a list of things that are not yet awesome in PureScript
  • Configure Web Browser for Convenient Pursuit Lookup shows how to make it easy to search all documentation.
    • Consider using this same approach to setup a search using Starsuit, a Pursuit copy that works only packages in the latest package-set (a concept described more in the Build Tools folder of this repo). Use "https://spacchetti.github.io/starsuit/#search:" as your search.
  • PureScript Migration Guides when a breaking change is made.

Functional Programming Jargon

While reading through this repo, the Functional Programming Jargon might be a helpful reference whenever you come across a term that isn't covered in this project.

Writing Algorithms in an FP Language

This repo will not explain how to write algorithms in a performant way using an FP language. Consider reading Algorithm Design with Haskell which does teach algorithms using an FP language.

Differences From Haskell

If you're coming to PureScript with a Haskell background, be sure to consult the following resources:

Use GitHub Search to Find Things Search Engines (i.e. Google) Don't

At various times, you may try to use something like Google to find documentation / examples and little will appear in the search results.

A better solution is to use GitHub's search. To learn its syntax, read searching code.

Then, you use a search query like the following:

GoalSearch QueryMeaning
Find examples of projects that use a dependency (e.g. purescript-prelude)path:/ in:file purescript-preludeSearch a project's top-level files (e.g. bower.json/spago.dhall/psc-package.json) for the text purescript-prelude
Find real-world examples of code that uses libraries (e.g. aff-bus)language:purescript path:src/ in:file "Effect.Aff.Bus"Search a project's src directory for files whose content mentions the Effect.Aff.Bus module at some point. (Note: the full module name must be surrounded by quotes)
Find real-world examples of code that uses testing libraries (e.g. aff-bus)language:purescript path:test/ in:file "Test.QuickCheck"Search a project's test directory for files whose content mentions the Test.QuickCheck module at some point. (Note: the full module name must be surrounded by quotes)

Documentation

  • Anytime you need to look up the documentation for a package, you have two options:
    • use Pursuit.
      • Pros: One can navigate through a library's version and dependencies
      • Cons: Some of the deprecated packages mentioned above are still posted there. (e.g. purescript-dom-* packages, which are deprecated in favor of purescript-web-* packages)
    • use Starsuit
      • Pros: Only provides documentation for packages in the latest package set
      • Cons: One does not immediately know which version of a library is displayed, nor what its dependencies are.
  • Read Pursuit's Search Help page
  • Some libraries have not been updated to 0.13.8 and are still on the 0.11.7 release. Some still work; others won't. In this work, we will insure that you do not use any such libraries, but be aware of that if you browse the docs on your own.
  • Lastly, some libraries have not uploaded their latest versions' documentation. In these cases, we will forewarn you. Fortunately, spago docs will produce a local version of the source code's documentation that looks similar to Pursuit. It does not support all the features of Pursuit, but it's better than nothing. To do that, follow these commands:
    • spago docs --open will generate the documentation and then use your default web browser to open the file, generated-docs/html/index.html.

Undocumented Pursuit Tip

To get the latest version of the Pursuit docs of a package's function, package's type, or the package itself, simply remove the version in the url. Pursuit will load the latest version of that package: https://pursuit.purescript.org/packages/purescript-prelude/docs/Data.Eq

Composition Everywhere

TL;DR

Watch The Power of Composition


By "composition," we mean, "Assemble a few low-level reusable pieces into a higher-level piece." Here are some examples:

  • (Classic example) Legos. Using small blocks of plastic, people can create all sorts of interesting things.
  • Furniture. Using wood, metal, fabric, glass, and nails, people can create tables, chairs, desks, cabinets, etc.

Composition makes FP code easy to refactor because we can always reassemble the smaller pieces into something new or different.

But what kinds of things do we compose? In Functional Programming, we compose types (called algebraic data types) and functions.

Composing Types Algebraically

Algebraic Data Types (ADTs) use Algebra to define the total number of values a given type (i.e. named Set) can have.

There are two videos worth watching in this regard. The table and visualizations that follow merely summarize their points, except for the ideas behind the List and Tree types in the second video.

NameMath OperatorLogic OperatorPureScript TypeIdea
Product Typex * yANDTuple"One value from type x AND one value from type y"
Sum Typex + yOREither"One value from type x OR one value from type y"
Exponential Typey^x???InputType -> OutputType???

Composing Types

Composing Functions

Similar to types, functions also compose but in a slightly different way. Look over the below image and then watch the video at the end (if you haven't seen it already).

Composing Functions

Video link: Logging a function's name each time it is called: migrating an "object-oriented paradigm" solution to an "functional paradigm" solution

Pure vs Impure Functions

Visual Overview

Pure and Impure Functions

Functional Programming utilizes functions to create programs and focuses on separating pure functions from impure functions.

General Overview

Properties


The following table that shows a comparison of pure and impure functions is licensed under CC BY-SA 4.0:

Pure functions have 3 properties, but the third (marked with *) is expanded to show its full weight:

PurePure ExampleImpureImpure Example
Given an input, will it always return some output?Always
(Total Functions)
n + mSometimes
(Partial Functions)
4 / 0 == undefined
Given the same input, will it always return the same output?Always
(Deterministic Functions)
1 + 1 always equals 2Sometimes
(Non-Deterministic Functions)
random.nextInt()
*Does it interact with the real world?NeverSometimesfile.getText()
*Does it access or modify program stateNevernewList = oldList.removeElemAt(0)
Original list is copied but never modified
Sometimesx++
variable x is incremented by one.
*Does it throw exceptions?NeverSometimesfunction (e) { throw Exception("error") }

In many OO languages, pure and impure code are mixed everywhere, making it hard to understand what a function does without examining its body. In FP languages, pure and impure code are separated cleanly, making it easier to understand what the code does without looking at its implementation.

Programs written in an FP language usually have just one entry point via the main function. Main is an impure function that calls pure code.

Sometimes, FP programmers will still write impure code, but they will restrict the impure code to a small local scope to prevent any of its impurity from leaking. For example, sorting an array's contents by reusing the original array rather than copying its contents into a new array. Again, impure code is not being completely thrown out; rather, it is being clearly distinguished from pure code, so that one can understand the code faster and more easily.

Data Types

Principles

In order to abide by the principle of pure functions, FP Data Types tend to adhere to two principles:

  1. Immutable - the data does not change once created. To modify the data, one must create a copy of the original that includes the update.
  2. Persistent - Rather than creating the entire structure again when updating, an update should create a new 'version' of a data structure that includes the update

We'll use a linked-list (see below) to demonstrate the above two ideas.

flowchart RL
  3 ---> 2
  2 ---> 1
  1 ---> Nil

Each list is one of two values:

  • the end of the list (i.e. Nil)
  • a node that stores one element in the list and a pointer to the rest of the list (i.e. ...<--[3])

Mutability vs Immutability

Let's say we have a list that is assigned to the variable list1:

flowchart RL
  subgraph x["list1"]
    direction RL
    3 ---> 2
    2 ---> 1
    1 ---> Nil
  end

Let's say we want to change element 2 to 5. There are a few ways we could do this modification to list1.

The first way is to mutate list1 directly, so that list1 refers to new list. A data type that gets modified like this is a mutable data type.

Before our modification, this is what list1 looks like:

flowchart RL
  subgraph x["list1"]
    direction RL
    3 ---> 2
    2 ---> 1
    1 ---> Nil
  end

After our modification, this is what list1 looks like:

flowchart RL
  subgraph x["list1"]
    direction RL
    3 ---> 5
    5 ---> 1
    1 ---> Nil
  end

The second way is to create a new version of list1 called list1Again. A data type that gets modified like this is an immutable data type.

Before our modification, this is what list1 looks like:

flowchart RL
  subgraph x["list1"]
    direction RL
    3 ---> 2
    2 ---> 1
    1 ---> Nil
  end

After our modification, list1 is unchanged, but the "modified version" is now list1Again:

flowchart RL
  subgraph x["list1"]
    direction RL
    3 ---> 2
    2 ---> 1
    1 ---> Nil
  end

  subgraph y["list1Again"]
    direction RL
    3'["3"] ---> 5'
    5'["5"] ---> 1'
    1'["1"] ---> Nil'["Nil"]
  end

Pros & Cons

TopicMutable Data TypesImmutable Data Types
ReasoningCode is harder to reason about: list1 can refer to different values at different timesCode is easier to reason about: list1 always refers to the same value
Memory UsageUses less memory; low pressure on garbage collectorUses more memory; higher pressure on garbage collector
Multi-ThreadingRisk of deadlocks, race conditions, etc.No such risks

Immutable Data Types: Persistent or Not

There are two ways to modify an immutable data type.

First, one can choose NOT to share values across the copies. This is what was shown previously:

flowchart RL
  subgraph x["list1"]
    direction RL
    3 ---> 2
    2 ---> 1
    1 ---> Nil
  end

  subgraph y["list1Again"]
    direction RL
    3'["3"] ---> 5'
    5'["5"] ---> 1'
    1'["1"] ---> Nil'["Nil"]
  end

Second, one can choose to share values across the copies. This is an example of a persistent immutable data structure. Sharing is caring:

flowchart RL
  subgraph x["list1"]
    direction RL
    3 ---> 2
    2 ---> 1
    1 ---> Nil
  end

  subgraph y["list1Again"]
    direction RL
    3'["3"] ---> 5'
    5'["5"] ---> 1
  end

Big O Notation

FP data types have amortized costs. In other words, most of the time, using a function on a data structure will be quick, but every now and then that function will take longer. Amortized cost is the overall "average" cost of using some function.

These costs can be minimized by making data structures lazy or by writing impure code in a way that doesn't "leak" its impurity into the surrounding context.

Lazy vs Strict

A computation can either be lazy or strict. Before giving the below table, let's give a real-life example.

This is "Strict evaluation." Your parent tells you to immediately do some chore (e.g. wash dishes, etc.). You go and do so. Sometimes, you learn that this was necessary. Other times, you learn that the dishes were already washed by someone else. Despite telling your parent that they don't need to be washed, your parent insists and overrules you. This especially annoys you on days where "washing the dishes" will take a long time.

This is "Lazy evaluation." Your parent tells you to remember to do some chore but not to start until they tell you. On some days, they never tell you to start because the task wasn't needed after all. You love those days. On other days, they tell you to start in the morning, the afternoon, or the evening.

TermDefinitionProsCons
Strictcomputes its results immediatelyExpensive computations can be run at the most optimum timeWastes CPU cycles and memory for storing/evaluating expensive computations that are unneeded/unused
Lazydefers computation until its neededSaves CPU cycles and memory: unneeded/unused computations are never computedWhen computations will occur every time, this adds unneeded overhead

To make something lazy, we turn it into a function. This function takes one argument (Unit) and returns the value we desire. This is called a thunk: a computation that we know how to do but have not executed yet. To run the code stored in the thunk, we use the phrase forcing the thunk.

-- Given an Int, I can return another Int
strictlyCompute :: Int -> Int
strictlyCompute x = x + 4

-- otherwise known as 'thunking'
-- Given an Int, I can return a 'thunk.' When
-- this thunk is evaluated, it will return an Int.
lazilyCompute :: Int -> (Unit -> Int)
lazilyCompute x = (\unitValue__neverUsed -> x + 4)

forceThunk :: (Unit -> Int) -> Int
forceThunk thunk = thunk unit

-- somewhere in our code
thunk = lazilyCompute 5

-- somewhere else in our code, when we finally need it
result = forceThunk thunk

Other Resources

  • This resource is not necessary for you to read it to understand and use PureScript. However, it might satisfy those who are curious. It uses the Lisp language in its examples, so the code might be difficult to understand. Regardless, the book Structure and Interpretation of Computer Programs (SICP) (see this or that) has a chapter on lazy evaluation and thunks.

Looping via Recursion

In most OO languages, one writes loops using while and for. Looping in that matter makes it very easy to introduce impure code. So, in FP languages, one writes loops using recursion, pattern-matching, and tail-call optimization. The rest of this file will compare OO code to its FP counterpart

For i until condition do computation and then increment i

// factorial
var count = 5;
var result = 1;
for (var i = 2; i < count; i++) {
    result = result * i
}
-- This is a stack-unsafe function (explained and improved next)
factorial :: Int -> Int
factorial 1 = 1                       -- base case
factorial x = x * (factorial (x - 1)) -- recursive case

factorial 3
-- reduces via a graph reduction...
3 * (factorial (3 - 1))
3 * (factorial 2)
3 * 2 * (factorial (2 - 1))
3 * 2 * (factorial 1)
3 * 2 * 1
6 * 1
6

Stack-Safe

The above Purescript example illustrates a problem that comes with writing loops this way: stack overflows. Thus, when one says "this function is stack-safe", they mean that calling the function will not risk the possibility of a stack overflow runtime error being produced. One usually prevents this risk via tail-call optimization (which usually converts the recursive loop back into an OO loop) or trampolining (when tail-call optimization isn't possible)

Thus, one will usually write recursive functions in this manner. Rather than using recursion to calculate the value by creating a 'stack' of * operations (as done above), one will pass into the function an additional argument that acts as the accumulated value. The necessary state change / calculation is done and its result is passed in as the new accumulated value in the next iteration of the recursive function call:

factorial :: Int -> Int
factorial n = factorial' n 1

factorial' :: StartingInt -> AccumulatedInt -> AccumulatedInt
factorial' 1 finalResult = finalResult
factorial' amountRemaining accumulatedSoFar =                             {-
  -- This is the general idea being done in the single line of code
  -- after this comment
  let
    oneLess = amountRemaining - 1
    nextAccumulatedValue = accumulatedSoFar * amountRemaining
  in
    factorial' oneLess nextAccumulatedValue                               -}
  factorial' (amountRemaining - 1) (amountRemaining * accumulatedSoFar)

factorial 4
-- reduces via a graph reduction...
factorial' 4 1
factorial' 3 4
factorial' 2 12
factorial' 1 24
24

In some cases, one will need to write more complex code to get the desired performance using a combination of defunctionalization and continuation-passing style (CPS). This is covered in more detail in the Design Patterns/Defunctionalization.md file.

For ... Break If

// findFirst
var findFirst = (array, condition) => {
  var length = list.length();
  for (var i = 0; i < length; i++) {
      var value = list[i]
      if (condition(value)) {
        return value;
      }
  }
  return null;
}
findFirst([0, 1, 2], (i) => i == 1);
-- linked list
data List a
  = Nil             -- end of the list
  | Cons a (List a) -- head of a linked list & rest of list

data Maybe a
  = Nothing   -- could not find a value of type A
  | Just a    -- found a value of type A

findFirst :: forall a. List a -> (a -> Boolean) -> Maybe a
findFirst list condition = findFirst' list condition Nothing

findFirst' :: forall a. List a -> (a -> Boolean) -> Maybe a -> Maybe a
findFirst' Nil condition notFound = notFound
findFirst' (Cons head tail) condition theA@(Just alreadyFound) =
  findFirst' tail condition theA
findFirst' (Cons head tail) condition Nothing =
  let foundOrNot = if (condition head) then (Just head) else Nothing
  in findFirst' tail condition foundOrNot

findFirst (Cons 0 (Cons 1 (Cons 2 Nil))) (\el -> el == 1)
-- reduces via a graph reduction...
findFirst' (Cons 0 (Cons 1 (Cons 2 Nil))) (\el -> el == 1) Nothing
findFirst'         (Cons 1 (Cons 2 Nil))  (\el -> el == 1) Nothing
findFirst'                 (Cons 2 Nil)   (\el -> el == 1) (Just 1)
findFirst'                         Nil    (\el -> el == 1) (Just 1)
Just 1

Short-Circuiting

The above Purescript example illustrates another problem with writing loops this way: short-circuiting. There are times when we wish to break out of a recursion-based loop early, such as when we have found the first element of a collection. In the above example, the function does not short-circuit, so it continues to iterate through the list even after it has found the element, leading to wasted CPU time and work.

To make the function above short-circuit, we would rewrite the function to this:

-- linked list
data List a
  = Nil             -- end of the list
  | Cons a (List a) -- head of a linked list & rest of list

data Maybe a
  = Nothing   -- could not find a value of type A
  | Just a    -- found a value of type A

findFirst :: forall a. List a -> (a -> Boolean) -> Maybe a
findFirst Nil condition = Nothing
findFirst (Cons head tail) condition =
  if (condition head)
  then Just head
  else findFirst' tail condition

findFirst (Cons 0 (Cons 1 (Cons 2 Nil))) (\el -> el == 1)
-- reduces via a graph reduction...
findFirst         (Cons 1 (Cons 2 Nil))  (\el -> el == 1)
Just 1

Other Loops

The following Purescript examples are very crude ways of mimicking the following loops. More appropriate examples would require explaining and using type classes like Foldable and Monad (intermediate FP concepts). Thus, take these examples with a grain of salt.

While

while (condition == true) {
  if (shouldStop()) {
    condition = false
  } else {
    doSomething();
  }
}
data Unit = Unit

whileLoop :: Boolean -> (Unit -> Boolean) -> (Unit -> Unit) -> Unit
whileLoop false _ _ = -- body
whileLoop true shouldStop doSomething =
  -- `doSomething unit` is called in here somewhere
  -- at the end of the function's body, it will call
  whileLoop (shouldStop unit) shouldStop doSomething

For value in collection

// length
var count = 0;
for (value in list) {
  count += 1;
}
data List a
  = Nil
  | Cons a (List a)

length :: forall a. List a -> Int -> Int
length Nil totalCount = totalCount
length (Cons head tail) currentCount =
  length tail (currentCount  + 1)

Type Classes

What Problem Do Type Classes Solve?

Their primary use is to make writing some code more convenient / less boilerplatey. Rather than writing the same code 25 different times where it differs in only one way each time, we can write code once and "parameterize it" in 25+ different ways.

To see a bottom-up explanation of this idea, read through the bullet points below and then watch the video.

  • This video is a recording of a presentation given by Nathan Faubion, a core contributor to PureScript.
  • This video finishes explaining what type classes are around 22:54.
  • The parts that follow are more advanced concepts. They explain how to make "real world code" easily testable via type classes and interpreters. You might not understand those explanations until you are more familiar with PureScript syntax.
  • The presentation ends at 1:03:58. Nate starts answering people's questions after that.
  • Nate's answers to various questions ends at 1:13:12 and the rest of the video are people talking about various PureScript things.
  • While Nate explains that type classe enable "code reuse," one could use an approach called "scrap your type classes" (SYTC) to accomplish that goal. SYTC will be covered later in this file.

Video: Code Reuse in PureScript: Functions, Type Classes, and Interpreters (actual video title on YouTube: "PS Unscripted - Code Reuse in PS: Fns, Classes, and Interpreters")

Where Do Type Classes Come From?

Type classes are usually "encodings" of various concepts from mathematics.

Type classes make developers productive. They enable programmers... - to write 1 line of code that is the equivalent of writing 100s of lines of code. - to define complicated control flows that highlight the important parts and minimize the irrelevant boilerplatey parts (e.g. nested "if then else" statements) - to use (in general) 5 "dumb reusable data types" to solve most of our problems: - Maybe - a box that is either empty or has a value. - Either - a sum type: either has a Left value or a Right value - Tuple - a product type: has both an A value and a B value - List - self-explanatory - Tree - self-explanatory

Type Classes as Encodings of Mathematical Concepts

Type classes often encode ideas that are true regardless of what we call them (i.e. "necessary" concepts), but functional programmers will refer to them via jargon (i.e "arbitrary" names like Functor). (For more context on the usage of "necessary" and "arbitrary" as terms, see Arbitrary and Necessary Part 1: a Way of Viewing the Mathematics Curriculum).

Putting it differently, if Some type can implement some function(s)/value(s) with a specified type signature in such a way that the implementation adheres to specific laws, one can say it has an instance of the given type class. Some types cannot satisfy a given type class' conditions; others can satisfy them in only one way; and still others can satisfy them in multiple ways. Thus, one does not say "Type X is an instance of <some type class>." Rather, one says "Type X has an instance of <some type class>." To see this concept in a clearer way and using pictures, see https://www.youtube.com/watch?v=iJ7V1KXJpsE

Thus, type classes abstract general concepts into an "interface" that can be implemented by various data types. They are usually an encapsulation of 2-3 things:

  1. (Always) The definition of type signatures for one or more functions/values.
    • Functions may be put into infix notation using symbolic aliases (e.g. <$>) to make it easier to write them.
  2. (Almost Always) The laws by which implementations of a type class must abide.
    • These laws guarantee certain properties, increasing developers' confidence that their code works as expected.
    • They also help one to know how to refactor code. Given left-hand-side == right-hand-side, evaluating code on the left may be more expensive (memory, time, IO, etc.) than the code on the right.
    • Laws cannot be enforced by the compiler. For example, one could define a type class' instance for some type which satisfies the type signature. However, the implementation of that instance might not satisfy the type class' law(s). The compiler can verify that the type signature is correct, but not the implementation. Thus, one will need to insure an instance's lawfulness via tests, (usually by using a testing library called quickcheck-laws, which is covered later in this repo)
  3. (Frequently) The additional functions/values that can be derived once one implements the type class.
    • Most of the power/flexibility of type classes come from the combination of the main functions/values implemented in a type class' definition and these derived functions. When a type class extends another, the type class' power increases, its flexibility decreases, and its costs increase.

Examples

Here are some examples that demonstrate the combination of the 2-3 elements from above:

  • The Eq type class specifies a type signature for a function called eq/== and notEq//=, and laws for the two (e.g. if a == b and b == c, then a == c), but there are not any derived functions.
  • The Ord type class is similar to Eq, but it does have derived functions.
  • The Functor type class (explained in more detail later) has all three.

Similarities and Dual Relationships Among Type Classes

Some type classes have a corresponding "dual." While there are better ways to explain duals, the basic idea is that the "direction" of the function's arrow gets flipped. When this happens, we usually prefix them with "Co". For example, if we have a type class called Monad, the dual of it is called Comonad. If Monad has laws A and B, then it's likely that Comonad will have laws A' and B', which are "flipped" version of A and B.

For example, a function like toB would have its arrow flipped to produce toA::

-- original
toB :: a -> b
toB = -- function's implementation
                                                                              {-
-- 1. Drop the implementation
toB :: a <- b
toB =

-- 2. Flip the arrow
toB :: a <- b
toB =

-- 3. Reorder the arguments so that arrow is pointing to the right:
toB :: b -> a
toB =

-- 4. Rename the function
toA :: b -> a
toA =                                                                         -}

-- Dual version
toA :: b -> a
toA = -- function's implementation

Equational Reasoning

Functions in FP languages often work like equations: the left-hand side can be replaced by the right-hand side. We'll cover this idea more in the graph reduction section. This idea enables a developer to solve a problem using a simple but not performant solution that can be easily refactored to a much more performant version of the solution. We'll cover this in the "Optimizing Functions" section.

Graph Reduction: Running a Function

In source code, we can describe the various parts of a function based on which side of the = character the content appears:

  • Left-Hand Side (LHS): the function name and all of its arguments
  • Right-Hand Side (RHS): the body or implementation of the function
|         LHS         |    |     RHS     |
functionName int1 int2   =   int1 + int2

When using pure functions, one can replace the LHS with the RHS, and the program will still work the same. This concept is known as referential transparency:

functionName 4 3
-- replace LHS with RHS
4 + 3
-- reduce into final form
7
-- Calling `function 4 3` could be removed and replaced
-- with `7` and the program would work the same

-- Similarly, the below function (a longer form syntactically) and its arguments
-- could be replaced with `6` and the program would work fine.
(\arg1 arg2 arg3 -> arg1 + arg2 + arg3) 1 2 3
-- replace LHS with RHS
(\     arg2 arg3 ->    1 + arg2 + arg3)   2 3
(\          arg3 ->    1 +    2 + arg3)     3
(\               ->    1 +    2 +    3)
                       1 +    2 +    3
                       1 +    5
                       6

Although the above examples are very simple functions, imagine if one's entire program was one function that exhibited this behavior. If so, it would be very easy to understand and reason one's way through such a program.

Optimizing Functions: From Simplicity to Performant

This section summarizes the main ideas explained in Algorithm Design with Haskell (Cambridge, Amazon).

Above, we showed that functions are "run" by using graph reduction: the left-hand side is replaced with the right-hand side. However, this idea also applies when we refactor code, enabling the following developer workflow:

  1. Solve a programming problem by composing multiple high-level functions together. Initially, this version of the solution will not be performant.
  2. "Decompose" the high-level functions by replacing their call site (i.e. left-hand side) with their implementations (i.e. right-hand side)
  3. Use laws to refactor how those implementations compose to reduce unneeded work.

Typically, the 'laws' above are from type classes. When we see that a function "decomposes" to map function1 (map function 2), which iterates through some collection twice, we can rewrite it to map (\arg -> function1 (function2 arg)), which iterates through some collection once but still produces the same output.

Following this workflow makes it easier to solve all programming problems. In particular, this workflow helps when writing a greedy algorithm or a dynamic programming algorithm.

Let's provide two other examples of this idea. To keep things simple for those who don't understand PureScript's syntax, we'll not use laws to guide refactoring.

Example 1

As a very simple example, consider the following programming problem:

Given an array of integers, arr, that will have 0 - 20 elements, define a function, countTwoFourSum, that calculates how often a 2 or 4 appears in the array (i.e. its count) and sums the resulting counts together. For example

countTwoFourSum([1, 2, 3, 4]) == 2 countTwoFourSum([1, 3, 5, 7]) == 0 countTwoFourSum([2, 2, 3, 6]) == 2 countTwoFourSum([2, 2, 3, 4]) == 3

Using Step 1 above, the simplest solution would be to write something like this:

var countTwoFourSum = function(arr) {
  return count(arr, 2) + count(arr, 4);
};
var count = function (arr, value) {
  var accumulatedValue = 0;
  for (var i = 0; i < arr.length; i++) {
    var nextElem = arr[i];
    if (nextElem == value) {
      accumulatedValue = accumulatedValue + 1;
    }
  }
  return accumulatedValue;
};

Translating that to PureScript, we would write:

countTwoFourSum :: Array Int -> Int
countTwoFourSum arr =
  (count arr 2) + (count arr 4)

count :: Array Int -> Int -> Int
count arr value = foldl countIfValue initialAccumulatedValue arr
  where
  initialAccumulatedValue = 0

  countIfValue accumulatedValue nextElem =
    if nextElem == value then accumulatedValue + 1 else accumulatedValue

While it is easy to think of the solution to this code by writing it in this way, it's not performant because we loop through the array twice.

In step 2, we can replace the original function, count, with its implementation.

countTwoFourSum :: Array Int -> Int
countTwoFourSum arr =
  (foldl countIfTwo initialAccumulatedValue arr) +
  (foldl countIfFour initialAccumulatedValue arr)
  where
  initialAccumulatedValue = 0

  countIfTwo accumulatedValue nextElem =
    if nextElem == 2 then accumulatedValue + 1 else accumulatedValue

  countIfFour accumulatedValue nextElem =
    if nextElem == 4 then accumulatedValue + 1 else accumulatedValue

In step 3, we refactor the resulting computation to be more performant. Below, we iterate through the array once rather than twice by using only one foldl.

countTwoFourSum :: Array Int -> Int
countTwoFourSum arr =
  let finalCount = foldl countIfValues initialAccumulatedValue arr
  in finalCount.twoCount + finalCount.fourCount
  where
  initialAccumulatedValue = {twoCount: 0, fourCount: 0}

  countIfValues {twoCount, fourCount} nextElem =
    { twoCount: if nextElem == 2 then twoCount + 1 else twoCount
    , fourCount: if nextElem == 4 then fourCount + 1 else fourCount
    }

Example 2

The above example illustrates this workflow but isn't the most impressive example. Here's a slightly more complex example. Let's say a programmer is reading through a description of a problem and its desired output. Piece by piece, she types out the below code as a solution to each part of the problem:

map fromString stringArray -- 1. convert each string element into
                           --       an integer (if possible)
 # catMaybes               -- 2. remove elements where the string wasn't an integer
 # map (_ + 1)             -- 3. add one to each integer
 # sum                     -- 4. sum all the resulting integers into a value

While the above code solves the problem, it is not performant. It iterates through an array multiple times and creates multiple intermediate arrays. By using equational reasoning (not shown below), we can speed this up to a single iteration:

foldl f init stringArray
where
  init = 0
  f acc next =
    case fromString next of
      Nothing -> acc
      Just i -> acc + i + 1

Crucially, our first focus was on writing a correct solution and then on making it performant.

FP - The Big Picture

Here's a sneak peek as to what the design process for writing FP programs looks like. Note: I assume you're already familiar with pure and impure functions. If not, see FP Philosophical Foundations/Pure-vs-Impure-Functions.md:

FP--Big Picture

Build Tools

This folder accomplishes the following:

  • Explain the various tools used throughout the ecosystem and their usages/differences:
    • Document the differences between Bower and Spago dependency managers
    • Document the difference between Pulp and Spago build tools
  • Document the CLI options for the most popular tools (e.g. purs, pulp, spago, etc.)
  • Document a typical workflow from project start to finish (creation, fast-feedback development, initial publishing, 'bump' publishing)

History: How We Got Here

The following explanation does not cover all the tools used in PureScript's ecosystem. However it provides context for later files. In short, spago is both the official dependency manager and build tool. bower can be thought of as a deprecated dependency manager; the community is in the process of building a registry that will replace the Bower registry since it no longer accepts uploads. pulp is a build tool that uses bower; its usage will become more common again once the registry is built.

Phase 1: Initial Tooling

PureScript's compiler was originally called psc (PureScript Compiler) before later being renamed to purs. (We'll see this psc name reappear elsewhere in a related project).

PureScript did not use npm as a dependency manager because of an issue related to transitive dependencies. Thus, they used bower because it fit their goals/requirements better. (All of this is covered more in the Dependency Managers/Bower Explained.md file).

Bodil Stokke (with later contributions from Harry Garrood) later wrote a tool called pulp that worked with bower and purs to provide a user-friendly developer workflow:

  • download your dependencies
  • compile, build, and test your project
  • publish libraries and their docs
  • easily bump the project's version

Phase 2: The psc-package Experiment

Bower worked fine, but there were a few user-interface issues that made it difficult to use, especially when a new PureScript release was made that included breaking changes.

As a result, psc-package was developed as an experimental dependency manager. It solved some of the problems that bower faced. pulp later supported psc-package, so that one could benefit from the simple developer workflow.

However, psc-package encountered its own problems, too. People could not easily create and modify something called a "local package set" (a term that is explained later in the Spago-Explained.md file).

To resolve these problems, Justin Woo started a project called spacchetti (he likes to name his projects via food puns), which made it much easier to create and modify a "local package set."

See the below image to visualize this:

Build Tool Relationships - No Spago "Build Tool Relationships"

Phase 3: Improving the psc-package Developer Workflow via Spago

From the above image, one should infer that using pulp and bower was overall easier to use and explain. Thus, Justin Woo and Fabrizo Ferrai started a project called spago. spago evolved out of spacchetti and reimplemented parts of psc-package into one program with a seamless developer workflow. While psc-package can still be used, it's better to use spago.

The below image summarizes the current state:

Build Tool Relationships "Build Tool Relationships"

Phase 4: Spago becomes mainstream while psc-package is less used

Spago dropped support for psc-package commands in the v0.11.0 release. psc-package is still usable and is more or less feature-complete. However, no further work on it will be done. Rather, Spago has become the main dependency manager when utilizing package-sets.

The community is now split between pulp + bower workflows and spago workflows. One must still use pulp + bower if they want to do the following:

  • publish their library's docs to Pursuit
  • include their library in a package set, so spago users can use it

Phase 5: The need for a PureScript registry (Bower registry no longer accepts new uploads)

The Bower registry stopped accepting new uploads. The community quickly updated their tooling to workaround how libraries are published and installed. However, it was clear that PureScript now needed to create a registry.

Fabrizio Ferrai led the effort to build this registry with significant input from Harry Garrood. The registry is not yet complete, so the community is in this in-between stage.

Regardless, the following is still true:

See The bower registry is no longer accepting package submissions for more context.

Phase 6: Updating JavaScript output to ES modules and delegating bundling to 3rd-party tools

In PureScript 0.15.0, we stopped compiling PureScript source code to CommonJS modules and started compiling to ES modules. As a result, we dropped the buggy and broken bundler provided via purs bundle and instead directed endusers to use 3rd-party bundlers like esbuild, webpack, and parecel. Such bundlers often produced smaller bundles than purs bundle. Moreover, it gave the core team in charge of PureScript one less thing to maintain.

See the 0.15.0 Migration Guide for more details.

Phase 7: Producing Optimized JavaScript and Compiling to Other Languages

While the Purescript compiler produces correct JavaScript code, there were a number of optimizations that haven't been implemented in the compiler. However, the compiler was designed to output not just JavaScript, but an intermediate representation called CoreFn. This enables others to transform CoreFn to another language.

Soon after the time that PureScript 0.15.4 was released, a new project called purs-backend-es was released. This project works on the CoreFn representation and transforms it to JavaScript. However, it also optimizes the code significantly during this tranformation. For a few example, see the purs and purs-backend-es comparison table in its README.

While this tool's main purpose is to produce optimized JavaScript code, it enables others to produce new backends. A backend is a target language to which PureScript can be compiled. Before this tool, every backend had to reinvent a lot of code to make it work for that language. With the underlying library, purescript-backend-optimizer, one can more easily produce a new backend.

Overview of Tools

NameType/UsageCommentsURL
pursPureScript CompilerUsed to be called psc--
spagoBuild ToolFront-end to purs and package-set-based projectshttps://github.com/purescript/spago
pulpBuild ToolFront-end to purs. Builds & publishes projectshttps://github.com/purescript-contrib/pulp
bowerDependency Manager (being deprecated)--https://bower.io/
purs-tidyPureScript Formatter--https://github.com/natefaubion/purescript-tidy
purs-backend-esProduces optimized JavaScript from PureScriptOnly intended for production-level usagehttps://github.com/aristanetworks/purescript-backend-optimizer
psaPretty, flexible error/warning frontend for purs--https://github.com/natefaubion/purescript-psa
pscidpulp --watch build on steroidsSeems to be a more recent version of psc-pane (see below) and uses psahttps://github.com/kRITZCREEK/pscid
psvm-jsPureScript Version Manager--https://github.com/ThomasCrevoisier/psvm-js
esbuildLow-Config bundler--https://esbuild.github.io/

The following seem to be deprecated or no longer used:

NameType/UsageCommentsURL
psc-packageDependency Manager--https://github.com/purescript/psc-package
psc-paneSimplistic auto-reloading REPL-based IDENo longer used? Last updated 1 year ago...https://github.com/anttih/psc-pane
gulp-purescriptGulp-based Build ToolNo longer used? Last updated 1 year ago...https://github.com/purescript-contrib/gulp-purescript
Purify--Deprecated in light of psc-package--

For this repo, we will be using spago for our build tool and dependency manager.

Dependency Managers

There are two solutions to dependency management where each has a different 'target audience' per say:

  1. Bower (library developers)
  2. Spago (application developers)

The community needs both solutions for reasons that will be explained later.

Each one will be further explained in its own file. However, one can refer to each with a "crude name" that summarizes them:

  • Typical package manager (Bower)
  • Glorified git clone tool (Spago)

Dependency Managers Compared

This side-by-side comparison should be thought of as an "apples to oranges" comparison.

BowerSpago
Ideal User
  • Library Developers
  • People exploring the language/ecosystem
  • Application Developers
  • People learning the language via a CI-proven learning material
Official/UnofficialOfficial
(parts of the PureScript compiler depend on it)
Unofficial
TrajectoryMoving away?Moving towards?
Design Goals?See this summary of a project that later "evolved" into Spago
Pros
  • Bower is required for one to publish a library's docs to Pursuit
  • Bower caches its dependencies, so you can download a version of a package only once and use it everywhere on your system
  • It's the "official" dependency manager for Purescript
  • One never experiences the aforementioned "transitive dependency conflicts"
  • Uses Git to work, which is usually already installed on all systems
  • Reliably produces reproducible Purescript dependencies
  • Reduces verbosity somewhat by omitting the purescript- prefix on PureScript packages
Cons
  • When a recent compiler release has been made, one will encounter the 'transitive dependency' issue above quite frequently until all the libraries in the ecosystem are updated to the new release. Figuring out which version of a package works with another wastes a lot of time.
  • Uses node to work, which may not be installed on all systems
  • Doesn't always produce reproducible dependencies due to its caching feature.
  • One cannot publish a library's docs to Pursuit.
  • Uses more storage space due to duplicating code when one has multiple projects across their system.

Bower Explained

Note: The Bower registry has been deprecated. The PureScript community is in the process of creating a registry. When that is done, we will stop using bower entirely. In the meantime, you must still use bower + pulp to publish libraries' docs to Pursuit. One can still use bower as a dependency manager, however, one will need to depend on other libraries using the full repo url in the bower.json file:

"dependencies": {
-    "purescript-some-library": "^0.1.0"
+    "purescript-some-library":"https://github.com/githubUser/purescript-some-library#my-branch"
}

What is it?

A typical dependency manager that downloads dependencies from a centralized repository (i.e. Bower Registry) or GitHub.

Bower-flowchart

Why Use It?

When developing a library, one needs to refer to specific versions of dependencies that do not change over time.

If one uses spago, they can modify the "binary" of the dependency without changing the version to which it refers. For application developers, this can be desirable. Not so for library developers.

Some people prefer bower over spago while others do not. Learn about both and make your decision.

Why doesn't Purescript use npm?

The following provides a much shorter explanation of Why the PureScript Community Uses Bower

Short answer:

  • Because NPM doesn't produce an error when multiple versions of the same transitive dependency are used.

Long answer:

  • When package child1 requires parent v1.0.0 and package child2 requires parent v2.0.0, NPM, will "nest" the packages, so that the code will compile.
  • Should one or both packages export something that exposes parent and our code uses it, this will produce a runtime error, either because some API doesn't exist (e.g. one version changed/removed some API) or because a pattern match didn't work (e.g. an instanceOf check failed due to seeing different types defined in the parent package)
  • Bower uses "flat" dependencies, so it will notify you that such an issue exists by asking you to choose the library version you want to use to resolve the issue.

Problem Points?

  • The issues stated above.
  • You must use npm to install any JavaScript libraries for bindings. This is true for bower and spago alike.
  • pulp doesn't provide a command that wraps esbuild (e.g. pulp bundle-app) whereas spago does.

Solution to Most Common Bower Problem: The Cache Mechanism

When in doubt, run the following command, reinstall things, and see if that fixes your issue:

# Deletes the 'bower_components' and 'output' directories,
# ensuring that the cache mechanism is not corrupting your build
# and that the next build will be completely fresh.
bower cache clean && rm -rf bower_components/ output/

Horrible User Experience Occurs After a Breaking Change Release

The following issue is happening less and less frequently due to the PureScript language stabilizing, but it still needs to be stated.

Annoyance Defined

If a compiler release that includes breaking changes was released recently, it will take some time for libraries in the ecosystem to become compatible with that release. If you are using Bower as your dependency manager, it may try to install libraries that are not compatible with the new release, creating problems.

In such circumstances, follow these guidelines to help find the correct version of a library:

  • Go to Pursuit and look at the library's package page. Choose one of the library's versions and compare that version's publish date with the date of the compiler release. Those that occur after the compiler release are likely compatible with the new release.
  • Since purescript-prelude is a dependency for most libraries, see which version of purescript-prelude the library uses. That should indicate whether it's compatible with a new compiler release or not.
  • If all else fails, check the library's last few commit messages in its repository for any messages about updating to the new compiler release.

Spago Explained

What is it?

A way to use specific versions of libraries that are known to compile together without problems, as verified by CI.

Why Use It?

spago only allows you to use dependencies that compile together on a specific PureScript release. You do not have to track down which version of a DependencyA to use to ensure it compiles when you also use DependencyB. Moreover, you don't have to verify that DependencyA at v1.0.0 works on PureScript release 0.13.8 instead of 0.11.7.

When a new PureScript release with breaking changes occurs, using bower is painful until the ecosystem "catches up." Since a new release draws in a lot of people, their initial exploration of PureScript when using bower can be horrible.

spago also allows you to

  • 'patch' a dependency with your own version
    • fix a bug in its implementation
    • update a library to a newer PS release if it hasn't been done yet
    • update a library's transitive dependency to a newer release without needing to submit a PR
  • add local or cloud-based dependencies not found in the official package set
    • a project you use frequently, like a custom Prelude library.
    • a project with your preferred aliases to functions/values (i.e. using <!> for map instead of <$>)

How does it work?

Spago Terms

A package in this context is 4 things:

  1. a Git repo
  2. a tag in that repo
  3. a set of its dependencies (which are also packages).
  4. a name to refer to the combination of the above three things

Thus, a package is a unique named repo-tag-dependencies combination (e.g. prelude could indicate the Prelude repo at the 'v4.1.1' tag).

A package set consists of a set of packages. It's a JSON-like file that maps a package name to its corresponding repo-tag-dependencies combination. A package set gets verified to ensure that its set of packages compiles together on a given PureScript compiler release. Once verified, they are considered "immutable."

A package set includes all dependencies: direct ones and their transitive dependencies. For example, if the set includes the package, PackA, which depends on the package, PackB, the package set must include both packages:

  • PackA
    • Version: v1.0.0
    • Repo: https://exampleRepo.com/PackA.git
    • Dependencies: ["PackB"] (spago will look up "PackB" in the package set to resolve it)
  • PackB
    • Version: v1.0.0
    • Repo: https://exampleRepo.com/PackB.git
    • Dependencies: [] (no dependencies)

The Process It Uses

Here's a "big picture" flowchart for what a person does and how it fits into their developer workflow:

spago-flowchart

Problem Points?

  • Major
    • You cannot use this workflow to develop libraries. Use pulp and bower for that.
  • Minor / has workarounds
    • Just like bower, you still need to use npm to install JavaScript libraries for any PureScript bindings. To understand why, see https://github.com/purescript/spago#why-cant-spago-also-install-my-npm-dependencies

Why We Need Both

See @hdgarrood's Thoughts on PureScript package management in 2019.

Below are my thoughts on why we need both. I'm not sure whether this is entirely correct.

Think about what happens when a PureScript release is made that includes breaking changes.

Updating each library in the ecosystem to account for those breaking changes is similar to putting a plant inside a vase with colored water. The colored water will first enter its roots, then go up its branches, and finally appear in every leaf (Kids' experiment explained with photos)

In our above analogy, the purescript-prelude library and other libraries with no dependencies are the "roots" of the ecosystem. As they get updated, the libraries that depend on them (i.e. the "branches") can now be updated. A "leaf" corresponds to a library which has no dependents.

A package set is immutable. Thus, one cannot add to the package set a package that has been updated to the new release unless all of the packages in the package set can also be updated.

During that transitional time, spago cannot help. Rather, we must depend on Bower to slowly update each library to its new version that depends on transitive libraries that have been updated to new versions.

Again, spago is more suited for application developers and bower is more suited for library developers.

Spago: From Start to Finish

The below example uses spago as the build tool and dependency manager.

Create the project

# 1. Sets up the initial files and structure of the project
spago init

If you need to override/add packages to the standard package set, proceed to Configure the Package Set. Otherwise, continue to Install Dependencies

Configure the Package Set

# 2a) Open the below file, read its top-level comment,
#       and follow its instructions to override/add packages
nano packages.dhall

# 2b) When finished, either verify that a single patched/added package
#       works with the rest of the set...
spago verify singlePackageName
#       or re-verify the entire package set
spago verify-set

Freeze the Package Set

Note: Spago does this automatically now. So, one likely does not need to do this anymore. It is provided for context / historical purposes.

# 3) Freeze the package set to prevent security issues.
#     For a deeper explanation on what happens here,
#     see Dhall's safety guarantees wiki page:
#     https://github.com/dhall-lang/dhall-lang/wiki/Safety-guarantees
spago freeze

Install Dependencies

One of two ways

# Install a package from the package set to your project
spago install packageName1 packageName2 # ...

Write the Code

# Open the REPL to play with a few ideas or run simple tests
spago repl

# Build the docs
spago docs --open

# Automatically re-build project whenever a source/test file is changed/saved
# and clear the screen before rebuilding
spago build --watch --clear-screen

Build the Code

# Install all dependencies (if not done so already) and
# compile the code
spago build

# Build a developer-level executable file
spago bundle-app --main Module.Path.To.Main --to dist/index.js
node dist/index.js

# Build a production-level Node-backend file via Parcel
spago bundle-app --main Module.Path.To.Main --to dist/bundle-output.js
parcel build dist/bundle-output.js --target "node" -o app.js

To build a production-level browser-backend file...

# Build a production-level browser-backend file
spago bundle-app --main Module.Path.To.Main --to dist/app.js

Create an HTML file (dist/example.html) that references the 'app.js' file

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <!-- Insert your title here -->
    <title>Some Title</title>
  </head>
  <body>
    <!-- Reference the outputted bundle here -->
    <script src="/app.js" charset="utf-8"></script>
  </body>
</html>

Then use parcel to do minification and open the resulting web page

parcel build dist/example.html --target "browser" -o index.html --open
# it'll create a few files in the `dist/` folder and open the resulting
# "dist/index.html" file via your default web browser

Bower: From Start to Finish

Warning: This code hasn't been checked. Most of it should be correct, but some parts might be wrong.

Create the project

One of two ways

pulp init

Install dependencies

# Need to install NPM packages and initialize them
npm install npm-package1 npm-package2
npm install
bower install package1 package2 --save
bower install

Due to the Bower registry being deprecated, there are some packages that will have to be installed using a longer name format because the library couldn't be uploaded into the Bower registry. While the registry is deprecated, bower can still download the files from GitHub if one uses this longer name format. Harry described how one could do that here and also mentions bower link as another possible option:

in bower.json, instead of writing...

"dependencies":{
   "purescript-some-library":"^0.1.0"
}

... you can write

"dependencies": {
   "purescript-some-library":"https://github.com/githubUser/purescript-some-library#my-branch"
}

you can also use bower link which is similar but gives you a bit more flexibility

Write the Code

# Open the REPL to play with a few ideas or run simple tests
pulp repl

# Automatically re-build project whenever a source file is changed/saved
pulp --watch --before 'clear' build

# Automatically re-test project whenever a source/test file is changed/saved
pulp --watch --before 'clear' test

# Build a developer version
esbuild --bundle --outfile dist/fileName.js output/Main/index.js # if program

# Run the program and pass args to the underlying program
pulp run -- arg1PassedToProgram arg2PassedToProgram

Publish the Package for the First Time

See this help page for authors on Pursuit. Its instructions are more authoritative than what follows.

# Build the docs
pulp docs -- --format html
# Then read over them to insure there aren't any formatting issues or typos

# Set the initial version
pulp version v0.1.0

# Publish the version
pulp publish

Publish a New Version of an Already-Published Package

# Build and check the docs
pulp docs -- --format html

# bump project version
pulp version major
pulp version minor
pulp version patch
# or specify a version
pulp version v1.5.0

# publish it
# Note: you may need to run this command twice.
pulp publish

Continuous Integration

GitHub Actions - Bower-based

name: CI

# Run CI when a PR is opened against the branch `main`
# and when one pushes a commit to `main`.
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

# Run CI on all 3 latest OSes
jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, macOS-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v2

      - uses: purescript-contrib/setup-purescript@main
        with:
          purescript: "0.15.4"
          purs-tidy: "0.8.2"
          psa: "0.7.2"

      - uses: actions/setup-node@v
        with:
          node-version: "14"

      - name: Install dependencies
        run: |
          npm install -g bower
          npm install
          bower install --production

      # Compile the library/project
      #   censor-lib: ignore warnings emitted by dependencies
      #   strict: convert warnings into errors
      - name: Build source
        run: |
          pulp build -- --censor-lib --strict

      - name: Run tests
        run: |
          bower install
          pulp test

      - name: Check Formatting
        run: |
          purs-tidy check src test

GitHub Actions - Spago-based

name: CI

# Run CI when a PR is opened against the branch `main`
# and when one pushes a commit to `main`.
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

# Run CI on all 3 latest OSes
jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, macOS-latest, windows-latest]
    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v2

      - uses: purescript-contrib/setup-purescript@main
        with:
          purescript: "0.15.4"
          purs-tidy: "0.8.2"
          spago: "0.20.9"
          psa: "0.7.2"

      - name: Cache PureScript dependencies
        uses: actions/cache@v2
        with:
          key: ${{ runner.os }}-spago-${{ hashFiles('**/*.dhall') }}
          path: |
            .spago
            output

      - name: Set up Node toolchain
        uses: actions/setup-node@v2
        with:
          node-version: "14"

      - name: Cache NPM dependencies
        uses: actions/cache@v2
        env:
          cache-name: cache-node-modules
        with:
          path: ~/.npm
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
          restore-keys: |
            ${{ runner.os }}-build-${{ env.cache-name }}-
            ${{ runner.os }}-build-
            ${{ runner.os }}-

      - name: Install NPM dependencies
        run: npm install

      # Compile the library/project
      #   censor-lib: ignore warnings emitted by dependencies
      #   strict: convert warnings into errors
      # Note: `purs-args` actually forwards these args to `psa`
      - name: Build the project
        run: |
          spago build --purs-args "--censor-lib --strict"

      - name: Run tests
        run: |
          spago test

      - name: Check Formatting
        run: |
          purs-tidy check src test

Formatters

History

For the longest time, PureScript did not have a formatter. There are a number of reasons:

  1. Since PureScript is written in Haskell, people are less likely to contribute since Haskell is still different from PureScript (even if they share similarities).
    1. Ideally, PureScript would be written in PureScript. Unfortunately, if PureScript was self-hosted, runtime performance of the purs binary would suffer greatly otherwise. See purescript/purescript-in-purescript, which was stopped after that realization was made.
    2. Realistically, a formatter could be written in Haskell and reuse the PureScript language's parser. However, then less people would contribute as not everyone is familiar with Haskell
    3. If a formatter was written in PureScript, it would get more contributions.However, it would have to reimplement the PureScript language's parser and stay in sync with any changes made to the language. Moreover, it would likely be slower than writing it in a lower-level language (e.g. Haskell).
  2. Writing a formatter is very hard to do. It's typically a feat not done by your beginner or everyday programmer.
  3. Once written, maintainers can burn out because many individuals will want configuration added (e.g. "it should indent A in situation Y exactly N spaces but only M spaces in situation Z"). If the configuration is not added, people complain. If it is added, others might later complain about how it has TOO much configuration. Either way, it's typically the maintainer who adds the feature and those who want it don't contribute.

The first formatter written was purty. This formatter was written in Haskell. It was the only formatter for a number of years. Some in the community chose to use it while others did not.

Around March/April 2021, @natefaubion wrote a PureScript implementation of the PureScript language's parser: natefaubion/purescript-language-cst-parser.

The second and third formatters, purs-tidy and pose, respectively, were announced around the same time in August 2021. Both projects were under developement without knowing about each other. purs-tidy is a standalone formatter whereas pose is a plugin for the Prettier formatter.

Current Formatters

FormatterLanguageAuthorInitial Announcement
purs-tidyPureScript@natefaubionAnnouncing purs-tidy: a syntax tidy-upper for PureScript
posePureScript@Zelenaya/@i-am-the-slimeTiny announcement: yet another PureScript formatter
purtyHaskell@joneshfPurty 1.0.0 released

Syntax

This folder contains compileable Purescript syntax using meta-language (a language that describes the syntax). Thus, rather than saying something like

f :: String -> Int

which doesn't tell you anything, it'll say:

functionName :: ParameterType -> ReturnType

Since the syntax can be compiled, it can be verified as valid and correct syntax.

As a result, most files will appear like so:

-- The module will be declared at the top of the file
--   It can be ignored.
module Syntax.ModuleName where

-- The Prelude module might be imported
--   It, too, can be ignored.
import Prelude

-- The thing that the file is documenting usually goes here.
--    Don't ignore this stuff.
data Box a = Box a

-- Sometimes the comment "necessary to compile" will appear.
-- It makes the meta-language compileable. Ignore everything underneath it
--   as you read through the files.

-- necessary to compile
type SomeTypeName = String

If you want to play around with the syntax, follow these instructions:

  1. Go to a directory that has a spago.dhall file (otherwise, the rest of these commands won't work)
  2. Install the dependencies: spago install
  3. Start a REPL or build the files with watching (refer to the table below)
CommandIdeal UsageOther Comments
spago replPlay with <10 lines of syntaxEdit .purs-repl and add import ModuleName to automatically import that module whenver you run this command
spago build --watchTest out 10+ lines of syntaxSaving a file after running this command will re-compile the project

Basic Syntax

Read through these files in their order. To further grasp the concept, write your own version of the code and see if it still compiles by running:

spago build

To see what the documentation looks like, run this command:

spago docs --open

The above command will generate the docs, and then open the file, ./generated-docs/index.html.

00-Comments.purs

module Syntax.Basic.Comments where

-- This is a single-line comment
-- Anything past the "--" syntax on a line is regarded as a comment

{-
This is a multi-line comment
Anything between the bracket-dash syntax is regarded as a multi-line comment
-}

{- It can also be used to add a comment in-between stuff -}

01-Value-Function-Data-Syntax.purs

module Syntax.Meta where

-- This file simply shows enough syntax so that the
-- explanation on Kinds (next) makes sense.
--
-- entity_name :: Type Signature
-- entity_name = definition

integer_value :: Int
integer_value = 5

string_value :: String
string_value = "this is text"

-- | In other words...
-- | ```
-- | var one_arg_function = function (argument) {
-- |   return bodyThatReturnsType;
-- | };
-- | ```
one_arg_function :: ParameterType -> ReturnType
one_arg_function argument = bodyThatReturnsType

-- Below is an Algebraic Data Type. We'll explain these more later.
--
-- Here, we declare a type called `Type_Used_In_Functions_Type_Signatures`,
-- which has two implementations. The type is used in an entity's
-- Type Signatures while the implementations are used in an entity's
-- definition
data Type_Used_In_Functions_Type_Signatures
  = Type_Implementation1
  | Type_Implementation2

example1 :: Type_Used_In_Functions_Type_Signatures
example1 = Type_Implementation1

example2 :: Type_Used_In_Functions_Type_Signatures
example2 = Type_Implementation2

-- A "box" that can store only Ints
data Box_That_Stores_Ints = Box Int

example3 :: Box_That_Stores_Ints
example3 = Box 4

example4 :: Int -> Box_That_Stores_Ints
example4 x = Box x

-- A "box" type that can store values of another type.
data Box_That_Stores anotherType = Box_Storing anotherType

example5 :: Box_That_Stores Int
example5 = Box_Storing 4

example6 :: Int -> Box_That_Stores Int
example6 x = Box_Storing x

-- Look! An outer Box that stores an inner Box, that stores an Int
example7 :: Box_That_Stores (Box_That_Stores Int)
example7 = Box_Storing (Box_Storing 4)

-- The "forall someType." syntax will be explained later. It's needed here
-- to make this code compile. You can read example8's type signature as
-- "If you give me a value that has a given type, which I'll refer to as
-- `someType`, then I can give you back a Box that stores a value of
-- `someType`."
example8 :: forall someType. someType -> Box_That_Stores someType
example8 valueWhoseTypeIs_'someType' = Box_Storing valueWhoseTypeIs_'someType'

-- necessary to make this file compile

type ValueType = String
type ParameterType = String
type ParameterType1 = String
type ParameterType2 = String
type ReturnType = String

bodyThatReturnsType :: ReturnType
bodyThatReturnsType = "return value"

bodyOfFunction :: ReturnType
bodyOfFunction = "body of inline function"

Explaining Kinds

This code...

function :: Int -> String
function x = "an integer value!"

... translates to, "I cannot give you a concrete value (i.e. String) until you give me an Int value."

Similarly, this code...

data Box a = Box a

... translates to, "I cannot give you a concrete type (e.g. Box Int, a box that stores an Int value (rather than a String value or some other value)) until you tell me what a is."

Let's rewrite the above Box type. Things on the left of the = indicate type information. Things on the right of the = indicate value information.

{-
| Type information | Value information |                                     -}
data BoxType a     = BoxValue a

The above code now says, "I cannot give you a concrete type (e.g. BoxType Int) until you tell me what a is." Let's assume that a is Int. We would say that BoxValue 4 is a value whose type is BoxType Int.

What are Kinds and Kind Signatures?

Kinds = "How many more types do I need defined before I have a 'concrete' type?"^^

^^ This is a "working definition." There's more to it than that when one considers type-level programming, but for now, this will suffice."

We saw earlier that we annotate functions with type signatures via ->:

--              ||
--              \/
function :: Int -> String
function x = "an integer value!"

The -> indicates that the thing to the right (i.e. String) cannot be produced until it is given the thing to the left of it (i.e. Int).

Type signatures annotate value-level entities like values (i.e. 4 or BoxValue) and functions. Kind signatures annotate type-level entities like BoxType. They are basically type signatures for types, not values.

# of types that still need to be definedSpecial NameTheir "kind signature" (Purescript)^^Their "kind signature" (Haskell)^^
0Concrete Type Type *
1Higher-Kinded Type (by 1) Type -> Type * -> *
2Higher-Kinded Type (by 2)Type -> Type -> Type* -> * -> *
nHigher-Kinded Type (by n)... Type ... -> Type... * ... -> *

^^ These columns are right-aligned to show that the right-most Type/* is the "concrete" type. Also, the ... Type ... -> Type (and its Haskell equivalent) syntax is not real syntax but merely conveys the recursive idea in an n-kinded type. The other three (0 - 2) are real syntax.

Concrete Types

Concrete types can usually be written with literal values:

integerValue :: Int
integerValue = 1

(1 :: Int) -- this is notation for saying that `1` is a value of type, `Int`.

stringValue :: String
stringValue = "a literal string"

("a literal string" :: String)

data BoxType a = BoxValue a

boxWithOneIntValue :: BoxType Int
boxWithOneIntValue = BoxValue 4

((BoxValue 4) :: BoxType Int)

arrayOfIntsValue :: Array Int
arrayOfIntsValue = [1, 2, 3]

([1, 2, 3] :: Array Int)

Higher-Kinded Types

Higher-kinded types are those that still need one or more types to be defined.

-- Kind Signature: Type -> Type
-- Reason: the `a` type needs to be defined
data Box a = Box a

-- This is the same definition as above.
-- However, the kind signature of the above `Box` definition is implicit.
-- The below definition has an explicit kind signature.
data BoxType :: Type -> Type
data BoxType a = BoxValue a

-- As we can see, there can be many different concrete 'Box' types
-- depending on what 'a' is:
boxedInt :: Box Int
boxedInt = Box 4

boxedString :: Box String
boxedString = Box "string"

boxedBoxedInt :: Box (Box Int)
boxedBoxedInt = Box boxedInt

We can make the type's kind higher by adding more types that need to be specified. For example:

-- A box that holds two values of same or different types!
-- Kind Signature: `Type -> Type -> Type`
data BoxOfTwo a b = BoxOfTwo a b

data BoxOfTwo_ExplicitKindSignature :: Type -> Type -> Type
data BoxOfTwo_ExplicitKindSignature a b = BoxOfTwoValue a b

-- The below syntax is not valid because it is missing `forall a b.`,
--   but it gets the idea across. The "forall" syntax will be covered later.
higherKindedBy2 :: a -> b -> BoxOfTwo a b
higherKindedBy2 a b = BoxOfTwo a b

-- We can lower the kind by specifying one of the data types:
higherKindedBy1L :: b -> BoxOfTwo Int b
higherKindedBy1L b = BoxOfTwo 3 b

higherKindedBy1R :: a -> BoxOfTwo a String
higherKindedBy1R a = BoxOfTwo a "a string value"

concreteType :: BoxOfTwo Int String
concreteType = BoxOfTwo 3 "a string value"

Generic types can also be split across the values of a type:

-- It's either an A or it's a B, but not both!
-- Kind signature is implicit: `Type -> Type -> Type`
data Either a b
  = Left a
  | Right b

data Either_ExplicitKindSignature :: Type -> Type -> Type
data Either_ExplicitKindSignature a b
  = Left a
  | Right b

higherKindedBy2L :: a -> b -> Either a b
higherKindedBy2L a b = Left a

higherKindedBy2R :: a -> b -> Either a b
higherKindedBy2R a b = Right b

higherKindedBy1L_ignoreB :: b -> Either Int b
higherKindedBy1L_ignoreB b = Left 3

higherKindedBy1L_useB :: b -> Either Int b
higherKindedBy1L_useB b = Right b

higherKindedBy1L_ignoreBoth :: a -> b -> Either Int b
higherKindedBy1L_ignoreBoth a b = Left 3

Either (where the a and b are not yet specified) has kind Type -> Type -> Type because it cannot become a concrete type until both a and b types are defined, even if only constructing one of its values whose generic type is known.

In other words

allSpecified :: Either Int String
allSpecified = Right "foo"

{-
(value)                                                                       -}
(Right "foo")                                                                 {-

(value       :: Type             )                                            -}
(Right "foo" :: Either Int String)                                            {-

((value       :: Type             ) :: Kind)                                  -}
((Right "foo" :: Either Int String) :: Type)                                  {-

((value       :: Type           ) :: Kind        )                            -}
((Right "foo" :: Either a String) :: Type -> Type)

Table of Inferred Types

Inferred kind
UnitType
Array BooleanType
ArrayType -> Type
Either Int StringType
Either Int bType -> Type
Either a StringType -> Type
EitherType -> Type -> Type
......

03-The-Prim-Module.purs

module Syntax.Basic.PrimitiveTypesAndKinds where

import Prelude

{-
The following file documents the Prim module. This module is imported
by default into every PureScript file (unless one hides it using Module aliases,
which are described in the Module Syntax folder) and is embedded in the
compiler itself to provide value literals for certain types and syntax sugar.

See the full documenation here:
https://pursuit.purescript.org/builtins/docs/Prim

This file will document all the types whose kind signature is `Type`.
Their kind signatures aren't that important at this level in your understanding.

Note: To prevent conflicts between the real code and this compileable file,
we're appending underscores to the types. Remove the underscore to get the
real thing in Purescript.

In other words
Purescript:        DataType  :: Kind
This example: data DataType_ -- Kind
-}
data Number_ -- Type -- double-precision float number

exampleNumber1 :: Number
exampleNumber1 = 1.0

-- negative values must be wrapped in parenthesis:
exampleNumber2 :: Number
exampleNumber2 = (-1.0)

data Int_ -- Type

exampleInt1 :: Int
exampleInt1 = 1

exampleInt2 :: Int
exampleInt2 = 0x01 -- alternative way to write them

exampleInt3 :: Int
exampleInt3 = 1_000_000 -- use underscores for thousands character

-- negative values must be wrapped in parenthesis:
exampleInt4 :: Int
exampleInt4 = (-1)

exampleInt5 :: Int
exampleInt5 = (-0x01)

exampleInt6 :: Int
exampleInt6 = (-1_000_000)

data Boolean_ -- Type

exampleTrue :: Boolean
exampleTrue = true

exampleFalse :: Boolean
exampleFalse = false
{-
Note: The Boolean data type is used via true/false literal values instead of
True/False constructors as one might expect, especially those coming from Haskell
where such a simple data type would be defined as:

data Boolean = True | False

In Purescript, having Javascript as the main compilation target, the decision was
made to use true/false literal values for the Boolean data type instead of
having it be defined as a simple Algebric Data Type (ADT) as is the case in Haskell.
-}

data Char_ -- Type -- doesn't support astral plane characters (code points > 0xFFFF)

exampleChar :: Char
exampleChar = 'c'

unicodeA :: Char
unicodeA = '\x0061'

-- Astral plane characters (i.e. those with code point values greater than
-- `0xFFFF`) cannot be represented as `Char` values.
unicodeChar :: Char
unicodeChar = '\xFFFF'

unicodeChar2 :: Char
unicodeChar2 = '\xffff'

data String_ -- Type

literal_string_syntax :: String
literal_string_syntax = "literal string value"

-- Follows this regex pattern: \x[0-9a-fA-F]{1,6}
unicode_hex_escape_syntax :: String
unicode_hex_escape_syntax = "\xa4"

-- Syntax sugar for Strings
slashy_string_syntax :: String
slashy_string_syntax =
  "Enables multi-line strings that \
  \use slashes \
            \regardless of indentation \

    \and regardless of vertical space between them \

    \(though you can't put comments in that blank vertical space)"
    {-
    "This will fail \
    -- oh look a comment that breaks this!
    \to compile."
    -}

triple_quote_string_syntax :: String
triple_quote_string_syntax = """
  Multi-line string syntax that also ignores escaped characters, such as
  * . $ []
  It's useful for regular expressions
  """

-- Higher-Kinded Types
data Array_ -- Type -> Type

arrayOfStrings :: Array String
arrayOfStrings = ["string1", "string2"]

arrayOfInts :: Array Int
arrayOfInts = [0, 1, 2, 3]

-- The "forall a." syntax will be explained later. It's needed here
-- to make this code compile
array_of_one_A :: forall a. a -> Array a
array_of_one_A a = [a]

data Function_ -- Type (parameter type) -> Type (return type)  -> Type
-- In other words, give me the parameter type and the return type,
--   and I'll have a concrete type

function_no_syntax_sugar :: Function Int Int
function_no_syntax_sugar = (\x -> x + 4)

function_with_syntax_sugar1 :: (Int -> Int)
function_with_syntax_sugar1 = (\x -> x + 4)

function_with_syntax_sugar2 :: Int -> Int
function_with_syntax_sugar2 = (\x -> x + 4)

function_with_syntax_sugar3 :: Int -> Int
function_with_syntax_sugar3 x = x + 4

01-Defining-Values-and-Functions.purs

module Syntax.Basic.ValuesAndFunctions where

import Prelude

-- This file simply shows the syntax for how to define
-- values and types

-- A zero-arg function cannot exist in FP programming*
-- Thus, it counts as a static value
literal_value :: ValueType
literal_value = "literal value"

-- * function :: Unit -> ReturnType is as close as one can get to a
-- zero-arg function in functional programming. Unit will be explained later
-- in the "Hello World" folder.

result_of_function :: Int
result_of_function = 4 + 5 -- 9

one_arg_function :: ParameterType -> ReturnType
one_arg_function argument = bodyThatReturnsType

two_arg_function :: ParameterType1 -> ParameterType2 -> ReturnType
two_arg_function argument1 argument2 = bodyThatReturnsType

n_arg_function :: ParameterType1 -> {- ... ParameterTypeN -> ... -} ReturnType
n_arg_function arg1 {- arg2 arg3 ... argN -} = bodyThatReturnsType

function_using_inline_syntax :: (Int -> Int)
function_using_inline_syntax = (\x -> x + 4)

                                         {- function  -}
function_that_takes_a_function :: Int -> (Int -> String) -> String
function_that_takes_a_function i f = f i

                                           {- function -}
function_that_returns_a_function :: Int -> (Int -> Int)
function_that_returns_a_function x = (\y -> y + x)

-- Note: a "higher order function" either takes a function as an argument
-- or returns a function

-- examples
takes_a_function :: String
takes_a_function =
  function_that_takes_a_function 3 (\x -> show x)
  -- show: converts `Int` to `String`
  -- outputs: "3"

returns_a_function :: Int
returns_a_function =
  (function_that_returns_a_function 4) 10
  -- outputs: 14
  -- reason: (\10 -> 10 + 4)

-- necessary to make this file compile

type ValueType = String
type ParameterType = String
type ParameterType1 = String
type ParameterType2 = String
type ReturnType = String

bodyThatReturnsType :: ReturnType
bodyThatReturnsType = "return value"

bodyOfFunction :: ReturnType
bodyOfFunction = "body of inline function"

02-Function-Currying.purs

module Syntax.Basic.Function.Currying where

-- Remember this function?
one_arg_function_syntax_sugar :: ParameterType -> ReturnType
one_arg_function_syntax_sugar argument = bodyThatReturnsType

-- it's syntax sugar for
one_arg_function_no_syntax_sugar :: Function ParameterType ReturnType
one_arg_function_no_syntax_sugar argument = bodyThatReturnsType

-- Which means this function...
two_arg_function0 :: ParameterType1 -> ParameterType2 -> ReturnType
two_arg_function0 argument1 argument2 = bodyThatReturnsType

-- is in a "curried" form. In reality, it's
two_arg_function1 :: ParameterType1 -> (ParameterType2 -> ReturnType)
two_arg_function1 argument1 argument2 = bodyThatReturnsType
-- or without syntax sugar
two_arg_function2 :: Function ParameterType1 (ParameterType2 -> ReturnType)
two_arg_function2 argument1 argument2 = bodyThatReturnsType
-- and removing the last one
two_arg_function3 :: Function ParameterType1 (Function ParameterType2 ReturnType)
two_arg_function3 argument1 argument2 = bodyThatReturnsType

{-
In other words, give me an argument (ParameterType1) and I'll return a
  function (ParameterType2 -> ReturnType). If you give that function
  a parameter (ParameterType2), then it'll give you the ReturnType.
Since this happens in the background, we don't usually need to think about
  it, but it is an important distinction to make as it creates the following
  Javascript code:

two_arg_function = function (ParameterType1) -> {
  return function (ParameterType2) -> {
    return bodyThatReturnsType;
  }
}
-}

-- necessary to compile

type ParameterType = String
type ParameterType1 = String
type ParameterType2 = String
type ReturnType = String

bodyThatReturnsType :: String
bodyThatReturnsType = "body"

03-Abbreviated-Function-Body.purs

module Syntax.Basic.Function.BodyAbbreviation where

import Prelude

-- if the body of a function is another function that expects an argument,
-- one can omit the argument entirely.
--    Note: 'show' converts a value of most types into a `String` value.

function_normal      :: Int -> String
function_normal         x    = show    x
--    is the same as ...
function_abbreviated :: Int -> String
function_abbreviated {- x -} = show {- x -}
--    which is better written as ...
function_abbreviated2 :: Int -> String
function_abbreviated2 = show

-- example
exampleAbbreviation2 :: Boolean
exampleAbbreviation2 = (function_abbreviated2 4) == "4"

-- Going from "function_normal" to "function_abbreviated2" is called
-- "eta-reduction".
-- Goind from "function_abbreviated2" to "function_normal" is called
-- "eta-expansion" or "eta-abstraction"

warning :: String
warning = """

Sometimes, using an abbreviated function / eta-reduction will cause problems.
See this issue for more details:
https://github.com/purescript/purescript/issues/950

To fix it, just un-abbreviate the function body by eta-expanding it
(i.e. include the argument):
-- change this:
f :: Int -> String
f = show

-- to
f' :: Int -> String
f' x = show x
"""

04-Keyword--Data.purs

module Syntax.Basic.Keyword.Data where

-- Basic syntax for the `data` keyword
-- For most of these examples, we will not need to use explicit kind signatures.

data Singleton_no_Args = SingletonConstructor

data Singleton_with_Args = SingletonConstructor2 Arg1 Arg2 ArgN

data Singleton_with_Function_Arg
  = SingletonConstructor3 (ParameterType -> ReturnType)

data Type_with_Many_Implmementations
  = Implementation1
  | Implementation2
  | ImplementationN

data Type_with_Generic_Types aType bType
  = Stores_A aType
  | Stores_B bType
  | Stores_A_and_B aType bType

-- We can refer to various parts in these definitions by the following names.
-- Wherever a name appears, that's what you would call it if you were talking
-- to someone else about it. In this example, we will need a kind signature
-- because `typeParameter` isn't used in the data constructor.
data TypeConstructor :: Type -> Type
data TypeConstructor typeParameter = DataConstructor

---------------------------------

-- This syntax enables Algebraic Data Types (ADTs)
-- For an explanation on how 'data types' can be 'algebraic,' see this video:
-- https://youtu.be/Up7LcbGZFuo?t=19m8s

-- 2 basic version of ADTs: sum type and product type

-- the sum type
data SumType
  = SumConstructor1
  | SumConstructor2
  | SumConstructorN

-- example
data Fruit
  = Apple
  | Banana
  | Orange

-- the product type
data ProductType a b = ProductConstructor a b

-- example
data IntAndString = IAndS Int String

--------------------------------------------

-- Intermediate/Advance syntax

-- given this code
data Box a = Box a
-- then...
data Type_with_Nested_Types
  = SingleBox Int
  | NestedBox1 (Box Int)
  | NestedBox2 (Box (Box Int)) -- outer Box's "a" is "(Box Int)"

data Type_with_Higher_Kinded_Type f = TypeValue (f Int)

typeWithHigherKindedTypeExample :: Type_with_Higher_Kinded_Type Box
typeWithHigherKindedTypeExample = TypeValue (Box 4)

data Type_with_Higher_Kinded_Generic_Type higherKindedBy1 a
  = MyConstructor (higherKindedBy1 a)
  | OtherC (higherKindedBy1 Int)

data Type_with_Higher_Kinded_Generic_Type2 higherKindedBy2 a b
  = MyConstructor2 (higherKindedBy2 a b)
  | OtherCInt (higherKindedBy2 Int b)
  | OtherCIntString (higherKindedBy2 Int String)

-- In the next two examples, we need an explicit kind signature.
-- The reason will become more evident in later files, but you will
-- understand it in full when you read through the Type-Level Syntax folder
-- ============================================================================
-- Since `a` and `b` aren't defined here, we need an explicit kind signature
data Type_With_HigherKindedByTwo_Generic
  :: (Type -> Type -> Type) -- higherKindedBy2
  -> Type                   -- a
  -> Type                   -- b
  -> Type                   -- the "concrete" type
data Type_With_HigherKindedByTwo_Generic higherKindedBy2 a b
  = Example (higherKindedBy2 a b)


-- Since `ignoredType` isn't used in one of the data constructors
-- we need an explicit kind signature.
data Type_whose_implementations_ignore_generic_type :: Type -> Type
data Type_whose_implementations_ignore_generic_type ignoredType
  = Constructor_without_generic_type
  | Other_Constructor_no_generic_type Int String
-- ============================================================================

data Type_with_no_implementation -- no equals sign followed by right-hand-side


data Recursive_Type
  = No_Recursion_Here
  | Recursion_Here Recursive_Type
-- Recursion_Here (Recursion_Here (No_Recursion_Here))

data Recursive_type_with_generic_type a
  = End_Recursion_Here
  | Recursion_Here__Store_A a (Recursive_type_with_generic_type a)
{-
Recursion_Here__Store_A "first"
  (Recursion_Here__Store_A "second"
    End_Recursion_Here)
-}
------------------------------------------

-- Full Syntax
-- Here we need a kind signature because `ignored` does not appear
-- in any of the below data constructors.
data DataType :: Type -> Type -> (Type -> Type) -> Type -> Type
data DataType aType bType hktBy1 ignored
  = NoArgs
  | Args Type1 Type2 Type3
  | FunctionArg (Type1 -> Type2)
  | NestedArg (Box Int)
  | DoubleNestedArg (Box (Box Int))
  | HigherKindedGenericType1 (hktBy1 Int)
  | HigherKindedGenericType2 (hktBy1 aType)
  | Recursive (DataType aType bType hktBy1 ignored)
  | ArgMix Type_ (A -> B) bType (DataType aType bType hktBy1 ignored)

-- Necessary for this to compile
type Type1 = Int
type Type2 = Int
type Type3 = Int
type Type_ = Int
type A = Int
type B = Int
type Arg1 = Int
type Arg2 = Int
type ArgN = Int
type ParameterType = Int
type ReturnType = Int

05-Pattern-Matching-in-Functions.purs

module Syntax.Basic.PatternMatching where

import Prelude

-- Given a data type like this:
data Fruit
  = Apple
  | Orange
  | Banana
  | Cherry
  | Tomato -- because why not!?

-- Pattern Matching: Basic idea and order of matching
mkString :: Fruit -> String {-
         if the arg is _ = then return _ -}
mkString Apple           = "apple"

{-  else if the arg is _ = then return _ -}
mkString Orange          = "orange"

{-  else if the arg is _ = then return _ -}
mkString Banana          = "banana"

{-  else if the arg is _ = then return _ -}
mkString Cherry          = "cherry"

{-  else if the arg is _ = then return _ -}
mkString Tomato          = "tomato"

-- The above pattern match is "exhaustive" because there are no other
-- Fruit values against which one could match.

-- Pattern Matching: Literal values and catching all values

literalValue :: String -> String
literalValue "a" = "Return this string if arg is 'a'"
literalValue "b" = "Return this string if arg is 'b'"
literalValue "c" = "Return this string if arg is 'c'"
literalValue _   = "ignore input and return this default value"

-- syntax sugar for pattern-matching literal arrays
array :: Array Int -> String
array []           = "an empty array"
array [0]          = "an array with one value that is 0"
array [0, 1]       = "an array with two values, 0 and 1"
array [0, 1, a, b] = "an array with four values, starting with 0 and 1 \
                       \ and binding the third and fouth to names 'a' and 'b'"
array [-1, _ ]     = "an array of two values, '-1' and another value that \
                       \ will not be used in the body of this function."
array _            = "catchall for arrays. This is needed to make this \
                       \ example compile"


-- Pattern Matching: Unwrapping Data Constructors
data A_Type
  = AnInt Int
  | Outer A_Type -- recursive type!
  | Inner Int

f :: A_Type -> String {-
-- Syntax
f patternMatch = bodyToRunIfPatternWasMatched

  where 'patternMatch' is:
    - literal value
    - DataConstructorWithNoArgs
    - (DataConstructor withArgBoundToThisBinding)
    - (DataConstructor "with arg whose value is this literal value")
    - bindingForEntireValue@(literalValue)
    - bindingForEntireValue@(DataConstructorWithNoArgs)
    - bindingForEntireValue@(DataConstructor withArgBoundToThisBinding)
    - bindingForEntireValue@(DataConstructor "with arg whose value is this literal value")

-- Example

f the pattern match     = description of what was matched -}
f (Inner 0)             = "a value of type Inner whose value is 0"
f (Inner int)           = "a value of type Inner, binding its value to 'int' \
                          \name for usage in function body"
f (Outer (Inner int))   = "a value of type Outer, whose Inner value is bound \
                          \to `int` name for usage in function body"
f object@(AnInt 4)      = "a value of type AnInt whose value is '4', \
                          \binding the entire object to the `object` name for \
                          \usage in function body"
f _                     = "ignores input and matches everything; \
                          \acts as a default / catch all case"

-- Pattern Matching: Regular Guards
g :: Int -> Int -> String {-
g x y | condition1  = return this if condition1 is true
      | condition2  = return this if condition2 is true
      | ...         = ...
      | conditionN  = return this if conditionN is true
      | otherwise   = default case-}
g x y | x + y == 0 = "x == -y"
      | x - y == 0 = "x == y"
      | x * y == 0 = "x == 0 || y == 0"
      | otherwise = "some other value"

-- Pattern Matching: Single and Multiple Guards
h :: Int -> Int -> String
h x y | x == 4 && y == 5 = "body"

   -- | condition1, condition2 = body
      | x == 4, y == 6   = "body"

  {-  ... or when using syntax sugar...
      | condition1
      , condition2 = body -}
      | x == 3
      , y == 2           = "body"

-- It's wise to separate mulitple guards with a blank line for readability.
      | otherwise        = "default"

-- Pattern Matching: Single Pattern Guard
j :: Int -> String {-
j x | returnedValue <- function arg1 arg2 argN = body if match occurs -}
j x | (Box 2) <- toBox x = "Calling toBox x returned a Box with 2 inside of it"
    | (Box y) <- toBox x = concat "The 'y' value was: " (toString y)

-- Pattern Matching: Multiple Pattern Guards
p :: Int -> Int -> String {-
p x y | returnedValue1 <- functionCall1, returnedValue2 <- functionCall2 = body -}
p x y | (Box 2) <- toBox x, (Box 3) <- toBox (x - 1) = "without syntax sugar"

 {-   ... or for easier reading, there is sugar syntax:
p x y | returnedValue1 <- functionCall1
      , returnedValue2 <- functionCall2 = body -}
      | (Box a) <- toBox x
      , (Box b) <- toBox (x * 2) = "with syntax sugar"

      | otherwise = "some other value"

-- Different guards can be mixed:
q :: Int -> Int -> String
q x y | x == 3                   = "3"
      | x == 5, y == 5           = "5"
      | (Box 2) <- toBox x       = "2?"

      | (Box 2) <- toBox x
      , y == 4                   = "curious, no?"

      | (Box a) <- toBox x
      , (Box b) <- toBox (y * 2) = "something?"

      | otherwise                = "catch-all"

-- necessary for this to compile
data Box a = Box a

toBox :: Int -> Box Int
toBox 1 = Box 2
toBox _ = Box 0

concat :: String -> String -> String
concat left right = left <> right

toString :: forall a. Show a => a -> String
toString = show

01-Typed-Holes.purs

module Syntax.Basic.SpecialCompilerFeatures.Holes where

{-

Original credit: @paf31 / @kritzcreek

Link: https://github.com/paf31/24-days-of-purescript-2016/blob/master/23.markdown

Changes made:
- use meta-language to explain syntax and give a few very simple examples

Licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en_US

----------------------------------

Sometimes, when writing code, we're not always sure which function/value
we should use. In such cases, we can use a feature called
"Typed Holes" / "Type Directed Search" to ask the compiler to tell us
what it thinks should go there.

This feature can often be very helpful when debugging a compiler error
or when exploring a new library for the first time.

Syntax:
`?placeholderName`

Replace the function/value you want the compiler to suggest for you with
the above syntax.
-}

-- Note to self: don't delete this as functions from Prelude will be suggested below
import Prelude

warning :: String
warning =
  """
  When we compile our code and it includes a hole,
  the compiler will output a compiler error.

  The error message will include the compiler's guess as to what should
  be put there.

  Since this outputs a compiler error, we have to comment out the following code
  to make this project build on Travis CI and on local computers

  Uncomment the following examples, one at a time, and then build the folder to
  see how it works
  """

-- This example will show what the type signature for "?placeholder_name"
-- should be.

-- example1 :: Int -> String
-- example1 i = ?placeholder_name i

-- Caveats: While this file has 2 "holes," the second one will not be reported
-- by the compiler because it produces an error at the first hole. Thus,
-- this feature can only be used once in a project per compilation.

-- notice the infix notation used here via the backticks
-- example2 :: String
-- example2 = "hello" `?I_Don't_know` " world"

-- example3 :: Int
-- example3 = 1 + ?what_could_this_be

02-Typed-Wildcards.purs

module Syntax.Basic.SpecialCompilerFeatures.TypedWildcards where

{-
Sometimes, when writing code, we're not always sure what type something
should be in a type signature.

In such cases, we can use a feature called "typed wildcards"
to ask the compiler to tell us which type it thinks should go there.

This feature can often be very helpful when debugging a compiler error
or when exploring a new library for the first time.

Typed Wildcards can be either unnamed ("_") or named "?name".

Named typed wildcards will emit a compiler error whereas
unnamed typed wildcards will emit a compiler warning.
-}

someValue :: _ -- unnamed typed wildcard
someValue =
  """
  The '_' character above is an unnamed typed wildcard
  It will produce a compiler warning whose message will include
  the compiler's guess as to what goes there.
  """

-- Uncomment the below code and then rebuild the folder to see what happens

-- someValue2 :: ?TellMeTheType
-- someValue2 =
--   """
--   The "?name" syntax above is a named typed wildcard.
--   It will produce a compiler error whose message will include
--   the compiler's guess as to which type goes there.
--   """

41-Typed-Holes-On-Complex-Expressions.purs

-- This module uses syntax that hasn't been explained this far into
-- this reference work. It'll make more sense after one understands
-- - Let bindings (e.g. `example1` and `example2`)
-- - do notation (e.g. `example4`)
-- - ado notation (e.g. `example5`)
module Syntax.Basic.SpecialCompilerFeatures.HolesOnComplexExpressions where

import Prelude

-- The previous examples in this folder showed
-- how to use typed holes in one direction:
--    `Type Signature --> Expression`
--
-- This workflow helps one know what expression to provide to "fill"
-- the typed hole.
--
-- One can also go in the opposite direction. Sometimes, a developer
-- will write a complex expression. It can be hard for the developer
-- to determine what the type signature for that expression is.
-- Thus, one can go in the opposite direction:
--    `Expression --> Type Signature`
--
-- This can be accomplished via type annotations on bindings.
-- Since the typed holes produce compiler errors, the below
-- code will not compile. Uncomment the code to see the result.

-- example1ViaWhere :: Int
-- example1ViaWhere = 4
--   where
--   -- Verbose way
--   example1 :: ?Help
--   example1 = "foo"

--   -- one-liner way
--   example2 :: ?Help = "foo"

-- example2ViaLet :: Int
-- example2ViaLet =
--   let
--     -- Verbose way
--     example1 :: ?Help
--     example1 = "foo"

--     -- one-liner way
--     example2 :: ?Help = "foo"
--   in
--     4

-- These work in `case _ of` expressions
-- example3ViaCase :: Int
-- example3ViaCase = 4
--   where
--   foo = case 4 + 4 :: ?Help of
--     eight -> eight

-- These also work when using "do notation" and "ado notation".
-- example4InDo :: Int -> Int
-- example4InDo = do
--   four :: ?Help <- (\inputArg -> 4)
--   pure 4

-- example5InAdo :: Int -> Int
-- example5InAdo = ado
--   four :: ?Help <- (\inputArg -> 4)
--   in four

01-Keyword--Forall.purs

module Syntax.Basic.Keyword.Forall where

import Prelude

{-
When using generic data types in functions, such as the one below...
genericFunction0 :: a -> a
  Read:
    Given a value of any type,
  this function will return a value of the same type. -}

-- ... we need to explicitly say the function works for all types.
-- We do so by adding the "forall a." syntax to the front of our
-- type signature. Note: the "forall" syntax is still a part of our type
-- signature, but it always appears first before anything else.
genericFunction1 :: forall aType {- bType ... nType -}. aType -> aType
genericFunction1 x = x
{- Read:
    For any type,
      which we'll refer to as, 'a',
  when given a value of type 'a',
then I will return a value of type 'a'
-}

genericFunction2 :: forall a b c. a -> b -> c -> a
genericFunction2 a b c = a
{- Read:
    For any three types,
      which we'll refer to as, 'a', 'b', and 'c',
  when given
    a value of type 'a', and
    a value of type 'b', and
    a value of type 'c',
then I will return a value of type 'a'
-}

-- Sometimes, we'll see multiple 'forall' in the same type signature.
--
--    f :: forall a b. a -> b -> (forall c. c -> String) -> String
--
-- These are called "Rank-N Types."
-- This means that the third argument, the function with `forall c`,
-- can be used on different types. Thus, we can write something like this:

ignoreArg_returnString :: forall a. a -> String
ignoreArg_returnString _ = "some string"

example :: forall a b. a -> b -> (forall c. c -> String) -> String
example a b function = concat (function a) (function b)

testExample :: String
testExample = example true 5 ignoreArg_returnString

-- needed to compile

concat :: String -> String -> String
concat = append

02-Keyword--Type.purs

module Syntax.Basic.Keyword.Type where

-- Syntax
type TypeAliasForCompileTime = RunTimeType

-- Example
type ComplexFunction = Int -> (forall a b. a -> (forall c. c-> b) -> b)

-- and then use it here:
-- someFunction :: String -> ComplexFunction -> ReturnType

-- One could also do this...
----------------------
type Age = Int

{-
functionName :: ParamType1 -> ReturnType -}
functionName :: Age        -> String
            -- 'Age' is a more descriptive type name than 'Int'
functionName age = "body"

----------------------
-- ... but to do the above, one should use `newtype` instead,
--   which is explained later.

-- a type alias can also take a type parameter
type ConvertAToString a = (a -> String)

example :: forall a. a -> ConvertAToString a -> String
example a convertAToString = convertAToString a

-- Type aliases also have kind signatures. The above examples have
-- implicit kind signatures. The below example has an explicit one:
data Pair a b = Pair a b

-- kind signature (implicit): Type -> Type
-- reason: the `a` needs to be defined before we have a "concrete" type alias
type IntAnd a = Pair Int a

type IntAnd_ExplicitKindSignature :: Type -> Type
type IntAnd_ExplicitKindSignature a = Pair Int a

type SomeTypeAndInt :: Type -> Type
type SomeTypeAndInt a = Pair a Int

-- required to get this to compile correctly
data RunTimeType

03-Keywords--Case-expression-of.purs

module Syntax.Basic.Keyword.CaseOf where

import Prelude

-- Returning to our previous basic pattern match example:
data Fruit
  = Apple
  | Orange
  | Banana
  | Cherry
  | Tomato

-- The following is tedious to write due to rewriting 'mkString' on every line:
mkString :: Fruit -> String
mkString Apple = "apple"
mkString Orange = "orange"
mkString Banana = "banana"
mkString Cherry = "cherry"
mkString Tomato = "tomato"

-- Fortunately, there is an easier way using 'case _ of' syntax:
function1 :: String -> String
function1 expression =
  -- syntax
  case expression of
    -- pattern match -> bodyOfFunctionIfMatched

    -- These show a few examples from pattern matching
    "patternMatch1" -> bodyOfFunction
    "patternMatch2" -> bodyOfFunction
    x | length x == 4 -> bodyOfFunction    -- guards are also allowed here
      | length x == 5 -> bodyOfFunction
    _ -> bodyOfFunction -- catch all

-- If 'expression' is the next argument in a function, we could decide
-- to not bind to name (e.g. 'a') and instead use function abbreviation
-- using the underscore syntax:

data Data = Constructor1 | Constructor2 | Constructor3 | ConstructorN

function2 :: Data -> String
function2 = case _ of
  Constructor1 -> bodyOfFunction
  Constructor2 -> bodyOfFunction
  Constructor3 -> bodyOfFunction
  _ -> bodyOfFunction -- catch all


-- Returning to our example
mkString2 :: Fruit -> String
mkString2 = case _ of
  Apple -> "apple"
  Orange -> "orange"
  Banana -> "banana"
  Cherry -> "cherry"
  Tomato -> "tomato"

-- We can also match multiple expressions by adding commas between them:
-- Syntax:
function3 :: String -> String -> String
function3 firstExpression secondExpression =
  case firstExpression, secondExpression {-, nExpression -} of
    "firstResultPatternMatch", "secondResultPM" {-, nResult -} -> bodyOfFunction
    "firstResultPatternMatch", "secondResultPM" {-, nResult -} -> bodyOfFunction
    _, _ -> bodyOfFunction -- catchall

-- example
mkString3 :: Fruit -> Fruit -> String
mkString3 a b = case a, b of
  Apple, Apple -> "Two apples"
  Apple, Cherry -> "An apple and a cherry"
  _, _ -> "You didn't really think I would type out all of them, did you?!?"

-- This compiles: Pattern Matching -> Case -> Pattern guard
test :: Int -> Boolean
test a
  | false =
      case false of
        true | a > 12 -> true
        _ -> false
  | otherwise = true

-- Necessary to get this file to compile
length :: String -> Int
length _ = 4

bodyOfFunction :: String
bodyOfFunction = "body of function"

04-Keywords--Where-and-Let-In.purs

module Syntax.Basic.Keyword.WhereAndLetIn where

import Prelude

data Box a = Box a
{-
The 'let..in' keywords and the `where` keyword enables us to break large
  functions down into smaller functions (or values) that compose.             -}

{-
The 'let...in' syntax lets us define "bindings" before we use them
in the block that follows the `in` keyword: -}
letInFunction1 :: String -> String
letInFunction1 expression =
  let
    -- Start of the "let" block
    binding = expression
    -- End of the "let" block
  in
    -- Start of the "in" block
    somethingThatUses binding -- wherever `binding` is used, we mean `expression`
    -- End of the "in" block

{-
We can define multiple bindings. Earlier bindings cannot refer to later
bindings, but later ones can refer to earlier ones. -}
letInFunction2 :: String -> String -> String
letInFunction2 expression1 expression2 =
  let
    -- Start of the "let" block
    binding1 = expression1
    binding2 = expression2
    binding3 = binding1
    -- End of the "let" block
  in
    -- Start of the "in" block
    somethingThatUses (binding1 <> binding2 <> binding3)
    -- End of the "in" block

letInFunction2_WithTypeSignatures :: String -> String -> String
letInFunction2_WithTypeSignatures expression1 expression2 =
  let
    -- we can also add type signatures above the bindings to help with
    -- readability or type inference.
    binding1 :: String
    binding1 = expression1

    binding2 :: String
    binding2 = expression2
  in
    somethingThatUses (binding1 <> binding2)

-- One can also define functions as a let binding
letInFunction3 :: String -> String
letInFunction3 value =
  let
    function "firstMatch"  = bodyOfPatternMatch
    function "secondMatch" = bodyOfPatternMatch
    function catchAll      = bodyOfPatternMatch
  in
    function value

letInFunction3_WithTypeSignatures :: String -> String
letInFunction3_WithTypeSignatures value =
  let
    function :: String -> String
    function "firstMatch"  = bodyOfPatternMatch
    function "secondMatch" = bodyOfPatternMatch
    function catchAll      = bodyOfPatternMatch
  in
    function value

-- One can also use guards with let
letWithGuards :: Int -> String
letWithGuards x =
  let result
        | x == 0 = "zero"
        | x == 1 = "one"
        | otherwise = "something else"
  in computeSomethingWithString result

-- Let bindings can also have type signatures. We'll see in the next file
-- why this can be very important.
letWithGuards_WithTypeSignatures :: Int -> String
letWithGuards_WithTypeSignatures x =
  let
    result :: String
    result
          | x == 0 = "zero"
          | x == 1 = "one"
          | otherwise = "something else"
  in computeSomethingWithString result

{-
The `where` clause is "syntax sugar" for let bindings.
Using the `where` clause, we could rewrite the below function using the
`where` clause
    whereFunction0 = let x = 4 in x        -}
whereFunction0 :: Int
whereFunction0 = x
  where
  x = 4

-- Here is a more typical example where multiple bindings are defined
-- in a single "where block"
whereFunction1 :: String -> String -> Int
whereFunction1 arg1 arg2 =
  returnFour (madeUpFunction arg1 arg2) 9

  where
  -- functions defined below the 'where' keyword can be used in the main
  -- function and any other made-up functions defined in this block
  madeUpFunction :: String -> String -> Int
  madeUpFunction s1 s2 =
    returnFour (createComplexDataTypeUsing s1)
      (mutuallyRecursiveFunction1 s2)

  createComplexDataTypeUsing :: String -> Box String
  createComplexDataTypeUsing s = Box s

  -- Note: If 'whereFunction1' had used 'forall' syntax above to specify
  -- generic types, we would not need to respecify them in any made-up functions
  -- that appear in this where block. However, since we want `returnFour` to work
  -- for multiple types, we'll need to specify that here.
  returnFour :: forall a b. a -> b -> Int
  returnFour _ _ = 4

  -- Mutually recursive functions are allowed
  mutuallyRecursiveFunction1 :: String -> String
  mutuallyRecursiveFunction1 "a" = "a"
  mutuallyRecursiveFunction1 x = mutuallyRecursiveFunction2 (x <> "b") -- "<>" means concat

  mutuallyRecursiveFunction2 :: String -> String
  mutuallyRecursiveFunction2 "b" = mutuallyRecursiveFunction1 "b"
  mutuallyRecursiveFunction2 x = mutuallyRecursiveFunction1 "a"

{-
See the indentation rules to correctly indent your `where` clause
  and the expressions that define a given binding.
-}

-- necessary to make this file compile:

somethingThatUses :: String -> String
somethingThatUses x = x

bodyOfPatternMatch :: String
bodyOfPatternMatch = "body of pattern match"

computeSomethingWithString :: String -> String
computeSomethingWithString _ = "string value"

05-Indentation-Rules.purs

module Syntax.Basic.IndentationRules where

import Prelude

-- Indentation Rules:

function_normal :: String -> String
function_normal a = bodyOfFunction

function_body_indented :: String -> String
function_body_indented a = {- this shows valid and invalid indentation:
wrongIndentation -}
  validAndConventional <>
    validButNotConventional {-
      and so forth... -}

whereFunction1 :: String -> String
whereFunction1 a = validFunctionPosition1 <> validFunctionPosition2 <> validValuePosition
  where
  validFunctionPosition1 :: TypeSignature
  validFunctionPosition1 = "a"

  validFunctionPosition2 :: TypeSignature
  validFunctionPosition2 = "b"

  validValuePosition :: TypeSignature
  validValuePosition = "c"

whereFunction2 :: String -> String
whereFunction2 a = validFunctionPosition1 <> validFunctionPosition2 <> validValuePosition
  where
    validFunctionPosition1 :: TypeSignature
    validFunctionPosition1 = "a"

    validFunctionPosition2 :: TypeSignature
    validFunctionPosition2 = "b"

    validValuePosition :: TypeSignature
    validValuePosition = "c"

letInFunction1 :: String -> String
letInFunction1 expression =
  -- this format makes it harder to add a new binding if more are needed
  let binding = expression
  in bodyOfFunctionThatUses binding

letInFunction2 :: String -> String
letInFunction2 expression =
  -- this format makes it easy to add a new binding
  let
    binding = expression                                                    {-
    binding2 = some other expression                                        -}
  in
    bodyOfFunctionThatUses binding

-- See the `do` notation syntax for how to use `let` properly there

-- Necessary to make this file compile
type TypeSignature = String

bodyOfFunctionThatUses :: String -> String
bodyOfFunctionThatUses x = x

bodyOfFunction :: String
bodyOfFunction = ""

validAndConventional :: String
validAndConventional = ""

validButNotConventional :: String
validButNotConventional = ""

06-Let-Lacks-Generalization.purs

module Syntax.Basic.LetLacksGeneralization where

import Prelude

{-
We saw previously that we can define functions inside a `let` binding
and use it later after the `in`. The below example does NOT have a type
signature annotating it.                                                      -}
letBindingExample :: Int
letBindingExample =
  let
    add4 x = x + 4
  in add4 6 -- outputs 10

{-
We also saw that we can add type signatures to the `let` binding
to make it easier to read:                                                    -}
letBindingExampleWithTypeSignature :: Int
letBindingExampleWithTypeSignature =
  let
    add4 :: Int -> Int
    add4 x = x + 4
  in add4 6

{-
All of the above examples use monomorphism (i.e. each function only works
on 1 type / there IS NOT a "forall" anywhere), not polymorphism
(i.e. each function works on multiple types / there IS a "forall" somewhere).
In some situations, we may want to use polymorphism/"forall" in our
`let` bindings:                                                             -}
letBindingWithPolymorphicTypeSignature :: Int
letBindingWithPolymorphicTypeSignature =
  let
    ignoreArgumentAndReturn4 :: forall a. a -> Int
    ignoreArgumentAndReturn4 _ = 4
  in
    (ignoreArgumentAndReturn4 8) + (ignoreArgumentAndReturn4 "foo")

{-
In the above example, when you remove the type signature above the let binding,
you will discover that "`let` bindings lack generalization". The below example
will not compile. You can uncomment it and see for yourself:                  -}
-- failsToCompile :: Int
-- failsToCompile =
--   let
--     -- based on the usage below, it would appear that this function's
--     -- type signature is "forall a. a -> Int"
--     polymorphicLetBindingWithNoTypeSignature _ = 4
--   in
--     (polymorphicLetBindingWithNoTypeSignature 8) +   -- argument is Int
--     (polymorphicLetBindingWithNoTypeSignature "foo") -- argument is String

{-
Running `failsToCompile` will produce the following error:

  Error found:
  in module Syntax.Basic.LetLacksGeneralization
  at src/04-Various-Keywords/05-Let-Lacks-Generalization.purs:48:34 - 48:39 (line 48, column 34 - line 48, column 39)

    Could not match type

      String

    with type

      Int


  while checking that type String
    is at least as general as type Int
  while checking that expression "foo"
    has type Int
  in value declaration failsToCompile

  See https://github.com/purescript/documentation/blob/master/errors/TypesDoNotUnify.md for more information,
  or to contribute content related to this error.
-}

{-
When the compiler comes across the first usage of
`polymorphicLetBindingWithNoTypeSignature`, the type of the first argument, 8,
is Int. Rather than making this binding polymorphic, the compiler assumes
that the function is monomorphic and its type signature will be
"Int -> Int". Thus, when it encounters the second usage of the function,
`polymorphicLetBindingWithNoTypeSignature "foo"`, it fails because
`String` is not the same type as `Int`.

This missing feature is called "`let` generalization." Its absence is
intentional. For more context, see the paper titled,
"Let should not be generalized"
https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tldi10-vytiniotis.pdf
-}

{-
Since the `where` clause is syntax sugar for `let` bindings, this issue can
also arise when you use bindings in the `where` clauses that are polymorphic
and do not have a type signature.
-}

-- alsoFailsToCompile :: Int
-- alsoFailsToCompile =
--     (polymorphicFunctionWithNoTypeSignature 8) + -- argument is Int
--     (polymorphicFunctionWithNoTypeSignature "foo")
--
--     where
--       polymorphicFunctionWithNoTypeSignature _ = 4

-- This version will compile because the type signature
-- has been specified.
polymorphicWhereClauseWithTypeSignature :: Int
polymorphicWhereClauseWithTypeSignature =
    (polymorphicFunctionWithTypeSignature 8) + -- argument is Int
    (polymorphicFunctionWithTypeSignature "foo")

    where
      polymorphicFunctionWithTypeSignature :: forall a. a -> Int
      polymorphicFunctionWithTypeSignature _ = 4

07-Keywords--If-Then-Else.purs

module Syntax.Basic.Keyword.IfThenElse where

-- There's support for if-then-else statements
test1 :: Boolean -> String
test1 condition = if condition then "true path" else "false path"

-- Or write it like this
test2 :: Boolean -> String
test2 condition =
  if condition
  then "true path"
  else "false path"

-- Or this
test3 :: Boolean -> String
test3 condition =
  if
    condition
  then
    "true path"
  else
    "false path"

-- One can also write nested if-then-else-if-then-else statements
test4 :: forall a. (a -> Boolean) -> (a -> Boolean) -> a -> String
test4 condition1 condition2 a =
  if condition1 a then "first path"
  else if condition2 a then "second path"
  else "default path"

01-Basic-Syntax.purs

module Syntax.Basic.Record.Basic where

import Prelude

-- Records have a different kind than "Type"
-- Their kind signature is `Row Type -> Type`.

-- `Row` kinds are 0 to N number of "label-to-kind" associations
-- that are known at compile time. `Row` kinds will be covered more fully
-- in the Type-Level Programming Syntax folder.
-- Most of the time, you will see the labels associated with the kind, `Type`.
-- In other words:
type Example_Row = (rowLabel :: ValueType)

-- Rows can have 1 or many label-Type associations...
type Example_of_a_Single_Row = (labelName :: ValueType)
type Example_of_a_Multiple_Rows = (first :: ValueType, second :: ValueType)

type PS_Keywords_Can_Be_Label_Names =
  (data :: ValueType, type :: ValueType, class :: ValueType)

-- Rows can also have kind signatures. The right-most entity/kind
-- will be `Row Type`:
type SingleRow_KindSignature :: Row Type
type SingleRow_KindSignature = (labelName :: ValueType)

type MultipleRows_KindSignature :: Row Type
type MultipleRows_KindSignature = (first :: ValueType, second :: ValueType)

-- Rows can also be empty.
type Example_of_an_Empty_Row :: Row Type
type Example_of_an_Empty_Row = ()

-- Rows can take type parameters just like data, type, and newtype:
type Takes_A_Type_Parameter :: Type -> Row Type
type Takes_A_Type_Parameter a = (someLabel :: Box a)

-- That's enough about rows for now.
-- Let's see why they are useful for Records.

data Record_ -- Row Type -> Type

-- Think of records as a JavaScript object / HashMap / big product types.
-- There are keys (the labels) that refer to values of a given type.
type RecordType_Desugared = Record ( label1 :: String
                                -- , ...
                                   , labelN :: Int
                                   , function :: (String -> String)
                                   )
-- However, there is syntax sugar for writing this:
-- "Record ( rows )" becomes "{ rows }"
type RecordType = { label1 :: String
               -- , ...
                  , labelN :: Int
                  , function :: String -> String
                  }

-- ## Create Records

-- We can create a record using the "{ label: value }" syntax...
createRec_colonSyntax :: RecordType
createRec_colonSyntax = { label1: "value", labelN: 1, function: (\x -> x) }

-- We can also create it using the "names exist in immediate context" syntax

createRec_immediateContextSyntax :: RecordType
createRec_immediateContextSyntax = { label1, labelN, function }
  where
    label1 = "value"
    labelN = 1
    function = \x -> x

-- We can also create it using the "label names exist in external context" syntax
-- Given the below record type...
type PersonRecord = { username :: String
                    , age :: Int
                    , isCool :: String -> Boolean
                    }
-- ... and some values/functions with the same name as that record's labels...
username :: String
username = "Bob"

age :: Int
age = 4

isCool :: String -> Boolean
isCool _ = true

-- ... the compiler will infer below that 'username' should be "Bob"
-- because `username` is a value that exists in this module.
-- Note: this syntax won't pick up things that exist in other files.
createRec_externalContextSyntax :: PersonRecord
createRec_externalContextSyntax = { username, age, isCool }

createRec_noUnderscore :: String -> Int -> (String -> String) -> RecordType
createRec_noUnderscore label1 labelN function = { label1, labelN, function }

createRec_withUnderscore :: String -> Int -> (String -> String) -> RecordType
createRec_withUnderscore = { label1: _, labelN: _, function: _ }

-- same type signature as 'createRec_withUnderscore'
type InlineWithUnderscoreType = String -> Int -> (String -> String) -> RecordType

inlineExample1 :: InlineWithUnderscoreType
inlineExample1 =
  \label1 labelN function -> { label1: label1, labelN: labelN, function: function }

inlineExample2 :: InlineWithUnderscoreType
inlineExample2 =             { label1: _     , labelN: _     , function: _      }

-- ## Get the corresponding values in records

getLabel1 :: RecordType -> String
getLabel1 obj = obj.label1

-- ## Overwrite Labels' Values in Records

-- We can update a record using syntax sugar:
overwriteLabelValue_equalsOperator :: RecordType -> String -> RecordType
overwriteLabelValue_equalsOperator rec string = rec { label1 = string }

-- or by using an underscore to indicate that the next argument is the
-- record type
setLabelValue_recordUnderscore :: String -> RecordType -> RecordType
setLabelValue_recordUnderscore string = _ { label1 = string }                      {-

setLabelValue_recordUnderscore "bar" { label1: "foo" } == { label1: "bar" }        -}

-- or by using an underscore for both args if they come in the correct order:
-- record first and then the argument to apply to that record's label
setLabelValue_recordAndArgUnderscore :: RecordType -> String -> RecordType
setLabelValue_recordAndArgUnderscore = _ { label1 = _ }                            {-

setLabelValue_recordAndArgUnderscore { label1: "foo" } "bar" == { label1: "bar" }  -}

syntaxReminder :: String
syntaxReminder = """

Don't confuse the two operators that go in-between label and value!

"label OPERATOR value" where OPERATOR is
  "=" means "update the label of a record that already exists":
          record { label = newValue }
  ":" means "create a new record by specifying the label's value":
                 { label: initialValue }
"""

-- ## Nested Records

type NestedRecordType = { person :: { skills :: { name :: String } } }

nestedRecord_create :: String -> NestedRecordType
nestedRecord_create newName = { person: { skills: { name: newName } } }

nestedRecord_get :: NestedRecordType -> String
nestedRecord_get rec = rec.person.skills.name

nestedRecord_overwrite1 :: String -> NestedRecordType -> NestedRecordType
nestedRecord_overwrite1 newName p  = p { person { skills { name = newName } } }

nestedRecord_overwrite2 :: String -> NestedRecordType -> NestedRecordType
nestedRecord_overwrite2 newName    = _ { person { skills { name = newName } } }

nestedRecord_overwrite3 :: NestedRecordType -> String -> NestedRecordType
nestedRecord_overwrite3            = _ { person { skills { name = _       } } }

-- -- This fails to compile because the fields aren't specified
-- nestedRecord_overwrite4 :: NestedRecordType -> String -> NestedRecordType
-- nestedRecord_overwrite4            = _ { _      { _      { name = _       } } }

-- ## Pattern Matching on Records

-- We can also pattern match on a record. The label names must match
-- the label names of the record
patternMatch_allLabels :: Int
patternMatch_allLabels =
  let { label1, label2 } = { label1: 3, label2: 5 }
  in label1 + label2

patternMatch_someLabels :: String
patternMatch_someLabels =
  -- notice how we don't include 'label2' here
  -- in the pattern match
  let { label1 } = { label1: "a", label2: "b" }
  in label1

-- needed to compile
type ValueType = String
data Box a = Box a

02-Quoted-Key-Syntax.purs

module Syntax.Basic.Record.Quoted where

-- Credit goes to Justin Woo where I found out this documentation
-- was even possible:
-- https://github.com/justinwoo/quoted-record-property/blob/master/src/Main.purs

type QuotedKey = { "key" :: String }

creation :: QuotedKey
creation = { "key" : "value" }

getValue :: String
getValue = creation."key"

emojiKey :: String
emojiKey = { "😆" : "value" }."😆"

asianLanguageKey :: String
asianLanguageKey = { "日本語" : "Japanese" }."日本語"

03-Row-Polymorphism.purs

module Syntax.Basic.Record.RowPolymorphism where

import Prelude

-- We can also use literal records in our function type signatures:
getName1 :: { name :: String } -> String
getName1 { name: nameValue } = nameValue

getName2 :: { name :: String } -> String
getName2 person = person.name -- this syntax also works

-- example
test1 :: Boolean
test1 =
  (getName1 { name: "hello" }) == "hello"

{-
However, this definition does not allow additional fields.
The following code...

  getName1 { name: "hello", age: 4 }

...will output a compiler error since no other fields are allowed!
-}

{-
Rows can either be "closed" or "open." "Closed" rows means that we will
not be adding any other 'fields' to it at a later time. So far, we
have only shown examples of "closed" rows.
"Open" rows means that we might add more 'fields' to it at a later time.
We'll now show the syntax for that.
-}

-- open rows
type Example_of_Closed_Row              = (first :: ValueType)
type Example_of_Open_Row additionalRows = (first :: ValueType | additionalRows)

type Closed_Record1 = Record (first :: ValueType)
type Open_Record1 r = Record (first :: ValueType | r)

type Closed_Record2 = { first :: ValueType }
type Open_Record2 r = { first :: ValueType | r}

type OpenRecord1 rowsAreDefinedLater = Record ( | rowsAreDefinedLater)
type OpenRecord2 rowsAreDefinedLater = { | rowsAreDefinedLater}

{-
We can get rid of the compiler error by using open rows and row polymorphism

The below function can be read as
    "Given a record that has the field, 'name',
     and zero or more other rows I don't care about,
  I can give you a String value."                                            -}
rowPolymorphism1 :: forall anyOtherFieldsThatMayExist
                  . { name :: String | anyOtherFieldsThatMayExist }
                 -> String
rowPolymorphism1 { name: nameValue } = nameValue

-- Rather than the "anyOtherFieldsThatMayExist" type name, convention is to
-- use "r" for "rows". Rewriting our above function to use 'r' convention:
getName4 :: forall r. { name :: String | r } -> String
getName4 { name: nameValue } = nameValue

-- examples
test2 :: Boolean
test2 =
  (getName4 { name: "a name", age: 4, stuff: "?" }) == "a name" -- now it works!

-- A compiler error will arise when the required field doesn't exist,
-- such as this example:
--
--    getName4 { age: 4, stuff: "?" }


-- needed to compile
type ValueType = String

01-Regular.purs

module Syntax.Basic.InfixNotation.Regular where

import Prelude

two_arg_function :: Int -> Int -> Int
two_arg_function x y | x < 0 = (x + 1) * (y + 14)
                     | otherwise = y + x

infix_notation :: Int
infix_notation =
  -- infix notation available via backticks
  (1 `two_arg_function` 2)
  -- becomes two_arg_function 1 2

data List a = Nil | Cons a (List a)
data Box a = Box a

-- Some types given here to make things easier...
type TypeAlias = forall a b. List a -> Box b
data DataType = Constructor Int Int

{-
Infix Syntax:
infix/infixl/infixr precedence function/constructor as symbolicAlias

... or for type aliases:
infix/infixl/infixr precedence type TypeName as symbolicAlias

... where 'precedence' is a number (0..9)
    and 'symbolicAlias' is a sequence of symbolic character(s)
      (i.e. cannot use alphanumeric characters, nor an underscore character)
-}

-- Example
infixl 4 two_arg_function as >>
infix  2 Constructor as ?->
infix  4 type TypeAlias as :$>

mostCharactersForSymbolicAlias :: forall a. a -> a
mostCharactersForSymbolicAlias x = x

-- Notes:
-- 1. '@', '\', and '.' cannot be used alone (see next comment)
-- 2. the below symbols are the ones you will typically see. However,
--      characters where Haskell's `isSymbol` returns true also work.
--      (https://hackage.haskell.org/package/base-4.14.1.0/docs/Data-Char.html#v:isSymbol)
infix 4 mostCharactersForSymbolicAlias as ~!@#$%^&*-+=\|:<>./?

{-
These characters, when used individually as aliases, are illegal:
  infix 4 illegalAlias1 as \
                           ^ That's reserved for lambdas
  infix 4 illegalAlias2 as @
                           ^ That's reserved for pattern matching: a@(Foo 1)

  infix 4 illegalAlias3 as .
                           ^ That's reserved for record-related things: foo.bar
-}
-- When used with more characters than themselves, they're fine and compile.
whenMultipleExist_itsFine :: forall a. a -> a
whenMultipleExist_itsFine x = x

infix 4 whenMultipleExist_itsFine as \\
infix 4 whenMultipleExist_itsFine as @@
infix 4 whenMultipleExist_itsFine as ..
infix 4 whenMultipleExist_itsFine as \.
infix 4 whenMultipleExist_itsFine as .@

-- Infix is all about where to put the parenthesis as indicated by precedence:
-- precedence is 0 = group first
--               n = group after first but before last
--               9 = group last
--
-- Each type of infix will be shown by reducing it to its final call

-- make depth small (like a tree)
-- infix 0 concatString as $$$$$

{-  "a" $$$$$ "b"  $$$$$  "c" $$$$$ "d"
== ("a" $$$$$ "b") $$$$$ ("c" $$$$$ "d"))
== ($$$$$ "a" "b") $$$$$ ($$$$$ "c" "d"))
== concatString (concatString "a" "b") (concatString "c" "d")  -- desugared
-}

-- infixl 9 concatString as |>>|

{-    "a" |>>| "b"  |>>| "c"  |>>| "d"
==   ("a" |>>| "b") |>>| "c"  |>>| "d"
==  (("a" |>>| "b") |>>| "c") |>>| "d"
== |>>| (("a" |>>| "b") |>>| "c") "d"
== |>>| (|>>| ("a" |>>| "b") "c") "d"
== |>>| (|>>| (|>>| "a" "b") "c") "d"
== concatString (concatString (concatString "a" "b") "c") "d"
-}

-- infixr 7 concatString as |<<|

{- "a" |<<|  "b" |<<|  "c" |<<| "d"
== "a" |<<|  "b" |<<| ("c" |<<| "d")
== "a" |<<| ("b" |<<| ("c" |<<| "d"))
== |<<| "a" ("b" |<<| ("c" |<<| "d"))
== |<<| "a" (|<<| "b" ("c" |<<| "d"))
== |<<| "a" (|<<| "b" (|<<| "c" "d"))
== concatString "a" (concatString "b" (concatString "c" "d"))
-}

02-Extended.purs

module Syntax.Basic.InfixNotation.Extended where

-- Original credit: @paf31
-- Link: https://github.com/paf31/24-days-of-purescript-2016/blob/master/1.markdown
-- Changes made: use meta-language to explain syntax of extended infix notation
--
-- Licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
--   https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en_US

-- regular infix
-- function1 :: Type1 -> Type2 -> ReturnType
-- function1 a b = ...
--
-- a `function1` b

-- Given a function with this signature...
function2 :: String -> String -> String -> String
function2 first second third = "result"

example :: String
example = "second" `function2 "first"` "third"

-- This can be useful for combining function if it reads well
--    list1 `combineUsing concat` list2
-- But it can also quickly lead to unreadable code
-- Be careful and selective when using it.

07-Functions-and-Data-with-Higher-Kinded-Types.purs

module Syntax.Basic.Function.HigherKindedTypes where

import Prelude

-- == Review ==
data Box a = Box a
-- `Box a` is a higher-kinded type (HKT). In other words,
-- it has a kind of `Type -> Type`, not `Type` like Int.
-- Its 'a' still needs to be specified before it is fully concrete.

-- we can define a function when the 'a' of Box is known ("Int" in this case)...
add1 :: Box Int -> Box Int
add1 (Box x) = Box (x + 1)

-- we can also define a function when 'a' of Box is not known
modify :: forall a. Box a -> (a -> a) -> Box a
modify (Box a) function = Box (function a)

-- However, how might one write a function that works on all of
-- the four types below? In other words, how would we define a function
-- when we know we will have a higher-kinded type like `Box`, but
-- we don't know the exact type of that Box-like type?

data Box1 a = Box1 a
data Box2 a = Box2 a
data Box3 a = Box3 a
data Box4 a = Box4 a

-- The following function shows the syntax to follow.
hktFunction0 :: forall f a. f a -> f a
hktFunction0 boxN = boxN
{-
Read
  "f a"
as
  "f is a higher-kinded type
    that needs one type, `a`, specified
    before it can be a concrete type"

When using higher-kinded types, convention is to start with `f` and continue
down the alphabet for each higher-kinded type thereafter (e.g. `g`, `h`, etc.). -}

hktFunction1 :: forall f g h a. f a -> g a -> h a -> h a
hktFunction1 _ _ hOfA = hOfA

-- I think the convention of using 'f' has something to do with a Type Class
-- called Functor (covered in the Hello-World folder).


-- If the higher-kinded type we want to include in our function takes more than
-- one type, we just add the extra types beyond it
data HigherKindedTypeWith4Types a b c d = Constructor a b c d

hktFunction2 :: forall f a b c d. f a b c d -> f a b c d
hktFunction2 f_abcd = f_abcd
{-
Read the above
  "f a b c d"
as
  "f is a higher-kinded type
     that takes 4 types, 'a', 'b', 'c', and 'd',
     all of which need to be specified
     before 'f' can be a concrete type"
-}

-- We can also specify specific types in the function:
hktFunction3 :: forall f a b c. f a b c Int -> f a b c Int
hktFunction3 f_abc_Int = f_abc_Int
{-
Read
  "f a b c Int"
as
  "f is a higher-kinded type
    that takes 4 types, 'a', 'b', 'c', and 'd'.

  'd' has already been specified to 'Int',
    but
  the other types (a, b, and c) have yet to be specified.

  The compiler will complain
    if one passes in an 'f' type whose fourth type is not an Int.
-}


-- Returning to our previous question...
boxFunction :: forall f a. f a -> (f a -> a) -> (a -> a) -> (a -> f a) -> f a
boxFunction boxN unwrap changeA rewrap =
  rewrap (changeA (unwrap boxN))
-- The unwrap and rewrap functions in the above function are only needed to make
-- this compile. In many functions, they won't be needed due to
-- typeclasses (explained later).

-- If we specified functions like below for each of the box type...
unwrapBox2 :: forall a. Box2 a -> a
unwrapBox2 (Box2 a) = a

unwrapBox3 :: forall a. Box3 a -> a
unwrapBox3 (Box3 a) = a

rewrapBox2 :: forall a. a -> Box2 a
rewrapBox2 a = Box2 a

rewrapBox3 :: forall a. a -> Box3 a
rewrapBox3 a = Box3 a

-- The following code will compile
box2Example :: Box2 Int
box2Example = boxFunction (Box2 2) unwrapBox2 (_ + 1) rewrapBox2 -- Box2 3

box3Example :: Box3 Int
box3Example = boxFunction (Box3 3) unwrapBox3 (_ + 1) rewrapBox3 -- Box3 4

-- Keep in mind that any type that follows a 'forall' keyword could be
-- a higher-kinded type

-- Higher kinded types can also occur in data declarations:
data Type_with_HKT :: (Type -> Type) -> Type -> Type
data Type_with_HKT hkt a = Type_With_HKT_Constructor (hkt a)
{-
Thus we could have multiple values of this specific type, depending on what
type the `hkt` is:
  Type_With_HKT Array Int
  Type_With_HKT Box Int
-}

data Type_with_2_HKT :: (Type -> Type) -> (Type -> Type) -> Type -> Type
data Type_with_2_HKT hkt1 hkt2 a = Type_With_2_HKT_Constructor (hkt1 a) (hkt2 a)
-- Type_with_2_HKT Array Array a
-- Type_with_2_HKT Array Box a
-- Type_with_2_HKT Box Array a
-- Type_with_2_HKT Box Box a

01-Single-Paramter.purs

module Syntax.Basic.Typeclass.SingleParameter where

import Prelude

-- Basic Type classes

-- a type class definition...
class TypeClassName parameterType where
  functionName :: parameterType -> ReturnType

-- Or the parameter type could be the return type:
class TypeClassName_ parameterType where
  fromString :: String -> parameterType

-- example
class ToInt a where
  toInt :: a -> Int

-- ... and its implementation for SomeType
instance TypeClassName SomeType where
  functionName type_ = ReturnType

-- ## Type Class Instance Name Requirement
--
-- Previously, naming type class instances was required.
-- As of `v0.14.2`, this requirement has been dropped as the compiler
-- generates the instance name instead now.
--
-- Below is an example of a named type class instance. You
-- may continue to see these while the ecosystem catches up.
-- The name typically follows this naming convention:
--    `classNameType1NameType2Name...TypeNName`.
{-
instance typeClassNameSomeType :: TypeClassName SomeType where
  functionName type_ = ReturnType
-}

-- The rest of this repo will use unnamed type class instances.


instance ToInt Boolean where
  toInt true = 1
  toInt false = 0

test :: Boolean
test = (toInt true) == 0

-- Type classes can also specify values:

class TypeClassDefiningValue a where
  value :: a

instance TypeClassDefiningValue Int where
  value = 42

-- Type classes usually only specify one function, but sometimes
-- they specify multiple functions and/or values:

class ZeroAppender a where
  append :: a -> a -> a
  zeroValue :: a

instance ZeroAppender Int where
  append = (+)
  zeroValue = 0

warning_orphanInstance :: String
warning_orphanInstance = """

Be aware of what an 'orphan instance' is.

See the following link for more info:
https://github.com/purescript/documentation/blob/master/errors/OrphanInstance.md
"""

{-
Note: Type class instances that use type aliases (i.e. the `type` keyword)
will fail to compile. The following code demonstrates this.
-}

-- Uncomment me and I'll become a compiler error
-- type Age = Int
-- instance TypeClassDefiningValue Age where
--   value = 2

-- Type classes are useful for constraining types, which will be covered next.

-- necessary to make file compile
data ReturnType = ReturnType
data SomeType

02-Constraining-Types-Using-Typeclasses.purs

module Syntax.Basic.Typeclass.Constraints where

import Prelude

-- Adding a Type Class constraint to a type signature
-- enables usage of the corresponding type class' function in that context:

-- Syntax: Adding constraints on Function's type signature
function :: TypeClass1 Type1 => TypeClass2 Type2 => {- and so on -} Type1 -> ReturnType
function arg = "return result"

-- example

class ToInt a where
  toInt :: a -> Int

data List a
  = Nil               -- end of list
  | Cons a (List a)   -- a head element and the rest of the list (tail)

                      -- 'a' must have an 'ToInt' instance for this to compile
stringList_to_intList :: forall a. ToInt a => List a -> List Int
stringList_to_intList Nil = Nil
stringList_to_intList (Cons head tail) = Cons (toInt head) (stringList_to_intList tail)

-- Coupling this with the `forall` syntax:
function0 :: forall a b. TypeClass1 a => TypeClass2 b => a -> b -> String
function0 a b = "return result"



-- Syntax: Adding constraints on type class instances

-- This type class turns any type into a String so we can
-- print it to the console when needed
class Show_ a where -- this is the same signature for Show found in Prelude
  show_ :: a -> String

-- Problem:
-- Say we have a data type called "Box" that just contains a value:
data Boxx a = Boxx a

-- If we want to implement the `Show` typeclass for it, we are limited to this:
instance Show (Boxx a) where
  show (Boxx _) = "Box(<unknown value>)"

{-
We would like to also show the 'a' value stored in Box. How do we do that?
  By constraining our types in the Box to also have a Show instance: -}

-- Syntax
instance (TypeClass1 a) => {-
                   (TypeClassN a) => -} TypeClass1 (IntanceType a) where
  function1 _ = "body"

data Box a = Box a
{- example: Read the following as:
"I can 'show' a Box only if the type stored in the Box can also be shown."
-}
instance (Show a) => Show (Box a) where
  show (Box a) = "Box(" <> show a <> ")"

-- We have names for specific parts of the instance
instance (InstanceContext a) => A_TypeClass (InstanceHead a) where
  function2 _ = "body"

-- Implicit Usage: Since we know that the values below are of type "Box Int"
-- We can use "show" without constraining any types.
test1 :: Boolean
test1 =
  show (Box 4) == "Box(4)"

test2 :: Boolean
test2 =
  show (Box (Box 5)) == "Box(Box(5))"

-- Explicit Usage: The only thing we know about 'a' is that it can be shown.
showIt :: forall a. Show a => a -> String
showIt showableThing = show showableThing

-- All of these work because they all have a Show instance.
test3 :: String
test3 = showIt 4

test4 :: String
test4 = showIt (Box 5)

test5 :: String
test5 = showIt (Box (Box (Box 5)))

-- necessary to make file compile

class TypeClass1 a where
  function1 :: a -> String

class InstanceContext :: Type -> Constraint
class InstanceContext a

instance InstanceContext a

data InstanceHead :: Type -> Type
data InstanceHead a = InstanceHead

class A_TypeClass a where
  function2 :: a -> String

instance TypeClass1 String where
  function1 a = a

class TypeClass2 :: Type -> Constraint
class TypeClass2 a

instance TypeClass2 String

type Type1 = String
type Type2 = String
type ReturnType = String

data IntanceType a = InstanceType a

03-Dictionaries--How-Type-Classes-Work.purs

module Syntax.Basic.Typeclass.Dictionaries where

{-
Previously, we saw that `show` could be used "implicitly" when we
knew what the type was and "explicitly" when we did not know what
the type was but knew it had a constraint

In both cases, the compiler automatically figures out how which instance's
implementation to use. But how does it do this? How do Type Classes work?

Dictionaries are what enable a function/value to magically appear in the
implementation of a function's body. The below code is a summary of
this article about type class 'dictionaries' written by Jonathan Fischoff:
https://web.archive.org/web/20200116160958/https://www.schoolofhaskell.com/user/jfischoff/instances-and-dictionaries

I could not explain it clearer nor more concisely than Jonathan did.
-}

-- This code....
class ToBoolean a where
  toBoolean :: a -> Boolean

  unUsed :: a -> String

example :: forall a. ToBoolean a => a -> Boolean
example value = toBoolean value

-- ... gets desugared to this code

data ToBooleanDictionary a =
  ToBooleanDictionary
    { toBoolean :: a -> Boolean
    , unUsed :: a -> String
    }

example' :: forall a. ToBooleanDictionary a -> a -> Boolean
example' (ToBooleanDictionary record) value = record.toBoolean value

01-Partial.purs

module Syntax.Basic.Typeclass.Special.Partial where

-- This function is imported from the `purescript-partial` library.
import Partial.Unsafe (unsafePartial)

-- Normally, the compiler will require a function to always exhaustively
-- pattern match on a given type. In other words, the function is "total."

data TwoValues = Value1 | Value2

renderTwoValues :: TwoValues -> String
renderTwoValues = case _ of
  Value1 -> "Value1"
  Value2 -> "Value2"

-- In the above example, removing the line with `Value2 -> "Value2"`
-- from the source code would result in a compiler error as the function
-- would no longer be "total" but "partial."
-- However, there may be times when we wish to remove that compiler restriction.
-- This can occur when we know that a non-exhaustive pattern match will
-- not fail or when we wish to write more performant code that only works
-- when the function has a valid argument.

-- In such situations, we can add the `Partial` type class constraint
-- to indicate that a function is no longer a "total" function but is now
-- a "partial" function. In othe rwords, the pattern match is no longer
-- exhaustive. If someone calls the function with an invalid invalid argument,
-- it will produce a runtime error.

renderFirstValue :: Partial => TwoValues -> String
renderFirstValue Value1 = "Value1"
        -- There is no `Value2` line here!

-- When we wish to call partial functions, we must remove that `Partial`
-- type class constraint by using the function `unsafePartial`.

-- unsafePartial :: forall a. (Partial a => a) -> a

callWithNoErrors_renderFirstValue :: String
callWithNoErrors_renderFirstValue = unsafePartial (renderFirstValue Value1)

-- Uncomment this code and run it in the REPL. It will produce a runtime error.
callWithRuntimeErrors_renderFirstValue :: String
callWithRuntimeErrors_renderFirstValue =
  unsafePartial (renderFirstValue Value2)

02-Coercible.purs

module Syntax.Basic.Typeclass.Special.Coercible where

import Prelude
import Prim.Coerce (class Coercible)
import Safe.Coerce (coerce)

-- ## Linking to the paper for an (optional) detailed explanation

-- In this file, we'll provide a beginner-friendly summary of the paper
-- that is linked below. For our purposes, we will only explain the bare
-- minimum necessary to make the rest of this file make sense.

-- If you wish to know more, read the paper below. However, be warned that
-- those who are new to functional programming will likely not understand
-- as much until they understand the `Functor` and/or `Foldable` type classes.
-- These are covered in the `Hello World/Prelude-ish` folder in this project.

-- Here's the paper: "Safe zero-cost coercions for Haskell"
-- https://repository.brynmawr.edu/cgi/viewcontent.cgi?referer=&httpsredir=1&article=1010&context=compsci_pubs

---------------------------------------------------------------------------

-- ## Summary of the Problem

-- While we have stated earlier that newtypes are "zero-cost abstractions"
-- in that one does not incur a performance penalty for wrapping and unwrapping
-- a newtyped value, there are some situations where this is not true.

-- For example, let's say you had the following types:

-- | A comment that has multiple lines of text.
newtype MultiLineComment = MultiLineComment String

-- | A comment that has only 1 line of text.
newtype SingleLineComment = SingleLineComment String

-- Let's say we wish to convert an `Array MultiLineComment` into
-- `Array SingleLineComment` via the function,
--     `exposeLines :: String -> Array String`

-- While newtypes are "zero-cost abstractions," this particular algorithm
-- would incur a heavy performance cost. Here's what we would have to do:
-- 1. Convert the `MultiLineComment` type into the `String` type
--      by iterating through the entire `Array MultiLineComment` and unwrapping
--      the `MultiLineComment` newtype wrapper.
-- 2. Use `exposeLines` to convert each multi-line `String` into an `Array`
--      of Strings by iterating through the resulting array.
--      Each `String` in the resulting array would have only 1 line of content.
-- 3. Combine all `Arrays` of single-line `String`s into one Array.
--      In other words, `combine :: Array (Array String) -> Array String`
-- 4. Convert the `String` type into the `SingleLineComment` type
--       by iterating through the final `Array` and wrapping each `String` in a
--      `SingleLineComment` newtype.

-- Steps 1 and 4 are necessary to satisfy type safety. At the type-level,
-- a `String` is not a `MultiLineComment`, nor a `SingleLineComment`.
-- However, those three types do have the same runtime representation. Thus,
-- Steps 1 and 4 are an unnecessary performance cost. Due to using newtypes
-- in this situation, we iterate through the array two times more than needed.

-- A `MultiLineComment` can be converted into a `String` safely and
-- a `String` into a `SingleLineComment` safely. This type conversion
-- process is safe and therefore unnecessary. The problem is that the developer
-- does not have a way to provide the compiler with a proof of this safety.
-- If the compiler had this proof, it could verify it and no longer complain
-- when the developer converts the `Array MultiLineComment` into an
-- `Array String` through a O(1) functio.

-- The solution lays in two parts: the `Coercible` type class
-- and "role annotations."

-- ## Coercible

-- This is the exact definition of the `Coercible` type class. However,
-- we add the "_" suffix to distinguish this fake one from the real one.
class Coercible_ a b where
  coerce_ :: a -> b

-- The `Coercible` type class says, "I can safely convert a value of type `a`
-- into a value of type `b`." This solves our immediate problem, but it
-- introduces a new problem. Since the main usage of `Coercible` is to
-- remove the performance cost of newtypes in specific situations, how do
-- make it impossible to write `Coercible` instances for invalid types?

-- For example, a `DataBox` is a literal box at runtime because it uses the
-- `data` keyword. It actually has to wrap and unwrap the underying value:
data DataBox a = DataBox a

-- The `NewtypedBox` below is NOT a literal box at runtime because
-- it doesn't actually wrap/unwrap the underlying value.
newtype NewtypedBox theValue = NewtypedBox theValue

-- Thus, while we could have a type class instance for `MultiLineComment`,
-- `String`, and `SingleLineComment`, should we have an instance
-- between `DataBox` and `NewtypedBox`? The answer is no.
--
-- However, how would we tell that to the compiler, so it could verify that
-- for us? The answer is "role annotations."

-- ## Role Annotations

-- For another short explanation, see the answer to the post,
-- "What is a role?" https://discourse.purescript.org/t/what-is-a-role/2109/2

-- Role annotations tell the compiler what rules to follow when determining
-- whether a Coercible instance between two types is valid. There are
-- three possible values: representational, phantom, and nominal.

-- Role annotation syntax follows this syntax pattern:
-- `type role TheAnnotatedType oneRoleAnnotationForEachTypeParameter`

-- ### Representational

-- Representational says,
--  "If `A` can be safely coerced to `B` and the runtime representation of
--     `Box a` does NOT depend on `a`, then `Box a` can be safely
--     coerced to `Box b`." (in contrast to `nominal`)

-- Given a type like Box, which only has one type parameter, `a`...
data Box a = Box a

-- ... we would write the following:
type role Box representational

-- Here's another example that shows what to do when we have
-- multiple type parameters
data BoxOfThreeValues a b c = BoxOfThreeValues a b c
type role BoxOfThreeValues representational representational representational

-- ### Phantom

-- Phantom says,
-- "Two phantom types never have a runtime representation. Therefore,
-- two phantom types can always be coerced to one another."

-- Given a box-like type that has a phantom type parameter, `phantomType`...
data PhantomBox :: Type -> Type
data PhantomBox phantomType = PhantomBox

-- ... we would write the following:
type role PhantomBox phantom

-- Here's another example that mixes role annotations:
data BoxOfTwoWithPhantom :: Type -> Type -> Type -> Type
data BoxOfTwoWithPhantom a phantom b = BoxOfTwoWithPhantom

type role BoxOfTwoWithPhantom representational phantom representational

-- ### Nominal

-- Nominal says,
--  "If `A` can be safely coerced to `B` and the runtime representation of
--     `Box a` DOES depend on `a`, then `Box a` can NOT be safely
--     coerced to `Box b`." (in contrast to `representational`)

-- When we don't have enough information (e.g. writing FFI), we default
-- to the nominal role annotation. Below, we'll see why.

-- For example, let's consider `HashMap key value`. Let's say we use a type class
-- called `Hashable` to calculate the hash of a given key. Since newtypes
-- can implement a different type class instance for the same runtime
-- representation, wrapping that value in a newtype and then hashing it
-- might not produce the same hash as the original. Thus, we would return
-- a different value.

class Hashable key where
  hash :: key -> Int

instance hashableInt :: Hashable Int where
  hash key = key

newtype SpecialInt = SpecialInt Int
derive instance eqSpecialInt :: Eq SpecialInt
instance hashableSpecialInt :: Hashable SpecialInt where
  hash (SpecialInt key) = key * 31

data Map key value = Map key value

type role Map representational representational

data Maybe a = Nothing | Just a
derive instance eqMaybe :: (Eq a) => Eq (Maybe a)

lookup :: forall key1 key2 value
        . Coercible key2 key1 => Hashable key1
       => Map key1 value -> key2 -> Maybe value
lookup (Map k value) key =
  let
    coercedKey :: key1
    coercedKey = coerce key
  in if hash k == hash coercedKey
       then Just value
       else Nothing

normalMap :: Map Int Int
normalMap = Map 4 28

-- This will output `true`
testLookupNormal :: Boolean
testLookupNormal = (lookup normalMap 4) == (Just 4)

-- This will output `false`
testLookupSpecial :: Boolean
testLookupSpecial = (lookup specialMap 4) == (Just 4)
  where
    -- changes `Map 4 28` to `Map (SpecialInt 4) 28`
    specialMap :: Map SpecialInt Int
    specialMap = coerce normalMap

-- To prevent this possibility from ever occurring, we indicate that
-- a type parameter's role is 'nominal'. Rewriting our `Map` implementation
-- so that `key` is nominal would prevent this from occurring. Since
-- the `value` type parameter does not affect the runtime representation,
-- it can be representational.

data SafeMap key value = SafeMap key value

type role SafeMap nominal representational

04-Typeclass-Relationships.purs

module Syntax.Basic.Typeclass.RequiredTypeClasses where

import Prelude

{-
Type classes can also have relationships with other type classes.

While the syntax looks hierarchial (i.e. parent-child relationships),
they aren't necessarily hierarchical. Rather, one should see them as
"conditional," which will be shown soon.
-}

-- Here's the syntax. It reads,
--    "Type 'a' can have an instance of the type class,
--    'ActualTypeClass.' However, it must also have an instance
--    of the type class, 'RequiredTypeClass.'"
class RequiredTypeClass a <= ActualTypeClass a where
  functionName :: a -> ReturnType

-- examples
-- the required type class of 'PlusFive'
class ToInt a where
  toInt :: a -> Int

class ToInt a <= PlusFive a where
  plusFive :: a -> Int

-- Writing an instance of ActualTypeClass does not require a constraint
-- from RequiredTypeClass in its type signature as this is already known due
-- to `ActualTypeClass`'s definition
instance ActualTypeClass TheType where
  functionName _ = "body"

-- example
instance ToInt Boolean where
  toInt true = 1
  toInt false = 0

instance PlusFive Boolean where
  plusFive b = 5 + toInt b

-- using it in code
test1 :: Boolean
test1 = (plusFive true) == 6

test2 :: Boolean
test2 = (plusFive false) == 5

-- Now let's explain what we mean by "conditional."
instance ToInt Int where
  -- notice how the required type class, `ToInt`, is using functions
  -- from its extension type class, `PlusFive`.
  toInt x = plusFive x

instance PlusFive Int where
  plusFive b = 5 + b

-- If a type implements instances for a number of type classes, its instances
-- can use any of these type class' functions/values. Still,
-- one of those instances will actually need to be independent from the
-- others (i.e. it doesn't use any functions/values from other type classes).



-- A type class can also combine multiple typeclasses. Sometimes,
-- they will add additional functionality or laws. Other times,
-- they simply combine two or more type classes into one.

class RequiredTypeClass1 a where
  fn1 :: a -> String

class RequiredTypeClass2 a where
  fn2 :: a -> String

-- Example of combining and adding additional functionality:
class (RequiredTypeClass1 a, RequiredTypeClass2 a {-, ... -}) <= TheTypeClass a where
  function :: a -> a

-- Example of only combining and not adding any additional functionality.
-- Sometimes, this will add another law; other times, it only combines
-- multiple type classes together:
class (RequiredTypeClass1 a, RequiredTypeClass2 a {-, ... -}) <= CombineOnly a

-- necessary to make file compile

type ReturnType = String
data TheType = TheType

class RequiredTypeClass a where
  fn :: a -> String

instance RequiredTypeClass TheType where
  fn _ = "body"

05-Typeclasses-with-No-Definitions.purs

module Syntax.Basic.Typeclasses.NoDefinition where

import Prelude

{-
Some type classes do not declare any functions or values in their definition.

This usually occurs for one of two reasons:

1. They specify another law on top of the previous type class.

For example, the `ToString` type class converts any type's value into a String.
-}
class ToString a where
  toString :: a -> String

-- However, it doesn't specify how big or small that String should be.
-- So, we can extend it with another type class that adds a law on how
-- long the String can be before it's too long.

class (ToString a) <= ToString_50CharLimit a -- no "where" keyword here!
  -- no function or value here!

-- Assuming we've already written the `ToString` instance,
-- to create an instance for the above type class, we'd write:
instance ToString_50CharLimit Int -- no "where" keyword!
-- This instance means the developer who wrote it asserts that
-- the given type, Int, satisfies the given law.

{-
A developer making a library that specifies such a type as this has
documented through the types what users of that library should expect:
  calling 'toString a' will produce a String that is 50 chars or less.

Note:
Since the following are true...
  - `toString` defines a function `toString`
  - Int has an instance of `ToString`
  - `ToString_50CharLimit` adds a law to `ToString`'s `toString` function
  - Int has an instance of `ToString_50CharLimit`

... then a function that uses `show Int` will still output a String
that is 50 chars or less, even if the Int is not constrained to
the `ToString_50CharLimit` type class.

Since the latter imposes more restrictions on the former, the former must
also abide by those restrictions.

If one wanted to avoid this, they should define a new type class that
adds a function specifically for that:

  class (ToString a) <= ToString_50CharLimt_2 a where
    toString_limited a = -- implementaton that uses 'show'
-}

-- 2. Some type classes merely combine two or more type classes together:

data Box a = Box a

class Wrap a where
  wrapIntoBox :: a -> Box a

class Unwrap a where
  unwrapFromBox :: Box a -> a

class (Wrap a, Unwrap a) <= Boxable a

-- To create an instance of `Boxable`, we need to define instances
-- for Wrap, Unwrap, and Boxable, even if `Boxable` doesn't require
-- you to implement any functions/values.

instance Wrap Int where
  wrapIntoBox i = Box i

instance Unwrap Int where
  unwrapFromBox (Box i) = i

instance Boxable Int

useBoxable :: forall a. (Boxable a) => a -> a
useBoxable a = unwrapFromBox (wrapIntoBox a)

-- Necessary to compile

instance ToString Int where
  toString = show

06-Type-Class-Kind-Signatures.purs

module Syntax.Basic.Typeclass.KindSignatures where

import Prelude

{-
We saw previously that a data type can have a kind signature:
-}

-- Kind Signature: Type -> Type -> Type
data ImplicitKindSignature1 a b = ImplicitKindSignature2 a b String

data ExplicitKindSignature1 :: Type -> Type -> Type
data ExplicitKindSignature1 a b = ExplicitKindSignature1 a b String

-- Kind Signature:         Type -> Type
type ImplicitKindSignature2 a = ImplicitKindSignature1 a Int

type ExplicitKindSignature2 :: Type -> Type
type ExplicitKindSignature2 a = ExplicitKindSignature1 a Int

-- We also saw that we can use type classes to constrain data types
showStuff :: forall a. Show a => a -> String
showStuff a = "Showing 'a' produces " <> show a

{-
It turns out that type classes can also have kind signatures.
However, rather than the right-most value representing a "concrete" type,
these represent a "concrete" constraint.                                      -}

-- Kind Signature: Type -> Consraint
class ImplicitKindSignature a where
  someValue1 :: a -> String

class ExplicitKindSignature :: Type -> Constraint
class ExplicitKindSignature a where
  someValue2 :: a -> String

-- Remember, `data` and `type`'s right-most entity/kind is `Type` whereas
-- type classes' right-most entity/kind is `Constraint`.

07-Multi-Paramter.purs

module Syntax.Basic.Typeclass.MultiParameters where

import Prelude

-- A type class can have more than just a single parameter as its type.

-- Syntax
class MultiParameterTypeClass1 type1 type2 {- typeN -} where
  functionName1 :: type1 -> type2 -> {- typeN -> -} ReturnType

-- Again, a parameter could be the return type
class MultiParameterTypeClass2 type1 type2 {- typeN -} where
  functionName2 :: Int -> type1 -> {- typeN -> -} type2

-- example (not practical, but gets the idea across)
class ConvertFromAToB a b where
  convert :: a -> b

instance ConvertFromAToB Boolean String where
  convert true = "true"
  convert false = "false"

instance ConvertFromAToB Boolean Int where
  convert true = 1
  convert false = 0

toString :: forall a. ConvertFromAToB a String => a -> String
toString a = convert a

test :: Boolean
test = (toString true) == "true"

-- necessary to make file compile
type ReturnType = String

08-Functional-Dependencies.purs

module Syntax.Basic.Typeclass.MultiParameters.FunctionalDependencies where

{-
Sometimes in multi-parameter type classes, there is a relationship
between the types. In such cases, we call them 'functional dependencies' (FDs).

The next block summarizes these links:
- https://stackoverflow.com/questions/20040224/functional-dependencies-in-haskell/20040427#20040427
- https://stackoverflow.com/questions/20040224/functional-dependencies-in-haskell/20040343#20040343
- Section 2.1.2 shows an example where it needs FDs to work correctly
    https://jgbm.github.io/pubs/morris-icfp2010-instances.pdf

Syntax:
  Read
    class SomeClass type1 type2 | type1 -> type2
  as
    "Once you tell the type inferencer what the types on the left-hand side
    of the arrow are (e.g. `type1`), then the type inferencer will stop
    trying to infer what the types on the right-hand side of the arrow are
    (e.g. `type2`).

    Rather, the compiler will look for an instance where
    the left-hand side types are defined and use that instance
    to determine what the right-hand side types are. If the compiler finds
    multiple instances where the left-hand side types are the same types
    between instances but the right-hand side types are different,
    it will throw a compiler error.
-}
class TypeClassWithFunctionalDependency type1 type2 | type1 -> type2  where
  functionName1 :: type1 -> type2

-- Example

data Box a = Box a

class Unwrap a b | a -> b where
  unwrap :: a -> b

-- Here, the type of "a" (i.e. Box String) determines what "b" will be:
instance Unwrap (Box String) String where
  unwrap (Box s) = s

{-
If we defined another instance of `Unwrap` where
"a" is the same type (e.g. `Box String`) but `b` is different,
the compiler will throw an error:

instance Unwrap (Box String) Int where
  unwrap (Box s) = length s                                                 -}

------------------------

-- If multiple types determine what another type is, use this syntax:
class ManyTypesDetermineAnotherType a b c | a b {- n -} -> c  where
  functionName2 :: a -> b -> c

class OneTypeDeterminesManyTypes a b c | a -> b c where
  functionName3 :: a -> b -> c

-- We can also add an explicit kind signature here:
class OneInfersMany_ExplicitKindSignature :: Type -> Type -> Type -> Constraint
class OneInfersMany_ExplicitKindSignature a b c | a -> b c where
  functionName4 :: a -> b -> c

------------------------

{-
In some situations, there might be multiple ways to determine
a type. In such cases, we can use multiple FDs to tell the compiler
how to infer a given type in the type class.

The following two FDs can be read as,
  "Make the type checker try to find an instance of ManyFDRelationships where
  the `a` type and `b` type are known and then use
  the instance to infer what the `c` type is.

  However, if the type checker can't ultimately find such an instance,
  then try to find an instance where the `c` type is known and
  use that instance to infer what the `a` type and `b` type are."
-}
class ManyFDRelationships a b c | a b -> c, c -> a b where
  functionName5 :: a -> b -> c

-- Same thing but with a kind signature.
class ManyFDRelationships_KindSignature :: Type -> Type -> Type -> Constraint
class ManyFDRelationships_KindSignature a b c | a b -> c, c -> a b where
  functionName6 :: a -> b -> c

{-
In short, the type checker will use the FDs to determine how it should "unify"
the types together. If one FD fails, it'll go to the next one. If all of them
fail, it'll assume that there is no such type class instance.
-}

{-
In Haskell literature, functional dependencies can also be written as
"Type Families." To see how one can write the same concept in both
styles, see the below link:
https://wiki.haskell.org/GHC/Type_families#The_class_declaration_2

For advantages/disadvantages of both approaches, see these links:
https://wiki.haskell.org/Functional_dependencies_vs._type_families
https://ghc.haskell.org/trac/ghc/wiki/TFvsFD
-}

09-Instance-Chains.purs

module Syntax.Basic.Typeclass.InstanceChains where

-- ## Instance Chains: Syntax

import Prelude -- imports the '+' operation below...

data Type1 = Type1
data Type2 = Type2
data Type3 = Type3

-- A kind signature is necessary here because `theType`
-- `ExampleClass1` doesn't define a function or value that refers to `theType`
-- in that function/value's type signature.
class ExampleClass1 :: Type -> Constraint
class ExampleClass1 theType

-- Instance chains are a workaround to the problem of "overlapping instances."
-- Here's how the syntax works:
instance ExampleClass1 Type1
else instance ExampleClass1 Type2
-- ...
else instance ExampleClass1 Type3

-- For readability, the `else` and `instance` keywords can appear on
-- their own line or with a newline separating the keywords
class ExampleClass2 :: Type -> Constraint
class ExampleClass2 theType

instance ExampleClass2 Type1
else
instance ExampleClass2 Type2
else

instance ExampleClass2 Type3

-- ## Instance Chains: Use Cases

-- Instance chains are useful because they allow you to define multiple
-- instances for a given type class, but define the order in which the
-- type class constraint is solved.

data SomeRandomType
  = FirstValue
  | SecondValue

class ProduceAnInt a where
  mkInt :: a -> Int

-- When solving for `ProduceAnInt someType`, the compiler will
-- solve for `someType` in the following order:
instance ProduceAnInt Int where
  mkInt theInt = theInt
else
instance ProduceAnInt String where
  mkInt _ = 13
else
instance ProduceAnInt SomeRandomType where
  mkInt FirstValue = 89
  mkInt SecondValue = 98
else
instance ProduceAnInt allOtherPossibleTypes where
  mkInt _ = 42

data HasNoInstance = HasNoInstance

example :: Int
example =
  (mkInt 1 ) + (mkInt "foo") + (mkInt FirstValue) + (mkInt HasNoInstance)     {-

  which, once the constraints are solved, will be the same as computing

  (1)        + (13)          + (89)               + (42)                      -}


-- ## Instance Chains Gotchas: No Backtracking

-- Given the following type class

class Stringify a where
  stringify :: a -> String

-- One might write an instance chain like so with the following idea:
-- 1. First attempt to show the item using that type class instance
-- 2. Otherwise, indicate that it cannot be shown.

instance (Show allPossibleTypes) => Stringify allPossibleTypes where
  stringify a = show a
else
instance Stringify a where
  stringify _ = "The value could not be converted into a String."

-- Then, one might attempt to use that code like so:
data Foo = Foo

-- failsToCompile :: String
-- failsToCompile = stringify "a normal string" <> stringify Foo

{-
Uncommenting that will produce the following compiler error:

  No type class instance was found for

      Data.Show.Show Foo


  while applying a function stringify
    of type Stringify t0 => t0 -> String
    to argument Foo
  while checking that expression stringify Foo
    has type String
  in value declaration failsToCompile

  where t0 is an unknown type

-}

-- Why does this occur? Because the `doMeFirst` instance will match on
-- every type since the parameter passed to Stringify is literally
-- `allPossibleTypes`. It will then attempt to find the `Show` instance
-- for `allPossibleTypes`. In the case of `Foo`, which does not
-- have such an instance, the compiler does not "backtrack" and
-- attempt to use the `defaultToMeOtherwise` instance. Rather, it immediately
-- fails with the above error.
-- Backtracking is a feature that has not yet been implemented in the
-- compiler.

11-Keyword--Newtype.purs

module Syntax.Basic.Newtype where

import Prelude

{-
The last data type keyword to explain here is `newtype`.

It is a compile-time-only type that only takes one type as its argument.

These are useful primarily for two reasons
  - They add a more meaningful name to another type (like type aliases)
      but act as a completely different type (unlike type aliases).
  - They are compile-time-only types, so one does not incur runtime overhead
      (like type aliases).
  - They need to be constructed/deconstructed in code (unlike type aliases).
      When used with Phantom Types (see the `Design Patterns` folder), they can
      restrict how developers can use the type in very useful ways.
  - They enable one to define multiple type class instances for the same type.
-}

-- Syntax:
newtype NewTypeName = OnlyAllowsOneConstructor WhichOnlyTakesOneArgument_TheWrappedType

newtype NamedStringType = NamedStringType String

-- Pattern matching on a type defined via the `newtype` keyword works
-- just like a type defined via the `data` keyword.
-- You can expose the value wrapped by the newtype constructor
-- using a pattern match and/or `case _ of` syntax
patternMatching :: String
patternMatching =
  unwrap_a_newtype_via_pattern_match
    (wrap_a_value_via_constructor
      (unwrap_a_newtype_via_pattern_match
        (NamedStringType "example")
      )
    )

  where
    wrap_a_value_via_constructor :: String -> NamedStringType
    wrap_a_value_via_constructor str = NamedStringType str

    unwrap_a_newtype_via_pattern_match :: NamedStringType -> String
    unwrap_a_newtype_via_pattern_match (NamedStringType str) = str

    unwrap_a_newtype_via_case_of_syntax :: NamedStringType -> String
    unwrap_a_newtype_via_case_of_syntax = case _ of
      NamedStringType str -> str

-- Given the following code:
data Box a = Box a

class Show_ a where
  show_ :: a -> String

instance (Show a) => Show (Box a) where
  show (Box a) = "Box(" <> show a <> ")"

-- What if we wanted to use a different type class instance for `Box` in some
-- situations, but not want to redefine `Box` as a new type with a different
-- name? We would do this:
newtype Box2 a = Box2 (Box a)

-- Since `Box2` is a different type than `Box`, we can define a type class
-- instance on it. This is a way to provide an alternative `Show` instance
-- on the underlying `Box` type.
instance (Show a) => Show (Box2 (Box a)) where
  show (Box2 (Box a)) = "Box with value of [" <> show a <> "] inside of it."

-- Or, to add more context to a type, we can use a newtype to ensure we
--   - don't use a `String` where we need to use a `Name`.
--   - don't use an `Int` where we need to use an `Age`.
newtype Name = Name String
newtype Age = Age Int
newtype Relationships = Relationships (List People)

-- Assuming all three above have a Show instance:
--
-- printPerson :: Name -> Age -> Relationships -> String
-- printPerson (Name n) (Age i) (Relationships l) =
--    "Name: " <> n <> ", Age: " <> show i <> ", Relationships: " <> show l

-- Similar to `data` and `type`, newtypes can also have a kind signature:

-- Implicit kind signature: Type -> Type
newtype SomeValue a = SomeValue (Box a)

newtype SomeValue_ExplicitKindSignature :: Type -> Type
newtype SomeValue_ExplicitKindSignature a = SomeValueExplicit (Box a)

-- needed to compile

type WhichOnlyTakesOneArgument_TheWrappedType = String

data List :: Type -> Type
data List a

data People

12-Recursive-Types-Newtypes.purs

module Syntax.Basic.Newtype.Recursive where

-- The following code does not compile because type synonyms are
-- expanded to their definition. So, the following code produces
-- an infinite loop
--
-- data Either l r
--   = Left l
--   | Right r
-- type Foo = { value :: Either Int Foo }
--
-- which expands to...
--  { value :: Either Int Foo }
--  { value :: Either Int { value :: Either Int Foo } }
--  { value :: Either Int { value :: Either Int { value :: Either Int Foo } } }
--  { value :: Either Int { value :: Either Int { value :: Either Int ... } } }

-- We can workaround that problem by wrapping the type in a newtype
data Either l r
  = Left l
  | Right r

newtype Foo = Foo { value :: Either Int Foo }

example1 :: Foo
example1 = Foo { value: Left 1 }

example2 :: Foo
example2 = Foo { value: Right (Foo { value: Left 1 }) }

example3 :: Foo
example3 = Foo { value: Right (Foo { value: Right (Foo { value: Left 1 }) }) }

21-Deriving-Common-Typeclass-Instances-for-Custom-Types.purs

module Syntax.Basic.Deriving.Typeclass where

import Prelude


-- Given the following type classes, Eq and Ord

-- | Determines whether two values of the same type are equal
class Eq_ a where
  eq_ :: a -> a -> Boolean


data Ordering_ = LT_ | GT_ | EQ_

-- | Determines whether left is less than, greater than, or equal to right
class Ord_ a where
  compare_ :: a -> a -> Ordering_

-- Original credit: @paf31
-- Link: https://github.com/paf31/24-days-of-purescript-2016/blob/master/3.markdown
-- Changes made: use meta-language to explain type class derivation syntax
--
-- Licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
--   https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en_US
data Type1
  = First Int
  | Second String

-- To create instances of `Eq` and `Ord` for `Type` we'd usually write it by hand:
instance Eq Type1 where
  eq (First a) (First b) = a == b
  eq (Second a) (Second b) = a == b
  eq _ _ = false

instance Ord Type1 where
  compare (First a) (First b) = compare a b
  compare (First _) _ = LT
  compare (Second a) (Second b) = compare a b
  compare (Second _) _ = GT

-- Imagine if we added a Third constructor to Type. We'd need to account for
--   that type as well now.
-- This gets tedious and, fortunately, the compiler can figure out what these
-- should be based on the 'shape' of the types. To reduce the boilerplate,
-- we can just add `derive` in front of the instance and not implement
-- the function:

data Type2
  = First2 Int
  | Second2 String

derive instance Eq Type2
derive instance Ord Type2

test2 :: Boolean
test2 =
  (compare (First2 1) (Second2 "Foo")) == LT

-- Ordering between "First" and "Second" depend on their sequence in the ADT.

data Type3
  = Second3 String
  | First3 Int

derive instance Eq Type3
derive instance Ord Type3

test3 :: Boolean
test3 =
  (compare (First3 1) (Second3 "Foo")) == GT

-- In other cases (like higher-kinded types),
-- we can use type class constraints to derive them:
data Box a = Box a
derive instance Eq a => Eq (Box a)
derive instance Ord a => Ord (Box a)

{-
Note: this works for only two reasons:

First, because Int and String
  both have an Eq and Ord instance. If one of these did not,
  then the compiler would not know how to create them.

Second, because we can only derive typeclasses for a few
  type classes:
  - Data.Eq (from `purescript-prelude`)
  - Data.Ord (from `purescript-prelude`)
  - Data.Functor (from `purescript-prelude`)

(These type classes can also be derived but they use a different syntax):
  - Data.Newtype (from `purescript-newtype`)
  - Data.Generic.Rep (from `purescript-generics-rep`)
-}

22-Deriving-Typeclass-Instances-for-Newtyped-Types.purs

module Syntax.Basic.Deriving.Newtype where

import Prelude

-- needed for deriving Newtype example
import Data.Newtype (class Newtype, over)

-- License applies to a portion of this document --> Start

-- Original credit: @paf31
-- Link: https://github.com/paf31/24-days-of-purescript-2016/blob/master/4.markdown
-- Changes made: use meta-language to explain newtype typeclass derivation syntax
--
-- Licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
--   https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en_US

-- Newtypes are used to wrap existing types with more information around them
newtype EmailAddress1 = EmailAddress1 String

-- However, defining type class instances for them
-- can get tedious:

instance Eq EmailAddress1 where
  eq (EmailAddress1 s1) (EmailAddress1 s2) = s1 == s2
-- same for Ord type class

-- same for other newtypes
newtype Phone = Phone String
newtype FirstName = FirstName String
-- etc...

-- This is boilerplate since we unpack the instance and delegate the function
-- to the wrapped type's implementation. It's also inefficient in terms of
-- evaluation. Purescript gives us a way to derive, for newtypes, any instance
-- the boxed type implements, using the following syntax.
-- We use 'derive newtype' in front of the instance:

newtype EmailAddress2 = EmailAddress2 String

derive newtype instance Eq EmailAddress2
derive newtype instance Eq Phone
derive newtype instance Eq FirstName

-- <--- End

-- License applies to a portion of this document --> Start

-- Original credit: @paf31
-- Link: https://github.com/paf31/24-days-of-purescript-2016/blob/master/5.markdown
-- Changes made: use meta-language to explain Newtype typeclass derivation syntax
--
-- Licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
--   https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en_US

-- Another useful class we can derive from is found in the `newtype` library: purescript-newtype
class Newtype_ new old | new -> old

newtype EmailAddress3 = EmailAddress3 String

-- no need to indicate what "_" is since the compiler can figure it out
derive instance Newtype EmailAddress3 _

-- Data.Newtype provides other useful functions that lets us avoid manually
-- wrapping and unwrapping. For example:
upperEmail :: EmailAddress3 -> EmailAddress3
upperEmail = over EmailAddress3 prefixWithText

-- To see the full list, look at the package's docs:
-- https://pursuit.purescript.org/packages/purescript-newtype/3.0.0/docs/Data.Newtype
-- <--- End

-- needed to compile

-- | prefixes a given string with 'text'
prefixWithText :: String -> String
prefixWithText str = "text" <> str

41-Type-Equality-Not-Propagate.purs

module Syntax.Basic.Typeclass.Gotchas.TypeEqualityNotPropagate where

import Unsafe.Coerce (unsafeCoerce)

-- ## Gotcha Number 1: Type Equality isn't yet included in Type Class Constraints

-- ### Example of the Problem

-- Given a type class like so...
class TwoTypesButTheyAreTheSameThing :: Type -> Type -> Constraint
class TwoTypesButTheyAreTheSameThing a b | a -> b, b -> a

-- and an instance like so...
instance TwoTypesButTheyAreTheSameThing Int Int

-- ... the below code will fail to compile
{-
foo :: forall int
     . TwoTypesButTheyAreTheSameThing Int int
    => int
foo = 8

Compiler Error:

  Could not match type

      Int

    with type

      int0


  while checking that type Int
    is at least as general as type int0
  while checking that expression 8
    has type int0
  in value declaration foo

  where sameAsInt0 is a rigid type variable
          bound at (line 9, column 7 - line 9, column 8)

-}

-- Why? Because the compiler does not also infer that `int` must be
-- the type, `Int`, when solving the type class constraint, even if the
-- instance and type class' functional dependencies indicate otherwise.

-- ### Current Workaround

class A_Determines_B :: Type -> Type -> Constraint
class A_Determines_B a b | a -> b

instance A_Determines_B Int String

-- The below "foreign import" syntax will be covered more in the FFI folder
foreign import data Computed :: Type -> Type

fromComputed :: forall a b. A_Determines_B a b => Computed a -> b
fromComputed = unsafeCoerce

toComputed :: forall a b. A_Determines_B a b => b -> Computed a
toComputed = unsafeCoerce

-- As hdgarood explained it,
-- "This is safe because you have to tell the compiler that you have an
-- `A_Determines_B a b` instance before it will coerce between the `a` and `b`.
-- But yes it’s expected that constraints with fundeps don’t propagate
-- type equalities. That’s not yet implemented."

Designing Type Classes

Why does PureScript disallow Orphan Instances while other languages (e.g. Haskell) allow them? Scala has type classes, but they don't function like PureScript's type classes.

If you are curious to learn more, read Type classes, class coherence and dependent types, which provides insight into the design and implementations of the 'type class' concept as well as implementations' tradeoffs.

Global Typeclasses

Type Class Instances: Global vs Local

To state that a given type (e.g. Box) can satisfy a type class' requirements, one writes a "type class instance". This instance actually defines how a given type (e.g. Box) implements that type class (e.g. Functor).

A language can implement type classes in two different ways:

  • Global: one type can only have one instance for any given type class.
  • Local: one type can have multiple instances for any given type class.

PureScript uses global type class instances whereas languages like Scala use local type class instances. So, what's the difference?

Benefits of Global Instances

Let's say I use functions that require the underlying data type to satisfy the Functor type class' requirements. Sometimes, that underlying data type is Array. Sometimes, it's Box. Sometimes, it's Maybe.

Global instances mean that a given data type (e.g. Box) can only have one instance for a given type class (e.g. Functor). Thus, every time and everywhere that I use Functor in my code where the underlying type is Box, the same Box-Functor instance is always used. This makes it easy for the compiler to figure out which instance to use, and the programmer does not have to think deeply about which instance will be used.

Local instances mean that a given data type (e.g. Box) can have an infinite number of instances for a given type class (e.g. Functor). Thus, any time I use Functor in my code and the underlying type is Box, any one of its instances could be used. This makes it harder for the compiler to figure out which instance to use. Ultimately, the compiler chooses an appropriate instance based on which instance is "closest" in the given scope. However, the programmer has to think more deeply about how to properly configure/arrange their code, so that the instance they want the compiler to choose is actually chosen and used.

Costs of Global Instances: Orphan Instances

Given this tradeoff, it may seem strange that global instances aren't used everywhere. If it's easier for the compiler and programmer, why use local instance?

The major pain point of global instances is "orphan instances."

For the below examples, let's say there is a type class, MyTypeClass, that is defined in Data.MyTypeClass. Let's say there is a data type, Box, that is defined in Data.Box. Let's say there is a third module, Data.Orphan, that has various other functions and values.

Due to how global instances work, an instance for a type class must be defined in one of two ways. There are two places where we could declare and implement the Box-MyTypeClass instance:

  • either in Data.MyTypeClass module, which imports the Box module and its type
  • or in the Data.Box module, which imports the Data.MyTypeClass module and its class.

If an instance is defined anywhere else (e.g. defined in Data.Orphan), it's called an "orphan instance." For example, Bob writes a library that exposes a type class (e.g. MyTypeClass). Sally, writes a data type that exposes a type (e.g. Box). The Box-MyTypeClass instance can be defined in either Bob's library or Sally's library. You are a third-party who wishes the Box-MyTypeClass instance was implemented differently than what either Bob or Sally implemented it as. However, since your module (e.g. Data.Orphan) is not one of those two modules, you cannot redefine the instance.

The only workaround to this situation is to define a newtype over Box that provides a different Box-MyTypeClass instance. While this seems simple to do, newtype wrapping and unwrapping can start to feel like "bloat" that gets in the way of other things.

Why Orphan Instances Are Painful

An Example

Let's say you have a library called purescript-unordered-collections that defines a data type called HashMap. Let's say you have another library called purescript-argonaut-codecs that defines two type classes called EncodeJson and DecodeJson. Where do you define HashMap's instances for those two type classes?

If in the data-type library (where the HashMap data type is declared), then that library will need to depend on the codec library. If in the codec library (where the EncodeJson/DecodeJson type classes are declared), then it will need to depend on the data-type library.

Either way, someone will get annoyed by something:

  • once the instance is defined in either library, everyone in the ecosystem is now stuck using that instance's definition. If they thought it should have been defined differently, they often have to write boilerplatey code via newtypes to be able to define their own instances.

Languages with local instances can shrug their shoulders as they have more control as to which instance gets chosen.

The Default Type Class

Type classes provide a "convenience" of sorts: rather than forcing the developer to pass in an implementation of the function, (a -> Boolean), the compiler can infer what that function's implementation is as long as it can infer what the type of a is.

Thus, new learners tend to reach the following conclusion. Let's say you are writing a library where you want to make it easier for the developer to use this library. At some point in the library, you need them to provide a default value. "Gee!" you think, "Why not use a type class called Default? The compiler can infer which instance to use and the developer's life will be that much easier!" While your intentions are good, that's a terrible idea as it will lead to "instance wars" due to orphan instances.

Although it can suffer from similar problems, a better choice is Monoid. See Gabriel Gonzalez' post on Defaults.

Similarly, read Don't Use Type Classes to Define Default Values.

Summary of Global vs Local Type Class Instances' Tradeoffs

TypeProsCons
Global
  • Easier for the compiler to find an instance
  • To the programmer, it's obvious which instance will be used
Orphan Instances or writing boilerplatey newtyp code to get around them
Local
  • Harder for the compiler to find an instance
  • To the programmer, it's not obvious which instance will be used and sometimes very difficult to properly configure/arrange one's code, so that the correct instance is eventually used
Best instance for the problem can be used without boilerplate

Scala uses local instances. Haskell uses global instances and orphan instances are disallowed by default; however, I believe Haskell has an "escape hatch" that allows orphan instances to exist.

PureScript uses global instances, and orphan instances are strictly disallowed. Unlike Haskell, there are no "escape hatches." For more context, see Harry's comment in 'Disallow Orphan Instances' (purescript/purescript#1247).

Scrap Your Type Classes (SYTC)

At the end of the day, mainstream usage of type classes provide a lot of convenience to the developer. Rather than defining a function that takes many arguments, it only takes a few arguments that highlight what you want to do.

As a result, some developers who encounter a problem will immediately decide to use type classes as their solution rather than some other language feature that is more appropriate (e.g. regular functions). For some problems, it is better to use regular functions rather than type classes. Regular functions might be less convenient than type classes, but they can be easier to use in some cases and more performant in others.

To understand the tradeoff, you must

  1. understand that type class constraints are replaced with arguments called 'type class dictionaries'
  2. realize that the possibly "larger" type class dictionary object argument could be replaced with a "smaller" single function

For more context, see Scrap Your Type Classes

Other Usages of Type Classes

Some type classes are purposefully designed to be lawless because they are used for other situations. Here are some examples:

  • Type-level documentation
    • Partial - represents a partial function: a function that does not always return a value for every input, but which will throw a runtime error on some inputs (covered in Design Patterns/Partial Functions)
  • Custom compiler warnings/errors
    • Warn/Fail - causes the compiler to emit a custom warning or a compiler error when the associated function/value is used in the code base (covered in Hello World/Debugging/Custom Type Errors)
  • Type-level functions
    • Symbol.Append - represents a type-level function (covered in Syntax/Type-Level Programming Syntax and Hello World/Type-Level Programming).
  • Function/Value Name Overloading (see next section's explanation and debate about this idea)

Debate: Must Type Classes Always Be Lawful?

While I already linked to the following link in the 'default type class' issue explained above, the link also covers another topic: why type classes should be lawful. Focusing on that aspect, the following is a (somewhat biased) summary of Don't Use Type Classes to Define Default Values

Those that say "yes" likely value the benefit of laws. Laws guarantee relationships between functions and values. In short, it's easier to understand and reason about code that uses lots of generic types (e.g. forall a. a -> String) if one knows that functions that operate on values of the type, a, or values that provide an a value adhere to certain laws.

Those that say "no" likely value the benefit of overloading a function name with different implementations. For example, what if one wanted to provide a default value of some type? Reusing the function name "default" is pretty easy to understand. However, what laws does it abide by? Without a deeper context, it's hard, if not impossible, to say.

The counterargument from those that say "laws must be required" is: "one usually hasn't thought through their design that deeply yet." As an example, is Default Int just a different name for Monoid's mempty, (i.e. 0 in addition (1 + 0 == 1 and 0 + 1 == 1)? Is their approach to their design actually flawed because there is a "better" way and they just haven't realized it yet?

Are there cases where the function name would "read well" in two contexts but mean two different things? For example, Context A's use of default might return 0 whereas Context B's use of default might return 12.

Thus, it seems that lawless type classes imply a domain-specific meaning in each context whereas lawful type classes imply a domain-independent meaning.

The reader is left with these question:

  • Are there ever times where gaining the convenience of overloaded function names are worth the loss of lawful-reasoning?
    • Does this change when adds in other factors?
      • Time (e.g. cost is great short-term but sucks long-term; cost stays the same through short- and long-term)
      • Business cost (e.g. cost to refactor non-lawful type classes vs cost of only making lawful type classes when overloaded functions names would have made development easier / accomplished the goal at the end of the day)
      • Hobby (e.g. I'm just making a fun project that no one will ever use)
  • If so, when should it be done? How would one know that it was the wrong approach and when would that likely happen?
  • What problems did developer X face when sticking to Side A instead of Side B?

21-Documentation.purs

-- | This is a single-line documentation.

-- | This
-- | is
-- | a
-- | multi-line
-- | documentation block, not a comment.
-- | Because it appears above the module declaration below,
-- | it will be combined with the next few documentation blocks.

-- | One can use markdown inside of documentation:
-- |
-- | Look an unordered list:
-- | - item 1
-- | - item 2
-- |
-- | An ordered list:
-- | 1. Item
-- | 2. Item
-- | 3. Item
-- |
-- | Unfortunately, markdown tables don't work...:
-- |
-- | | One | Two | Three |
-- | | --- | --- | ----- |
-- | | a   | b   | c     |
-- |
-- | # Headers level 1 work
-- |
-- | ## Headers level 2 work
-- |
-- | ### Headers level 3 work
-- |
-- | #### Headers level 4 work
-- |
-- | ##### Headers level 5 work
-- |
-- | ###### Headers level 6 work
-- |
-- | Some code:
-- | ```purescript
-- | f :: Int
-- | f = 4
-- | ```

-- | Documentation on a given module
module Syntax.Basic.Documentation where

-- | Documentation on a value
value :: Int
value = 4

-- | Documentation on a function
function :: Int -> String
function _ = "easy"

-- | Documentation on a given data type
data SomeData
  -- | Documentation on a particular data constructor
  = SomeData

-- | Documentation on a given type alias
type MyType = String

-- | Documentation on a given newtype
newtype SmallInt = SmallInt Int

-- | Documentation on a given type class
class MyClass a b | a -> b where
  -- | Documentation for a particular function/value
  -- | defined in a type class
  myFunction :: a -> b

-- | Documentation for a particular instance of a type class
instance MyClass String Int where
  myFunction _ = 4

22-Unicode-Syntax-Support.purs

module Syntax.Basic.Unicode where

-- Unicode sytax is supported

-- Original credit: @paf31
-- Link: https://github.com/paf31/24-days-of-purescript-2016/blob/master/2.markdown
-- Changes made:
--  - copied type signature that use unicode syntax except for union/intersect
--  - copied links showing unicode syntax in real libraries
--  - added library showing emoji operators in real library
--  - added forall / ∀ comparison
--
-- Licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
--   https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en_US

data ℕ = Zero | Succ ℕ

add :: ℕ -> ℕ -> ℕ
add _ _ = Zero

ε :: Number
ε = 0.001

{-
Using unicode syntax, instead of using a combination of characters,
one could use a single character to save space:

| Instead of...  | -> | <- | => | <= | :: | forall |
|----------------+----+----+----+----+----+--------+
| you can use... | →  | ←  | ⇒  | ⇐  | ∷  | ∀      |

Using Unicode syntax can make things unreadable, but sometimes it makes things more readable:
- https://github.com/paf31/purescript-isomorphisms/blob/f1a9e59f831cc3150dd9bc7aa66b2661df250ebe/src/Data/Iso.purs#L22
- https://github.com/paf31/purescript-pairing/blob/837638470c58df3971fe2e56395d65f391c9ba00/src/Data/Functor/Pairing.purs#L43

Yes, this does enable emoiji operators. See this library for an example of
why you might and might not want to use that syntax:
- https://pursuit.purescript.org/packages/purescript-prelewd/0.1.0

-}

Understanding Type Inference

The below video explains how type inference works. It can be very helpful to understand what is going on behind the scenes when you get compiler errors: How GHC's Type Inference Engine Works

Foreign Function Interface (FFI) Syntax

Alternate Backends

Besides compiling to Javascript, Purescript can also compile to other languages. See this link for a full list (may be outdated)

Syntax

This folder provides examples of FFI for simple cases regarding the JavaScript backend. However, see Wrapping JavaScript for PureScript for more detailed examples as to how to do FFI properly.

You should also look at the Purescript and Javascript source code for Effect.Uncurried:

Lastly, there may be some cases where you need to write FFI with Effect, but Effect isn't the best type to use. In such cases, take a look at Aff's FFI:

Using PureScript code within a JavaScript context

Imagine you defined a function in PureScript like gcd2 below that you wish to use in JavaScript code. What's a good practice to follow when calling that PureScript code from JavaScript?

$ cat src/GCD.purs
module GCD where

import Prelude

gcd2 :: Int -> Int -> Int
gcd2 n m | n == 0 = m
gcd2 n m | m == 0 = n
gcd2 n m | n > m = gcd (n - m) m
gcd2 n m = gcd (m - n) n

A good practice is to define a separate Interop module that looks like this:

module GCD.Interop where

import Prelude
import GCD as GCD
import Data.Function.Uncurried (mkFn2)

gcd2 :: Fn2 Int Int Int
gcd2 = mkFn2 GCD.gcd2

The above code will get compiled to ./output/GCD.Interop/index.js. So, in a JavaScript file, I would do the following:

import { gcd2 } from "./output/GCD.Interop/index.js";

gcd2(4, 5);

Defining an Interop module comes with two benefits that occur if gcd2 needs to do some breaking change (e.g. changing its number/order/type of args and return value):

  • If we have an Interop module, JavaScript consumers of the module don't have to account for breakage. Rather, the Interop version can change how this breakage propagates to the JavaScript consumer. In many cases, such breakage can be hidden entirely.
  • If we have an Interop module, the Interop module will produce a compiler error. This will remind us of every place where the Interop code is used by JavaScript and force us to verify that the JavaScript usage of the PureScript code is still correct.

01-Same-File-Name.js

export const basicValue = 4.0;

export function basicCurriedFunction(number) {
  return number * 4.0;
}

export function basicEffect() {
  return 4.0;
}

export function threeArgCurriedFunction(arg1) {
  return function(arg2) {
    return function(arg3) {
      // body of function
      return arg1 * arg2 * arg3;
    };
  };
}

export function curriedFunctionProducingEffect(string) {
  return function() {
    return string;
  };
}

export function basicUncurriedFunction(fn) {
  return function(arg1) {
    return function(arg2) {
      return fn(arg1)(arg2)();
    };
  };
}

var twoArgFunction = function(arg1, arg2) {
  console.log(arg1 + " " + arg2);
};

export {twoArgFunction as twoArgCurriedFunctionImpl};

01-Same-File-Name.purs

module Syntax.FFI.Simple where

import Prelude

import Effect (Effect)
import Effect.Uncurried (EffectFn2, runEffectFn2)

foreign import data DataType :: Type

foreign import data HigherKindedType :: Type -> Type

foreign import basicValue :: Number

foreign import basicEffect :: Effect Number

foreign import basicCurriedFunction :: Number -> Number

foreign import threeArgCurriedFunction :: Number -> Number -> Number -> Number

foreign import curriedFunctionProducingEffect :: String -> Effect String

foreign import basicUncurriedFunction :: EffectFn2 Number Number Number

foreign import twoArgCurriedFunctionImpl :: EffectFn2 String String Unit

twoArgFunction :: String -> String -> Effect Unit
twoArgFunction = runEffectFn2 twoArgCurriedFunctionImpl

02-Other-Bindings.js

// We'll use this binding in the next example
export function otherBindingFunction(arg) {
  console.log(`What is arg? It's ${arg}`);
  return 1;
}

03-Bindings-Tip.js

/*
When you are writing bindings to JavaScript,
the compiler will not pick up changes you make
to your FFI files unless it rebuilds the entire codebase.

So, it can be helpful to define your actual bindings
in one file and import and re-export them in this file
so that they are usable in PureScript.

Then, you can compile your code once and as long
as the arg number and types in PS don't change,
you can iterate on the FFI implementation
without having to recompile your code.
*/
export { otherBindings } from "../bindings/index.js";

03-Bindings-Tip.purs

module Syntax.FFI.BindingsTip where

import Prelude

import Effect (Effect)
import Effect.Uncurried (EffectFn2, runEffectFn2)

foreign import otherBindings :: forall a. a -> Int

Type-Level Programming Syntax

Read the files in order. It may take a few read-throughs for it to make sense as to why some things are needed but it will make sense eventually.

Note: there is an annoyance that can occur if you play around with the code: due to using the same names for some things, the IDE may try to import these names from other modules. If it does so, it may result it some definition being declared twice in the same file and produce a compiler error. If you experience such a problem, check the top of the file and delete any imports there.

Other Learning Sources

Consider purchasing Thinking with Types, a book that claims to be "the comprehensive manual for type-level programming". Abhinav Sarkar also made his notes public

An Overview of Terms and Concepts

Comparison

In programming, there are usually two terms we use to describe "when" a problem/bug/error can occur:

  • Compile-time: Turns source code into machine code. Compiler errors occur due to types not aligning.
  • Runtime: Executes machine code. Runtime errors occur due to values of types not working as expected/verified by the compiler (e.g. you expected a String at runtime but got null).

Definition

TermDefinition"Runtime"
Value-Level ProgrammingWriting source code that gets executed during runtimeNode / Browser
Type-Level ProgrammingWriting source code that gets executed during compile-timeType Checker / Type Class Constraint Solver^

^ First heard of this from @natefaubion in the PureScript chatroom.

What Are Types and Functions?

Types Reexamined

When we define a type like so...

data MyType
  = Value1
  | Value2

... we are saying there is a set or domain called MyType that has two members, Value1 and Value2. Thus, when we write...

value1 :: MyType
value1 = Value1

... we could also write it with more type information:

value1 :: MyType
value1 = (Value1 :: MyType)

The syntax (Value1 :: MyType) means Value1 is a value of the MyType type (or Value1 is a member of the MyType set/domain)

Functions Reexamined

Functions can be either pure or impure. Pure functions have 3 properties, but the third (marked with *) is expanded to show its full weight:

PurePure ExampleImpureImpure Example
Given an input, will it always return some output?Always
(Total Functions)
n + mSometimes
(Partial Functions)
4 / 0 == undefined
Given the same input, will it always return the same output?Always
(Deterministic Functions)
1 + 1 always equals 2Sometimes
(Non-Deterministic Functions)
random.nextInt()
*Does it interact with the real world?NeverSometimesfile.getText()
*Does it acces or modify program stateNevernewList = oldList.removeElemAt(0)
Original list is copied but never modified
Sometimesx++
variable x is incremented by one.
*Does it throw exceptions?NeverSometimesfunction (e) { throw Exception("error") }

Pure functions can better be explained as mapping some input to some output. The simplest example is pattern matching:

data Fruit = Apple | Orange

stringify :: Fruit -> String
stringify Apple = "Apple"
stringify Orange = "Orange"

The function, stringify, doesn't "do" anything: it doesn't modify its arguments, nor does it really "use" its arguments in some manner. Rather, it merely defines what to output when given some input.

In this way, functions merely specify how to map values of some type (e.g. Fruit) to values of another type (e.g. String). This idea is the heart of Category Theory. Thus, types and functions go hand-in-hand.

Kinds Redefined

Previously, we said:

Kinds = "How many more types do I need defined before I have a 'concrete' type?"

And using the table from earlier...

ExampleKindMeaning
StringTypeConcrete value
IntTypeConcrete value
Box aType -> TypeHigher-Kinded Type (by 1)
One type needs to be defined before the type can be instantiated
(a -> b)
Function a b
Type -> Type -> TypeHigher-Kinded Type (by 2)
Two types need to be defined before the type can be instantiated

This definition sufficed when we were learning only value-level programming. In reality, it's more like this:

NameMeaning
KindA "Type" for type-level programming
TypeThe "kind" (i.e. type-level type) that indicates a value-level type for value-level programming

Sometimes, pictures say a lot more than words: comparing-kinds-with-types

We can now modify the definition to account for this new understanding:

Kinds = "How many more type-level types do I need defined before I have a 'concrete' type-level type? Also, the kind, Type, is a type-level type whose 'values'/'members' are value-level types.

Summary of Inferred Kinds

Returning to a table we showed previously, we'll add the header that we removed (all caps) when we first displayed the table and include Record/Row.

TYPE-LEVEL EXPRESSIONInferred kind
UnitType
Array BooleanType
ArrayType -> Type
Either Int StringType
Either IntType -> Type
EitherType -> Type -> Type
Record (foo :: Int)Type
RecordRow Type -> Type
(foo :: Int)Row Type
......

Type-Level Programming Flow

Type-Level programming has 2-3 stages:

  • Creation
    • Define a type-level value by declaring a literal one
    • Reification - convert a value-level (i.e. runtime value) value into a type-level value via a Proxy type
  • (optional) Modify that value during compile-time
  • Terminal
    • Constrain types, so that an impossible state/code fails with a compiler error
    • Reflection - convert a type-level value stored in the Proxy type into a value-level value

01-Defining-Custom-Kinds.purs

module Syntax.TypeLevel.DefiningCustomKinds where

----------------------
-- To change the value-level type into a type-level type:
data Value_Level_Type
  = Value_Level_Value1
  | Value_Level_Value2
----------------------
-- We first define a data type that does not have any data constructors.
-- This indicates that `Type_Level_Type` is a kind we created.
data Type_Level_Type

-- Then, we use FFI-like syntax to declare the type-level values that kind
-- has. We do not declare a right hand side (RHS) since it has no values
--                    |--------- RHS -------|
--      data SomeType = Value1 | Value2
-- Rather, we indicate that the type is a member of that kind using
-- the following syntax:
foreign import data Type_Level_Value1 :: Type_Level_Type
foreign import data Type_Level_Value2 :: Type_Level_Type

-- Note: there is no corresponding javascript file for this one
-- despite the "foreign import" syntax!
----------------------

-- Using a Boolean-like value-level type as an example...
data YesNo = Yes | No

data YesNoKind

foreign import data YesK :: YesNoKind
foreign import data NoK  :: YesNoKind
----------------------

02-Kind-Signatures.purs

module Syntax.TypeLevel.KindSignatures where

-- We showed previously that `data`, `type`, `newtype`, and `class` declarations
-- can all have explicit kind signatures. In previous situations,
-- kind signatures only used the kind, `Type`. Now that we know how to
-- define our own custom kind, let's use it below.

data A_Kind_I_Created
foreign import data Only_Value_For_My_Kind :: A_Kind_I_Created

data DataExample :: A_Kind_I_Created -> Type
data DataExample a_kind_I_created_type_level_value = DataExample

-- This will succesfully compile because `Only_Value_For_My_Kind` has
-- kind, `A_Kind_I_Created`, which matches the one expected in the
-- declaration for `DataExample`.
compileStatus_success :: DataExample Only_Value_For_My_Kind
compileStatus_success = DataExample

-- This will fail to compile because `String` has
-- kind, `Type`, not kind, `A_Kind_I_Created`.
--
-- compileStatus_fail :: DataExample String
-- compileStatus_fail = DataExample

type TypeExample :: A_Kind_I_Created -> Type
type TypeExample a_kind_I_created_type_level_value = Int

newtype NewtypeExample :: A_Kind_I_Created -> Type
newtype NewtypeExample a_kind_I_created_type_level_value = NewtypeExample Int

-- Similarly, a type class can use kinds other than `Type`. This type class
-- does not have any functions/values:
class TypeClassExample :: A_Kind_I_Created -> Constraint
class TypeClassExample a_kind_I_created_type_level_value

03-Polymorphic-Kinds.purs

module Syntax.TypeLevel.PolymorphicKinds where

-- In the previous file, the following would not compile because `String` has
-- a kind that is different than the one expected by `MyData` below:

data MyKind
foreign import data OnlyValueForMyKind :: MyKind

data MyData :: MyKind -> Type
data MyData typeThatHasKind_'myKind' = MyData

-- Fails to Compile:
-- compileStatus_fail :: MyData String
-- compileStatus_fail = MyData

-- What if we wanted `MyData` to work "for all" kinds: `Type`, `MyKind`, or
-- one written by someone else? We would use "forall" syntax. This syntax
-- should look similar to how we would write it for a value-level function:
valueLevelFunction :: forall a. a -> a
valueLevelFunction valueOfType_a = valueOfType_a

-- Note: `kind` is often abbreviated as `k` to indicate kind.
data MyDataPolyKind :: forall kind. kind -> Type
data MyDataPolyKind typeThatHasAGivenKind = MyDataPolyKind

compilesSuccessfully1 :: MyDataPolyKind String -- kind is Type
compilesSuccessfully1 = MyDataPolyKind

compilesSuccessfully2 :: MyDataPolyKind OnlyValueForMyKind -- kind is MyKind
compilesSuccessfully2 = MyDataPolyKind

04-Proxy.purs

module Syntax.TypeLevel.Proxy where

-- When we write programs, arguments and definitions are at the value-level.
-- Since the values of type-level types (i.e. kinds) are types themselves,
-- how do we pass type-level values around in our program when the program
-- is written at the value level?

-- For type-level values, we use a simple type that "stores" the type-level
-- value as a phantom type (see `Design Patterns/Phantom Types.md`).
-- By making that phantom type polymorphic on kinds (i.e polykinded),
-- one type will work for all kinds:
data Proxy :: forall kind. kind -> Type
data Proxy k = Proxy

-- Given that we have the following two custom kinds...

-- data Kind_1
--   = Kind_1_Value_1
--   = Kind_1_Value_2
data Kind_1
foreign import data Kind_1_Value_1 :: Kind_1
foreign import data Kind_1_Value_2 :: Kind_1

data Kind_2
foreign import data Kind_2_Value :: Kind_2

-- ... we can define a value that stores the type-level value via `Proxy` ...

kind1Value1 :: Proxy Kind_1_Value_1
kind1Value1 = Proxy

kind2Value :: Proxy Kind_2_Value
kind2Value = Proxy

-- ... and pass around the type-level values by passing around the Proxy value
-- and annotating the type signature with the type-level value.

-- This function only works on the first `Kind_1_Value`.
kind1_to_kind2_specific :: Proxy Kind_1_Value_1 -> Proxy Kind_2_Value
kind1_to_kind2_specific _ {- Proxy, which can be ignored -} =
  Proxy -- Proxy whose type is different than first Proxy type

-- This function only works on all possible `Kind_1_Value`s.
kind1_to_kind2_generic
  :: forall (kind1Values :: Kind_1) (kind2Values :: Kind_2)
   . Proxy kind1Values
  -> Proxy kind2Values
kind1_to_kind2_generic _ {- Proxy, which can be ignored -} =
  Proxy -- Proxy whose type is different than first Proxy type

-- The above definition will make more sense in the future.

Defining Functions

Solve for X

Normally, when we define a function for value-level programming, it looks like this:

function :: InputType -> OutputType
function InputValue = OutputValue

In other words, when given InputValue, return OutputValue. The direction of this "relationship" is ALWAYS in one direction: to the right (i.e. ->).

When we define a function for type-level programming, we're not defining a function that takes some input and returns an output. Rather, we are defining a "relationship" between some input(s) and some output(s). In other words, these "relationships" can be applied in multiple directions to create multiple functions. One could say that type-level functions work in multiple directions. To put it another way...:

  • function InputValue outputs OutputValue
  • function OutputValue outputs InputValue

Let's give a much clearer example by solving an equation:

total = x + y

Right now, the equation is making us solve for total. However, with some simple rearranging, we can make it solve for x

total = x + y
total - y = x + y - y
total - y = x
x = total - y

We can also make it solve for y:

total = x + y
total - x = x - x + y
total - x = y
y = total - x

Thus, we can take this "relationship"/equation and figure out one entity if we know the other two entities. Putting it into programming terms, if we have one relationship/equation (like that above), we can define three functions:

  1. f1 :: X -> Y -> Total
  2. f2 :: X -> Total -> Y
  3. f3 :: Y -> Total -> X

This is the same idea used in type-level programming. So, how does this actually work in Purescript? Multi-parameter type classes and functional dependencies.

The Relationship/EquationThe Number of Functions & its type signatureThe implementation of a function
a multi-parameter type classfunctional dependencies (the exact number depends)type class instances

For example, assuming we had 1) a type-level number called IntK, 2) its value-level Proxy type, IProxy, and 3) instances for the below type class, we could write an add and two subtract functions using just one relationship:

-- the relationship itself
class AddOrSubtract :: IntK -> IntK -> IntK -> Constraint
class AddOrSubtract x y total
  -- the normal "add" function: "total = x + y"
  | x y -> total

  -- the first 'subtract' function: "y = total - x"
  , x total -> y
  -- the second 'subtract' function: "x = total - y"
  , y total -> x

Then, we could use this one relationship as three different functions:

-- given two IntK values, I can add them together by returning
-- `total`, which is "calculated" via the type class `AddOrSubtract`
addTwoIntK :: forall x y total
            . AddOrSubtract x y total
           => IProxy x -> IProxy y -> IProxy total
addTwoIntK _ _ = IProxyValue

-- given two IntK values, I can subtract one from another by
-- returning `x`/`y`, which is "calculated"
-- via the type class `AddOrSubtract`
subtractIntK_1 :: forall x y total
                . AddOrSubtract x y total
               => IProxy x -> IProxy total -> IProxy y
subtractIntK_1 _ _ = IProxyValue

subtractIntK_2 :: forall x y total
                . AddOrSubtract x y total
               => IProxy y -> IProxy total -> IProxy x
subtractIntK_2 _ _ = IProxyValue

Unification

An Overview and How Type-Level Functions "Compute"

Recall that the type checker / type constraint solver "computes" type-level expressions by figuring out what type something is. Thus, the above analogy is helpful for understanding type-level programming, but it is incomplete without an explanation on how types "unify". In short, unification is the way by which the compiler infers or figures out some type. For our context, it is how the type checker computes the "type-level output" of a type-level function. It does this by unifying the undefined types in a type class' definition with a concrete type's instance of that type class.

Let's review something first. In a type class definition and its instance, we have terms to refer to specific parts of it:

class Show a where
  show :: a -> String

{-            |  1   |         |  2  |                                -}
instance (Show a) => Show (Box a) where
  show (Box a) = show a
  1. Instance Context
  2. Instance Head

The "Instance Context" and "Instance Head" terms are crucial to understanding the unification rules below.

Unification is how logic programming works. A popular language which uses logic programming to compute is Prolog, which has a nice explanation on unification. (Curious readers can see the bottom of the file for links about Prolog). To see the rules for how this works in general, I've adapted the Prolog unification rules defined by Blackburn et al. below:

  1. Two concrete terms unify. A "term" for this explanation is either a Type or a Kind:
    • Type
      • String unifies with String
      • String does not unify with Int
    • Kind
      • kind BooleanK unifies with kind BooleanK
      • kind BooleanK does not unify with kind IntK
    • a Kind term only unifies with other Kind terms, not Type terms.
    • a Type term only unifies with other Type terms, not Kind terms.
  2. A concrete term and a polymorphic/generic term (i.e. term variable) unify and the term variable is assigned to a concrete term:
    • Similar to how a variable can be assigned a value, let a = 5, so one assigns a term to a term variable: a = Int (type variable assigned to a concrete type) or a = IntK (kind variable assigned to a concrete kind). By this analogy, every time one sees an a type/kind in a type/kind signature, they can replace it with Int/IntK.
  3. Two term variables unify and their relationship is saved
    • Ignoring the forall . syntax, given f :: Add a b c => Add c d e => a -> b -> d -> e, the c type/kind in both Add constraints are unified and their relationship is "saved". As soon as one of them is assigned to a concrete term, the other will be assigned that term, too.
  4. Complex "term chains" (e.g. a type class and a concrete type's instance of that type class) unify if and only if all of their corresponding arguments unify:
    • the number of parameter terms in the type class is the same number of terms in the instance
      • class MyClass first second
      • instance MyClass String Int
    • instance types unify with the class' constraints
      • class (SuperClass constrained) <= ThisClass constrained
      • instance SuperClass String
      • instance ThisClass String
    • types in the instance context unify with their corresponding class
      • instance OtherConstraint a
      • instance (OtherConstraint a) => FastClass a
    • the type of terms in the type class unify only with their corresponding term type in the instance:
      • The type class' Kind terms are made to unify only with other Kind terms, not Type terms, in the instance
      • The type class' Type terms are made to unify only with other Type terms, not Kind terms, in the instance.
    • a term variable is only assigned once and is not assigned to two different concrete term during the unification process

A type-level function can only "compute" a type-level expression when the types unify. This will fail in a few situations (this list may not be exhaustive):

  • infinite unification: to unify some term, a, one must unify some term, b, which can only be unified if a is unified. After making X many recursive steps, the type inferencer will eventually give up and throw an error. This is a hard-coded number in the Purescript compiler.
  • situations where the type inferencer cannot infer the correct type/kind
  • situations where one needs to do "backtracking".

Backtracking Is Not (Currently) Supported

Here is an example of "backtracking". It will make more sense after you have read through the Pattern-Matching-Using-Instance-Chains.purs file.

class MyClass a
  someValue :: Boolean

instance (SomeConstraint a) => MyClass a where
  someValue = true
else instance MyClass a where
  someValue = false

Here's the steps the compiler walks through:

  1. Find the first instance for MyClass ('firstInstance')
  2. Commit to that instance and check whether the a type fulfills the SomeConstraint type class, too.
  3. The a type does not satisfy that type class constraint.
  4. The type checker fails.

The issue lies in step 2: the instance head is checked before the instance context. Once the type inferer commits to some instance, it cannot 'backtrack' to the starting position after realizing that its current instance fails. Ideally, the type inferer would jump back to step 2 and realize that there is another instance ('secondInstance') that always works for any a type (since there is no constraint).

"Backtracking" could be implemented in the compiler by using instance guards, but this has not yet been done. For the current progress on this issue, see the related Purescript issue.

More Resources for Understanding Unification

To understand unification at a deeper level, see these links:

Functional Dependencies Reexamined

At times, it can be difficult for the type checker to infer what a given type is. Thus, one uses functional dependencies (FDs) to help the compiler. As a reminder, FDs inform the compiler how to infer what some types are given that it knows other types:

class Add :: IntK -> IntK -> IntK -> Constraint
class Add x y total
  | x y -> total
  , y total -> x
  , x total -> y

However, sometimes the functional dependencies get a bit more complicated because there are two types on the right-hand side of the arrow. This is where our analogy of a "FDs are type-level functions" starts to break down since a value-level function can only return one value at a time. (Granted, one can use a Tuple or Record to return multiple values in a container, but the principle still applies.) With our "relationships", a single FD can sometimes define multiple type-level functions depending on how we use them.

For example, look at the second FD of Prim.Row.Cons:

-- Note: Symbol is a type-level String
class Cons :: forall kind. Symbol -> kind -> Row kind -> Row kind -> Constraint
class Cons label a tail row
  | label a tail -> row
  , label row -> a tail

The first FD can be read as

If you give me a label, its type-level value, and a pre-existing row (i.e. tail), then I can append that "label and type-level value" association to the tail and give you back the result of the append (i.e. row)."

The second FD can be read as

If you provide me a row and the name of a label in that row, then I can give you either

  1. that label's type
  2. a row that excludes that label-value association (i.e. the tail)", or
  3. both

Let's demonstrate a few different examples via the REPL. This will be covered in more detail in this folder:

  1. Run spago repl
  2. Import the Prim.Row module via import Prim.Row
  3. Use the :paste followed by CTRL+D to paste the multi-line verify* functions below into the REPL.
  4. Pass the corresponding arguments into the function to verify that it compiles.
-- spago repl
-- import Prim.Row

-- :paste
verifyAddingRowToTailCompiles
  :: forall tail finalRow
   . Cons "first" String tail finalRow
  => Record tail -> Record finalRow -> String
verifyAddingRowToTailCompiles _ _ =
  "If you see this message rather than an error, the relationship is true."
-- CTRL+D

-- Run the below function
verifyAddingRowToTailCompiles {apple: "haha"} {apple: "haha", first: "text" }

-- Great! Let's now switch the `tail` and `finalRow` types
-- in the `Cons` relationship. 

-- :paste
verifyRemovingRowFromTailCompiles
  :: forall tail finalRow
   . Cons "first" String finalRow tail
  => Record tail -> Record finalRow -> String
verifyRemovingRowFromTailCompiles _ _ =
  "If you see this message rather than an error, the relationship is true."
-- CTRL+D

-- Run the below function
verifyRemovingRowFromTailCompiles {apple: "haha", first: "text" } {apple: "haha"}

Learning Prolog is not necessary to understand how to do type-level programming. However, one may want to learn more about it to understand the idea of unification better. If so, these links helped me understand Prolog:

Works Cited

(for lack of a better section header name...)

Blackburn, Patrick, et al. "2.1: Unification." Learn Prolog Now! vol. 7, College Publications, 2006, http://www.learnprolognow.org/lpnpage.php?pagetype=html&pageid=lpn-htmlse5. Accessed 9 Oct. 2018

01-Single-Arg-Syntax.purs

module Syntax.TypeLevel.Functions.SingleArgSyntax where

-- Given the following value-level and type-level types/values...

data InputType = InputValue
data OutputType = OutputValue

data InputKind
foreign import data InputValueK :: InputKind

data OutputKind
foreign import data OutputValueK :: OutputKind

-- ... a value-level function...

-- function's type signature
function :: InputType -> OutputType
-- function's implementation
function InputValue = OutputValue

-- ... can be converted to a type-level function using
--   - type classes
--   - functional dependencies

-- the relationship
class TypeLevelFunction :: InputKind -> OutputKind -> Constraint
class TypeLevelFunction input output
  -- one function's type signature
  | input -> output

  -- another function's type signature
  , output -> input

-- the implementation for both functions (since this is a simple example)
instance TypeLevelFunction InputValueK OutputValueK

02-Multi-Arg-Syntax.purs

module Syntax.TypeLevel.Functions.MultiArgSyntax where

-- Given the following value-level and type-level types/values...

data InputType1 = InputValue1
data InputType2 = InputValue2
data OutputType = OutputValue

data InputKind1
foreign import data InputValueK1 :: InputKind1

data InputKind2
foreign import data InputValueK2 :: InputKind2

data OutputKind
foreign import data OutputValueK :: OutputKind

-- ... a value-level function...

-- function's type signature
function :: InputType1 -> InputType2 -> OutputType
-- function's implementation
function InputValue1 InputValue2 = OutputValue

-- ... converts to

-- The relationship
class TypeLevelFunction :: InputKind1 -> InputKind2 -> OutputKind -> Constraint
class TypeLevelFunction input1 input2 output
  -- the functions' type signatures
  | input1 input2 -> output
  , input1 output -> input2
  , input2 output -> input1

-- functions sole implementation
instance implementation ::
  TypeLevelFunction InputValueK1 InputValueK2 OutputValueK

03-Pattern-Matching-Using-Instances.purs

module Syntax.TypeLevel.Functions.PatternMatching.InstancesOnly where

-- To handle more pattern matching, we add more values of the type class

-- This...
data InputType2
  = InputValue1
  | InputValue2
  | InputValue3

data OutputType2
  = OutputValue1
  | OutputValue2
  | OutputValue3

function2 :: InputType2 -> OutputType2
function2 InputValue1 = OutputValue1 -- first pattern match
function2 InputValue2 = OutputValue2 -- second pattern match
function2 InputValue3 = OutputValue3 -- third pattern match

-- ... converts to...

data InputKind
foreign import data InputValue1 :: InputKind
foreign import data InputValue2 :: InputKind
foreign import data InputValue3 :: InputKind

data OutputKind
foreign import data OutputValue1 :: OutputKind
foreign import data OutputValue2 :: OutputKind
foreign import data OutputValue3 :: OutputKind

-- the relationship
class TypeLevelFunction :: InputKind -> OutputKind -> Constraint
class TypeLevelFunction input output
  -- the functions' type signatures
  | input -> output
  , output -> input

-- the implementations via pattern matching
instance firstPatternMatch  :: TypeLevelFunction InputValue1 OutputValue1
instance TypeLevelFunction InputValue2 OutputValue2
instance thirdPatternMatch  :: TypeLevelFunction InputValue3 OutputValue3

--------------------------------------------
-- An example using YesNo and Zero/One
data YesNo     = Yes  | No
data ZeroOrOne = Zero | One

toInt :: YesNo -> ZeroOrOne
--    input = output
toInt Yes = One
toInt No = Zero

-- converts to

data YesNoKind
foreign import data YesK :: YesNoKind
foreign import data NoK  :: YesNoKind

data ZeroOrOneKind
foreign import data OneK  :: ZeroOrOneKind
foreign import data ZeroK :: ZeroOrOneKind

class ToInt :: YesNoKind -> ZeroOrOneKind -> Constraint
class ToInt input output | input -> output

instance ToInt YesK OneK
instance ToInt NoK  ZeroK

04-Pattern-Matching-Using-Instance-Chains.purs

module Syntax.TypeLevel.Functions.PatternMatching.InstanceChains where

{-
So far, our type-level function's pattern matches use literal values.

Whenever we write...
   instance TL_Function InputValue1 OutputValue1
   instance TL_Function InputValue2 OutputValue2

... it's the equivalent of writing

   tl_Function :: Input -> Output
   tl_Function InputValue1 = OutputValue1
   tl_Function InputValue2 = OutputValue2

-}

-- Let's say we have the given value-level and type-level types/values:
data Fruit
  = Apple
  | Orange
  | Banana
  | Blueberry
  | Cherry

data ZeroOrOne = Zero | One

data FruitKind
foreign import data AppleK     :: FruitKind
foreign import data OrangeK    :: FruitKind
foreign import data BananaK    :: FruitKind
foreign import data BlueberryK :: FruitKind
foreign import data CherryK    :: FruitKind

data ZeroOrOneKind
foreign import data ZeroK :: ZeroOrOneKind
foreign import data OneK  :: ZeroOrOneKind

-- To write pattern matches with a 'catch-all' underscore binding

fruitToInt :: Fruit -> ZeroOrOne
fruitToInt Apple                    = Zero
fruitToInt _ {- Orange .. Cherry -} = One

-- we can use a feature called "Type Class Instance Chains:"

class FruitToInt :: FruitKind -> ZeroOrOneKind -> Constraint
class FruitToInt a i
  | a -> i                                                                {-

  Notice that we have omitted this type signature because
  we don't know what to do when we know `i` but not `a`

  , i -> a                                                                -}


instance FruitToInt AppleK ZeroK
else instance FruitToInt a OneK

{-
which is the same as writing...

  instance FruitToInt AppleK     ZeroK
  instance FruitToInt OrangeK    OneK
  instance FruitToInt BananaK    OneK
  instance FruitToInt BlueBerryK OneK
  instance FruitToInt CherryK     OneK
-}

{-
As of this writing, Purescript does not support all of the features
described in the paper below (i.e. backtracking), but it does work
for simpler use cases like above

Here's the related Purescript issue:
https://github.com/purescript/purescript/issues/2315

See the original paper here:
http://homepages.inf.ed.ac.uk/jmorri14/pubs/morris-icfp2010-instances.pdf
-}

01-Reflection.purs

module Syntax.TypeLevel.Reflection where

-- ignore this
import Prelude (class Show)

-- Reflection syntax
--  Converting a type-level value into a value-level value

-- This code...
----------------------------
type Value_Level_Type = String -- for easier readability

data CustomType = TypeValue

reflectVL :: CustomType -> Value_Level_Type
reflectVL TypeValue = "value-level value"
----------------------------
-- ... converts to...
----------------------------
data CustomKind
foreign import data CustomKindValue :: CustomKind

data Proxy :: forall k. k -> Type
data Proxy kind = Proxy

-- "type-level value to value-level value"
class TLI_to_VLI :: CustomKind -> Constraint
class TLI_to_VLI customKind where
  reflectCustomKind :: Proxy customKind -> Value_Level_Type

instance TLI_to_VLI CustomKindValue where {-
  reflectCustomKind Proxy = "value-level value" -}
  reflectCustomKind _     = "value-level value"
----------------------------

-- An example using the Boolean-like data type YesNo:
data YesNo = Yes | No

data YesNoKind
foreign import data YesK :: YesNoKind
foreign import data NoK  :: YesNoKind

{-
Read yesK and noK as:
  yesK = (YesNoProxyValue :: YesNoProxy Yes) - a value of type "YesNoProxy Yes"
  noK  = (YesNoProxyValue :: YesNoProxy No)  - a value of type "YesNoProxy No" -}
yesK :: Proxy YesK
yesK = Proxy

noK :: Proxy NoK
noK = Proxy

class IsYesNoKind :: YesNoKind -> Constraint
class IsYesNoKind a where
  reflectYesNo :: Proxy a -> YesNo

instance IsYesNoKind YesK where
-- reflectYesNo (Proxy :: Proxy Yes) = Yes
   reflectYesNo _                    = Yes

instance IsYesNoKind NoK where
-- reflectYesNo (Proxy :: Proxy No) = No
   reflectYesNo _                   = No


-- We can also use instance chains here to distinguish
-- one from another

class IsYes :: YesNoKind -> Constraint
class IsYes a where
  isYes :: Proxy a -> YesNo

instance IsYes YesK where
  isYes _ = Yes
else instance IsYes a where
  isYes _ = No

-- Using instance chains here is more convenient if we had
-- a lot more type-level values than just 2. In some cases,
-- it is needed in cases where a type-level type can have an
-- infinite number of values, such as a type-level String

-- Open a REPL, import this module, and then run this code:
--    reflectYesNo yesK
--    reflectYesNo noK
--    isYes yesK
--    isYes noK


-- necessary for not getting errors while trying the functions in the REPL

instance Show YesNo where
    show Yes = "Yes"
    show No  = "No"

02-Reification.purs

module Syntax.TypeLevel.Reification where

-- ignore this
import Prelude (class Show)

-- Reification = value-level value -> type-level value

-- Given a yes/no data type
--
--  data YesNo = Yes | No

-- In value-level programming,
ignoreMe :: String
ignoreMe =
    -- we can write something like this...
    yesno_to_string_function   a_yesno_value_determined_at_runtime

{-
This function does not know which value of the YesNo type
(i.e. `Yes` or `No`) it will be when the program is executed.
However, since the function knows how to map both values
of the YesNo type into an value of a String type, it doesn't matter.

Similarly, for type-level programming, we won't always know which
value of the value-level type it will be. However, if we know how to
reify every value of that value-level type into an value of
a type-level type, it doesn't matter.

Reification works by using callback functions:
-}

-- Given the following code, which
--   - defines the type-Level YesNo and its two values
--   - defines a Proxy type and its two values
--   - defines the reflection function for both values ...
data YesNo = Yes | No

data YesNoKind
foreign import data YesK :: YesNoKind
foreign import data NoK  :: YesNoKind

data Proxy :: forall k. k -> Type
data Proxy kind = Proxy

yesK :: Proxy YesK
yesK = Proxy

noK :: Proxy NoK
noK = Proxy

class IsYesNoKind :: YesNoKind -> Constraint
class IsYesNoKind a where
  reflectYesNo :: Proxy a -> YesNo

instance IsYesNoKind YesK where
  reflectYesNo _ = Yes

instance IsYesNoKind NoK where
  reflectYesNo _ = No

-- We can reify a YesNo by defining a callback function that receives
-- the corresponding type-level value as its only argument
-- (where we do type-level programming):

reifyYesNo :: forall returnType
            . YesNo
            -> (forall b. IsYesNoKind b => Proxy b -> returnType)
            -> returnType
reifyYesNo Yes function = function yesK
reifyYesNo No  function = function noK

-- necessary for not getting errors while trying the functions in the REPL

instance Show YesNo where
    show Yes = "Yes"
    show No  = "No"

-- necessary to compile

yesno_to_string_function :: YesNo -> String
yesno_to_string_function Yes = "yes"
yesno_to_string_function No  = "no"

a_yesno_value_determined_at_runtime :: YesNo
a_yesno_value_determined_at_runtime = Yes

10-Conventions.purs

module Syntax.TypeLevel.Conventions where

-- This file shows the patterns and naming schemes used when writing
-- type-level programming code. Refer to this whenever you're lost.

-- Entities that have the comment "NANS" mean "no apparent naming scheme".
-- In other words, there is not a naming scheme that people seem to follow.
-- So, name it however you want.

-- Entities that do seem to have naming scheme will have their explanation
-- above them in a comment.

type Value_Level_Type = String

data KindName
foreign import data Value :: KindName

data Proxy :: forall k. k -> Type
data Proxy kind = Proxy

-- NANS
inst :: Proxy Value
inst = Proxy

-- The class name is usually "Is[KindName]"
class IsKindName :: KindName -> Constraint
class IsKindName a where
  -- and the reflect function is usually "reflect[KindName]"
  reflectKindName :: Proxy a -> Value_Level_Type

instance IsKindName Value where
  reflectKindName _ = "value-level value"

-- NANS
class IsKindName a <= ConstrainedToKindName a

-- NANS
instance ConstrainedToKindName Value

-- Usually reify[KindName]
reifyKindName :: forall r
           . Value_Level_Type
          -> (forall a. IsKindName a => Proxy a -> r)
          -> r
reifyKindName _valueLevel function = function inst

11-Symbol-Syntax.purs

module Syntax.TypeLevel.SymbolSyntax where

vl_string :: String
vl_string = "a value-level string!"

-- `Symbols` are type-level strings

-- Compiler imports this automatically via the Prim module
data Symbol_

-- This proxy type is defined in the purescript-prelude package
data Proxy :: forall k. k -> Type
data Proxy kind = Proxy

-- use literal string syntax
tl_literalString :: Proxy "a type-level string!"
tl_literalString = Proxy

-- use multi-line string syntax!
tl_multiLineString :: Proxy "a type-level \
                             \string!"
tl_multiLineString = Proxy

-- use triple-quote string syntax
tl_tripleQuoteStringSyntax :: Proxy """triple-quote string syntax
 works as long as each new line is indented, so that the compiler
 doesn't think the string is the definition for
 the 'tl_tripleQuoteStringSyntax' function.

 The string will automatically escape special characters
 (e.g. '.', '*', '/')."""
tl_tripleQuoteStringSyntax = Proxy

{-
Symbol's other type-level programming constructs are in other modules
that must be imported to work:
  - purescript-prelude:
      - `IsSymbol` typeclass
      - `reifySymbol` function
  - prim (type-level functions)
      - Compare: "a" compare "b" == LT
      - Append:  "hello" append "world" == "hello world"
      - Cons:    "a" cons "pple" = "apple"
      - Uncons:  "string" = "s" append "tring"
-}

12-Integer-Syntax.purs

module Syntax.TypeLevel.IntegerSyntax where

-- value level integers
vl_int1 :: Int
vl_int1 = 1

vl_int2 :: Int
vl_int2 = 0x01 -- alternative way to write them

vl_int3 :: Int
vl_int3 = 1_000_000 -- use underscores for thousands character


-- `Int` are type-level integers

-- Compiler imports kind `Int` automatically via the Prim module
data Int_

-- This proxy type is defined in the purescript-prelude package
data Proxy :: forall k. k -> Type
data Proxy kind = Proxy

-- use literal int syntax
tl_literalInt1 :: Proxy 1234
tl_literalInt1 = Proxy

-- use hexadecimal syntax!
tl_literalInt2 :: Proxy 0x01
tl_literalInt2 = Proxy

-- use underscore syntax
tl_literalInt3 :: Proxy 1_000_000
tl_literalInt3 = Proxy

-- negative values must be wrapped in parenthesis
-- use literal int syntax
tl_literalInt1' :: Proxy (-1234)
tl_literalInt1' = Proxy

-- use hexadecimal syntax!
tl_literalInt2' :: Proxy (-0x01)
tl_literalInt2' = Proxy

-- use underscore syntax
tl_literalInt3' :: Proxy (-1_000_000)
tl_literalInt3' = Proxy

{-
Int's other type-level programming constructs are in other modules
that must be imported to work:
  - purescript-prelude:
      - `Reflectable` typeclass
      - `reflectType` function
      - `reifyType` function
  - prim (type-level functions)
      - Add: "a" compare "b" == LT
      - Mul:  "hello" append "world" == "hello world"
      - ToString:    "a" cons "pple" = "apple"
-}

21-Row-Syntax.purs

module Syntax.TypeLevel.RowSyntax where

-- "row kinds" look like "Row k" where 'k' is another kind.
-- Usually, it's used with the kind, `Type`, to make Records (e.g. "Row Type")
-- You cannot find that much documentation on `Row` kinds because
-- they are built into the compiler.

type Example_of_an_Empty_Row :: forall k. Row k
type Example_of_an_Empty_Row = ()

type Example_of_a_Single_Row_of_Types = (fieldName :: ValueType)
type Example_of_a_Multiple_Row_of_Types = (first :: ValueType, second :: ValueType)

data Proxy :: forall k. k -> Type
data Proxy kind = Proxy

one_Key_Value_Pair :: Proxy (key :: Int)
one_Key_Value_Pair = Proxy

two_Key_Value_Pairs :: Proxy (key1 :: Int, key2 :: Int)
two_Key_Value_Pairs = Proxy

many_Key_Value_Pair :: Proxy ( key1 :: Int
                              , key2 :: String
                           -- , ...
                              , keyN :: (Int -> String)
                              )
many_Key_Value_Pair = Proxy

nested_Key_Value_Pair :: Proxy (outerKey :: Proxy (innerKey :: Int))
nested_Key_Value_Pair = Proxy

-- Since row kinds can be used with other kinds, one could also define
-- a row of Symbols:
type Example_of_a_Single_Row_of_Symbols = (a :: "a symbol")
type Example_of_a_Multiple_Row_of_Symbols = (a :: "a symbol", b :: "another symbol")

-- These can also be used with the quoted-key syntax (explained previously in the
-- Records folder):
type Quoted_Key_Row_of_Symbols = ("the key" :: "the symbol")

row_of_symbols_proxy :: Proxy ( firstField :: "this is a symbol"
                              , secondField :: "this is another symbol"
                              )
row_of_symbols_proxy = Proxy

{-
Just like Symbol, Row's other type-level programming constructs
are defined in the built-in `Prim` package and the `purescript-prelude` library.
-}

-- needed to compile
type ValueType = String

RowList

The final aspect of type-level programming to learn is RowList. This is where type-level programming often gets interesting because one can do interesting things with records at the type-level.

It is not covered in this folder. Rather, it is covered in Hello World/Type Level Programming/src/RowList because one needs to understand how Data.Foldable.foldl works (which is explained in Hello World/Collections and Loops). Until one understands what a "fold left" is, understanding RowList will not make much sense either.

Module Syntax

Self-explanatory

Due to the compiler being efficient and not wanting to import unused values/functions/etc., compiling this folder will emit a lot of warnings. If the warnings look like either of these two messages, they can be ignored:

First

Warning [current] of [total]:

  in module Syntax.Module.FullExample
  at src/11-Full-Module-Syntax.purs line 58, column 1 - line 58, column 37

    The import of module Module.SubModule.SubSubModule is redundant


  See https://github.com/purescript/documentation/blob/master/errors/UnusedImport.md for more information,
  or to contribute content related to this warning.

Second

Warning [current] of [total]:

  in module Syntax.Module.Importing
  at src/03-Basic-Importing.purs line 29, column 1 - line 29, column 58

    There is an existing import of ModuleDataType, consider merging the import lists


  See https://github.com/purescript/documentation/blob/master/errors/DuplicateSelectiveImport.md for more information,
  or to contribute content related to this warning.

File Location Conventions

-- a module named...
module Module1 where
-- imports and source code

-- ... should be located in the file...
-- src/Module1.purs

-- whereas

-- a submodule named...
module Module.SubModule.SubSubModule where
-- imports and source code

-- ... should be located in the file...
-- src/Module/SubModule/SubSubModule.purs

Real World Naming Conventions

See the Style Guide's section on Module Names

01-Basic-Syntax.purs

module Syntax.Module.Basic
  (
  -- exports appear here
  exportedFunction
  ) where

-- imports must appear at the top or you'll get a compiler error
import Prelude

-- everything else in the module goes underneath it

exportedFunction :: String -> String
exportedFunction x = x <> "more stuff"

-- an import cannot go here since we are no longer
-- in the "import section" of the file

notExportedValue :: Int
notExportedValue = 3

02-Basic-Exporting.purs

module Syntax.Module.Exporting
  -- exports go here by just writing the name
  ( value

  , function, (>@>>>) -- aliases must be wrapped in parenthesis

  -- when exporting type classes, there are two rules:
  -- - you must precede the type class name with the keyword 'class'
  -- - you must also export the type class' function (or face compilation errors)
  , class TypeClass, tcFunction

  -- when exporting modules, you must precede the module name with
  -- the keyword 'module'
  , module ExportedModule

  -- The type is exported, but no one can create a value of it
  -- outside of this module
  , ExportDataType1_ButNotItsConstructors

  -- syntax sugar for 'all constructors'
  -- Either all or none of a type's constructors must be exported
  , ExportDataType2_AndAllOfItsConstructors(..)

  -- Type aliases can also be exported
  , ExportedTypeAlias

  -- When type aliases are aliased using infix notation, one must export
  -- both the type alias, and the infix notation where 'type' must precede
  -- the infix notation
  , ExportedTypeAlias_InfixNotation, type (<|<>|>)

  -- Data constructor alias; exporting the alias requires you
  -- to also export the constructor it aliases
  , ExportedDataType3_InfixNotation(Infix_Constructor), (<||||>)

  , ExportedKind
  , ExportedKindValue
  ) where

-- imports go here
import ExportedModule

value :: Int
value = 3

function :: String -> String
function x = x

infix 4 function as >@>>>

class TypeClass a where
  tcFunction :: a -> a -> a

data ExportDataType1_ButNotItsConstructors = Constructor1A

data ExportDataType2_AndAllOfItsConstructors
  = Constructor2A
  | Constructor2B
  | Constructor2C

type ExportedTypeAlias = Int

data ExportedDataType3_InfixNotation = Infix_Constructor Int Int

infixr 4 Infix_Constructor as <||||>

type ExportedTypeAlias_InfixNotation = String

infixr 4 type ExportedTypeAlias_InfixNotation as <|<>|>

data ExportedKind

foreign import data ExportedKindValue :: ExportedKind

03-Basic-Importing.purs

-- For now, ignore the `module Exports` export
-- and the "import Module (value) as Exports" syntax
-- This will be explained later and is necessary now
-- to prevent the compiler from emitting lots of warnings.
module Syntax.Module.Importing (module Exports) where

-- One never just imports the entire module.
-- Rather, one must specify what is being imported.
-- The following import statements emit a compiler warning:
-- import Module
-- import Module.SubModule.SubSubModule

-- import values from a module
import ModuleValues (value1, value2) as Exports

-- imports functions from a module
import ModuleFunctions (function1, function2) as Exports

-- imports function alias from a module
import ModuleFunctionAliases ((/=), (===), (>>**>>)) as Exports

-- imports type class from the module
import ModuleTypeClass (class TypeClass) as Exports

-- import a type but none of its constructors
import ModuleDataType (DataType) as Exports

-- import a type and one of its constructors
import ModuleDataType (DataType(Constructor1)) as Exports

-- import a type and some of its constructors
import ModuleDataType (DataType(Constructor1, Constructor2)) as Exports

-- import a type and all of its constructors
import ModuleDataType (DataType(..)) as Exports

import ModuleKind (ImportedKind, ImportedKindValue) as Exports

-- To prevent warnings from being emitted during compilation
-- the above imports have to either be used here or
-- re-exported (explained later in this folder).

04-Resolving-Naming-Conflicts-Using-Keyword--Hiding.purs

-- There are situations where a function in one module
-- may be the same name as a function in another module

-- for example
-- module ModuleNameClash1 (sameFunctionName1) where -- ...
-- module ModuleNameClash2 (sameFunctionName1) where -- ...

-- In this file, how do we use both of them?
-- We can use the 'hiding' keyword
module Syntax.Module.ResolvingNamingConflicts.ViaHiding where

import ModuleNameClash1 (sameFunctionName1)
import ModuleNameClash2 hiding (sameFunctionName1)

-- now 'sameFunctionName1' refers to ModuleNameClash1's function,
-- not ModuleNameClash2's function
myFunction1 :: Int -> Int
myFunction1 a = sameFunctionName1 a

04-Resolving-Naming-Conflicts-Using-Module-Aliases.purs

{-
There are situations where a function in one module
may be the same name as a function in another module

For example
  module ModuleNameClash1 (sameFunctionName1) where -- ...
  module ModuleNameClash2 (sameFunctionName1) where -- ...

This can also arise when data type share the same name:
  module ModuleNameClash1 (SameDataName(..)) where
  module ModuleNameClash2 (SameDataName(..)) where

In this file, how do we use both of them?
We can use Module aliases -}
module Syntax.Module.ResolvingNamingConflicts.ViaModuleAliases where

import ModuleNameClash1 as M1
import ModuleNameClash2 as M2

myFunction2 :: Int -> Int
myFunction2 a = M1.sameFunctionName1 (M2.sameFunctionName1 a)

dataDifferences :: M1.SameDataName -> M2.SameDataName -> String
dataDifferences M1.Constructor M2.Constructor = "code works despite name clash"

05-Re-exporting-Modules-or-Submodules.purs

-- To get the "import RootModule.SubModule.SubModule" syntax
module Syntax.Module.ExportingModules
  ( module ModuleAlias
  ) where

-- We can use module alises to export multiple things
-- (e.g. types, constructors, functions, values)
-- from multiple modules conveniently

import Module1 (anInt1) as ModuleAlias
import Module2 (anInt2) as ModuleAlias
import Module3 (anInt3) as ModuleAlias

-- By convention, this is usually "Exports"
import Module4.SubModule1 (someFunction) as ModuleAlias

{-
This enables the syntax:
import Syntax.Module.ExportingModules (anInt, anInt2, anInt3, someFunction)

-- or we can use module aliases
import Syntax.Module.ExportingModules as EM

-- in code
EM.anInt
EM.someFunction
-}

06-Exporting-Entire-Current-Module.purs

{-
Let's say you have a module with A LOT of entities
and you want to export ALL of them. (This 'trick' doesn't
work if you want to export some but not all entities.)

Rather than typing all of the exports, you can use the
"re-export module" syntax to export the current module
-}
module Syntax.Module.ExportingEntireCurrentModule
  (
    -- By exporting the current module,
    -- we can export all of its entities at once.
    module Syntax.Module.ExportingEntireCurrentModule
  ) where

-- 14 entities in total
a :: String
a = "a"

b :: String
b = "b"

c :: String
c = "c"

d :: String
d = "d"

e :: String
e = "e"

f :: String
f = "f"

g :: String
g = "g"

h :: String
h = "h"

i :: String
i = "i"

j :: String
j = "j"

k :: String
k = "k"

l :: String
l = "l"

m :: String
m = "m"

n :: String
n = "n"

11-Full-Module-Syntax.purs

module Syntax.Module.FullExample
  -- exports go here by just writing the name
  ( value

  , function, (>@>>>) -- aliases must be wrapped in parenthesis

  -- when exporting type classes, there are two rules:
  -- - you must precede the type class name with the keyword 'class'
  -- - you must also export the type class' function (or face compilation errors)
  , class TypeClass, tcFunction

  -- when exporting modules, you must precede the module name with
  -- the keyword 'module'
  , module ExportedModule

  -- The type is exported, but no one can create a value of it
  -- outside of this module
  , ExportDataType1_ButNotItsConstructors

  -- syntax sugar for 'all constructors'
  -- Either all or none of a type's constructors must be exported
  , ExportDataType2_AndAllOfItsConstructors(..)

  -- Type aliases can also be exported
  , ExportedTypeAlias

  -- When type aliases are aliased using infix notation, one must export
  -- both the type alias, and the infix notation where 'type' must precede
  -- the infix notation
  , ExportedTypeAlias_InfixNotation, type (<|<>|>)

  -- Data constructor alias; exporting the alias requires you
  -- to also export the constructor it aliases
  , ExportedDataType3_InfixNotation(Infix_Constructor), (<||||>)

  , ExportedKind
  , ExportedKindValue
  ) where

-- imports go here

-- imports just the module
import Module

-- import a submodule
import Module.SubModule.SubSubModule

-- import values from a module
import ModuleValues (value1, value2)

-- imports functions from a module
import ModuleFunctions (function1, function2)

-- imports function alias from a module
import ModuleFunctionAliases ((/=), (===), (>>**>>))

-- imports type class from the module
import ModuleTypeClass (class TypeClass)

-- import a type but none of its constructors
import ModuleDataType (DataType)

-- import a type and one of its constructors
import ModuleDataType (DataType(Constructor1))

-- import a type and some of its constructors
import ModuleDataType (DataType(Constructor1, Constructor2))

-- import a type and all of its constructors
import ModuleDataType (DataType(..))

-- resolve name conflicts using "hiding" keyword
import ModuleNameClash1 (sameFunctionName1)
import ModuleNameClash2 hiding (sameFunctionName1)

-- resolve name conflicts using module aliases
import ModuleNameClash1 as M1
import ModuleNameClash2 as M2

-- Re-export modules
import Module1 (anInt1) as Exports
import Module2 (anInt2) as Exports
import Module3 (anInt3) as Exports
import Module4.SubModule1 (someFunction) as Exports

import ModuleKind (ImportedKind, ImportedKindValue) as Exports

import Prelude

import ExportedModule

-- To prevent warnings from being emitted during compilation
-- the above imports have to either be used here or
-- re-exported (explained later in this folder).

value :: Int
value = 3

function :: String -> String
function x = x

infix 4 function as >@>>>

class TypeClass a where
  tcFunction :: a -> a -> a

-- now 'sameFunctionName1' refers to ModuleF1's function, not ModuleF2's function
myFunction1 :: Int -> Int
myFunction1 a = sameFunctionName1 a

myFunction2 :: Int -> Int
myFunction2 a = M1.sameFunctionName1 (M2.sameFunctionName1 a)

dataDifferences :: M1.SameDataName -> M2.SameDataName -> String
dataDifferences M1.Constructor M2.Constructor = "code works despite name clash"

data ExportDataType1_ButNotItsConstructors = Constructor1A

data ExportDataType2_AndAllOfItsConstructors
  = Constructor2A
  | Constructor2B
  | Constructor2C

type ExportedTypeAlias = Int

data ExportedDataType3_InfixNotation = Infix_Constructor Int Int

infixr 4 Infix_Constructor as <||||>

type ExportedTypeAlias_InfixNotation = String

infixr 4 type ExportedTypeAlias_InfixNotation as <|<>|>

data ExportedKind

foreign import data ExportedKindValue :: ExportedKind

Prelude Syntax

This folder documents the syntax that is enabled by importing Prelude.

Ignore this folder's contents until you are reading through the Prelude folder in the Hello World folder. When you are in that folder and you see unfamiliar syntax, you should probably return here:

  • do notation
  • ado notation
  • Natural Transformations

Discard

There is a type class in Prelude called Discard that does not appear in our diagram of Prelude's type classes. It is implemented only by Unit.:

-- Pseudo-Syntax: combines the class and its only instance into one block:
class Discard Unit where
  discard :: forall f b. Bind f => f Unit -> (Unit -> f b) -> f b
  discard = bind

This seemingly pointless type class insures that you do not accidentally "throw away" the result of a computation when you did not intend to do so (covered next in 'do notation'). One should almost never implement it for another type, unless one knows what they are doing and they have a very rare use case for it.

02-Do-Notation.purs

module Syntax.Prelude.Notation.Do where

import Prelude

{-
In imperative programming, one often writes some sequential code like:
  x = 4
  y = x + 4
  z = toString x
  print z

Since each line depends on the line before it,
  this implies sequential computation, or Monads. We have "do" notation
  to imitate this style of code

Recall the type signature of `bind`...
      bind :: m a -> (a -> m b) -> m b
... and that ">>=" is an alias for `bind`

Thus, these two expressions are the same:
    bind computation (\result -> newComputationUsing result)
    computation >>= (\result -> newComputationUsing result)
-}

do1_bind :: Box Unit
do1_bind =
  bind get4 (\x ->
    bind (add4To x) (\y ->
      bind (toString y) (\z ->
        print z
      )
    )
  )
-- which is better understood by replacing `bind` with ">>=" as
do1_alias :: Box Unit
do1_alias =
  get4 >>= (\x ->
    -- only call `add4To x` if `get4` actually produces something
    add4To x >>= (\y ->
      toString y >>= (\z ->
        print z
      )
    )
  )
-- which is better understood and more readable as
do1_do_notation :: Box Unit
do1_do_notation = do
  x     <- get4
  y     <- add4To x
  z     <- toString y
  -- last line in do notation must not end with `value <- computation`
  -- but should just end in `computation`
  print z

-- Just like in regular functions, we could use 'let-in` syntax
do2_bind :: Box Unit
do2_bind =
  bind get4 (\x ->
      let y = x + 4
      in bind (toString y) (\z -> print z)
    )
-- which is better understood by replacing `bind` with ">>=" as
do2_alias :: Box Unit
do2_alias =
  get4 >>= (\x ->
      -- While replacing 'bind' with '>>=' is better,
      -- this let-in syntax is still not very readable here...
      let y = x + 4
      in toString y >>= (\z -> print z)
    )
-- but we can write it better as
do2_do_notation :: Box Unit
do2_do_notation = do
  x <- get4
  let y = x + 4   -- no need to have a corresponding `in` statement
  z <- toString y
  print z

do3_ignoreValue_bind :: Box Unit
do3_ignoreValue_bind =
  bind get4 (\x ->
    bind (takeValueAndIgnoreResult x) (\_ {- this underscore is 'unit' -} ->
      print x
    )
  )
-- which is better understood by replacing `bind` with ">>=" as in
do3_ignoreValue_alias :: Box Unit
do3_ignoreValue_alias =
  get4 >>= (\x -> takeValueAndIgnoreResult x >>= (\_ -> print x))
-- gets turned into...
do3_ignoreValue_do_notation :: Box Unit
do3_ignoreValue_do_notation = do
  x     <- get4
  _     <- takeValueAndIgnoreResult x

  print x

do4_discard_bind :: Box Unit
do4_discard_bind =
  bind (Box unit) (\unit_ ->
    bind (Box unit) (\unit__ ->
      print 5
    )
  )
-- which is better understood by replacing `bind` with ">>=" as in
do4_discard_alias :: Box Unit
do4_discard_alias =
  (Box unit) >>= (\unit_ ->
    (Box unit) >>= (\unit__ ->
        print 5
    )
  )
-- can be written as...
do4_discard_syntax :: Box Unit
do4_discard_syntax = do                                           {-
  When we omit the "binding <-" syntax, as in

      four <- Box 4   -- line 1
              Box a   -- line 2
      five <- Box 5   -- line 3

  the compiler translates `line 2` to
      "discard (Box a) (\_ -> (Box 5) >>= (\five -> ... ))"

  This is fine if the argument to the next function would be Unit

      four <- Box 4
      unit <- Box unit  -- here, we could omit the "unit <-" syntax
      five <- Box 5
              Box unit -- same thing

  If we had accidentally written code that amounted to this...

      four <- Box 4
              Box 10 -- notice how there is no "10 <-" fragment
      five <- Box 5

  ... the compiler would notify us that we had discarded a non-unit value
      (i.e. 10):
        "Could not find instance of Discard for Int"

  Why does it do this? To highlight that we have accidentally dropped the
  result of the computation. If you want to intentionally drop a result,
  use `void $ monad` or the "_ <- computation" syntax                         -}
  x <- (Box 5)
  (Box unit) -- since it returns unit, it's ok to use discard here

  -- rather than write this...
  map (\_ -> unit) (Box 5)

  -- or even this...
  (\_ -> unit) <$> (Box 5)

  -- we write this:
  void $ Box 5

  print 5

do_full_syntax :: Box Unit
do_full_syntax = do
  x <- get4

  _ <- takeValueAndIgnoreResult x

  (Box unit)
  void $ takeValueAndIgnoreResult x

  let y = x + 4
  z <- toString y
  -- last line in do notation must NOT end with `value <- expression`
  -- but should just end in `expression`
  print z

-- needed to make this file compile
data Box a = Box a

derive instance Functor Box

instance Apply Box where
  apply (Box f) (Box a) = Box (f a)

instance Applicative Box where
  pure a = Box a

instance Bind Box where
  bind (Box a) f = f a

get4 :: Box Int
get4 = Box 4

add4To :: Int -> Box Int
add4To i = Box (i + 4)

toString :: Int -> Box String
toString i = Box (show i)

takeValueAndIgnoreResult :: forall a. a -> Box a
takeValueAndIgnoreResult a = Box a

print :: forall a. a -> Box Unit
print _ = Box unit

Reading Do Notation as Nested Binds

Be aware of where the parenthesis appear when using multiple bind expressions (e.g. m a >>= aToMB >>= bToMC). Below provides a summary of the section called "Do notation" in this article:

data Maybe a
  = Nothing
  | Just a

instance Bind Maybe where
  bind :: forall a b. Maybe a -> (a -> Maybe b) -> Maybe b
  -- when given a Nothing, stop all future computations and return immediately.
  bind Nothing _ = Nothing
  -- when given a Just, run the function on its contents
  bind (Just a) f = f a

half :: Int -> Maybe Int
half x | x % 2 == 0 = Just (x / 2)
       | otherwise = Nothing

-- This statement
(Just 128) >>= half >>= half >>= half
-- desugars first to
(Just 128) >>= (\original -> half original >>= half >>= half )
-- which can be better understood as
(Just 128) >>= aToMB
-- which can be better understood as
bind (Just 128) >>= aToMB
-- since the latter ">>=" calls are nested inside of the first one, one
-- should read the above computation as "Only continue if the previous
--    `bind`/`>>=` call was successful."
-- In this situation, it is:
bind (Just 128) (\original -> half original >>= half >>= half)
-- reduces to
(\128 -> half 128 >>= half >>= half)
-- reduces to
half 128 >>= half >>= half
-- ... and so forth until we get the result:
Just 16


-- Similarly
Nothing    >>= half >>= half >>= half == Nothing
-- desguars first to
Nothing    >>= (\value -> half value >>= half >>= half) == Nothing
-- which can be better understood as
Nothing    >>= aToMB == Nothing
-- which can be better understood as
bind Nothing aToMB == Nothing
-- and, looking at the instance of Bind above, reduces to Nothing
-- The other `half` computations are never executed.

-- Thus, given this function...
half3Times :: Maybe Int -> Maybe Int
half3Times maybeI = do
  original <- maybeI
  first <- half original    -- ===
  second <- half first      --  | a -> m b
  third <- half second      --  |
  pure third                -- ===
-- ... passing in `Nothing` doesn't compute anything
half3Times Nothing == Nothing

-- Likewise, passing in a bad starting value will also stop the computation
-- as soon as possible:
(Just 3) >>= half >>= (\thisWontRun -> pure thisWontRun)
-- will desugar to
bind (Just 3) half =
-- will desugar to
half 3
-- which desugars to
half 3 | 3 % 2 == 0
       | otherwise = Nothing
-- which tests whether `3 % 2 == 0` (false) the 'otherwise path'
Nothing >>= (\thisWontRun -> pure thisWontRun)
-- which desugars to
bind NOthing (\thisWontRun -> pure thisWontRun)
-- which desugars to
Nothing

04-Ado-Notation.purs

{-
Link to original issue's comment
where this is fully explained:
https://github.com/purescript/purescript/pull/2889#issuecomment-301260299

Following the 'do' notation of Monads, the 'ado' notation is for Applicative
Since Applicative can be used for parellel computation, one **might**
   read the following code as
     "produces some value at the same time it's producing another value"
   rather than sequential computation, which is
     "produces some value, and then uses that value to produce another value"
It depends on whether parallel applicatives are used or not.
-}
module Syntax.Prelude.Notation.Ado where

import Prelude

data Box a = Box a

instance Functor Box where
  map :: forall a b. (a -> b) -> Box a -> Box b
  map f (Box a) = Box (f a)
-- infixl 4 map as <$>

instance Apply Box where
  apply :: forall a b. Box (a -> b) -> Box a -> Box b
  apply (Box f) (Box a) = Box (f a)
-- infixl 4 apply as <*>

instance Applicative Box where
  pure :: forall a. a -> Box a
  pure a = Box a
------------------------------
pure_no_sugar :: forall a b. (a -> b) -> a -> Box b
pure_no_sugar f a = pure (f a)

pure_sugar :: forall a b. (a -> b) -> a -> Box b
pure_sugar f a = ado
  in f a
------------------------------
map_no_sugar :: forall a b. (a -> b) -> Box a -> Box b
map_no_sugar f g = (\x -> f x) <$> g

map_sugar :: forall a b. (a -> b) -> Box a -> Box b
map_sugar f g = ado
  x <- g
  in f x
------------------------------
-- See `lift2` from Apply: https://pursuit.purescript.org/packages/purescript-prelude/4.1.0/docs/Control.Apply#v:lift2
liftN_no_sugar :: forall a b c. (a -> b -> c) -> Box a -> Box b -> Box c
liftN_no_sugar f g h = (\x y -> f x y) <$> g <*> h

liftN_sugar :: forall a b c. (a -> b -> c) -> Box a -> Box b -> Box c
liftN_sugar f g h = ado
  x <- g
  y <- h
  in f x y
------------------------------
liftN_unit_no_sugar :: forall a b. (a -> b) -> Box a -> Box Unit -> Box b
liftN_unit_no_sugar f g h = (\x _ -> f x) <$> g <*> h

liftN_unit_sugar :: forall a b. (a -> b) -> Box a -> Box Unit -> Box b
liftN_unit_sugar f g h = ado
  x <- g
  h
  in f x
------------------------------
liftN_Let_no_sugar :: forall a. (Int -> Int -> a) -> Box Int -> Box Unit -> Box a
liftN_Let_no_sugar f g h = (\x -> let y = x + 1 in (\_ -> f x y)) <$> g <*> h

liftN_Let_sugar :: forall a. (Int -> Int -> a) -> Box Int -> Box Unit -> Box a
liftN_Let_sugar f g h = ado
  x <- g
  let y = x + 1
  h
  in f x y

05-Natural-Transformation.purs

module Syntax.Prelude.NaturalTransformations where

-- Given this code
data Box1 a = Box1 a
data Box2 a = Box2 a

-- This function's type signature...
box1_to_box2_noisy :: forall a. Box1 a -> Box2 a
box1_to_box2_noisy (Box1 a) = Box2 a
-- ... has a lot of noise and could be re-written to something
-- that communicates our intent better via Natural Transformations...

-- Read: given an 'a' that is inside of a 'container' or 'context',
-- change the container F to container G.
-- I don't care what type 'a' is since it's irrelevant
type NaturalTransformation_ f g = forall a. f a -> g a

infixr 4 type NaturalTransformation_ as ~>

box1_to_box2 :: Box1 ~> Box2 {- much less noisy than
box1_to_box2 :: forall a. Box1 a -> Box2 a -}
box1_to_box2 (Box1 a) = Box2 a

Modifying Do/Ado Syntax Sugar

This folder documents how one can modify what occurs when using "do notation" and "ado notation." You will likely not need to use these as a beginner. Over time, once you have learned more about FP, these features may be useful. Feel free to skip or skim through this on your first read.

The Problem

"do notation" and "ado notation" are purely syntax sugar. Rather than having to write some rather verbose code, we can use these two keywords to make the compiler do all of that for us.

It would be nice if one could modify how this syntax sugar gets desugared in some situations. For example...

  • ado notation:
    • removing some of the boilerplate needed to use Applicatives to validate data.
  • do notation:
    • using IndexedMonad-based computations (i.e. monads with phantom types that provide more context about what can/can't happen at that computation step) in the same way we would use Monad-based computations.

Presently, there are two ways to do this:

  • Rebindable Do/Ado
  • Qualified Do/Ado (available since the 0.12.2 release)

Each will be covered in the following folders. To keep it simple, we'll use the Box monad to explain how it works. Unfortunately, this monadic type isn't a good example as to why one would want to use this.

01-Rebindable-Ado.purs

module Syntax.Modification.RebindableAdo where

-- I assume you are already familiar with how 'ado notation' desugars.
-- If not, go read through that explanation again.

import Prelude

-- We'll use a qualified import to make it easier to see
-- when we're referring to the REAL 'apply' and 'map' as defined
-- in Prelude and not our customized versions.
import Data.Functor as NormalMap
import Control.Apply as NormalApply

-- Given this monad (type class instances are at bottom of file)
data Box a = Box a

{-
"ado notation" works by
- desugaring the "<-" notation via the "apply" function within scope
- desugaring the "in <function>" notation via the "map" function within scope

Thus, to change how these two things desugar, we change what 'apply' and 'map'
mean via a let binding or a where clause.

Note: rebinding 'ado' will produce the following compiler warnings:
"Name `apply` was shadowed."
"Name `map` was shadowed."
-}

-- This is how we would use "rebindable syntax" via a 'let binding'
-- to write normal "ado notation" (i.e. apply and map are unchanged)
normalApply_let_in :: Box Int
normalApply_let_in =
  let
    {-
    These do not work

    -- Compiler error: "The value of apply is undefined here,
    -- so this reference is not allowed."
    -- The compiler thinks 'apply' is defined as itself.
    apply :: forall f a b. Apply f => f (a -> b) -> f a -> f b
    apply = apply
    -}

    -- These do work.
    -- apply :: forall f a b. Apply f => f (a -> b) -> f a -> f b
    apply = NormalApply.apply

    -- map :: forall f a b. Functor f => (a -> b) -> f a -> f b
    map = NormalMap.map
  in ado
    three <- Box 3
    two <- Box 2
    in three + two

-- Redefining them in a 'where' clause is more readable.
normalApply_where :: Box Int
normalApply_where = ado
  three <- Box 3
  two <- Box 2
  in three + two
  where
    apply :: forall f a b. Apply f => f (a -> b) -> f a -> f b
    apply = NormalApply.apply

    map :: forall f a b. Functor f => (a -> b) -> f a -> f b
    map = NormalMap.map

-- Now, let's change how `<-` gets desugared by actuallying changing how
-- `apply` is defined. Whenever `apply` gets called, it will add 1
-- to the result of apply by using `map (_ + 1)`.
plusApply_where :: Box Int
plusApply_where = ado
  three <- Box 3
  two <- Box 2
  in three + two
  where
    -- Our modified 'apply' will add 1 each time it gets called.
    -- To support this modification, we'll modify the type signature
    -- to force this to only work on boxes of Ints.
    apply :: forall f. Apply f => f (Int -> Int) -> f Int -> f Int
    apply boxedF boxedArg =
      NormalMap.map (_ + 1) (NormalApply.apply boxedF boxedArg)         {-
                             ^ Warning: if we used `apply` here instead of
                               `NormalApply.apply`, we would have
                               created an infinite loop                  -}

    -- Since `map` isn't defined here, the closest "in-scope" definition
    -- is the `map` that is imported from Data.Functor in the 'import Prelude'
    -- line.

{-
Here's the graph reduction of the above `plus1Apply` code:
ado
  three <- Box 3
  two <- Box 2
  in three + two

((\three two -> three + two)
  `NormalMap.map` Box 3)
  `apply` Box 2 -- i.e. our modified apply

(NormalMap.map (\three two -> three + two) (Box 3)) `apply` Box 2

Box (\two -> 3 + two) `apply` Box 2

apply (Box (\two -> 3 + two)) (Box 2)

-- now reduces 'apply' with our modified definition
-- Note that "<*>" refers to the normal apply definition.

map (_ + 1) (NormalApply.apply (Box (\two -> 3 + two)) (Box 2))
map (_ + 1) (                  (Box (\2   -> 3 + 2  ))        )
map (_ + 1) (                  (Box (        5      ))        )
map (_ + 1) (Box 5)
Box 6
-}

-- Applicative types allow us to use things like `*>` and `<*`
-- Why not rebind ado notation to use that for apply?
realWorldExample :: Box Int
realWorldExample = ado
  three <- Box 3
  two <- Box 2
  in three + two
  where
    -- Our modified `apply` will run the normal computation
    -- and then return the result, Box 4, rather than the actual computation.
    -- A better example would be using "ado notation" to validate
    -- data and use something like, `boxedF <*> boxedArg <* pure unit`,
    -- to reduce boilerplate.
    apply :: forall f. Applicative f => f (Int -> Int) -> f Int -> f Int
    apply boxedF boxedArg = boxedF <*> boxedArg *> pure 4

{-
The above graph reduction is:
ado
  three <- Box 3
  two <- Box 2
  in three + two

(\three two -> three + two)
  `NormalMap.map` (Box 3)
  `apply` (Box 2)

NormalMap.map (\three two -> three + two) (Box 3) `apply` (Box 2)

Box (\two -> 3 + two) `apply` (Box 2)

-- here we use replace 'apply' with our modified definition

Box (\two -> 3 + two) <*> (Box 2) *> Box 4
NormalApply.apply (Box (\two -> 3 + two)) <*> (Box 2) *> Box 4
                  (Box (\    -> 3 + 2  ))             *> Box 4
                  (Box          5       )             *> Box 4
                  applyRight (Box 5) (Box 4)

applyRight (Box 5) (Box 4)
(\left right -> right) <$> (Box 5) <*> (Box 4)
Box 4
-}

-- I don't have an example of remapping `map` because I couldn't find a way
-- to do that without getting a compiler error.
-- Still, this file demonstrates how to do this.

{-
Lastly, one can change `apply` multiple times throughout the 'ado notation'
if one were to use lets. For example,

ado
  -- apply not set, so use normal definition
  a <- someA
  let apply = definition -- apply now uses modified version
  b <- someB
  c <- someC
  let apply = definition2 -- apply now uses a second modified version
  d <- someD
  in computation a b c d

For readability, this is not recommended. I only include it here to be
complete in this explanation.

-}

-- Type class instances

instance Functor Box where
  map :: forall a b. (a -> b) -> Box a -> Box  b
  map f (Box a) = Box (f a)

instance Apply Box where
  apply :: forall a b. Box (a -> b) -> Box a -> Box  b
  apply (Box f) (Box a) = Box (f a)

instance Applicative Box where
  pure :: forall a. a -> Box a
  pure a =  Box a

instance (Show a) => Show (Box a) where
  show (Box a) = "Box(" <> show a <> ")"

01-Rebindable-Do.purs

module Syntax.Modification.RebindableDo where

-- I assume you are already familiar with how 'do notation' desugars.
-- If not, go read through that explanation again.

import Prelude

-- We'll use a qualified import to make it easier to see
-- when we're referring to the REAL 'bind` defined
-- in Prelude and not our customized version.
import Control.Bind as NormalBind

-- Given this monad (type class instances are at bottom of file)
data Box a = Box a

{-
"do notation" works by
- desugaring a line with the "<-" notation via the "bind" function within scope
- desugaring a line without the "<-" notation via the "discard" function within scope

Thus, to change how these two things desugar, we change what 'bind' and 'discard'
mean via a let binding or a where clause.
However, since `discard = void $ bind`, we almost never need to remap `discard`
to a different definition. While one could, I don't know why one would.

Note: rebinding 'do' will produce the following compiler warnings:
"Name `bind` was shadowed."
"Name `discard` was shadowed."
-}

-- While 'bind' has been imported above, we don't have to use that 'bind' explicitly
normalBind_let_in :: Box Int
normalBind_let_in =
  let
    bind = NormalBind.bind

    -- this isn't necessary, but we'll include it here anyway.
    discard = NormalBind.discard
  in do
    three <- Box 3
    Box unit
    two <- Box 2
    pure (three + two)


-- Redefining them in a where clause is more readable.
normalBind_where :: Box Int
normalBind_where = do
  three <- Box 3
  two <- Box 2
  pure (three + two)
  where
    bind = NormalBind.bind

    -- Again, this isn't necessary, but we'll include it here anyway.
    discard = NormalBind.discard

-- Similar to `ado notation`, we can rebind `do notation` to use a different
-- implementation than the default `bind`.
plusBind_where :: Box Int
plusBind_where = do
  three <- Box 3
  two <- Box 2
  pure (three + two)
  where
    bind boxedArg aToMB =
      NormalBind.bind boxedArg aToMB >>= \result -> pure (result + 1)        {-
      ^ Warning: using `bind` here would lead to an infinite loop during
        runtime that will stack overflow. We need to refer to the normal
        bind using `NormalBind.bind` or `>>=`                                   -}

    -- discard is not included here because the next closest discard definition
    -- in scope is the one imported via "import Prelude"

{-
The above code's graph reduction is:
do
  three <- Box 3
  two <- Box 2
  pure (three + two)

bind (Box 3) (\three ->
  bind (Box 2) (\three ->
    pure (three + two)
  )
)

let firstBindResult = NormalBind.bind (Box 3) (\x -> pure (x + 1))
in NormalBind.bind firstBindResult (\three ->
  bind (Box 2) (\three ->
    pure (three + two)
  )
)

let firstBindResult =                         (\3 -> pure (3 + 1))
in NormalBind.bind firstBindResult (\three ->
  bind (Box 2) (\three ->
    pure (three + two)
  )
)

let firstBindResult =                         (      pure 4     )
in NormalBind.bind firstBindResult (\three ->
  bind (Box 2) (\three ->
    pure (three + two)
  )
)

let firstBindResult = Box 4
in NormalBind.bind firstBindResult (\three ->
  bind (Box 2) (\three ->
    pure (three + two)
  )
)

NormalBind.bind (Box 4) (\three ->
  bind (Box 2) (\two ->
    pure (three + two)
  )
)
                        (\4 ->
  bind (Box 2) (\two ->
    pure (4 + two)
  )
)

bind (Box 2) (\two ->
  pure (4 + two)
)

let secondBindResult = NormalBind.bind (Box 2) (\y -> pure (y + 1))
in NormalBind.bind secondBindResult (\two ->
  pure (4 + two)
)

let secondBindResult =                         (\2 -> pure (2 + 1))
in NormalBind.bind secondBindResult (\two ->
  pure (4 + two)
)

let secondBindResult =                         (      pure 3      )
in NormalBind.bind secondBindResult (\two ->
  pure (4 + two)
)

let secondBindResult = Box 3
in NormalBind.bind secondBindResult (\two ->
  pure (4 + two)
)

NormalBind.bind (Box 3) (\two ->
  pure (4 + two)
)

                        (\3 ->
  pure (4 + 3)
)

pure (4 + 3)

Box 7

-}

{-
This example would require using a monad that supports MonadWriter.
One could rebind `bind` to log the argument before continuing the computation.

For example, someting like:
  bind computation aToMB =
    computation >>= (\result ->
      -- log what the argument was here via `tell`
      tell result >>= (\_ ->
        -- then continue the computation like normal
        aToMB result
      )
-}

-- Type class instances

instance Functor Box where
  map :: forall a b. (a -> b) -> Box a -> Box  b
  map f (Box a) = Box (f a)

instance Apply Box where
  apply :: forall a b. Box (a -> b) -> Box a -> Box  b
  apply (Box f) (Box a) = Box (f a)

instance Bind Box where
  bind :: forall a b. Box a -> (a -> Box b) -> Box b
  bind (Box a) f = f a

instance Applicative Box where
  pure :: forall a. a -> Box a
  pure a =  Box a

instance Monad Box

instance (Show a) => Show (Box a) where
  show (Box a) = "Box(" <> show a <> ")"

Introducing Qualified Do/Ado

Possible Readability Issue with Rebindable Do/Ado Notation

When using Rebindable do/ado notation, I'd recommend using the let ... in do/ado aproach for rebinding function names. Let me give an example why. If we used the 'where' clause approach, it isn't immediately clear whether do/ado notation desugars to the standard functions or to some remapped version until the very end. For example,

-- Reader thinks, "Oh hey! It's do notation.
-- It's just standard `bind` desugaring."
comp3 :: Box Int
comp3 = do
  a <- Box 1
  b <- Box 1
  c <- Box 1
  d <- Box 1
  e <- Box 1
  f <- Box 1
  g <- Box 1
  h <- Box 1
  i <- Box 1
  j <- Box 1
  k <- Box 1
  l <- Box 1
  m <- Box 1
  n <- Box 1
  o <- Box 1
  p <- Box 1
  q <- Box 1
  r <- Box 1
  pure 5
  where
    someValue = "some really long boilerplate-y string..."

    anotherComputation = case _ of
      Just x -> Right $ foldl ((:)) Nil x
      Nothing -> Left "Not sure what went wrong here..."

    -- Reader now thinks, "Oh crap. My understanding is completely off
    -- now that I know `bind` really means the below definition..."
    bind = -- my custom bind definition...

The above problem can be alleviated by bumping bind to the top using a let binding.

-- Reader thinks, "Oh hey! It's do notation.
-- It's just standard `bind` desugaring."
comp3 :: Box Int
comp3 = do
  -- Reader thinks, "Oh wait. It's using a custom bind definition.
  -- I'll need to read through this next part carefully..."
  let bind = -- my custom bind definition...
  in do
    a <- Box 1
    b <- Box 1
    -- the rest of the code in the example above...

Problems with Rebindable Do/Ado Notation

There are generally two problems with Rebindable do/ado notation.

First, each function that uses this feature must rebind do/ado notation to the correct definition. If one was building a library where each function used this, it would get very tedious.

For example,

comp1 :: Box Int
comp1 = let bind = NormalBind.bind in do
  three <- Box 3
  Box unit
  two <- Box 2
  pure (three + two)

comp2 :: Box Int
comp2 = let bind = NormalBind.bind in do
  three <- Box 3
  pure (three + two)

-- ok, this is really getting tedious...
comp3 :: Box Int
comp3 = let bind = NormalBind.bind in do
  three <- Box 3
  Box unit
  two <- Box 2
  pure (three + two)

Second, rebindable do/ado notation might not be easily redable when running computations in various monadic contexts. For example

someComputation :: Box Int
someComputation = let bind = NormalBind.bind in do
  -- Box monadic context... use standard bind here
  value1 <- takesMonad1Argument (let bind = customBind in do
    -- Monad1 monadic context... use custom bind here
    value2 <- runMonad1Computation
    takesMonad2Argument (let bind = NormalBind.bind in do
      -- Monad2 monadic context... use a different custom bind here...
      value3 <- runMonad2Computation)
      pure (value3 + 5))
  pure (value1 + 8)

As can be seen, "rebindable" do/ado notation is good when functions do not use many lines and one is not switching back and forth between monadic contexts.

Still, Qualified Do/Ado helps "solve" each of these problems. What follows is the requirements one needs to implement before this feature will work. In this example, we'll use a more complicated example: IndexedMonad/IxMonad.

12-MonadLikeTypeClasses.purs

module Syntax.Modification.MonadLikeTypeClasses
  ( class IxFunctor, imap, map
  , class IxApply, iapply, apply
  , class IxApplicative, ipure, pure
  , class IxBind, ibind, bind
  , class IxMonad
  , Box(..)
  ) where

import Data.Unit (Unit)
import Data.Show (class Show, show)
import Data.Semigroup ((<>))

-- Given a data type with instances for the IndexedMonad type class
-- hierarchy (type class instances are below each type class)
data Box :: forall k. k -> k -> Type -> Type
data Box phantomInput phantomOutput storedValue = Box storedValue

instance (Show a) => Show (Box x x a) where
  show (Box a) = "Box(" <> show a <> ")"

-- Requirement 1: type classes that are similar to Functor to Monad hierarchy
--  - ado requirements: Functor, Apply, and Applicative
--  - do requirements: Functor, Apply, Applicative, Bind, and Monad

class IxFunctor :: forall k. (k -> k -> Type -> Type) -> Constraint
class IxFunctor f where
  imap :: forall a b x. (a -> b) -> f   x x a -> f   x x b

instance IxFunctor Box where
  imap :: forall a b x. (a -> b) -> Box x x a -> Box x x b
  imap f (Box a) = Box (f a)


class IxApply :: forall k. (k -> k -> Type -> Type) -> Constraint
class (IxFunctor f) <= IxApply f where
  iapply :: forall a b x y z. f   x y (a -> b) -> f   y z a -> f   x z b

instance IxApply Box where
  iapply :: forall a b x y z. Box x y (a -> b) -> Box y z a -> Box x z b
  iapply (Box f) (Box a) = Box (f a)


class IxApplicative :: forall k. (k -> k -> Type -> Type) -> Constraint
class (IxApply f) <= IxApplicative f where
  ipure :: forall a x. a -> f   x x a

instance IxApplicative Box where
  ipure :: forall a x. a -> Box x x a
  ipure a = Box a


class IxBind :: forall k. (k -> k -> Type -> Type) -> Constraint
class (IxApply m) <= IxBind m where
  ibind :: forall a b x y z. m   x y a -> (a -> m   y z b) -> m   x z b

instance IxBind Box where
  ibind :: forall a b x y z. Box x y a -> (a -> Box y z b) -> Box x z b
  ibind (Box a) f =
    -- `f a` produces a value with the type, `Box y z b`, which is
    -- not the return type of this function, `Box x z b`.
    --
    -- So, we can either `unsafeCoerce` the result of `f a` or just
    -- rewrap the 'b' value in a new Box. We've chosen to take the
    -- latter option here for simplicity.
    case f a of Box b -> Box b


class IxMonad :: forall k. (k -> k -> Type -> Type) -> Constraint
class (IxApplicative m, IxBind m) <= IxMonad m

instance IxMonad Box

-- Requirement 2: define functions whose names correspond to the ones used
-- in the regular type classes: `map`, `apply`, 'pure', 'bind', and
-- 'discard' (for when bind returns 'unit')
map :: forall f a b x. IxFunctor f => (a -> b) -> f x x a -> f x x b
map = imap

apply :: forall f a b x y z. IxApply f => f x y (a -> b) -> f y z a -> f x z b
apply = iapply

pure :: forall f a x. IxApplicative f => a -> f x x a
pure = ipure

bind :: forall m a b x y z. IxBind m => m x y a -> (a -> m y z b) -> m x z b
bind = ibind

discard :: forall a x y z m. IxBind m => m x y a -> (a -> m y z Unit) -> m x z Unit
discard = ibind

13-Qualified-Do.purs

module Syntax.Modification.QualifiedDo where

-- we'll import Prelude so that the regular functions (e.g. "pure" "bind")
-- are in scope to prove that they don't cause problems here.
import Prelude


-- Requirement 3: import the module using a module alias, making it possible
-- to use the same function names to refer to different "bind"-like functions
import Syntax.Modification.MonadLikeTypeClasses as I
import Syntax.Modification.MonadLikeTypeClasses (Box)

-- Requirement 4: When we want to use 'qualified do' syntax, we need to call
-- the separate functions above and constrain the types to use IxMonad
doExample :: forall input. Box input input String
doExample = I.do        -- signifies that we're using the "bind" and "discard"
                        --   functions defined in the "MonadLikeTypeClasses"
                        --   module to desugar "<-" and lines that lack it
                        --   (i.e. discard)

  a <- I.pure "test1"   -- signifies that we're using the "pure" function
                        --   defined in the "MonadLikeTypeClasses" module
  b <- I.pure "test2"
  I.pure (a <> b)

14-Qualified-Ado.purs

module Syntax.Modification.QualifiedAdo where

-- we'll import Prelude so that the regular functions (e.g. "map" "apply")
-- are in scope to prove that they don't cause problems here.
import Prelude


-- Requirement 3: import the module using a module alias, making it possible
-- to use the same function names to refer to different "apply"-like functions
import Syntax.Modification.MonadLikeTypeClasses as I
import Syntax.Modification.MonadLikeTypeClasses (Box)

-- Requirement 4: When we want to use 'qualified ado' syntax, we need to call the separate
-- function above and constrain the types to use IxApplicative
adoExample :: forall x. Box x x String
adoExample = I.ado       -- signifies that we're using the "apply" and "map"
                         --   functions defined in the "MonadLikeTypeClasses"
                         --   module to desugar "<-" and "in <function>"
                         --   notation.

  a <- I.pure "test1"    -- signifies that we're using the "pure" function
                         --   defined in the "MonadLikeTypeClasses" module
  b <- I.pure "test2"
  in twoArgFunction a b

twoArgFunction :: String -> String -> String
twoArgFunction a b = a <> b

mixingAdosTogether :: String
mixingAdosTogether =
  """
  I think "qualified ado" and "unqualified ado" can be mixed together,
  but I don't know of any examples
  """

Hello World

This folder will document everything necessary to create a simple console-based program in Purescript. It will explain:

  • The philosophical foundations of FP programming
  • The Prelude library (including Functor, Apply, Applicative, Bind, and Monad explanations)
  • A simple "Hello World" program and other Effects
  • Custom Compiler Warnings/Errors
  • The difference between Local Mutable State vs Global Mutable State
  • How to test code
  • How to benchmark / profile code
  • How to structure an FP application
  • An overview of various type-level programming libraries
  • A few console-based games written in Purescript (putting it all together)

While you may not be at the top of this Haskell Competency Matrix by the end of this repo, you will have taken a significant step towards that direction. This repo will not explain how to write algorithms in a performant way using an FP language. Consider reading Algorithm Design with Haskell which does teach algorithms using an FP language.

In pursuing these goals, it will overview the following libraries:

  • Basic
    • Prelude
    • Prim.TypeError
  • Effects
    • Effect
    • Console
    • Random
    • Aff
  • State
    • ST
    • Refs
  • Testing
    • Spec
    • Quick Check
    • Quick Check Laws
  • Benchmarking
    • Benchotron
  • Advanced
    • Variant/VariantF
    • MTL
    • Free
    • Run
  • UIs
    • Node ReadLine
    • Halogen

Other Learning Resources

Besides this repo, we have a few choices in terms of understanding functional programming. These are not necessarily "either X or Y or Z" choices but could be "X supplemented by Y with a little bit of Z")

Purescript

  • The Purescript By Example book. (See ROOT_FOLDER/Getting Started/Other Important Info.md for links and clarifications around it)
  • Purescript Resources - Justin Woo's Read the Docs (RTD) work

JavaScript

Make the Leap from JavaScript to PureScript

Haskell

Since Purescript is heavily inspired by and very similar to Haskell, one can learn a lot about Purescript by learning from these Haskell learning resources. Note: the Haskell names and type classes do not always correspond to the Purescript versions.

ActionProsCons
Read the documentation and source code for a type class and a few data types' implementations of said type classesFreeTakes a lot of time; requires intuition to understand type class' usefulness / relation to others.
Read through the articles on or pay for training from FP Complete's opinionated Haskell websiteFree / Paid(Haven't done it so I don't know)
Read through the intermediate-level Haskell articles in the Applied Haskell 2018 GitHub RepoFree(Haven't done it so I don't know)
Read through some of the free course materials taught by someone well informed about Haskell here (you'll need to scroll towards the bottom)Free; more principled explanationsLooking at just slides without hearing someone teach using them is not usually as clear as when someone does teach using them or reading through a textbook on the same matter.
Read through the extremely lengthy "What I wish I knew when learning Haskell" siteFree; provides a better overview of basic to advanced topicsVery long; not necessarily deep and clear in its explanations
Read and do the exercises from The Haskell BookThe "standard" for teaching Haskell and FP concepts in general: good explanations; good exercises; teaches "programming in the small"Costs money; costs time; the exercises will stretch you
Read and do the exercises from Haskell Cookbook, and then its follow up book Haskell Cookbook 2Free/Cheap; simpler than the Haskell book; gets to ideas faster; teaches "programming in the large"May be harder for a new beginner (I haven't read it yet)
Watch the Intro to FP course on edX.org hereFree (or paid)(Haven't done it so I don't know)
Read the relevant chapters from Learn You a Haskell for Great GoodFreeI read elsewhere that it's "outdated". See this Reddit comment's warning about learning from LYAHH

Prelude-ish

This folder will cover three things:

  • Basic data types
  • The Prelude library
  • Two additional type classes: Foldable and Traversable

Sum and Product Types

There are generally two data types in FP languages. These are otherwise known as Algebraic Data Types (ADTs):

  • Sum types (corresponds to addition)
  • Product types (corresponds to multiplication)

These are better explained in this video as to how they get their names.

The simplest form of them are Either and Tuple

-- sum
data Either a b   -- a value of htis type is an `a` value OR  a `b` value
  = Left a
  | Right b

-- product        -- a value of htis type is an `a` value AND a `b` value
data Tuple a b
  = Tuple a b

-- both           --  a value of this type is one of the following:
data These a b
  = This a        --  - an `a` value
  | That b        --  - a `b` value
  | Both a b      --  - an `a` value AND a `b` value

-- For example, These could be rewritten to
-- use a combination of Either and Tuple:
type These_ a b = Either a (Either b (Tuple a b))

However, these types can also be 'open' or 'closed':

SumProductSum and Product
ClosedEither a b

Variant (a :: A, b :: B)
Tuple a b

Record (a :: A, b :: B)
(e.g. { a :: A, b :: B }
These a b
OpenVariant (a :: A | allOtherRows)Record (a :: A | allOtherRows)
(e.g. { a :: b | allOtherRows })
-

What does 'Open' mean?

Using this example from the Syntax folder...

-- the 'r' means, 'all other fields in the record'
function :: forall r. { fst :: String, snd :: String | r } -> String
function record = record.fst <> record.snd

-- so calling the function with both record arguments below works
function { fst: "hello", snd: "world" }
function { fst: "hello", snd: "world", unrelatedField: 0 } -- works!
-- If this function used Tuple instead of Record,
--    the first argument would work, but not the second one.

Here's another way to think about this:

  • Records are 'nested Tuples'
  • Variants are 'nested Eithers'
-- We could write
Tuple a (Tuple b (Tuple c (Tuple d e)))
-- or we could write
{ a :: A, b :: B, c :: C, d :: D, e :: E }
-- which desugars to
Record ( a :: A, b :: B, c :: C, d :: D, e :: E )

-- We could write
Either a (Either b (Either c (Either d e)))
-- or we could write
Variant ( a :: A, b :: B, c :: C, d :: D, e :: E)

Keep in mind that records/variants can be but do not necessarily have to be open. If we changed the above function's type signature to remove the r, it would restrict its arguments to a closed Record:

closed :: { fst :: String, snd :: String } -> String
closed record = record.fst <> record.snd

closed { fst: "hello", snd: "world" } -- compiles
closed { fst: "hello", snd: "world", unrelatedField: 0 } -- compiler error

The Types

Tuple

data Tuple a b = Tuple a b
PackageType name"Plain English" name
purescript-tuplesTuple a b2-value Box
UsageValues & their Usage
Stores two ordered unnamed values of the same/different types.
Can be used to return or pass in multiple unnamed values from or into a function.
Tuple a b

Record

forall r. { a :: A, b :: B, {- ... -} | r } -- open record
          { a :: A, b :: B, {- ... -}     } -- closed record
PackageType name"Plain English" name
prim{ field :: ValueType }an N-value Box
UsageValues & their Usage
Stores N ordered named values of the same/different types.
Can be used to return or pass in multiple unnamed values from or into a function.
{ field :: ValueType }

Either

data Either a b
  = Left a
  | Right b
PackageType name"Plain English" name
purescript-eitherEither a bChoice of 2 types
UsageValues & their Usage
Used to indicate one type or a second type
  • Left a - a value of a
  • Right b - a value of b
Error handing (when we care about the error)
  • Left a - the error type that is returned when a computation fails
  • Right b - the output type when a computation succeeds

Maybe

data Maybe a
  = Nothing
  | Just a

Maybe a is the same as Either unimportantType a

PackageType name"Plain English" name
purescript-maybeMaybe aA full or empty box
UsageValues' Representation
Indicates an optional value
  • Nothing - value does not exist
  • Just a - value does exist
Used for error-handling when we don't care about the error (replaces null)
  • Nothing - An error occurred during computation
  • Just a - successful computation returned output.

Variant

This is an advanced type that will be covered in the Hello World/Application Structure folder.

PackageType name"Plain English" name
purescript-variantVariant (a :: A, b :: B)Choice of N types
UsageValues & their Usage
Used to indicate one type among many typesSee docs

These

data These a b
  = This a      -- Left  a
  | That b      -- Right b
  | Both a b    -- Tuple a b
PackageType name"Plain English" name
purescript-theseThese a bSame as Either a (Either b (Tuple a b))

Concluding Thoughts

Performance-wise, it's generally better to use Record instead of Tuple, and it's definitely better to use Record instead of a nested Tuple.

Similarly, it's better to use Variant instead of a nested Either. However, sometimes Either is all one needs and Variant is overkill.

For people new to the language and algebraic data types (ADTs) in general, stick with Tuple, Either, and closed Records.

List

List is what FP programmers typically use to store a sequence of values because it is friendly to recursion. Array is what most mainstream languages use. Due to JavaScript's strict runtime (as opposed to Haskell's lazy runtime), most PureScript developers will use Array instead of List. However, explaining some FP concepts are easier to do using List rather than Array.

Understand the upcoming definition using this diagram:

    List
   /   \
head   tail
      /  \
   head  tail
           \
           ....
          /  \
       head  Nil
-- Data.List.Types

data List a
  = Nil
  | Cons a (List a)

infixr 6 Cons as :

-- example
1 : 2 : Nil -- Cons 1 (Cons 2 Nil) -- [1, 2]
PackageType name"Plain English" name
purescript-listList aImmutable strict singly-linked list
UsageValues & their Usage
Recursive-friendly, not-best-performant list type
  • Nil - Indicates the end of a List in pattern matching
  • Cons a (List a) - stores one value of the list (head) and the tail, which is either the rest of the list (another Cons) or the end of the list (Nil).

Useful Types

The following is not an exact copy of the code, but accurate enough to get the idea across

Void

A type with no values that is useful for proving that a type can never exist or a computation path can never occur

-- Data.Void (Void, absurd)

newtype Void = Void Void

-- needed when one needs to refer to void
absurd :: forall a. Void -> a

-- for example...
data Either a b
  = Left a
  | Right b

-- if this function compiles, it asserts that
-- only the `Right i` path is ever taken
function :: Either Void Int -> Int
function Left v  = absurd v
function Right i = i

Unit

A type with 1 value, Unit, though most will see it used via unit. It usually indicates a "side effect", mutation, or impure code.

-- Data.Unit (Unit, unit)

data Unit = Unit

unit :: Unit
unit = Unit

It's also used to indicate a thunk, a computation that we know how to do but have chosen to delay executing/evaluating until later:

type ComputationThatReturns a = (Unit -> a)

thunk :: forall a. a -> ComputationThatReturns a
thunk a = (\_ -> a)

-- We run the pending computation (force the thunk) by passing
-- unit to it:
runPendingComputation :: ComputationThatReturns a -> a
runPendingComputation thunk = thunk unit

Natural Transformations

Takes an a out of some Box-like type and puts it into another Box-like type

-- Data.NaturalTransformation (NaturalTransformation, (~>))

-- Given this code
data Box1 a = Box1 a
data Box2 a = Box2 a

-- This function's type signature...
box1_to_box2 :: forall a. Box1 a -> Box2 a
box1_to_box2 (Box1 a) = Box2 a
-- ... has a lot of noise and could be re-written to something
-- that communicates our intent better via Natural Transformations...

-- Read: change the container F to container G.
-- I don't care what type 'a' is since it's irrelevant
type NaturalTransformation f g = forall a. f a -> g a

infixr 4 NaturalTransformation as ~>

box1_to_box2 ::           Box1   ~> Box2 {- much less noisy than
box1_to_box2 :: forall a. Box1 a -> Box2 a -}
box1_to_box2             (Box1 a) = Box2 a

Useful Functions

These all come from Data.Function in Prelude.

Const

const :: forall a b. a -> b -> a
const x _ = x

-- Example
const 1 "hello" = 1
const 1 true    = 1
const 1 42      = 1

Flip

-- Flip the argument order
flip :: forall a b c. (a -> b -> c) -> b -> a -> c
flip twoArgFunction secondArg firstArg = twoArgFunction firstArg secondArg

-- example
     (append "world!" "Hello ") == "world!Hello "
(flip append "world!" "Hello ") == "Hello world!"

Apply

Forewarning: apply via $ shows up EVERYWHERE! Bookmark this until you get it.

I read somewhere (I think @garyb mentioned this in the PureScript chatroom) that $ was chosen because it's two parenthesis with a line through it, symbolizing that it removes the need to use parenthesis.

-- Reduce the number of parenthesis needed
apply :: (a -> b) -> a -> b
apply function arg = function arg

infix 0 apply as $

-- example
print (5 + 5) == print $ 5 + 5

print (append "foo" (4 + 4)) == print $ append "foo" $ 4 + 4

Other Less-Used Functions

-- apply with its arguments flipped
applyFlipped :: forall a b. a -> (a -> b) -> b
applyFlipped = flip apply

infxl 1 applyFlipped as #

-- example
print (5 + 5) == 5 + 5 # print

-- apply a function with the given arg totalTimes
applyN :: forall a. (a -> a) -> Int -> a -> a
applyN function totalTimes arg = -- implementation
-- no infix

-- Example
applyN (+) 2 2       -- reduces to...
2 + (applyN (+) 1 2) -- reduces to...
2 + 2

-- When the desired function takes b, but you have 'a'.
-- So, we change 'a' to 'b' and then call the function
on :: forall a b c. (b -> b -> c) -> (a -> b) -> a -> a -> c
on function changeAToB a1 a2 = function (changeAToB a1) (changeAToB a2)

-- Example

on (+) stringToInt "4" "5" == 9

Prelude's Type Classes

Relationships

Below is a dependency graph / type class categorization of the type classes found in Prelude. The usage frequency key is my current understanding and may be inaccurate for the "somewhat"/"rare" type classes: prelude-typeclasses

Tricks for Implementing a Type Class Instance

Keep in mind that when implementing a type class, one does not always need to implement its function with a specific implementation for a given type. There are two situations in which this can occur:

First, this situation can arise when a type class defines two or more functions. Sometimes, a function in a type class can be defined using another function from that same type class. Take, for example, the Eq type class:

class Eq a where
  eq :: a -> a -> Boolean

  notEq :: a -> a -> Boolean

Granted, an Eq instance can be derived by the compiler. However, assuming this wasn't the case, there are two ways we could implement it:

  1. We could implement only eq and implement notEq by inverting eq's result.
  2. We could implement only notEq and implement eq by inverting notEq's result

Second, sometimes, a function in a type class can be defined using a function from a required type class. Take, for example, the Ord type class:

data Ordering
  = LT
  | EQ
  | GT

class (Eq a) <= Ord a where
  compare :: a -> a -> Ordering

If we implement compare, we can also implement eq:

data ColoredBox
  = RedBox
  | GreenBox

instance Ord ColoredBox where
  compare RedBox GreenBox = LT
  compare GreenBox RedBox = GT
  compare _ _ = EQ {- which expands to...
  compare RedBox RedBox = EQ
  compare GreenBox GreenBox = EQ
  -}

instance Eq ColoredBox where
  eq a b = (compare a b) == EQ

  notEq a b = (compare a b) /= EQ

Laws

This is a cheatsheet for various terms used to describe laws. Not all of these will appear in Prelude and some may be explained in a type class' definition. Still, it helps to be aware of them:

LawDefinitionExampleExplanation of example
reflexivex function x == truex <= ya <= a is true for any number a you pick
irreflexivex function x == falsex < ya < a is false for any number a you pick
coreflexiveif (x function y) then (x == y)(x <= 15) && (x == y)given (a <= 15) && (a == b) , then a == b
(the converse is not true, though!)
symmetricif (x function y) then (y function x)x == ygiven a == b, then b == a
antisymmetricif (x function y && y function x) then (x == y)x <= ygiven a <= b && b <= a, then a == b
asymmetricif (x function y) then (y function x == false)x < ygiven a < b, then !(b < a)
(asymmetric = anti-symmetric + irreflexive)
transitiveif (x function y && y function z) then (x function z)x == y, x <= ygiven a == b && b == c, then a == c
given a <= b && b <= c, then a <= c

Objecty

In Java, every object has 3 functions:

  • toString
  • equals
  • hashCode

However, some types do not need these functions (e.g. singletons, lambda functions, etc.). Furthermore, equals should only work between objects of the same type (i.e. 4 == "4" shouldn't compile).

In PureScript, we can only determine whether a value of type A is equal to another value of type A if it has an Eq instance. Similarly, values of a given type can only be "ordered" if the type has an instance of the Ord type class.

Whether a type implements a type class or not restricts or increases what one can do with it.

Show, Eq, Ord, Bounded

Since the documentation for these type classes are clear, we will redirect you to them instead of repeating them here:

  • Show converts a value into a String. Unfortunately, people out of convenience use it for multiple purposes. See hdgarrood's Down With Show 3-part series as to why he thinks we should replace Show with something that better suits the purposes for which it is normally used.
  • Eq determines whether two values of the same type are equal. In this way, it avoids the problem that Java has above.
  • Ordering is a data type for specifying whether something is less than (LT), equal to (EQ), or greater than (GT) something else. Ord takes two values of type, a, and returns an Ordering. Ord uses total ordering. There are a different kinds of ordering that require a different number and type of laws:
    • Preorder: reflexive and transitive laws
    • Parital order: reflexive, transitive, and antisymmetric
    • Total order: reflexive, transitive, antisymmetric, and total (e.g. it can order every value of a given type)
  • Bounded just adds an upper and lower bound to Ord.

Useful Derived Functions

Most of these come from Ord:

  • min/max - self-explanatory
  • clamp - clamp lowerBound upperBound value
  • between - between lowerBound upperBound value

Arrows

We'll explain this idea before Control Flow-related type classes so that you understand a notation we'll use in them.

Cleaner Function Notation

Let's say I have two functions:

(\x -> x + 1)
(\y -> y * 10)

If we want to apply an argument to the first and pass its output into the second, we would have to write something ugly-looking:

(\x -> (\y -> y * 10) (x + 1) )

What we mean is something like this

f = (\x -> x + 1)
g = (\y -> y * 10)

expression = (\arg -> g (f arg))

We have just defined function composition, which can be written in such a way to reduce "noise:"

-- The arrow determines where the output goes
(\a -> g (f a)) == (g <<< f)
(\a -> g (f a)) == (f >>> g)

Moreover, sometimes we want a function that returns the input:

(\x -> x)
-- so that we can use it like...
(\x -> x) 4 == 4

We call this function, identity:

(\x -> x) == identity
-- same thing
identity 4 == 4

Generalizing to More Types

To summarize...

NameMeaningShortcut
compose(\a -> g (f a))(g <<< f)
composeFlipped(\a -> g (f a))(f >>> g)
identity(\x -> x) aidentity a

If we were to turn compose into a function, it would appear with the type signature below:

compose :: forall a b c. (b -> c)     -> (a -> b)     -> (a -> c)
-- However, "->" is just sugar syntax for Function:
compose :: forall a b c. Function b c -> Function a c -> Function a c
-- Notice that Function appears to be just another data structure.
-- If it works for that data structure, why not generalize it for any data structure?
compose :: forall f a b c. f b c -> f a b -> f a c

-- Let's rename our generics, so that it starts with `a` rather than `f`:
compose :: forall a b c d. a c d -> a b c -> a b d

-- We'll line up the types for easier reading:
compose :: forall a b c.           (b -> c) ->         (a -> b) ->         (a -> c)

compose :: forall a b c.   Function b    c  -> Function a    b  -> Function a    c

compose :: forall f a b c. f        b    c  -> f        a    b  -> f        a    c

compose :: forall a b c d. a        c    d  -> a        b    c  -> a        b    d

-- We've now just defined the function `compose` for Semigroupoid:

class Semigroupoid a where
  compose :: forall b c d. a c d -> a b c -> a b d

-- Doing the same for identity is trivial:
identity :: forall a.            a -> a

identity :: forall a.   Function a    a

identity :: forall f a. f        a    a

identity :: forall a b. a        b    b

class (Semigroupoid a) <= Category a where
  -- we'll use 't' instead of 'b'
  identity :: forall t. a t t

Here's the docs. You likely won't be using these that often (unless perhaps you're designing a library), but it's good to know of them:

You will see g <<< g or its flipped version g >>> f a lot and we'll use it in the upcoming files.

Appendable: Semigroup to Monoid

This file will only cover the first two type classes in the type class hierarchy. The rest will be covered later.

These type classes often take two values of a given type and 'append' them into one new value. One could also think of this as 'reducing' two values into one value.

Semigroup

class Semigroup a where
  append :: a -> a -> a

infixr 5 append as <>

Examples

One example is String. Two String values can be 'appended/reduced' into one value by concatenating them together: append "hello " "world" == "hello world".

Another example is Boolean (although its functions are not defined in this way as there is a better type class for them). Two Boolean values can be 'appended/reduced' into one value via the usual suspects:

  • true && true == true
  • false || true == true

A third example is Int, which has two possible instances for 'appending/reducing' two values into one value. How? One could

  • add them: 1 + 1
  • multiple them: 2 * 2

A fourth is List. One can take two values of List and combine them together by putting both lists' elements into one new list.

Monoid

class Semigroup a <= Monoid a where
  mempty :: a

mempty is the "identity" value. In other words mempty <> a == a and a <> mempty == a. In some contexts, mempty acts like a "default value."

The name, mempty, is used rather than empty because empty is the name of a function that a different but similar type class called Plus defines. We won't cover Plus here.

Using the same examples above,

Typemempty valueExample 1Example 2
String"""foo" <> "" == "foo""" <> "foo" == "foo"
Boolean (and)truex && true == xtrue && x == x
Boolean (or)falsex `
Int (plus)0x <> 0 == x0 <> x == x
Int (multiply)1x <> 0 == x1 <> x == x
ListNilx <> Nil == XNil <> x == x

Docs

Some of these type classes also specify specific helper types (sub bullets under a type class) that serve to reduce boilerplate:

  • Semigroup.
    • When one wants to reduce two values down to one by ignoring the second value and taking the first value, one can use First
    • Or vice versa (ignore first, take second), via Last
  • Monoid
    • When one wants to use Semiring's add/+ and zero as the meaning of <> and mempty, one can use Additive.
    • When one wants to use Semiring's mul/* and one as the meaning of <> and mempty, one can use Multiplicative
    • When one wants to use HeytingAlgebra's conj/&& and tt as the meaning of <> and mempty, one can use Conj
    • When one wants to use HeytingAlgebra's disj/|| and ff as the meaning of <> and mempty, one can use Disj
    • When one wants to use Category's compose/<<< and identity as the meaning of <> and mempty, one can use Endo
    • When one wants to use Category's composeFlipped/>>> and identity as the meaning of <> and mempty, one can use Dual

For derived functions (if any), see the type classes' docs.

Overview

There are type classes that control the flow of the program (e.g. whether the program should do X and then Y or should do X and Y at the same time).

Functor, Apply, and Bind Type Classes Explained in Pictures

We've linked to an article below that explains these abstract notions in a clear manner using pictures and the Maybe a data structure. However, since these concepts are explained in Haskell, which uses different terminology than Purescript, use the following table to map Haskell terminology to Purescript terminology:

Haskell TerminologyPurescript Terminology
fmap (function)map (function)
Applicative (type class)Apply (type class)
Array/[] (syntax sugar for List a)List a
map (Array function)see the implementation in Purescript
IO ()Effect Unit, which will be explained/used in a later part of this folder

Here's the link: Functors, Applicatives, and Monads in Pictures

Lists' Map Function in Purescript

Here's the map List function implemented in Purescript:

data List a = Nil | Cons a (List a)

instance Functor List where
  map :: forall a b. (a -> b) -> List a -> List b
  map f Nil = Nil
  map f (Cons head tail) = Cons (f head) (map f tail)

Functor, Apply, Applicative, Bind, Monad

In Short

ConceptArgument is NOT inside a Box / contextArgument is inside a Box / contextName
1-arg function applicationfunction argfunction <$> (Box arg)Functor
2+-arg function applicationfunction arg1 arg2function <$> (Box arg1) <*> (Box arg2)Applicative
function compositionaToB >>> bToCaToBoxB >=> bToBoxCMonad

Somewhat longer

These will be covered at a slower and clearer pace in the upcoming files. This is just an overview of them.

Typeclass"Plain English"FunctionInfixLawsUsage
FunctorMappablemap :: forall a b. (a -> b) -> f a -> f b<$>
(Left 4)
  • identity: map (\x -> x) fa == fa
  • composition: map (f <<< g) = map f <<< map g
Change a value, a, that's currently stored in some box-like type, f, using a function, (a -> b)
ApplyBoxed Mappableapply :: forall a b. f (a -> b) -> f a -> f b<*>
(Left 4)
  • Associative composition: (<<<) <$> f <*> g <*> h == f <*> (g <*> h)
Same as Functor except the function is now inside of the same box-like type.
ApplicativeLiftable
Parallel Computation
pure :: forall a. a -> f a
  • identity: (pure (\x -> x) <*> v == v)
  • composition: pure (<<<) <*> f <*> g <*> h == f <*> (g <*> h)
  • Homomorphism: (pure f) <*> (pure x) == pure (f x)
  • interchange: u <*> (pure y) == (pure (_ $ y)) <*> u
Put a value into a box
Run code in parallel
BindSequential Computationbind :: forall m a b. m a -> (a -> m b) -> m b>>=
(Left 1)
Associativity: (x >>= f) >>= g == x >>= (\x' -> f x' >>= g)Given an value of a box-like type, m, that contains a value, a, extract the a from m, and create a new m value that stores a new value, b.
Take m a and compute it via bind/>>= to produce a value, a. Then, use a to describe (but not run) a new computation, m b. When m b is computed (via a later bind/>>=), it will return b.
MonadFP Program
  • Left Identity: pure x >>= f = f x
  • Right Identity: x >>= pure = x
  • Applicative Superclass: apply = ap
The data structure used to run FP programs by executing code line-by-line, function-by-function, etc.

Simplest Monad Implementation

data Box a = Box a

instance Functor Box where
  map        f  (Box a) = Box (f a)

instance Apply Box where
  apply (Box f) (Box a) = Box (f a)

instance Bind Box where
  bind  (Box a) f       = f a

instance Applicative Box where
  pure a = Box a

instance Monad Box

Function Reduction

In these files, we will "evaluate" functions by using graph reductions: replacing the left-hand side (LHS) of the = sign (the function's call signature) with the right-hand side (RHS) of the = sign (the function's implementation / body). In other words...

someFunction arg1 arg2 arg3 = bodyOfFunction
| call signature (LHS)    | = | body (RHS) |

Functor: Mappable

Usage

Change a value, `a`,
  that's currently stored in some box-like type, `f`,
into `b`
  using a function, `(a -> b)`.

Definition

See its docs: Functor

class Functor f where
  map :: forall a b. (a -> b) -> f a -> f b

infixl 4 map as <$>

data Box a = Box a

instance Functor Box where
  map :: forall a b. (a -> b) -> Box a ->  Box  b
  map                 f         (Box a) =  Box (f a)

Put differently, Functor solves a specific problem. If I have a function of type (a -> b), I cannot use that function on values of a if they are stored in a box-like type:

function :: Int -> String
function 0 = "0"
function _ = "1"

function 5 -- This works!
function (Box 5) -- compiler error! Oh noes!

One could also see map as "transforming" a function, so that it also operates on Box-like types. This is often described as "lifting" a function into a Box-like type:

map :: forall a b. (a -> b) -> (Box a -> Box b)
map f = (\(Box a) -> Box (f b))

Laws

Identity

Definition: (\x -> x) <$> fa == fa

-- Start!
(\a -> a) <$> (Box 4)
-- De-infix "<$>" to map
map (\a -> a) (Box 4)
-- Replace map's "call signature" with its "body"
Box ((\a -> a) 4)
-- Apply argument by replacing '\a' with its argument '4'
Box ((\4 -> 4)  )
-- Keep only the body of function
Box ((      4)  )
-- Remove parenthesis and whitespace
Box 4
-- Check whether left-hand side (LHS) equals right-hand side (RHS)
(Box 4) == (Box 4)
-- Law met!
true

Composition

(Remember, g <<< f means (\a -> g (f a)))

Definition: map (g <<< f) = (map g) <<< (map f)

-- # Reduce left side of the law #

-- Start!
map ((\y -> y * 10) <<< (\x -> x + 1)) (Box 4)
-- Remember that `f <<< g` means `(\a -> f (g a))`
-- Reduce the "<<<" into one function
map (\x -> 10 * (x + 1)) (Box 4)
-- Replace map's "call signature" with its "body"
Box ((\x -> 10 * (x + 1)) 4)
-- Apply argument by replacing '\x' with its argument '4'
Box ((\4 -> 10 * (4 + 1))  )
-- Keep only the body of function
Box ((      10 * (4 + 1))  )
-- Reduce the body of function to its end result:
Box ((      10 * (5    ))  )
Box ((      10 *  5     )  )
Box ((      50          )  )
-- Remove parenthesis and whitespace
Box 50

-- # Reduce right side of the law #

-- Start!
(map (\y -> y * 10)) <<< (map (\x -> x + 1)) (Box 4)
-- Reduce "<<<" into one function
(\box4    -> map (\y -> y * 10) ( map (\x -> x + 1)  box4  ) ) (Box4)
-- Apply argument
(\(Box 4) -> map (\y -> y * 10) ( map (\x -> x + 1) (Box 4)) )
-- Keep only the body of function
(            map (\y -> y * 10) ( map (\x -> x + 1) (Box 4)) )
-- Replace 2nd map "call signature" with its "body"
(            map (\y -> y * 10) ( Box (\x -> x + 1) 4) )
-- Apply the argument
(            map (\y -> y * 10) ( Box (\4 -> 4 + 1)  ) )
-- Keep only the body of the function
(            map (\y -> y * 10) ( Box (      4 + 1)  ) )
-- Calculate the function
(            map (\y -> y * 10) ( Box (      5    )  ) )
-- Remove unneeded parenthesis
(            map (\y -> y * 10)   Box        5         )
-- Remove unneeded whitespace
(            map (\y -> y * 10) Box 5                  )
-- Replace map's "call signature" with its "body"
(                               Box ((\y -> y * 10) 5) )
-- Apply the argument
(                               Box ((\5 -> 5 * 10)  ) )
-- Keep only the function
(                               Box ((      5 * 10)  ) )
-- Calculate the function
(                               Box ((      50    )  ) )
-- Remove unneeded parenthesis
                                Box         50
-- Shift everything left
Box 50

-- Test if LHS equals RHS
(Box 50) == (Box 50)
-- Law met!
true

Derived Functions

See the docs above for their definitions and read through the source code:

  • Ignore the a value and just replace it with
    • the value towards which the arrow points...
      • (voidLeft / $>): (Box 4) $> "a" == (Box "a")
      • (voidRight / <$): "a" <$ (Box 4) == (Box "a")
    • Unit (void): void (Box 4) == (Box unit)
      • Note: void is used heavily to make it work with the Discard type class in do notation.
  • Flip the order of map's arguments (mapFlipped / <#>)
  • Generalize flip, so that it works for all types (flap / <@>)

Apply

Usage

Shorter: Same as Functor, but the function is also in the box-like type, f.

Longer:

Change a value, `a`,
  that's currently stored in some box-like type, `f`,
into `b`
  using a function, `(a -> b)`,
    that is also stored in the same box-like type, `f`.

Definition

See its docs: Apply

class (Functor f) <= Apply f where
  apply :: forall a b. f (a -> b) -> f a -> f b

infixl 4 apply as <*>

data Box a = Box a

instance Functor Box where
  map :: forall a b.       (a -> b) -> Box a -> Box  b
  map                       f         (Box a) = Box (f a)

instance Apply Box where
  apply :: forall a b. Box (a -> b) -> Box a -> Box  b
  apply               (Box  f     )   (Box a) = Box (f a)

Put differently, Apply solves a problem that occurs when using Functor. If I have a function of type (a -> b -> c), I can use Functor's map/<$> to lift that function into a Box-like type as before....

mapResult :: Box (Int -> Int)
mapResult = map (\first second -> first + second) (Box 1)

However, the resulting value stored in that Box-like type is a function. In other words, mapResult == Box (\second -> 1 + second). Functor's map only works if the function takes only one argument. If it takes 2+ arguments, map will return a function stored in a Box.

This is where Apply comes to the rescue. We can continue to apply boxed arguments to that function until we eventually get a Box with a value in it:

finalResult :: Box Int
finalResult =
  apply mapResult (Box 2)                                 {-

  ...which is the same as...
  Box ((\second -> 1 + second) 2)
  Box ((\2      -> 1 + 2     )  )
  Box ((           3         )  )
  Box 3                                                   -}

Thus, map lifts functions that take n-many arguments into a Box-like type, and Apply's apply/<*> continues to pass n-1-many boxed arguments into that function until the function executes.

Laws

Associative Composition

Definition: (<<<) <$> f <*> g <*> h == f <*> (g <*> h)

TODO: prove the above law using Box (a lot of work, so ignoring for now...)

Derived Functions

  • Do two computations, but only return...
    • the first: applyFirst / <*
    • the second: applySecond / *>
  • liftN is explained below:

LiftN Notation

Let's rename that Functor's map function to lift1:

{-
map   (\oneArg -> doStuffWith oneArg) (Box 4) -}
lift1 (\oneArg -> doStuffWith oneArg) (Box 4)

This function can only take one arg. What if want to take two args? We should call it lift2:

lift2 (\arg1 arg2 -> andThen (doStuffWith arg1) arg2) (Box 4) (Box 4)

That works, but we could also write it:

(\arg1 arg2 -> andThen (doStuffWith arg1) arg2) <$> (Box 4) <*> (Box 4)

Using meta-language

function_NotInBox_takes_n_args <$> boxedArg1 <*> boxedArg2 -- <*> boxedArgN ...

Applicative

Usage

  • Lift any value/function/etc. into a box-like type, f
  • Parallel Computation: Do all three simultaneously: X, Y, and Z.

(Note: Javascript is currently single-threaded, so this isn't entirely true. If it gets multi-thread support, that will change.)

Definition

See its docs: Applicative

class (Apply f) <= Applicative f where
  pure :: forall a. a -> f a

data Box a = Box a

instance Functor Box where
  map :: forall a b.       (a -> b) -> Box a -> Box  b
  map                       f         (Box a) = Box (f a)

instance Apply Box where
  apply :: forall a b. Box (a -> b) -> Box a -> Box  b
  apply               (Box  f     )   (Box a) = Box (f a)

instance Applicative Box where
  pure :: forall a. a -> Box a
  pure              a =  Box a

Laws

Identity

Definition: (pure (\x -> x) <*> v == v)

-- Start: 'v' == (Box 4)
(pure (\x -> x)) <*> (Box 4)
-- Replace pure call signature with body
( Box (\x -> x)) <*> (Box 4)
-- De-infix <*> to apply
apply (Box (\x -> x)) (Box 4)
-- Replace apply call signature with body
Box (\x -> x) 4)
-- Apply argument by replacing all 'x' with '4'
Box (\4 -> 4)  )
-- Keep body of function
Box (      4)  )
-- Remove whitespace and parenthesis
Box 4
-- Check law
(Box 4) == (Box 4)
-- Law met!
true

Composition

Definition: pure (<<<) <*> f <*> g <*> h == f <*> (g <*> h)

TODO: prove the above law using Box (a lot of work, so ignoring for now...)

Homomorphism

Definition: (pure f) <*> (pure x) == pure (f x)

TODO: prove the above law using Box (a lot of work, so ignoring for now...)

Interchange

Definition: u <*> (pure y) == (pure (_ $ y)) <*> u

TODO: prove the above law using Box (a lot of work, so ignoring for now...)

Derived Functions

  • Define an instance of Apply and Applicative and you get a Functor implementation for free!: liftA1
  • Do a computation...
    • if some condition is true: when
    • if some condition is false: unless

Note: when/unless is strict. For a lazy version, see purescript-call-by-name

Bind

Usage

Short:

  • Sequential Computation: do X, and once finished do Y, and once finished do Z

Long:

  1. Given a value, a, that is stored in a box-like type, m
  2. Extract the a from the box, m
  3. Pass it into a function, (a -> m b)
    • The function uses the value a to compute a new value, b.
    • The function wraps the new value b into the same box-like type, m.
    • The function returns a box, m, that stores the new value, b.
  4. Refer to the b value as a and repeat Steps 1-3 until we run out of functions
  5. The last function returns the final b value that is stored in the same box-like type, m.

Definition

See its docs: Bind

Below, we'll show two instances for Bind:

  1. A flipped version of bind that shows how it relates to Functor and Apply
  2. The correct version:
-- in real definition, 'f' (functor) is really 'm' (monad)
class (Appy f) <= Bind f where
  bind :: forall a b. f a -> (a -> f b) -> f b

infixl 1 bind as >>=

data Box a = Box a

instance Functor Box where
  map :: forall a b.         (a ->     b) -> Box a -> Box  b
  map                         f             (Box a) = Box (f a)

instance Apply Box where
  apply :: forall a b.   Box (a ->     b) -> Box a -> Box  b
  apply                 (Box  f         )   (Box a) = Box (f a)

-- Wrong: Flipped order of two args!
instance Bind_ Box where
  bindFlipped :: forall a b. (a -> Box b) -> Box a -> Box  b
  bindFlipped                 f             (Box a) = f a

-- Correct order of args
instance Bind Box where
  bind :: forall a b.  Box a -> (a -> Box b) -> Box b
  bind                (Box a)    f            = f a

bind's type signature is weird. How did we ever come up with that? To understand it (and understand why we have the below 'derived functions'), watch Category Theory 10.1: Monads and refer to the code below for help in understanding it:

-- >=> the "fish operator" is
-- called "composeKleisli"
infix composeKleisli 6 >=>

-- see "(a -> m b)" as the definition
-- of `pure` from the `Applicative` type class.
composeKleisli :: forall m a b c. Functor f => (a -> m b) -> (b -> m c) -> (a -> m c)
composeKleisli f g =
  \a ->
    let mb = f a
    in (bind mb g)

  where
    bind :: m b -> (b -> m c) -> m c
    bind functor function = join (map function functor)

    join :: m (m a) -> m a
    join = -- implementation

Laws

Associativity

(This law enables "do notation", which we'll explain soon.)

Definition: (x >>= f) >>= g == x >>= (\x' -> f x' >>= g)

TODO: prove the above law using Box (a lot of work, so ignoring for now...)

Derived Functions

  • If you have nested boxes and need to remove the outer one (join):
    • join (Box (Box a)) == Box a
  • Make chained multiple aToMB functions easier to read...
    • going forwards (composeKleisli/>=>):
      • ma >=> aToMB >=> bToMC >=> ...
    • going backwards (composeKleisliFlipped/<=<):
      • ... <=< bToMC <=< aToMB <=< ma
  • if computeCondition then truePathComputation else falsePathComputation (ifM)
  • If you want bind/>>= to go in the opposite direction (bindFlipped/=<<):
    • ma >>= aToMB == aToMB =<< ma

Monad

Usage

Monad = Sequential Computation (Bind) + Lift a Value/Function into Box-like Type (Applicative)

Definition

See its docs: Monad

class (Applicative m, Bind m) <= Monad m

data Box a = Box a

instance Functor Box where
  map :: forall a b. (a -> b) -> Box a -> Box  b
  map f (Box a) = Box (f a)

instance Apply Box where
  apply :: forall a b. Box (a -> b) -> Box a -> Box  b
  apply (Box f) (Box a) = Box (f a)

instance Bind Box where
  bind :: forall a b. Box a -> (a -> Box b) -> Box b
  bind (Box a) f = f a

instance Applicative Box where
  pure :: forall a. a -> Box a
  pure a =  Box a

instance Monad Box

Laws

Unofficial

Taken from this slide in this YouTube video, here's an "unofficial" but clearer way to understand the laws for Monad by comparing them to a Function:

-- Recall: `identity a == (\x -> x) a`

-- Given a function whose type signature is...
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
(aToMB >=> bToMC) a = (aToMB a) >>= (\b -> bToMC b)

-- ... Monad could be defined by these laws:
-- 1a. Function's identity law
(function >>> identity) a == function a
 aToMB    >=> pure        == aToMB

-- 1b. its inverse
(identity >>> f)        a == f a
 pure     >=> f           == f

-- 2. Function Composition
f >>> (g >>> h) == (f >>> g) >>> h
f >=> (g >=> h) == (f >=> g) >=> h

Official

Identity

Definition (left) : pure x >>= f = f x

-- start
pure x  >>= f
-- replace call signature with body
(Box x) >>= f
-- de-infix `>>=` to `bind`
bind (Box x) f
-- replace call signature with body
f x
-- check LHS with RHS
f x == f x
-- Law met!
true

Definition (right): x >>= pure = x

Applicative Superclass

Definition: apply = ap (where ap is a derived function)

Derived Functions

  • Define an instance of Applicative, Bind, and Monad and...
    • you get a Functor implementation for free!: liftM1
    • you get an Apply implementation for free!: ap
  • Do a computation...
    • if some condition is true: whenM
    • if some condition is false: unlessM

How the Computer Executes FP Programs

Or "What does sequential computation look like using Monads"

Below, we'll be defining a long chain of nested functions. Since functions usually place their argument on the right of the body like this...

(\x -> body) actualArgument

... we'll be putting it on the left using #

actualArgument # (\x -> body)

In other words...

function      arg == arg # function
(\x -> x + 1) arg == arg # (\x -> x + 1)

This will help the upcoming examples be much clearer and more understandable.

Using this type and its instance...

data Box a = Box a

instance Functor Box where
  map :: forall a b. (a -> b) -> Box a -> Box  b
  map f (Box a) = Box (f a)

instance Apply Box where
  apply :: forall a b. Box (a -> b) -> Box a -> Box  b
  apply (Box f) (Box a) = Box (f a)

instance Bind Box where
  bind :: forall a b. Box a -> (a -> Box b) -> Box b
  bind (Box a) f = f a

instance Applicative Box where
  pure :: forall a. a -> Box a
  pure a =  Box a

... we will translate the Javascript "program" below...

const four = 4
const five = 1 + four
const five_string = toString(five); // or whatever the function called
print(five_string); // print the String to the console, which returns nothing

... into its corresponding Purescript "program" (next).

In the following snippet of code, you will need to scroll to the right, so that the a previous reduction aligns with the next reduction. Note: Read through this and practice writing it out multiple times until you get sick of it as this is at the heart of FP programming! Failure to understand this == Failure to write FP code. Here's the code:

unsafePerformEffect :: forall a. Box a -> a
unsafePerformEffect (Box a) = a

-- Compute what the final Box value is in `main`
-- and then call `unsafePerformEffect` on the final Box
runProgram :: Unit
runProgram = unsafePerformEffect main

main :: Box Unit
main =
  (Box 4) >>= (\four -> Box (1 + four) >>= (\five -> Box (show five) >>= (\five_string -> print five_string)))

-- Step 1: De-infix the first '>>=' alias back to bind
  bind (Box 4)  (\four -> Box (1 + four) >>= (\five -> Box (show five) >>= (\five_string -> print five_string)))

-- Step 2: Look up Box's bind implementation...
--   ...and replace the left-hand side with the right-hand side
            4 # (\four -> Box (1 + four) >>= (\five -> Box (show five) >>= (\five_string -> print five_string)))

            -- Step 3: Apply the arg to the function (i.e. replace "four" with 4)
                (\4    -> Box (1 + 4   ) >>= (\five -> Box (show five) >>= (\five_string -> print five_string)))

                -- Step 4: Reduce the function to its body
                          Box (1 + 4   ) >>= (\five -> Box (show five) >>= (\five_string -> print five_string))

                          -- Step 5: Reduce the argument in "Box (1 + 4)" to "Box 5"
                          Box (5       ) >>= (\five -> Box (show five) >>= (\five_string -> print five_string))

                          -- Step 6: Remove the parenthesis
                          Box  5         >>= (\five -> Box (show five) >>= (\five_string -> print five_string))

                          -- Step 7: Remove the extra whitespace and push right
                                   Box 5 >>= (\five -> Box (show five) >>= (\five_string -> print five_string))

                                   -- Step 8: Repeat Steps 1-7 for the next ">>="
                                   bind (Box 5)  (\five -> Box (show five) >>= (\five_string -> print five_string))

                                             5 # (\five -> Box (show five) >>= (\five_string -> print five_string))

                                                 (\5    -> Box (show 5   ) >>= (\five_string -> print five_string))

                                                           Box (show 5   ) >>= (\five_string -> print five_string)

                                                           Box ("5"      ) >>= (\five_string -> print five_string)

                                                           Box  "5"        >>= (\five_string -> print five_string)

                                                                   Box "5" >>= (\five_string -> print five_string)

                                                                   -- Step 8: Repeat Steps 1-6 for the next ">>="
                                                                   bind (Box "5")  (\five_string -> print five_string)

                                                                             "5" # (\five_string -> print five_string)

                                                                                   (\"5"         -> print "5")

                                                                                                    print "5"

                                                                  -- Step 9: Look up `print`'s definition
                                                                  --
                                                                  --   print :: forall a. a -> Box Unit
                                                                  --   print a =
                                                                  --      -- Assume that 'a' is printed to the console
                                                                  --      Box unit
                                                                  --
                                                                  -- ... and replace the LHS with RHS

                                                                                                     Box unit

                                                                  -- Step 10a: Shift everything to the left again
-- 10b) ... and re-expose the 'main' function:
main :: Unit
main = Box unit -- after all the earlier computations...

-- Step 12: call `unsafePerformEffect` to get the final Box's value
runProgram :: Unit
runProgram = unsafePerformEffect (Box unit)
-- becomes
runProgram :: Unit
runProgram = unit

Now go read the code snippet above again and write it out!

Do and Ado Notation

At this point, you should look back at the Syntax/Prelude-Syntax folder and read through its files. Feel free to ignore the Qualified Do/Ado Explained file and those that follow.

Once finished, read the next file.

Useful Monads

Note: This file's contents assumes you have read through and are somewhat familiar with the contents of the file, Syntax/Prelude Syntax/Reading Do as Nested Binds.md.

So far, we have only shown you the Box monad to help you get used to the syntax and see the logic for how Monad and bind/>>= works. (The Box type is a learner-friendly name for the Identity monad, which we'll cover later in the Application Structure folder.)

However, Monads are used to compose two or more computations that occur within the same context (where context refers to the monadic type being used). Whereas the monadic type, Box/Identity, only has one possible value, the below types have two possible values. Functions that produce two possible outputs don't typically compose. However, the Monad type class enables us to compose them using "railway-oriented programming" (~Scott Wlaschin).

When we compose different monadic types, we get different control flows. The do notation helps us avoid the Pyramid of Doom boilerplate code and emphasizes developer intent.

The Maybe Monad

JavaScript Code

In JavaScript, we might write this code:

let a = computation();
if (a == null) {
  return null;
} else {
  let b = compute1(a);
  if (b == null) {
    return null;
  } else {
    let c = compute2(b);
    if (c == null) {
      return null;
    } else {
      return compute3(c);
    }
  }
}

PureScript Code (non-idiomatic)

In PureScript, we could write the following non-idiomatic code that repeats this Pyramid of Doom:

data Maybe a
  = Nothing
  | Just a

someComputation :: Maybe Unit
someComputation =
  case computation of
    Nothing -> Nothing
    Just a -> case compute1 a of
      Nothing -> Nothing
      Just b -> case compute2 b of
        Nothing -> Nothing
        Just c -> compute3 c
  where
    computation :: Maybe a
    compute1 :: a -> Maybe b
    compute2 :: b -> Maybe c
    compute3 :: c -> Maybe Unit

PureScript Code (idiomatic)

Or, we could use Maybe's Monad instance via do notation to write idiomatic PureScript code:

data Maybe a
  = Nothing
  | Just a

instance Bind Maybe where
  bind :: forall a b. Maybe a -> (a -> Maybe b) -> Maybe b
  -- when given a Nothing, stop all possible future computations
  -- and return immediately.
  bind Nothing _ = Nothing
  -- when given a Just, run the function on its contents
  -- and continue any Monadic computations
  bind (Just a) f = f a

someComputation :: Maybe ReturnValue
someComputation = do
  a <- computation
  b <- compute1 1
  c <- compute2 b
  compute3 c
  where
    computation :: Maybe a
    compute1 :: a -> Maybe b
    compute2 :: b -> Maybe c
    compute3 :: c -> Maybe Unit

If a Nothing value is given at any point in the nested-bind computations, it will short-circuit and return immediately.

What is a real-world example of using the Maybe monad? One often writes monadic code using Maybe as the Monad to lookup values in some structure (e.g. Map, Array, List, or Tree). Often, this control flow reads like this: "Try to get value X. If it exists, try to get value Y. If that exists, do something with both. If either one of them does not exist, stop and return immediately." In other words...

example :: Maybe String
example = do
  x <- index 4 array
  y <- lookup "fooKey" map
  pure (x + y)

The Either Monad

JavaScript Code

In JavaScript, we might write this code:

let a = computation();
if (isError(a)) {
  return a;
} else {
  let b = compute1(a);
  if (isError(b)) {
    retun b;
  } else {
    let c = compute2(b);
    if (isError(c)) {
      return c;
    } else {
      return compute3(c);
    }
  }
}

PureScript Code (non-idiomatic)

data Either a b
  = Left a
  | Right b

someComputation :: Either ErrorType ReturnValue
someComputation = do
  case computation of
    Left err -> Left err
    Right a -> case compute1 a of
      Left err -> Left err
      Right b -> case compute2 b of
        Left err -> Left err
        Right c -> compute3 c
  where
    computation :: Either ErrorType a
    compute1 :: a -> Either ErrorType b
    compute2 :: b -> Either ErrorType c
    compute3 :: c -> Either ErrorType Unit

PureScript Code (idiomatic)

Or, we could use Either's Monad instance via do notation to write idiomatic PureScript code:

data Either a b
  = Left a
  | Right b

instance Bind (Either a) where
  bind :: forall b c. Either a b -> (b -> Either a c) -> Either a c
  -- when given a Left, stop all possible future computations
  -- and return immediately.
  bind l@(Left _) _ = l
  -- when given a Right, run the function on its contents
  -- and continue any Monadic computations
  bind (Right a) f = f a

someComputation :: Either ErrorType ReturnValue
someComputation = do
  a <- computation
  b <- compute1 1
  c <- compute2 b
  compute3 c

If a Left value is given at any point in the nested-bind computations, it will short-circuit and return immediately.

What is a real-world example of using the Either monad? One often uses it to validate that some data is correct. It reads like, "Try to parse the given String into an Int. If it fails, stop. Otherwise, try to parse the given String into a Foo. If it fails, stop. Otherwise, take the Int and the Foo and do something with them."

example :: String -> Either String ValidatedData
example string = do
  intValue <- parseString string
  fooValue <- parseNextPart
  doSomethingWith intValue fooValue

The List / Array Monad

We use the List type below in our examples. However, the Array type works exactly the same way.

JavaScript Code

In JavaScript, we would might write this code:

let list1 = [1, 2, 3];
let list2 = [2, 3, 4];
let list3 = [3, 4, 5];
var finalList = [];

for (i of list1) {
  for (h of list2) {
    for (j of list3) {
      finalList.push(i + h + j);
    }
  }
}
return finalList;

PureScript Code (idiomatic)

The non-idiomatic version of the PureScript code below is complicated because it uses a lot of recusion. Thus, I do not show it here. Rather, we'll only show the idiomatic version:

data List a
  = Nil
  | Cons a (List a)

-- bind implementation not shown here
instance Bind List where
  bind :: forall a b. List a -> (a -> List b) -> List b
  -- when given a Nil (end of list), stop all potential future computations and return immediately.
  bind Nil _ = Nil
  -- when given a non-empty list, run the future computations on the head
  -- and then prepend it to the rest of the computations on the tail.
  bind (head : tail) f = append (f head) (bind tail f)

append :: List x -> List x -> List x
append =
  -- implementation not shown here, but the result will be
  -- append (1 : 2 : Nil) (3 : 4 : Nil) == (1 : 2 : 3 : 4 : Nil)

someComputation :: List Int
someComputation = do
  a <- (1 : 2 : 3 : Nil)
  b <- (2 : 3 : 4 : Nil)
  c <- (3 : 4 : 5 : Nil)
  pure (a + b + c)

which outputs:

-- a = 1, b = 2
( 6 : 7 : 8
-- a = 1, b = 3
: 7 : 8 : 9
-- a = 1, b = 4
: 8 : 9 : 10

-- a = 2, b = 2
: 7 : 8 : 9
-- a = 2, b = 3
: 8 : 9 : 10
-- a = 2, b = 4
: 9 : 10 : 11

-- a = 3, b = 2
: 8 : 9 : 10
-- a = 3, b = 3
: 9 : 10 : 11
-- a = 3, b = 4
: 10 : 11 : 12

: Nil)

Concluding Thoughts

Different monadic types lead to different control flow statements. We've only shown a few here.

We will see more control flow options in the Application Structure folder, but there's more ground-work to cover before it'll make sense.

Monoids Reconsidered

The below table is a summarized version of something cvlad explained:

When we want to compose......and we don't need "empty" value, we use...and we do need "empty" value, we use
two values of same typeSemigroupMonoid
two values of different types where the second value DOES NOT have a runtime dependency on the first valueApplyApplicative
two values of different types where the second value DOES have a runtime dependency on the first valueBindMonad

What was @cvlad explaining? The meaning to this famous quote (in category theory terms):

A Monad is just a Monoid in the category of Endofunctors

To understand the quote more, see this link's 2-minute concise summary, which was also provided by cvlad: https://twitter.com/kenscambler/status/955441793465696257

One Monadic Type Per Monadic Context

Before we can continue further, we must understand one of the implications of the bind function.

Defining the Problem

Let's look at the type signature for the bind function.

class Apply boxLike <= Bind boxLike where
  bind :: forall a b. boxLike a -> (a -> boxLike b) -> boxLike b

If we ignore the (a -> boxLike b) argument, we can summarize it like this:

If you give me a "box-like" type, I will output the same "box-like" type.

In other words, once we use bind in a given computation (e.g. do notation), we restrict all usages of bind within that same computation to the same "box-like" type we originally passed in. This restriction is a good thing. There are ways to workaround the limitation. We'll cover one workaround below, but the other workarounds will be covered in the Application Structure folder.

Throughout this work, we will refer to this restriction as the "bind outputs the same box-like type it receives" restriction.

For now, let's provide an example of this problem.

Example of the Problem

Let's say we have two Box types. They differ only in their name. Each implements the Functor, Apply, and Bind instances in the exact same way. Below, we will only show the Bind instance, but assume they have implemented the other type classes:

data Box1 a = Box1 a
data Box2 a = Box2 a

class Apply m <= Bind m where
  bind :: forall a b. m    a -> (a -> m    b) -> m    b

instance Bind Box1 where
  bind :: forall a b. Box1 a -> (a -> Box1 b) -> Box1 b
  bind               (Box1 a)   f              = f a
instance Bind Box2 where
  bind :: forall a b. Box2 a -> (a -> Box2 b) -> Box2 b
  bind               (Box2 a)   f              = f a

Recall that do notation desugars into multiple bind calls:

example :: Box1 int
example = do
  u <- Box unit
  five <- Box 5
  pure (five + 1)

-- desugars to
example =
  bind (Box unit) \u ->
    bind (Box 5) \five ->
      pure (five + 1)

The below Box1 computation compiles fine.

box1Computation :: Box1 Unit
box1Computation = Box1 unit

The below Box2 computation compiles fine:

box2Computation :: Box2 Unit
box2Computation = Box2 unit

If I write the following code, which (if any) will compile?

box1ThenBox2 :: Box2 Unit
box1ThenBox2 = do
  box1Computation
  box2Computation

box2ThenBox1 :: Box1 Unit
box2ThenBox1 = do
  box2Computation
  box1Computation

Neither will compile. In box1ThenBox2, the first computation is box1Computation. Thus, this computation should eventually output a value of the Box1 someOutput type. However, we attempt to run a computation that uses a different monad (i.e. Box2) within the Box1 monadic context. Since Box2 isn't Box1, we get a compiler error. This same error occurs when you attempt to compile box2ThenBox1.

The First Workaround: Lifting One Monad into Another

Sometimes, this restriction actually helps us write safer code. Other times, this restriction is problematic and we need to get around it.

To help develop the necessary foundation for later understanding, we'll show a general approach to workaround this restriction. We use a type class that follows this idea:

class LiftSourceIntoTargetMonad sourceMonad targetMonad where {-
  liftSourceMonad :: forall a. sourceMonad a -> targetMonad a -}
  liftSourceMonad ::           sourceMonad   ~> targetMonad

-- Note: instances of this idea might be more complicated than this one
instance LiftSourceIntoTargetMonad Box2 Box1 where {-
  liftSourceMonad :: forall a. Box2 a -> Box1 a                      -}
  liftSourceMonad ::           Box2   ~> Box1
  liftSourceMonad (Box2 a) = Box1 a

This enables something like the following. It can be pasted into the REPL and one can try it out by calling bindAttempt:

import Prelude -- for the (+) and (~>) function aliases

data Box1 a = Box1 a
data Box2 a = Box2 a

class LiftSourceIntoTargetMonad sourceMonad targetMonad where                 {-
  liftSourceMonad :: forall a. sourceMonad a -> targetMonad a                 -}
  liftSourceMonad ::           sourceMonad   ~> targetMonad

instance LiftSourceIntoTargetMonad Box2 Box1 where
  liftSourceMonad :: Box2 ~> Box1
  liftSourceMonad (Box2 a) = Box1 a

bindAttempt :: Box1 Int
bindAttempt = do
  four <- Box1 4
  six <- liftSourceMonad $ Box2 6
  pure $ four + six

-- type class instances for Monad hierarchy

instance Functor Box1 where
  map :: forall a b. (a -> b) -> Box1 a -> Box1  b
  map f (Box1 a) = Box1 (f a)

instance Apply Box1 where
  apply :: forall a b. Box1 (a -> b) -> Box1 a -> Box1  b
  apply (Box1 f) (Box1 a) = Box1 (f a)

instance Bind Box1 where
  bind :: forall a b. Box1 a -> (a -> Box1 b) -> Box1 b
  bind (Box1 a) f = f a

instance Applicative Box1 where
  pure :: forall a. a -> Box1 a
  pure a =  Box1 a

instance Monad Box1

-- Needed to print the result to the console in the REPL session

instance (Show a) => Show (Box1 a) where
  show (Box1 a) = show a

Appendable: Numeric Hierarchy

After Semigroup and Monoid, the rest of PureScript's Numeric type classes (e.g. Semiring, Ring, etc.) and all the mathematical notations they use are very clearly explained elsewhere in hdgarrood's Numeric Hierarchy Overview.

Once one finishes reading it, be sure to check out his full-screen cheatsheet and his overview of PureScript's numeric types

Another resource that might be helpful is A Brief Guide to A Few Algebraic Data Structures. However, this is more of a reference material than a tutorial like Harry's above overview.

Docs

For derived functions (if any), see the type classes' docs.

Effect and Aff

This folder accomplishes the following goals:

  • An explanation of what "native side-effects" are and how this is modeled via Effect.
  • A demonstration of how to write the infamous "Hello World" app in Purescript
  • A demonstration of the various Effect types out there and their usage.
  • An overview of Aff and how to use some of its API.
  • An explanation and example of using the first workaround to the "bind outputs the same box-like type it receives" restriction.
  • A how-to guide for dealing with "callback hell" via Aff and using Node.ReadLine as an example.

These examples are compilable, enabling the reader to do two things.

REPL

First, one can interact with the code in this folder by using the REPL via the command, spago repl. Once initialized, one can import a module into the REPL and play with the code from there (e.g. run main).

For example, one might input the following command sequence:

spago repl
import HelloWorld
main

Note: some of the code in this folder will not work properly when used with the REPL. When it doesn't, use the second approach below.

Compilation

Second, one can compile the examples into their resulting JavaScript files. One can view just the module (i.e. the JavaScript code generated from a single PS file) or the entire program as an executable file (i.e. the JavaScript code generated from a call to file's main function). The latter can be run using Node:

Single ModuleEntire Program
Commandspago make-module --main [moduleName] --to dist/module.jsspago bundle-app --main [moduleName] --to dist/app.js
Javascript files' locationdist/module.jsdist/app.js

Effect Folder

To run each program in the Effect folder, use these commands:

# Syntax
# spago run --main Module.Path.To.Main.Module

spago run --main HelloWorld
spago run --main HelloNumber
spago run --main HelloDoNotation
spago run --main RandomNumber
spago run --main CurrentDateAndTime
spago run --main TimeoutAndInterval
spago run --main MutableState.Global
spago run --main MutableState.Local

Aff Folder

To run each program in the Aff folder, use these commands:

spago run -m AffBasics.LaunchAff
spago run -m AffBasics.Delay
spago run -m AffBasics.ForkJoin
spago run -m AffBasics.SuspendJoin
spago run -m AffBasics.CachedJoin
spago run -m AffBasics.SwitchingContexts
spago run -m TimeoutAndInterval.Aff

The following examples must be compiled first and then run by node:

spago bundle-app -m ConsoleLessons.ReadLine.Effect -t dist/readline-effect.js && node dist/readline-effect.js

spago bundle-app -m ConsoleLessons.ReadLine.Aff -t dist/readline-aff.js && node dist/readline-aff.js

The Effect Monad

(The following section is copied from here and slightly edited. I would add the license for that here, but it's not listed. Since the documentation is supposed to be public anyways, I doubt this is an issue.)

When we talk about side-effects, we are referring to two possible meanings. The first are "non-native" side-effects that we can emulate using pure functions (e.g. state manipulation on immutable data structures, returning additional output from a computation, etc.). The second are "native side-effects", which are effects provided by the RunTime System (RTS) and which can't be emulated by pure functions.

Some examples of native effects are:

  • Shared
    • Random number generation
    • Exceptions
    • Rendering content to the screen
  • Node only:
    • User input via the terminal
    • Interacting with the File System
  • Browser only:
    • DOM manipulation
    • XMLHttpRequest / AJAX calls
    • Interacting with a websocket
    • Interacting with Cookies

PureScript's purescript-effect package defines a monad called Effect, which is used to handle native effects. The goal of the Effect monad is to provide a typed API for effectful computations, while at the same time generating efficient Javascript.

(The remainder of this article is my own work.)

Understanding the Effect Monad

The following code is not necessarily how Effect is implemented, but it does help one quickly understand it by analogy:

data Unit = Unit

unit :: Unit
unit = Unit

-- | A computation that will only be run when passed in a `unit`
type PendingComputation a = (Unit -> a)

-- | A data structure that stores a pending computation.
data Effect a = Box (PendingComputation a -> a)

-- | This unwraps the data structure to get the
-- | pending computation, uses it to compute a value,
-- | and returns its result.
unsafePerformEffect :: Effect a -> a
unsafePerformEffect (Box pendingComputation) = pendingComputation unit

Some readers may realize that this is similar to the idea we introduced back in ROOT_FOLDER/Hello-World/Prelude/Control-Flow--Functor-to-Monad.md when we showed how an FP program does sequential computation using Monads. If you replace Box from that example with Effect, you would have a working FP program.

The whole idea of Effect is to use unsafePerformEffect as little as possible and ideally only once as the program's main entry point, explained next.

Main: A Program's Entry Point

The entry point into each program written in Purescript is the main function. It's type signature must be: main :: Effect Unit.

The following explanation is not what happens in practice, but understanding it this way will help one understand the concepts it represents:

When one executes the command spago bundle-app, one could say that, conceptually, spago will compile unsafePerformEffect main into Javascript and the resulting Javascript is what gets run by the RunTime System (RTS) when the program is executed.

In other words, spago "creates" a function called runProgram and tells the RunTime System (RTS) to execute it

runProgram :: Unit
runProgram = unsafePerformEffect main

This limits our impure code as much as possible to the program's start. Hopefully, everything else in our code is pure.

However, one might still call unsafePerformEffect in otherwise pure code in situations where they know what they are doing. In other words, they know the pros & cons, costs & benefits of doing so, and are willing to pay for those costs to achieve their benefits.

02-Hello-World.purs

-- This is the infamous "Hello World" app in Purescript.
module HelloWorld where

import Prelude  -- imports Unit

-- new imports
import Effect (Effect)
import Effect.Console (log) -- log :: String -> Effect Unit

-- | Describes but does not run a computation until RTS "calls unsafePerformEffect".
-- | The type signature of `log` is `String -> Effect Unit`. Thus, by applying
-- | a value of type `String` as an argument to `log`,
-- | `log "Hello World!"` has the type signature `Effect Unit`.
-- | Thus, it can be used as a main entry point into a program.
main :: Effect Unit
main = log "Hello world!"

03-Hello-Number.purs

module HelloNumber where

import Prelude
import Effect (Effect)

-- new imports

-- logShow :: forall a. Show a => a -> Effect Unit
-- logShow arg = log $ show arg
import Effect.Console (logShow)

main :: Effect Unit
main = logShow 5

04-Hello-Do-Notation.purs

module HelloDoNotation where

import Prelude
import Effect (Effect)
import Effect.Console (log)

-- A refresher on 'do-notation'

-- This chain of functions via log
main' :: Effect Unit
main' = (log "This is outputted first") >>= (\_ ->
          (log "This is outputted second") >>= (\_ ->
            log "This is outputted third"
          )
        )

-- can become more readable using sugar syntax (do-notation):
main :: Effect Unit
main = do
  log "This is outputted first"
  log "This is outputted second"
  log "This is outputted third"

01-Random-Number.purs

module RandomNumber where

import Prelude
import Effect (Effect)
import Effect.Console (log)

-- new import
import Effect.Random (random)
-- random :: Effect Number

main :: Effect Unit
main = do
  n <- random
  log $ "A random number between 0.0 and 1.0: " <> show n

  -- The above two lines could also be combined into one
  --   if we resort to using bind-notation again:
  random >>= (\n2 -> log $ "Another random number: " <> show n2)

  -- The above line still works because `log` returns `Effect Unit`

02-Current-Date-and-Time.purs

module CurrentDateAndTime where

import Prelude
import Effect (Effect)
import Effect.Console (log, logShow)

-- new import
import Effect.Now as Now

main :: Effect Unit
main = do
  dateAndTime <- Now.nowDateTime
  logShow dateAndTime

  log "------------"

  -- To reduce the above to one line, we'll combine the two using bind-notation.
  -- Since `logShow` has the type signature `Effect Unit`, the do-notation
  -- still works.

  Now.nowDate >>= (\x -> logShow x)
  Now.nowTime >>= (\x -> logShow x)

  -- We could make the above even shorter by removing the 'x' argument
  Now.nowDate >>= logShow
  Now.nowTime >>= logShow

03-Timeout-and-Interval.purs

module TimeoutAndInterval where

import Prelude
import Effect (Effect)
import Effect.Console (log)

-- new import
import Effect.Timer as T

-- Unfortunately, the code below won't work as expected because
-- the callbacks never run. Not enough time passes before they get triggered.
-- We'll see how to fix this using the `Aff` monad later in this folder.
main :: Effect Unit
main = do
  timeoutID <- T.setTimeout 1000 (log "This will run after 1 second")
  intervalID <- T.setInterval 10 (log "Interval ran!")

  log "Doing some other things...."
  log (evaluate 10 20)
  log "... Finished."

  log "Now cancelling interval"
  T.clearInterval intervalID

  log "Now cancelling timeout"
  T.clearTimeout timeoutID

  log "Program is done!"

evaluate :: Int -> Int -> String
evaluate x y | x < y = show x <> " is less than " <> show y
             | x > y = show x <> " is greater than " <> show y
             | otherwise = show x <> " is equal to " <> show y

Mutable State: Global vs Local

There are two types of mutable state:

GlobalLocal
Creatable in...AnywhereLocal scope
Readable from...Anywhere that has its referenceLocal Scope
Writable to...Anywhere that has its referenceLocal Scope
Destroyed when...Program Exits?Exit Local Scope

Using JavaScript as an example...

var global_state = "some state";
var doStuffUsingLocalState() = {
  var local_state = "some value";
  local_state = local_state + "some other string";
  return local_state.length();
}
var example1() {
  // change global state
  global_state = "first change!";
  // localState changed during the execution of the below
  // function, but we can't change it outside of that function.
  doStuffUsingLocalState();
}
example1();
var example2() {
  global_state = "second change!";
  return;
}
example2();

01-Global.purs

module MutableState.Global where

import Prelude
import Effect (Effect)
import Effect.Console (log)

-- new import
import Effect.Ref as Ref

main :: Effect Unit
main = do
  box <- Ref.new 0

  x0 <- Ref.read box
  log $ "x0 should be 0: " <> show x0

  Ref.write 5 box
  Ref.read box >>= (\x1 -> log $ "x1 should be 5: " <> show x1)

  Ref.modify_ (\oldState -> oldState + 1) box
  x2 <- Ref.read box
  log $ "x2 should be 6: " <> show x2

  newState <- Ref.modify (\oldState -> oldState + 1) box
  x3 <- Ref.read box
  log $ "x3 should be 7: " <> show x3 <> " | newState should be 7: " <> show newState

  value <- Ref.modify' (\oldState -> { state: oldState * 10, value: 30 }) box
  x4 <- Ref.read box
  log $ "value should be 30: " <> show value
  log $ "x4 should be 70: " <> show x4

  let loop 0 = Ref.read box
      loop n = do
        _ <- Ref.modify (_ + 1) box
        loop (n - 1)
  _ <- loop 20
  log "Finished"

02-Local.purs

module MutableState.Local where

import Prelude

import Control.Monad.ST as ST
import Control.Monad.ST.Ref as STRef
import Effect (Effect)
import Effect.Console (log)

main :: Effect Unit
main = do
  log "We will run some modifications on some local state \
      \and then try to modify it out of scope."
  let result = ST.run do
        {-
        At this level of indentation, we are in the ST monadic context.
        Since `log` returns an `Effect a`, we can't use it here.

        At this point in our understanding, we don't currently have a way
        to print the values of the local state to the console.

        We'll explain why this is a good thing later on in the `Debugging` folder.
        -}

        box <- STRef.new 0
        x0 <- STRef.read box

        _ <- STRef.write 5 box
        x1 <- STRef.read box

        newState <- STRef.modify (\oldState -> oldState + 1) box
        x3 <- STRef.read box

        value <- STRef.modify' (\oldState -> { state: oldState * 10, value: 30 }) box
        x4 <- STRef.read box

        let loop 0 = STRef.read box
            loop n = do
              _ <- STRef.modify (_ + 1) box
              loop (n - 1)

        loop 20

  log $ "Result of computation that used local state was: " <> show result

  log "Attempting to access `box` in this monadic context will result \
      \in a compiler error."

Summary of Effect Libraries

Since the spago.dhall file does not allow me to explain what each dependency does, I've offloaded that to the table below. These are not all of the Effects that exist. For example, I did not cover Avar, Aff, and others (see the full list on Pursuit here. Not all of the Effects found on there are truly Effects as they might be newtypes for something else). Rather, they give you something to use as you learn more and more of Purescript and FP concepts:

LibraryIncluded ModuleUsage
purescript-effectEffectProvides the Effect type itself.
purescript-consoleEffect.ConsoleProvides bindings to the Console
purescript-randomEffect.RandomType used to create random values
purescript-nowEffect.NowGet current Date/Time from machine. (Note: see the date-time repo for additional related functions)
purescript-js-timersEffect.TimerBindings to low-level JS API: set/clearTimeout and set/clearInterval
purscript-refsEffect.RefGlobal mutable state
purscript-stControl.Monad.STLocal mutable state

Effect, Eff, and Aff

Some History

Before the 0.12.0 release, the Effect monad used to be called Eff.

In short, the decision was made to drop Eff's "extensible effects". Presumably, to prevent code breakage, Eff and package location in imports was unchanged. Rather, it can now be found in the purescript-eff package. Its replacement was called Effect.

(You can read more about the decision making process here. If one is curious about Eff, read through the related section in the "Purescript by Example" book as it won't be covered here.)

Aff

The Aff monad was introduced and in use before this decision was made. Thus, history explains the naming behind Aff: if Eff was for synchronous effects, then Aff is for asychronous effects.

Aff will be covered in more depth in the upcoming files.

Aff

If you're writing an application (as opposed to a library), you should almost always use Aff to run your native side-effectful computations rather than Effect. Here are some of its advantages:

  • prevents "callback hell" for which Node.js is well-known.
  • enables concurrent programming (but not parallel programming as JavaScript is single-threaded).
  • is a stack-safe monad (Effect is not currently stack-safe).

Aff is basically what one would get if one implemented JavaScript Promises as a Monad.

Before continuing one with this folder's contents, watch Async Programming in PureScript to learn what problem Aff solves and a tour of its API for how to use it (actual video on YouTube is titled: "LA PureScript Meetup 12/05/17").

If, after watching the above video, you are tempted to figure out how Aff works internally, let me strongly recommend against that. The JavaScript code used to implement Aff is difficult to understand. Your time would be better invested elsewhere. Rather, I'd recommend looking at it when you have a better grasp of FP concepts.

Folder's Contents

First, we'll overview some of Affs API via some working examples that one can play with. Since all programs must be run in Effect, this will show the simplest way to start running computations in the Aff monad: launchAff_

Second, we'll show one way for making it possible to run an Effect-based computations in an Aff monadic context. (Note: this solution won't work for the ST monadic context in the Effect folder's Local-State.purs example.) Then, we'll show how to fix the issue we experienced in our the Effect folder's Timeout-and-Interval.purs file.

Third, we'll use the Node.ReadLine library to show how to convert Effect-based computations that require callbacks into Aff-based computations via makeAff. We'll also show the more complicated way to run a computation in the Aff monad, but which exposes all of Aff's features: runAff.

Finally, we'll link to other Aff-based libraries that one will likely find helpful.

Aff Basics

This folder shows some of the API of Aff. As stated before, all applications (not libraries) must be started in the Effect monad (or Eff if that's what you're using instead). An Aff-based computation can be converted into an Effect-based on by using launchAff/launchAff_.

So far, we've been printing values to the screen via log. That function's type signature is String -> Effect Unit. Since the "bind outputs the same box-like type it receives" restriction exists, we normally would not be able to use/compute in a different monadic context. For the time being, we will work around that problem by using a special function called specialLog. We'll explain how that's possible in the next folder, Lifting Monads, but for now just read it like you would log.

API not covered here (though it shouldn't be that hard to figure out how it works after reading through these examples and watching Nate's video)

  • supervisor
  • bracket
  • killFiber

01-Launching-Aff.purs

module AffBasics.LaunchAff where

import Prelude

import Effect (Effect)
import Effect.Aff (launchAff, launchAff_)
import Effect.Console (log)
import SpecialLog (specialLog)

main :: Effect Unit
main = do
  log "This is an Effect computation (Effect monadic context).\n"

  void $ launchAff do
    specialLog "This is an Aff computation (Aff monadic context)."

    specialLog "Aff provides the `launchAff` function that enables an \
               \Aff computation to run inside an Effect monadic context.\n"

  launchAff_ do
    specialLog "`launchAff_` is just `void $ launchAff`.\n"

  log "Program finished."

02-Delay.purs

module AffBasics.Delay where

import Prelude

import Effect (Effect)
import Effect.Aff (Milliseconds(..), delay, launchAff_)
import SpecialLog (specialLog)

main :: Effect Unit
main = launchAff_ do
  specialLog "Let's print something to the console and then \
             \wait 1 second before printing another thing."

  delay $ Milliseconds 1000.0 -- 1 second

  specialLog "1 second has passed."
  specialLog "Program finished."

03-Fork-Join.purs

module AffBasics.ForkJoin where

import Prelude

import Effect (Effect)
import Effect.Aff (Milliseconds(..), delay, forkAff, joinFiber, launchAff_)
import SpecialLog (specialLog)

main :: Effect Unit
main = launchAff_ do
  let
    fiber1 = "Fiber 1"
    fiber2 = "Fiber 2"
    fiber3 = "Fiber 3"

  specialLog "Let's run multiple computations concurrently. Then, \
             \we'll use `joinFiber` to ensure all computations have \
             \finished before we do another computation."

  firstFiber <- forkAff do
    specialLog $ fiber1 <> ": Waiting for 1 second until completion."
    delay $ Milliseconds 1000.0
    specialLog $ fiber1 <> ": Finished computation."

  secondFiber <- forkAff do
    specialLog $ fiber2 <> ": Computation 1 (takes 300 ms)."
    delay $ Milliseconds 300.0

    specialLog $ fiber2 <> ": Computation 2 (takes 300 ms)."
    delay $ Milliseconds 300.0

    specialLog $ fiber2 <> ": Computation 3 (takes 500 ms)."
    delay $ Milliseconds 500.0
    specialLog $ fiber2 <> ": Finished computation."

  thirdFiber <- forkAff do
    specialLog $ fiber3 <> ": Nothing to do. Just return immediately."
    specialLog $ fiber3 <> ": Finished computation."

  joinFiber firstFiber
  specialLog $ fiber1 <> " has finished. Now joining on " <> fiber2

  joinFiber secondFiber
  specialLog $ fiber3 <> " has finished. Now joining on " <> fiber3

  joinFiber thirdFiber
  specialLog $ fiber3 <> " has finished. All fibers have finished their \
                         \computation."

  specialLog "Program finished."

04-Suspend-Join.purs

module AffBasics.SuspendJoin where

import Prelude

import Effect (Effect)
import Effect.Aff (Milliseconds(..), delay, joinFiber, launchAff_, suspendAff)
import SpecialLog (specialLog)

main :: Effect Unit
main = launchAff_ do
  let
    fiber1 = "Fiber 1"
    fiber2 = "Fiber 2"
    fiber3 = "Fiber 3"

  specialLog "Let's setup multiple computations. Then, we'll use \
             \`joinFiber` to actually run the computations for the first time. \
             \When they run, they will block until finished.\n"

  specialLog "Setting up the first fiber, but not yet running its computation."
  firstFiber <- suspendAff do
    specialLog $ fiber1 <> ": Waiting for 1 second until completion."
    delay $ Milliseconds 1000.0
    specialLog $ fiber1 <> ": Finished computation."

  specialLog "Setting up the second fiber, but not yet running its computation."
  secondFiber <- suspendAff do
    specialLog $ fiber2 <> ": Computation 1 (takes 300 ms)."
    delay $ Milliseconds 300.0

    specialLog $ fiber2 <> ": Computation 2 (takes 300 ms)."
    delay $ Milliseconds 300.0

    specialLog $ fiber2 <> ": Computation 3 (takes 500 ms)."
    delay $ Milliseconds 500.0
    specialLog $ fiber2 <> ": Finished computation."

  specialLog "Setting up the third fiber, but not yet running its computation."
  thirdFiber <- suspendAff do
    specialLog $ fiber3 <> ": Nothing to do. Just return immediately."
    specialLog $ fiber3 <> ": Finished computation."

  delay $ Milliseconds 1000.0

  specialLog "Now running the first fiber's computation and blocking \
             \until it finishes."
  joinFiber firstFiber

  specialLog "Now running the second fiber's computation and blocking \
             \until it finishes."
  joinFiber secondFiber

  specialLog "Now running the third fiber's computation and blocking \
             \until it finishes."
  joinFiber thirdFiber

  specialLog $ "All fibers have finished their computation."

  specialLog "Program finished."

05-Cached-Join.purs

module AffBasics.CachedJoin where

import Prelude

import Effect (Effect)
import Effect.Aff (Milliseconds(..), delay, forkAff, joinFiber, launchAff_, suspendAff)
import SpecialLog (specialLog)

main :: Effect Unit
main = launchAff_ do
  let
    fiber1 = "Fiber 1"
    fiber2 = "Fiber 2"

  specialLog "Let's compute multiple computations. Then, we'll refer to the \
             \value they produced multiple times to see that the result is \
             \cached.\n"

  firstFiber <- forkAff do
    specialLog $ fiber1 <> ": You will only see this message once!"
    delay $ Milliseconds 1000.0
    pure 10

  secondFiber <- suspendAff do
    specialLog $ fiber2 <> ": You will only see this message once!"
    delay $ Milliseconds 1000.0
    pure 50

  result1 <- joinFiber firstFiber
  result2 <- joinFiber secondFiber

  specialLog "Finished joining fibers. After small pause, will join again \
             \to see whether their computations are rerun."
  delay $ Milliseconds 2000.0

  specialLog "Rejoining fibers!"
  result1_again <- joinFiber firstFiber
  result2_again <- joinFiber secondFiber
  specialLog "Finished joining fibers again."

  specialLog $ "Result 1 is the same? " <> show (result1 == result1_again)
  specialLog $ "Result 2 is the same? " <> show (result2 == result2_again)

  specialLog "Program finished."

06-Switching-Contexts.purs

module AffBasics.SwitchingContexts where

import Prelude

import Effect (Effect)
import Effect.Aff (Milliseconds(..), delay, joinFiber, launchAff, launchAff_, launchSuspendedAff)
import Effect.Console (log)
import SpecialLog (specialLog)

-- This example was created to show what happens when `launchSuspendedAff`
-- is used and its requirement to be run in another Aff computation later.
--
-- It also shows the unpredictability of switching
-- between the synchronous Effect and asychronous Aff in this way.
main :: Effect Unit
main = do
  let
    fiber1 = "Fiber 1"
    fiber2 = "Fiber 2"

  log "This is an Effect computation (Effect monadic context).\n"

  -- Runs an Aff computation and returns the fiber that, when joined,
  -- will produce the computed value. It must be joined in a new
  -- `Aff` computation.
  firstFiber <- launchAff do
    specialLog $ fiber1 <> ": You will only see this message once!"
    delay $ Milliseconds 1000.0
    pure 10

  -- Creates an Aff computation, but does not run it. Rather, returns
  -- the fiber that, when joined, will start and finish the computation,
  -- returning the computed value when done. It must be joined in a new
  -- `Aff` computation.
  secondFiber <- launchSuspendedAff do
    specialLog $ fiber2 <> ": You will only see this message once!"
    delay $ Milliseconds 1000.0
    pure 50

  log "\nJust some other stuff we need to do in the Effect monadic context...\n"

  launchAff_ do
    specialLog "Back inside an Aff monadic context. Let's see what those \
               \fibers did!\n"

    result1 <- joinFiber firstFiber
    result2 <- joinFiber secondFiber

    specialLog "Finished joining fibers. After small pause, will join again \
               \to see whether their computations are rerun."
    delay $ Milliseconds 2000.0

    specialLog "Rejoining fibers!"
    result1_again <- joinFiber firstFiber
    result2_again <- joinFiber secondFiber
    specialLog "Finished joining fibers again."

    specialLog $ "Result 1 is the same? " <> show (result1 == result1_again)
    specialLog $ "Result 2 is the same? " <> show (result2 == result2_again)

  log "Back outside. Now in the Effect monadic context."
  log "=======================\n\
      \The end of our code, but not the end of our program.\n\
      \=======================\n"

MonadEffect

In the previous folder, we saw that we could print content to the console using specialLog. Underneath, we're using log, the function with the type, String -> Effect Unit. Since "bind outputs the same box-like type it receives," how was this possible?

In this file, we'll show one way to workaround this limitation. This solution will be used frequently in real code wherever the Effect monad is used. However, this solution doesn't necessarily work for other monads. Still, it is conceptually easy to understand and creates scaffolding. That scaffolding will make it easier to understand other workarounds to this restriction that we'll discuss in the Application Structure folder.

Reviewing a Previous Workaround: Lifting one Monad into another

When overviewing the ""bind outputs the same box-like type it receives" restriction, we described the previous workaround:

import Prelude -- for the (+) and (~>) function aliases

data Box1 a = Box1 a
data Box2 a = Box2 a

class LiftSourceIntoTargetMonad sourceMonad targetMonad where                 {-
  liftSourceMonad :: forall a. sourceMonad a -> targetMonad a                 -}
  liftSourceMonad ::           sourceMonad   ~> targetMonad

instance LiftSourceIntoTargetMonad Box2 Box1 where
  liftSourceMonad :: Box2 ~> Box1
  liftSourceMonad (Box2 a) = Box1 a

bindAttempt :: Box1 Int
bindAttempt = do
  four <- Box1 4
  six <- liftSourceMonad $ Box2 6
  pure $ four + six

-- type class instances for Monad hierarchy

instance Functor Box1 where
  map :: forall a b. (a -> b) -> Box1 a -> Box1  b
  map f (Box1 a) = Box1 (f a)

instance Apply Box1 where
  apply :: forall a b. Box1 (a -> b) -> Box1 a -> Box1  b
  apply (Box1 f) (Box1 a) = Box1 (f a)

instance Bind Box1 where
  bind :: forall a b. Box1 a -> (a -> Box1 b) -> Box1 b
  bind (Box1 a) f = f a

instance Applicative Box1 where
  pure :: forall a. a -> Box1 a
  pure a =  Box1 a

instance Monad Box1

-- Needed to print the result to the console in the REPL session

instance (Show a) => Show (Box1 a) where
  show (Box1 a) = show a

MonadEffect

When we have an Effect-based computation that we want to run in some other monadic context, we can use liftEffect from MonadEffect if the target monad has an instance for MonadEffect:

class (Monad m) <= MonadEffect m where
  liftEffect :: Effect ~> m

Aff has an instance for MonadEffect, so we can lift Effect-based computations into an Aff monadic context. Below was how we defined specialLog. You can see it in the next file:

specialLog :: String -> Aff Unit
specialLog message = liftEffect $ log message

Referring back to our previous "local state" example, the ST monad does not have an instance for MonadEffect. This decision is intentional: state manipulation of that kind should be pure and not have any side-effects. That's why it exists inside of its own monadic context: to ensure that those who attempt to do so get compiler errors. This is a safety precaution, not a "we wanted to be jerks who frustrate you" decision.

As we saw previously in the Switching-Context.purs file, running multiple Aff computations in an Effect monadic context doesn't always lead to a predictable output. However, running multiple Effect-based computations in an Aff monadic context is much more predictable.

02-SpecialLog.purs

module SpecialLog (specialLog) where

import Prelude

import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Effect.Console (log)

specialLog :: String -> Aff Unit
specialLog msg = liftEffect $ log msg

03-Timeout-and-Interval.purs

module TimeoutAndInterval.Aff where

import Prelude

import Effect (Effect)
import Effect.Aff (Milliseconds(..), delay, launchAff_)
import Effect.Class (liftEffect)
import Effect.Console (log)
import Effect.Timer as T

main :: Effect Unit
main = launchAff_ do
  timeoutID <- liftEffect $ T.setTimeout 1000 (log "This will run after 1 second")
  intervalID <- liftEffect $ T.setInterval 10 (log "Interval ran!")

  liftEffect do
    -- Since these three log calls use `bind` to sequence them into
    -- a single `Effect Unit` computation, we can reduce verbosity
    -- by lifting all of them using one `liftEffect`.
    log "Doing some other things...."
    log (evaluate 10 20)
    log "... Finished."

  liftEffect do
    log "Now cancelling interval"
    T.clearInterval intervalID

  -- Here, we'll delay the computation long enough to ensure the
  -- above timeout callback actually runs.
  delay (Milliseconds 1300.0)
  liftEffect do
    log "Now cancelling timeout"
    T.clearTimeout timeoutID

    log "Program is done!"

evaluate :: Int -> Int -> String
evaluate x y | x < y = show x <> " is less than " <> show y
             | x > y = show x <> " is greater than " <> show y
             | otherwise = show x <> " is equal to " <> show y

Node ReadLine API

Node's ReadLine API docs are here. In this folder, we'll use the following Purescript bindings of the API (The Pursuit docs are outdated as they only show docs for an earlier release. So, either run spago docs and look at the Node.ReadLine module's docs, or look at the source code to see all of what is supported.

Below, I cover some of the API (some of the functions below had their 'foreign import' part removed to shorten the type signature):

-- Copyright is at the end of this file
foreign import data Interface :: Type

-- | A function which performs tab completion.
type Completer
  = String
  -> Effect
      { completions :: Array String
      , matched :: String
      }

-- | A completion function which offers no completions.
noCompletion :: Completer
noCompletion s = -- implementation

-- | Create an interface with the specified completion function.
createConsoleInterface :: Completer -> Effect Interface
createConsoleInterface compl = -- implementation

-- | Writes a message to the output and adds a listener to the
-- | interface that invokes the callback function when an
-- | event occurs (i.e. user inputs some text and presses Enter).
question :: String -> (String -> Effect Unit) -> Interface -> Effect Unit
question message handleUserInput interface = -- implementation

-- | Closes the specified `Interface` and cleans up resources.
close :: Interface -> Effect Unit
close interface = -- implementation

Copyright for above code:

The MIT License (MIT)

Copyright (c) 2014 PureScript

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

02-ReadLine-Effect.purs

module ConsoleLessons.ReadLine.Effect where

import Prelude
import Effect (Effect)
import Effect.Console (log)

{-
This file will demonstrate why using `Effect` to work with `Node.ReadLine`
creates the Pyramid of Doom.

Look through the code and then use the command in the folder's
ReadMe.md file to run it using Node (not Spago) to see what happens.
-}

-- new imports
import Node.ReadLine ( createConsoleInterface, noCompletion
                     , question, close)


type UseAnswer = (String -> Effect Unit)

main :: Effect Unit
main = do
  log "\n\n" -- separate output from program output

  log "Creating interface..."
  interface <- createConsoleInterface noCompletion
  log "Created!\n"

  log "Requesting user input..."
  interface # question "Type something here (1): " \answer1 -> do
    log $ "You typed: '" <> answer1 <> "'\n"
    interface # question "Type something here (2): " \answer2 -> do
      log $ "You typed: '" <> answer2 <> "'\n"
      interface # question "Type something here (3): " \answer3 -> do
        log $ "You typed: '" <> answer3 <> "'\n"
        interface # question "Type something here (4): " \answer4 -> do
          log $ "You typed: '" <> answer4 <> "'\n"
          interface # question "Type something here (5): " \answer5 -> do
            log $ "You typed: '" <> answer5 <> "'\n"

            log "Now closing interface"
            close interface
            log "Finished!"

          log "This will print as we wait for your 5th answer."
        log "This will print as we wait for your 4th answer."
      log "This will print as we wait for your 3rd answer."
    log "This will print as we wait for your 2nd answer."
  log "This will print as we wait for your 1st answer."

Basic Aff Functions

In this file, we'll show the second way to run an Aff computation called runAff and how to convert Node.ReadLine's question function (i.e. an Effect-based function that requires a callback) into an Aff-based computation using makeAff.

Aff Overview

Let's first overview some of Aff's concepts, so that the upcoming code is easier to understand. To be a truly asynchronous effect monad, Aff must support the following features:

  • handles errors that may arise during its computation
  • returns some computation's output
  • can be cancelled if it's no longer needed

To model the possibility for a computation to return an error or actual output, we can use Either a b. Handling errors and output implies a function. Aff uses the type signature, Either errorType outputType -> Effect Unit, for that.

Lastly, cancelling implies what to do when the computation is either no longer needed or it has failed (but we aren't using the function just discussed above). As an example, one will use Cancelers to clean up resources (e.g. clearTimeout).

newtype Canceler = Canceler (Error -> Aff Unit)

Since our present interests do not require cancellation, we can use a no-op Canceler: nonCanceler

Understanding runAff

For our purposes, we need an Aff to run inside of an Effect monadic context. If one looks through Aff's docs, the only one that does this besides launchAff and its variants is runAff_:

runAff_ :: forall a.
           (Either Error a -> Effect Unit) ->  -- arg 1
           Aff a ->                            -- arg 2
           Effect Unit                         -- outputted value

Breaking this down, runAff_ takes two arguments (explained in reverse):

  • an Aff computation to run
  • a function for handling a possible asynchronous Error if the computation fails or the computation's output, a, if it succeeds.

Using it should look something like:

runAff_ (\either -> case either of
    Left error -> log $ show error
    Right a -> -- do something with 'a' or run cleanup code
  )
  affValue

We could make the code somewhat easier by using Data.Either (either)

runAff_ (either
          (\error -> log $ show error   ) -- Left value
          (\a -> {- usage or cleanup -} ) -- Right value
  )
  affValue

Understanding makeAff

Next, we need to convert question from an Effect-based computation into an Aff-based one. Looking through Pursuit again, makeAff is the only function that does this:

makeAff :: forall a. ((Either Error a -> Effect Unit) -> Effect Canceler) -> Aff a

Breaking this down, makeAff takes only one argument. However, the argument is a bit quirky since it takes a function as its argument. We should read it as...

  Given a function
    that returns an `Effect Canceler`
    by using the function that `runAff_` requires
      (i.e. `(Either Error a -> Effect Unit)`),
output an `Aff` computation that produces a value of type `a` when `bind`ed

To create this type signature, we'll write something like this:

affValue :: Aff String
affValue = makeAff go
  where
  go :: (Either Error a -> Effect Unit) -> Effect Canceler
  go runAff_RequiredFunction = -- implementation

Since the implementation will need to return an Effect Canceler, we can do one of two things:

  1. Lift a canceller into Effect via pure. This is pointless because then our Aff wouldn't do anything.
  2. Create an Effect a and use Functor's dervied function, voidRight (<$), with nonCanceler
-- for a refresher on voidRight
2 `voidRight` (Box 1) == 2 <$ (Box 1) == (Box 2)

-- alias is: "<$"
voidRight :: forall f a b. Functor f => b -> f a -> f b
voidRight b box = (\_ -> b) <$> box

-- or ignore the monad's inner 'a' and replace it with 'b'

Updating our code to use these two ideas, we now have:

affValue :: Aff String
affValue = makeAff go
  where
  go :: (Either Error a -> Effect Unit) -> Effect Canceler
  go runAff_RequiredFunction = nonCanceler <$ (effectBox runAff_RequiredFunction)

  effectBox :: (Either Error a -> Effect Unit) -> Effect Unit
  effectBox runAffFunction = -- implementation

We want to use question to print something to the console, get the user's input, and return that value. It's type signature is:

question :: String -> (String -> Effect Unit) -> Interface -> Effect String
question message handleUserInput interface = -- Node binding implementation

The only place we could insert runAffFunction is in (String -> Effect Unit). Thus, we come up with this function:

effectBox :: (Either Error String -> Effect Unit) -> Effect Unit
effectBox runAffFunction =
  question message (\userInput -> runAffFunction (Right userInput)) interface
                              -- (runAffFunction <<< Right) -- less verbose; same thing

Putting it all together and excluding the required arguments, we get:

affValue :: Aff String
affValue = makeAff go
  where
  go :: (Either Error a -> Effect Unit) -> Effect Canceler
  go runAff_RequiredFunction = nonCanceler <$ (effectBox runAff_RequiredFunction)

  effectBox :: (Either Error a -> Effect Unit) -> Effect Unit
  effectBox runAffFunction = question message (runAffFunction <<< Right) interface

Cleaning it up and including the arguments, we get:

affQuestion :: String -> Interface -> Aff String
affQuestion message interface = makeAff go
  where
  go :: (Either Error a -> Effect Unit) -> Effect Canceler
  go runAffFunction =
    nonCanceler <$ question message (runAffFunction <<< Right) interface

04-ReadLine-Aff.purs

module ConsoleLessons.ReadLine.Aff where

import Prelude

import Data.Either (Either(..))
import Effect (Effect)
import Effect.Aff (Aff, runAff_, makeAff, nonCanceler)
import Effect.Class (liftEffect)
import Effect.Console (log)
import Node.ReadLine (Interface, createConsoleInterface, noCompletion, close)
import Node.ReadLine as ReadLine

-- This is `affQuestion` from the previous file
question :: String -> Interface -> Aff String
question message interface = makeAff go
  where
    -- go :: (Either Error a -> Effect Unit) -> Effect Canceler
    go runAffFunction = nonCanceler <$
      ReadLine.question message (runAffFunction <<< Right) interface

main :: Effect Unit
main = do
  log "\n\n" -- separate output from program

  log "Creating interface..."
  interface <- createConsoleInterface noCompletion
  log "Created!\n"
                                                                               {-
  runAff_ :: forall a. (Either Error a -> Effect Unit) -> Aff a -> Effect Unit -}
  runAff_
    -- Ignore any errors and output and just close the interface
    (\_ -> closeInterface interface)
    (useInterface interface)
  where
    closeInterface :: Interface -> Effect Unit
    closeInterface interface = do
      log "Now closing interface"
      close interface
      log "Finished!"

    -- Same code as before, but without the Pyramid of Doom!
    useInterface :: Interface -> Aff Unit
    useInterface interface = do
      liftEffect $ log "Requesting user input..."

      answer1 <- interface # question "Type something here (1): "
      liftEffect $ log $ "You typed: '" <> answer1 <> "'\n"

      answer2 <- interface # question "Type something here (2): "
      liftEffect $ log $ "You typed: '" <> answer2 <> "'\n"

      answer3 <- interface # question "Type something here (3): "
      liftEffect $ log $ "You typed: '" <> answer3 <> "'\n"

      answer4 <- interface # question "Type something here (4): "
      liftEffect $ log $ "You typed: '" <> answer4 <> "'\n"

      answer5 <- interface # question "Type something here (5): "
      liftEffect $ log $ "You typed: '" <> answer5 <> "'\n"

Useful Aff Libraries

Based on Aff

These were found using a purescript-aff- search on Pursuit:

Aff Wrappers Around Node

Debugging

This folder helps you debug problems in your code by

  • explaining some tips/tricks to use to help debug compiler errors
  • forewarning about some potential misunderstandings
  • helping you to read some compiler errors

Running The Lessons

You should NOT use the REPL for these lessons.

Rather, you should use spago to run them using this syntax:

spago run --main Debugging.OverviewAPI

When compiling these examples, you will likely see a warning like below:

Warning found:
in module Debugging.CustomTypeErrors.TypeClassInstances
at src/03-Custom-Type-Errors/04-Type-Class-Instances.purs line 41, column 1 - line 41, column 23

  A custom warning occurred while solving type class constraints:

    No worries! This warning is supposed to happen!

    [Some warning message here...]


in value declaration warnInstance

See https://github.com/purescript/documentation/blob/master/errors/UserDefinedWarning.md for more information,
or to contribute content related to this warning.

This is supposed to happen, so don't be alarmed. When we hit that part of our lessons, we'll tell you how to remove the warnings so you can get rid of the "compiler noise."

General Debugging

The following sections are tips for debugging issues that may arise in a strongly-typed language via the compiler.

There is currently no "Actual Type / Expected Type" distinction

In the following error...

  Could not match type

    A

  with type

    B

  ... rest of error ...

... one might expect A to be the "actual" type and B to be the "expected" type. However, sometimes the two are swapped, so that A is the "expected" type and B is the "actual" type. This is not desirable, but is currently how the compiler works.

Why? Because the compiler uses a mixture of unification and type inference to check types. See purescript/purescript#3399 for more information.

Distinguishing the Difference between {...} and (...) errors

(thomashoneyman recommended I document this. These examples might be incorrect since I am not fully aware of the comment that garyb made, but the general idea still applies.)

Recall that { label :: Type } is syntax sugar for Record (label :: Type)

So, the below error means a Record could not unify with some other type:

  Could not match type
    { label :: Type }
  with type
    String

Whereas the below error means a Record was the correct type, but some of its label-type associations were missing.

  Could not match type
    Record (label :: Type)
  with type
    Record (label :: Type, forgottenLabel :: OtherType)

Otherwise known as "typed holes."

If you recall in Syntax/Basic Syntax/src/Data-and-Functions/Type-Directed-Search.md, we can use type-directed search to

  1. help us determine what an entity's type is
  2. guide us in how to implement something
  3. see better ways to code something via type classes

As an example, let's say we have a really complicated function or type

main :: Effect Unit
main = do
  a <- computeA
  b <- computeB
  c <- (\a -> (\c -> doX c) <$> box a) <$> (Box 5) <*> (Box 8)

If we want to know what the type will be for doX, we can rewrite that entity using a type direction search, ?doX, and see what the compiler outputs:

main :: Effect Unit
main = do
  a <- computeA
  b <- computeB
  c <- (\a -> (\c -> ?doX c) <$> box a) <$> (Box 5) <*> (Box 8)

If we're not sure what type a function outputs, we can precede the function with our search using ?name $ function:

main :: Effect Unit
main = do
  a <- computeA
  b <- computeB
  c <- (\a -> (\c -> ?help $ doX c) <$> box a) <$> (Box 5) <*> (Box 8)

If you encounter a problem or need help, this should be one of the first things you use.

Getting the Type of an Expression from the Compiler

This is known as "typed wildcards".

In a function body, wrapping some term with (term :: _) will cause the compiler to infer the type for you.

main :: Effect Unit
main = do
  a <- computeA
  b <- computeB
  c <- (\w x -> ((doX x) :: _)) <$> box a) <$> (Box 5) <*> (Box 8)

Getting the Type of a Function from the Compiler

There are two possible situations where this idea might help:

  • You know how to write the body for a function but aren't sure what it's type signature is
  • You're exploring a new unfamiliar library and are still figuring out how to piece things together. So, you attempt to write the body of a function but aren't sure what it's type signature will be.

In such cases, we can completely omit the type signature and the compiler will usually infer what it is for us:

-- no type signature here for `f`,
-- so the compiler will output a warning
-- stating what its inferred type is
f = (\a -> (\c -> doX c) <$> box a) <$> (Box 5) <*> (Box 8)

However, the above is not always useful when one only wants to know what the type of either an argument or the return type. In such situations, one can use typed wildcards from above in the type signature:

doesX :: String -> _ -> Int
doesX str anotherString = length (concat str anotherString)

Determining why a type was inferred incorrectly

Sometimes, I wish we could have a 'unification trace' or a 'type inference trace'. I know the code I wrote works, but there's some mistake somewhere in my code that's making the compiler infer the wrong type at point X, which then produces the type inference problem much later at point Y. To solve Y, I need to fix the problem X, but I'm not sure where X is.

Here's an example:

type Rec = { a :: String }

f :: String -> String
f theString = wrap (unwrap theString)

  where
    wrap :: String -> Rec
    wrap theString = { a: theString }

    {-
      the mistake! Compiler says
      Cannot match type
        { a :: String }
      with type
        { a :: String, b :: String }
    unwrap :: Rec -> String
    unwrap rec = rec.b

In the PureScript chatroom, garyb mentioned passing the --verbose-errors flag to the compiler. This will output a LOT of information, but it's that or nothing. To do that, run this code:

spago build -u --verbose-errors
spago build -u -v

Could not match type Monad with type Function (Argument -> Monad a)

Whenever you get an error like this....

Error found:
in module Try
at src/example.purs:10:3 - 12:6 (line 10, column 3 - line 12, column 6)

  Could not match type

    Effect

  with type

    Function (String -> Effect Unit)


while trying to match type Effect Unit
  with type (String -> Effect Unit) -> t0
while inferring the type of (log "Here's a message") log
in value declaration main

where t0 is an unknown type

It's because you forgot to add the do keyword. Here's the code that produces the error:

main :: Effect Unit
main = -- missing `do` keyword!
  log "Here's a message"

  log "Here's another message."

Improve Error Messages when using unsafePartial to un-Partial Functions

(This section assumes familiarity with the Design Patterns/Partial Functions/ folder)

Taken from safareli's comment in "When should you use primitive types instead of custom types?"", there might be times where you want to use a partial function to get or compute some value that might not be there. If one just uses unsafePartial $ <unsafeFunction>, the error message will likely not be helpful:

-- Don't do this.
foo :: forall a. Maybe a -> a
foo mightBeHere =
                  -- we assume that 'mightBeHere' is the "Just a" constructor
  unsafePartial $ fromJust mightBeHere

sarafeli's suggestion is to pattern match on the value and use unsafeCrashWith instead to provide a much better error message in case your assumption is proven invalid.

foo :: forall a. Maybe a -> a
foo mightBeHere = case mightBeHere of
  Nothing -> unsafeCrashWith "'mightBeHere' should be a valid 'a'"
  Just v -> v

Custom Type Errors

Pre-reqs for This Folder

To understand this folder's contents, you should read and be somewhat familiar with Type-Level Syntax. If you haven't already done so, go read through that folder's contents.

Scope of This Folder

This folder will demonstrate how to write Custom Type Errors (i.e. custom compiler warnings and errors) and why one might find it useful. It provides the foundations for understanding why something happens in the next folder's code. This folder does not get deep into how to do type-level programming. That will be covered later.

In this folder, we'll be using the infix aliases from purescript-typelevel-eval. We won't be directly importing it here because it does not yet exist in the default package set (as of this writing).

01-Overview-API.purs

module Debugging.CustomTypeErrors.OverviewAPI where

{-
# Prim Special Submodules

Every Purescript project imports the Prim module by default:
https://pursuit.purescript.org/builtins/docs/Prim

This module defines `kind Type` and the types
for `Int`, `Array`, and `Function`.

In addition, the Prim module has sub module called "TypeError"
that is not imported by default. Within it, Prim defines a few
things for writing your own custom type warnings/errors.

Similar to what we did in the Syntax folder, we'll show the
value-level definitions of these type-level types, instances, and functions
-}

{-
The following is commented out to prevent a compiler warning:
  "import is redundant"

-- new imports
import Prim.TypeError (
  -- type-level type
    kind Doc

  -- type-level instances
  , Text
  , Quote
  , Above
  , Beside

  -- type-level functions
  , class Warn
  , class Fail
  )

-}

data Doc_
  = Text_ String      -- wraps a Symbol
  | Quote_ String     -- the Type's name as a Symbol
  | QuoteLabel_ String -- Similar to Text but handles things differently
                       -- Used particularly for 'labels', the 'keys'
                       -- in rows/records (see functions file)
  | Beside_ Doc_ Doc_ -- Similar to "left <> right" ("leftright") in that
                      -- it places documents side-by-side. However, it's
                      -- different in that these documents are aligned at
                      -- the top.
  | Above_ Doc_ Doc_  -- same as "top" <> "\n" <> "bottom" ("top\nbottom")

type Explanation = String

warn :: Explanation
warn = """
         Usage:
              - Constrain a type with Warn in a value/function declaration
              - Constrain a type in a type class instance with Warn
         Result:
              If value/function is used, outputs a warning during compile-time
         Compilation succeeds?:
              Yes
         Use Cases:
              - "Soft" Deprecation - Indicate to users of library that this
                  function/value will be removed/changed in future
              - Warning indicating developer/debug code should be removed
                  before production code is released
         Does the REPL display it?:
              No (as of this writing)
         """

fail :: Explanation
fail = """
         Usage:
              - Constrain a type with Fail in a value/function declaration
              - Constrain a type in a type class instance with Fail
         Result:
              If instance is used, outputs an error during compile-time
         Compilation succeeds?:
              No
         Use Cases:
              - "Hard" Deprecation - Remove support for a value/function and
                  force users of library to migrate to new approach or
                  use a different value/function that does the same thing.
              - Provide better error messages for specific type class instances
                  that cannot exist.
         Does the REPL display it?:
              Yes
         """

02-Values.purs

module Debugging.CustomTypeErrors.Values where

import Effect (Effect)
import Effect.Console (log)
import Data.Show (show)
import Data.Unit (Unit)
import Data.Function (($))
import Control.Bind (discard)

import Prim.TypeError (Text, class Warn, class Fail)

warnFunction :: Warn
  (  Text "Deprecated! Use betterFunction instead"
  ) => Int -> Int
warnFunction x = x

betterFunction :: Number
betterFunction = 5.0

failFunction :: Fail
  (  Text "Broken! Use betterFunction instead"
  ) => Int -> Int
failFunction _ = 20

regularFunction :: Int -> Int
regularFunction _ = 4

main :: Effect Unit
main = do
  log $ show $ regularFunction 8
  log $ show $ warnFunction 3

  -- Uncomment the next line, save the file, run it, and see what happens
  -- log $ show $ failFunction 12

03-Functions.purs

module Debugging.CustomTypeErrors.Functions where

import Prelude
import Effect (Effect)
import Effect.Console (log)
import Type.Proxy (Proxy(..))

import Prim.TypeError (Text, Quote, Above, Beside, QuoteLabel, class Warn, class Fail)

data Doc_
  = Text_ String      -- wraps a Symbol
  | Quote_ String     -- the Type's name as a Symbol
  | QuoteLabel_ String -- Similar to Text but handles things differently
                       -- Used particularly for 'labels', the 'keys'
                       -- in rows/records (see below)
  | Beside_ Doc_ Doc_ -- Similar to "left <> right" ("leftright") in that
                      -- it places documents side-by-side. However, it's
                      -- different in that these documents are aligned at
                      -- the top.
  | Above_ Doc_ Doc_  -- same as "top" <> "\n" <> "bottom" ("top\nbottom")

-- The following aliases are taken from purescript-typelevel-eval:
-- https://pursuit.purescript.org/packages/purescript-typelevel-eval/0.2.0/docs/Type.Eval
-- I would use it and import them here, but it's not yet in the default package set
infixr 2 type Beside as <>
infixr 1 type Above as |>

besideExample :: Warn
  (  Text "Beside lays out documents side-by-side, aligned at the top:"
  |> Text ""
  |> ( (  Text "A"
       |> Text "B"
       ) <> Text "C"
     )
  |> Text ""
  |> ( Text "C" <> (  Text "A"
                   |> Text "B"
                   )
     )

  ) => Int
besideExample = 2

warnValue :: Warn
  (  Text "This warning appears only when you use this value or function"
  |> Text ""
  |> Text "The requested value of type, " <> Quote Int <> Text ","
  |> Text "is no longer something you should use!"
  |> Text ""
  |> Text "Use betterValue instead since it is of type " <> Quote Number
  ) => Int
warnValue = 5

betterValue :: Number
betterValue = 5.0

failValue :: Fail
  (  Text "This error only appears when you use this value"
  |> Text ""
  |> Text "Error: Value is no longer valid"
  ) => Int
failValue = 20

regularValue :: Int
regularValue = 4

-- QuoteLabel vs Text
labelAsText :: forall l. Warn
  ( Text "Text Label " <> Text l
  ) => Proxy l -> String
labelAsText _ = ""

labelAsQuote :: forall l. Warn
  ( Text "Quote Label " <> QuoteLabel l
  ) => Proxy l -> String
labelAsQuote _ = ""

main :: Effect Unit
main = do
  log $ show regularValue
  log $ show warnValue

  log $ show besideExample

  -- Demonstrates the difference between Text and QuoteLabel
  log $ show (labelAsText (Proxy :: Proxy "boo"))
  log $ show (labelAsQuote (Proxy :: Proxy "boo"))

  log $ show (labelAsText (Proxy :: Proxy "b\"oo"))
  log $ show (labelAsQuote (Proxy :: Proxy "b\"oo"))

  log $ show (labelAsText (Proxy :: Proxy "b o o"))
  log $ show (labelAsQuote (Proxy :: Proxy "b o o"))

  -- Uncomment the next line, save the file, run it, and see what happens
  -- log $ show failValue

04-Type-Class-Instances.purs

module Debugging.CustomTypeErrors.TypeClassInstances where

import Effect (Effect)
import Effect.Console (log)
import Data.Show (show)
import Data.Unit (Unit)
import Data.Function (($))

import Prim.TypeError (Text, Above, class Warn, class Fail)

infixr 1 type Above as |>

class ExampleClass a where
  emitMessage :: a -> String

instance ExampleClass Int where
  emitMessage _ = "an integer I'm sure..."

data WarnType = WarnType
data FailType = FailType

instance Warn
  (  Text "No worries! This warning is supposed to happen!"
  |> Text ""
  |> Text "[Some warning message here...]"
  ) => ExampleClass WarnType where
  emitMessage _ = "The message!"

instance Fail
  (  Text "Using this instance will cause code to fail"
  ) => ExampleClass FailType where
  emitMessage _ = "This will never occur"

useInstanceOfExampleClass :: forall a. ExampleClass a => a -> String
useInstanceOfExampleClass a = emitMessage a

main :: Effect Unit
main = do
  log $ show regularInstance

regularInstance :: String
regularInstance = useInstanceOfExampleClass 0

-- Even though this is never used in main,
-- it still emits a warning.
warnInstance :: String
warnInstance = useInstanceOfExampleClass WarnType

-- Even though this is never used in main,
-- it still emits a compiler error
-- failInstance :: String
-- failInstance = useInstanceOfExampleClass FailType

Debug Trace

Previously, we got around the "bind outputs the same box-like type it receives" restriction by using MonadEffect. However, we also explained that ST, the monad used to run a computation that uses local mutable state, did not have an instance for MonadEffect. This decision is intentional.

When we run production code, we want to uphold this restriction. However, when we are debugging code, this restriction can be very annoying. Fortunately, the Debug package exists to help you use print debugging in any monadic context. You should use it when initially prototyping things. It should never appear in production code, nor as a solution for production-level logging. (We'll show how to do that in the Application Structure folder).

WARNING: Debug's functions are not always reliable when running concurrent code (i.e. Aff-based computations).

Compilation Instructions

Seeing the Custom Type Errors

The warnings that will appear when compiling this code only appear once. Once you have built the code, spago will reuse the already-compiled JavaScript and thus won't retrigger these warnings. If you want to see them again, follow these instructions.

# Remove all previously compiled JavaScript
rm -rf output/

# Now build the code to see the warnings.
spago build

Running the Examples

Use these commands

spago run -m Debugging.Debug
spago run -m Debugging.LocalState

01-Debug.purs

-- When you compile this file, it will output compiler warnings.
-- If you wish to remove that noise, comment out everything below
-- the "module" declaration.
module Debugging.Debug where

-- Comment out everything below this line to prevent compiler warning.
----------------------------------------------------------------------

import Prelude
import Effect (Effect)
import Effect.Console  (log)
import Debug (spy, trace, traceM)

-- Given a simple Box Monad
data Box a = Box a
-- all type class instances are at the end of the file

-- And a way to convert a Box computation into an Effect computation
runBox :: Box ~> Effect
runBox (Box a) = pure a

boxed4 :: Box Int
boxed4 = Box 4

printAndReturnTheValue :: Int -> Int
printAndReturnTheValue x = spy "x" x

printMessageThenRunComputation :: String -> (Unit -> Int) -> Int
printMessageThenRunComputation msg x = trace msg x

-- When running this, you'll notice that the debug messages
-- are outputted in a font color different than the normal output.
main :: Effect Unit
main = do
  -- spy
  log $ show $ printAndReturnTheValue 5

  -- another way to do this
  let _ = spy "four" 4

  -- trace
  log $ show $ printMessageThenRunComputation "before computation" (\_ -> 10)

  log "Right now we are inside of the Effect monad context, \
      \which means we CAN use the `log` function here."
  value <- runBox do

    four <- boxed4
    -- now we are inside of the Box monad context,
    -- which means we CANNOT use `log` here since
    -- it returns `Effect Unit`, not `Box Unit`

    -- Instead, we'll use traceM
    traceM ("Four is: " <> show four)

    pure (four + 8)

  log $ "Value is: " <> show value

-- Box's type class instances
instance Functor Box where
  map f (Box a) = Box (f a)

instance Apply Box where
  apply (Box f) (Box a) = Box (f a)

instance Applicative Box where
  pure a = Box a

instance Bind Box where
  bind (Box a) f = f a

instance Monad Box

DebugWarning

Debug uses Custom Type Errors to warn the developer when it is being used.

Let's examine it further since it provides an example for us to follow should we wish to do something similar in the future. The source code is here, but we'll provide type signatures for the parts we need below and explain their usage:

-- See the copyright notice at the bottom of this file for this code:

-- | Nullary type class used to raise a custom warning for the debug functions.
class DebugWarning

instance Warn (Text "Debug usage") => DebugWarning

foreign import trace :: forall a b. DebugWarning => a -> (Unit -> b) -> b

-- same idea as 'trace' for all the other functions

In short, rather than writing function :: Warn (Text "Debug usage") => [function's type signature] on every function, they use an empty type class whose sole instance adds this for every usage of that type class.


Copyright notice for the above code:
The MIT License (MIT)

Copyright (c) 2015 Gary Burgess

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

03-Local-State.purs

-- Now that we understand how `Debug.Trace` works, let's show
-- what's going on in our previous local mutable state computation.
--
-- When you compile this file, it will output compiler warnings due to
-- usage of `Debug.Trace (traceM)`. If you wish to remove that noise,
-- comment out every usage of `traceM` in this file.
module Debugging.LocalState where

import Prelude

import Control.Monad.ST as ST
import Control.Monad.ST.Ref as STRef
import Debug (traceM)
import Effect (Effect)
import Effect.Console (log)

main :: Effect Unit
main = do
  log "We will run some modifications on some local state \
      \and then try to modify it out of scope."
  log $ show $ ST.run do
    box <- STRef.new 0
    x0 <- STRef.read box
    traceM $ "x0 should be 0: " <> show x0

    _ <- STRef.write 5 box
    x1 <- STRef.read box
    traceM $ "x1 should be 5: " <> show x1

    -- Note: `STRef.modify_` doesn't exist.

    newState <- STRef.modify (\oldState -> oldState + 1) box
    x2 <- STRef.read box
    traceM $ "x2 should be 6: " <> show x2 <> " | newState should be 6: " <> show newState

    value <- STRef.modify' (\oldState -> { state: oldState * 10, value: 30 }) box
    x3 <- STRef.read box
    traceM $ "value should be 30: " <> show value
    traceM $ "x3 should be 60: " <> show x3

    let loop 0 = STRef.read box
        loop n = do
          _ <- STRef.modify (_ + 1) box
          loop (n - 1)

    loop 20

  log "Attempting to access box here will result in a compiler error"

Console-Based Debugging

A library that may be useful to use when debugging is purescript-debugger (3.0.0's source code & 2.0.0's docs). This library's latest release is 3.0.0, but the updated docs have not been pushed to Pursuit yet. I'm not sure what changed

I will not cover this library here (though I might later). I'm including it, so that you are aware of it in the future once we've covered more things.

Other Tips

This file includes tips I've seen people share in the PureScript chatroom:

How do you guys typically go about debugging runtime issues? Especially bad FFI interactions? Right now I end up using Debug.Trace pretty frequently. I’m trying to move toward a workflow where I can resolve more of these issues with automated testing and in the REPL. I think the REPL is probably the way to go for most logic issues in PS? However, if API definitions are wonky in the FFI then the REPL isn’t generally super useful since I’ll just get some error from deep in the bundle in some dependency typically.

From @kritzcreek:

I use Debug.Trace and break points in the browser. In general, I minimize having to interact with the FFI. If you're crossing the boundary too often maybe you can change your design to move more into PS or isolate and group the FFI interactions more clearly. I rarely use the REPL at all, parcel or webpack reloading the webpage on change in my editor is fast enough for my feedback cycle needs

Collections and Loops

This folder will overview the purescript-foldable-traversable and purescript-filterable libraries. Together, these libraries provide most of the functions one would use when working with collections.

These two type classes are being overviewed because their concepts will arise later in the "Hello World" folder. Rather than explaining them there when we need them, we'll explain them here so that they are more familiar when we get to them there.

Foldable

Usage

Plain English names:

  • Summarizeable
  • Reducible
  • FP version of the Iterator Pattern
  Given
    a box-like type, `f`,
      that stores zero or more `a` values
        and
    an initial value of type, `b`
        and
    a `Semigroup`/`Monoid`-like function
      that produces a `b` value if given an `a` and a `b` argument,
        of which there are two versions
        (
          either `a -> b -> b`
          or     `b -> a -> b`
        ),
return a single value of type, `b` by
  (first application) passing the initial `b` and initial `a` values into the function,
    which produces the next `b` value,
  (recursive application) passing the previously-computed `b` value with the next `a` value into the function
    which produces the next `b` value,
    which will eventually be the final `b` value returned
      when there are no more `a` values
        in the box-like `f` type.

It enables:

  • a way to reduce a List of Strings into one String (the combination of all the Strings)
  • a way to reduce a List of Ints into one Int (the sum/product of all the ints)
  • a way to take a List Int and create a Map String Int (each value is an Int from the list and its key is the output of show int)
  • a way to take a List Int and double each Int in the list (i.e. write List's Functor instance by implementing map via this type class).

Definition

Code Definition

Don't look at its docs until after looking at the visual overview in the next section: Foldable

class (Functor f) => Foldable f where
  foldMap :: forall a m. Monoid m => (a -> m) -> f a -> m

  foldr :: forall a b. (a -> b -> b) -> b -> f a -> b
  foldl :: forall a b. (b -> a -> b) -> b -> f a -> b

Visual Overview

For a cleaner visual, see Drawing foldl and foldr

foldl

This version creates a tree-like structure of computations that starts evaluating immediately via function Binput A1 and continues evaluating towards the bottom-right. Since each step evaluates immediately, this version is "stack safe."

(b -> a -> b)  Binit   //=== `f a` ===\\
|               |      ||             ||
|               |      || A1  A2  A3  ||
|               |      \\=+===+===+===//
|               |         |   |   |
\               \         /   |   |
 \=========>     ---------    |   |
  |                 B2        |   |
  |                 |         |   |
  \                 \         /   |
   \==========>      ---------    |
    |                   B3        |
    |                   |         |
    \                   \         /
     \==========>        ---------
                             Boutput

foldr

This version creates a tree-like structure of computations that doesn't start evaluating until it gets to the bottom right of the tree. Once it reaches the bottom right, it evaluates function A3 Binput and then evaluates towards the top-left of the tree Since each step towards the bottom-right of the tree allocates a stack, this function is not always "stack safe".

      Boutput
    -----     <==========\
   /      \               \
   |      |               |
   |      |               |
   |      B3              |
   |    ------    <=======\
   |   /       \           \
   |   |       |           |
   |   |       B2          |
   |   |    -------   <====\
   |   |   /        \       \
   |   |   |        |       |
//=+===+===+===\\   |       |
|| A1  A2  A3  ||   |       |
||             ||   |       |
\\=== `f a` ===//   Binit   (a -> b -> b)

Examples

We'll implement instances for three types: Box a, Maybe a, and List a. Each implementation will become more complicated that the previous one.

Box's Instance

data Box a = Box a

-- Box's implementation doesn't show the difference between `foldl` and `foldr`.
-- Moreover, the initial `b` value isn't really necessary.
instance Foldable Box where
  foldl :: forall a b. (b -> a -> b) -> b -> Box a -> b
  foldl reduceToB initialB (Box a) = reduceToB initialB a

  foldr :: forall a b. (a -> b -> b) -> b -> Box a -> b
  foldr reduceToB initialB (Box a) = reduceToB a initialB

  foldMap :: forall a m. Monoid m => (a -> m) -> Box a -> m
  foldMap aToMonoid (Box a) = aToMonoid a

Maybe's instance

-- Maybe's implementation doesn't show the difference between `foldl` and `foldr`.
-- However, the initial `b` value is necessary
-- because of the possible `Nothing` case.
instance Foldable Maybe where
  foldl :: forall a b. (b -> a -> b) -> b -> Maybe a -> b
  foldl reduceToB initialB (Just a) = reduceToB initialB a
  foldl _         initialB Nothing  =           initialB

  foldr :: forall a b. (a -> b -> b) -> b -> Maybe a -> b
  foldr reduceToB initialB (Just a) = reduceToB a initialB
  foldr _         initialB Nothing  =             initialB

  -- While we could implement this the same way as `Box`, let's reuse
  -- `foldl` to implement it
  foldMap :: forall a m. Monoid m => (a -> m) -> Maybe a -> m
  foldMap aToMonoid maybe =
    foldl (\b a -> b <> (aToMonoid a)) mempty maybe

List's instance

-- Cons 1 (Cons 2 Nil)
-- 1 : (Cons 2 Nil)
-- 1 : 2 : Nil
-- same as [1, 2]
-- In the below implementations, `op` stands for `operation`
instance Foldable List where
  -- Same as...
  -- ((((intialB `op` firstElem) `op` secondElem) `op` ...) `op` lastElem)
  foldl :: forall a b. (b -> a -> b) -> b -> List a -> b
  foldl _         accumB   Nil           = accumB
  foldl op initialB (head : tail) =
    foldl op (op initialB head) tail

  -- Same as...
  -- (firstElem `op` (secondElem `op` (... `op` (lastElem `op` initialB))))
  foldr :: forall a b. (a -> b -> b) -> b -> List a -> b
  foldr op accumB   Nil           = accumB
  foldr op initialB (head : tail) =
    op head (foldl op initialB tail)

  -- Unlike Box, reusing `foldl`/`foldr` is actually the cleaner way
  -- to implement `foldMap` for `List`.
  foldMap :: forall a m. Monoid m => (a -> m) -> List a -> m
  foldMap aToMonoid list =
    foldl (\b a -> b <> (aToMonoid a)) mempty list

instance Functor List where
  map f list =
    -- Due to stack safety, this is not how this is implemented
    -- but it communicates the same idea
    foldr (\prevHead tail -> (f prevHead) : tail) Nil list

General Usage Patterns

We'll see more of this in the upcoming overview of the derived functions. However, foldl and its corresponding members tend to follow a few patterns:

reduceAllAsIntoOneAValue = foldl reduce initial foldableType
  where
    iniital = -- a type class value or hard-coded value
              -- like `mempty` or `true` or `Data.Ordering.LT`, etc.

    reduce = -- some type class function like `<>` or `&&` or `+`, etc.
             -- Note: sometimes this function will change the `a` to
             -- a different type before the function receives it as an argument

-- allows this type of computation: "a1 `operation` a2 `operation` a3"
thereIsNoInitialB_iterateThroughAllAValues =
  let record = foldl reduce initial foldableType
  in record.value

  where
    initial = { isFirstRun: true, value: initialValue }
    reduce b a =
      { isFirstRun: false, value:
          if b.isFirstRun then a else (realReduceFunction b.value a)
      }

buildHigherKindedData = foldl build initial foldableType
  where
    initial = Map.empty
    build mapSoFar nextValue =
      let
        key = show nextValue
        value = someComplicatedFunction nextValue
      in
        Map.add mapSoFar key value

forEachA_doSomeComputation = foldl compute initial foldableType
  where
    initial :: Effect Unit
    initial = pure unit

    compute :: a -> Effect Unit
    compute _ nextValue = do
      someValue <- computeUsing nextValue
      allIsGood <- doSomethingElse someValue
      pure unit

Laws

None

Derived Functions

We'll overview the derived functions by first grouping them into a few categories, and then providing a general definition for what each one does.

Default implementations for the members of the Foldable type class

foldMap can be implemented using either foldl or foldr. Likewise, both foldl and foldr can be implemented using foldMap.

Thus, once one has implemented one of these sets, they can use a default implementation to implement the other set:

  • if foldl and foldr both are implemented, you can implement foldMap by using one of the two function below:
  • if foldMap is implemented, you can use the functions below to implement foldl and foldr:

Use another type class to reduce multiple a values into one value.

  • via Semigroup's append/<> function:
    • fold == a1 <> a2 <> ... <> aLast <> mempty
    • intercalate == a1 <> separator <> a2 <> separator <> a3 ...
      • fold but with a separator value appended in-beteeen a values.
    • surround == value <> a1 <> value <> a2 <> value ...
      • The inverse of intercalate
    • surroundMap == value <> (aToMonoid a1) <> value <> (aToMonoid a2) <> value ...
      • Same as surround, but the a can be changed to b before being appended to value.
  • via HeytingAlgebra's conj/&amp;&amp; or disj/|| functions.
    • and == a1 && a2 && a3 && ...
    • or == a1 || a2 || a3 || ...
    • all == (aToB a1) && (aToB a2) && (aToB a3) && ...
      • Same as and, but the a can be changed to b before being &&'d.
    • any == (aToB a1) || (aToB a2) || (aToB a3) || ...
      • Same as or, but the a can be changed to b before being ||'d.
  • via Semirings plus/+ or multiply/* functions:
    • sum == a1 + a2 + a3 + ...
    • product == a1 * a2 * a3 * ...
  • via Alt's alt/<|> and Plus's empty functions (very similar to the Semigroup and Monoid relationship):
    • oneOf == [1, 2, 3] == [1] <|> [2] <|> [3] <|> ... == foldl <|> empty [[1], [2], [3], ...]
    • oneOfMap

Determine information about the Foldable type based on the a values it contains / get an a value

Note: the below functions are not as performant as they could be because they will iterate through all of the a values in the Foldable type, even if the desired information is found as soon as possible when testing the first a value. In other words, these functions do not "short circuit".

  • via Eq's eq/== and notEq//= functions:
    • elem == (a1 == test) || (a2 == test) || (a3 == test) || ...
    • notElem == (a1 /= test) && (a2 /= test) && (a3 /= test) && ...
  • Get the index of an a value within the Foldable type:
  • Get first element which satisfies some predicate:
  • via Ord's compare function and its derivations (e.g. <, >, etc.):
  • Calculate the length or emptiness of the type:

Execute a "for loop" that runs an applicative/monadic computation (e.g. Effect) using each a in the Foldable type

In the Philosophical Foundations folder, we used a recursive function to implement a "for loop." I mentioned there that one could implement the same thing using a type class called Foldable. It is these last three functions that show how to do that.

In JavaScript, we might write something like this:

var array = [1, 2, 3];
for (int i = 0; i < array.length; i++) {
  var elem = array[i];
  console.log(elem);
}

In PureScript, we would write the same thing via Foldable:

  • *for_ == for_ array log
  • *traverse_ == traverse_ log array
    • Same as for_ but the function comes first
  • *sequence_ == sequence_ [ log "1", log "2", log "3" ]
    • Same as for_ but the a values are applicative computations that have yet to be executed
  • Note: that each of these computations must output only Unit. Traversable, which is covered next, removes that limitation.

A related function is foldM, which allows one to run a monadic computation multiple times where the next computation depends on the output of the previous computation. As the docs indicate, this function is not generally stack-safe.

foldM

Here's an example:

main :: Effect Unit
main = do
  int <- randomInt 1 10
  output <- foldM recursiveComputation 1 [1, 2, 3]
  log $ "Output was: " <> show output
  where
    recursiveComputation initialOrAccumulatedValue nextValueInArray = do
      anotherInt <- randomInt 1 nextValueInArray
      pure (anotherInt + initialOrAccumulatedValue)

... which is the same as writing...

main :: Effect Unit
main = do
  int <- randomInt 1 10

  -- begin loop
    -- initialOrAccumulatedValue = 1; nextValueInArray = 1
  anotherInt1 <- randomInt 1 1
  accmulatedValue1 <- pure (anotherInt1 + 1)

    -- initialOrAccumulatedValue = 1; nextValueInArray = 2
  anotherInt2 <- randomInt 1 2
  accmulatedValue2 <- pure (anotherInt2 + accmulatedValue1)

    -- initialOrAccumulatedValue = 1; nextValueInArray = 1
  anotherInt3 <- randomInt 1 3
  output <- pure (anotherInt3 + accmulatedValue2)
  -- end loop

  log $ "Output was: " <> show output

Traversable

Usage

The answer is always traverse

While Foldable allowed us to use things like for_, traverse_, and sequence_, these three functions restricted computations to only outputting Unit. Traversable removes that restriction and stores each computation's output in the same Traversable type. Moreover, its derived functions enable a few other nice things.

Plain English names:

  • BoxSwap (sequence)
  • ForEach (traverse)

Sequence

Use Case 1: Swap the box types

I have Array (Maybe a). I need Maybe (Array a). The box-like types, Array and Maybe need to swap places.

sequence [Just 1, Just 2, Nothing] == Nothing -- because the array had at least 1 `Nothing`.
sequence [Just 1, Just 2, Just 8] == Just [1, 2, 8] -- because the array only had `Just`s.

Use Case 2: run all computations in a Traversable type and store their outputs in the same Traversable type

This box-swapping property is quite useful as the below example illustrates.

We'll start with sequence first. I could write:

main :: Effect Unit
main = do
  let produceInt = randomInt 1 10

  output1 <- produceInt
  output2 <- produceInt
  output3 <- produceInt
  output4 <- produceInt
  -- ...
  log $ "Generated Ints were: " <> show [output1, output2, output3, output4]

The above code works. However, if I want to add a fifth one, I need to add another outputN <- produceInt line and add the outputN to the array.

Instead, I could write

main :: Effect Unit
main = do
  outputArray <- sequence
    [ produceInt
    , produceInt
    , produceInt
    , produceInt
    -- ...
    ]
  log $ "Generated Ints were: " <> show outputArray

Traverse: convert each a value in the Traversable type into a computation, run all computations, and store their outputs in the same Traversable type

I could write:

main :: Effect Unit
main = do
  let produceInt = \maxBound -> randomInt 1 maxBound

  output1 <- produceInt 8
  output2 <- produceInt 20
  output3 <- produceInt 40
  output4 <- produceInt 90
  -- ...
  log $ "Generated Ints were: " <> show [output1, output2, output3, output4]

The same problems as before arise. Instead, I could write

main :: Effect Unit
main = do
  let produceInt = \maxBound -> randomInt 1 maxBound
  outputArray <- traverse produceInt [8, 20, 40, 90]
  log $ "Generated Ints were: " <> show outputArray

Definition

class (Functor t, Foldable t) <= Traversable t where
  traverse :: forall a b m. Applicative m => (a -> m b) -> t a -> m (t b)
  sequence :: forall a m. Applicative m => t (m a) -> m (t a)

See its docs: Traversable

Laws

None, but the members should be compatible in the following ways:

traverse f xs = sequence (f <$> xs)
sequence = traverse identity

Derived Functions

Default implementations for the members of the Traversable type class

traverse can be implemented using sequence and sequence can be implemented using traverse. Similar to Foldable, once one has implemented one of these when writing a Traversable instance for a data type, they can use a default implementation to implement the other:

  • if traverse is implemented, you can implement sequence by using sequenceDefault
  • if sequence is implemented, you can implement traverse by using traverseDefault

for is traverse with its arguments flipped

Using the same traverse example as above:

main :: Effect Unit
main = do
  outputArray <- for [8, 20, 40, 90] \maxBound -> randomInt 1 maxBound
  log $ "Generated Ints were: " <> show outputArray

Outputting each step's accumulated value at that time for a foldl/foldr computation

The downside of using foldl/foldr is that you only know the foldl/foldr computation's final output. You don't know how that output was reached / what each step's accumulated value was.

foldl (+) 0 [1, 2, 3, 4,  5 ] ==
            15 -- <= know the output, but don't know how we reached that conclusion
               --    What was the output of `accumulatedValueAtThatPoint + 2`?

In such cases, you use

foldl (+) 0 [1, 2, 3, 4,  5 ] ==
            15 -- <= know the output, but don't know how we reached that conclusion

scanl (+) 0 [1, 2, 3, 4,  5 ] ==
            [1, 3, 6, 10, 15] -- <= now we can see how that conclusion was reached
                              --    if traversing from the left

scanr (+) 0 [1,  2,  3,  4, 5] ==
            [15, 14, 12, 9, 5] -- <= now we can see how that conclusion was reached
                               --    if traversing from the right

In other words, the value at index n in the outtputted array is the output of passing the value at index n in the input array and the accumulated value at that point in time into the folding function (i.e. +). Using the scanr example, the input array's index 2 value (i.e. 3) and the accumulated value at that time (the output array's index 3 value, 9) were both passed into the folding function, +, to produce the output array's index 2 value (i.e. 12).

Outputting both the output and each step's accumulated value at that time for a foldl/foldr computation

The downside of using scanl/scanr is that we don't have access to both the final output of the fold and the path it took to get there.

foldl (+) 0 [1, 2, 3, 4,  5 ] ==
            15 -- <= know the output, but don't know the path of how we got there

scanl (+) 0 [1, 2, 3, 4,  5 ] ==
            [1, 3, 6, 10, 15] -- <= know the path, but not the output

In such cases, you can use

foldl (+) 0 [1, 2, 3, 4,  5 ] ==
            15 -- <= know the output, but don't know the path of how we got there

scanl (+) 0 [1, 2, 3, 4,  5 ] ==
            [1, 3, 6, 10, 15] -- <= know the path, but not the output

type Accum s a = { accum :: s, value :: a }

mapAccumL (\accumulationSoFar nextValue ->
    let outputAtThisStep = accumulationSoFar + nextValue
    in { accum: outputAtThisStep, value: outputAtThisStep}
  ) 0                [1, 2, 3, 4,  5 ] ==
  {accum: 15, value: [1, 3, 6, 10, 15]} -- <= know both output and path

You can see how mapAccumL/mapAccumR enables you to write even complex computations fairly easily. Still, these two functions are more expressive than just a combining the outputs of foldl and scanl in one computation, since they allow for more types to be used in the computation.

Below is a nonsensical example demonstrating this:

import Prelude
import Data.Traversable (mapAccumL)
import Data.Traversable.Accum (Accum)
import Data.Foldable (sum, length)
import Data.Array (snoc)

-- type Accum s a = { accum :: s, value :: a }

nonsensicalExample :: Accum (Array Int) (Array Int)
nonsensicalExample = mapAccumL reducer [] [1, 2, 3, 4, 5]
  where
  reducer :: Array Int -> Int -> Accum (Array Int) Int
  reducer accumulationSoFar nextValue =
    let
      arrayLength = length accumulationSoFar
      arraySum = sum accumulationSoFar -- foldl (+) 0 accumulationSoFar
    in { accum: accumulationSoFar `snoc` arrayLength `snoc` arraySum
       , value: show $ nextValue + arraySum
       }

produces { accum: [0,0,2,0,4,2,6,8,8,22], value: ["1", "2", "5", "12", "27"] }

Their Variations

Once you get how Foldable and Traversable works, the following variations should be pretty easy to grasp

Variants that include the a value's index

The same as their base type classes, but an additional Int argument represent the a's index in f is included in the map/fold/traverse functions.

Variants where the f cannot be empty

The same as their base type classes, but the f type must always have at least 1 a value. As a result, the Applicative requirement in the type class' functions can be downgraded to just Apply:

Variants where the f can have 2 types

The same as their base type classes, but as though it was f (Tuple a b) rather than f a:

PureScript Filterable

The following type classes come from purescript-filterable.

Compactable

Compactable

class Compactable f where
  compact :: forall a.    f (Maybe a)    ->           f a

  separate :: forall l r. f (Either l r) -> { left :: f l, right :: f r }

catMaybes is a function that removes all Nothings in an Array. compact and separate generalize this idea to work across more f types and works for both Maybe and Either:

catMaybes [Just 1, Nothing] == [1]

compact [Just 1, Nothing] == [1]
compact (Just 1 : Just 2 : Nothing : Nil) == 1 : 2 : Nil

separate [Left 1, Left 2, Right 3, Right 4] == { left: [1, 2], right: [3, 4] }

Filterable

Filterable generalizes the concept of Array.filter and Array.partition to work arcross more f types.

class (Compactable f, Functor f) <= Filterable f where
  filter :: forall a.
    (a -> Boolean)    -> f a ->         f a

  filterMap :: forall a b.
    (a -> Maybe b)    -> f a ->         f b

  partition :: forall a.
    (a -> Boolean)    -> f a -> { no :: f a, yes :: f a }

  partitionMap :: forall a l r.
    (a -> Either l r) -> f a -> { left :: f l, right :: f r }

Witherable

Witherable is the same as Traversable's traverse but either removes the resulting Nothings like compact or distinguishes the Lefts and the Rights like separate.

-- traverse :: forall a b m. Applicative m =>
--  (a -> m b           ) -> t a -> m          (t b)

class (Filterable t, Traversable t) <= Witherable t where
  wither :: forall m a b. Applicative m =>
    (a -> m (Maybe b)   ) -> t a -> m          (t b)

  wilt :: forall m a l r. Applicative m =>
    (a -> m (Either l r)) -> t a -> m { left :: t l, right :: t r }

Its derived functions, wilted and withered, use sequence instead of traverse.

-- sequence :: forall a m. Applicative m =>
--  t (m a           ) -> m          (t a)

withered :: forall t m x. Witherable t => Applicative m =>
    t (m (Maybe x)   ) -> m          (t x)

wilted :: forall t m l r. Witherable t => Applicative m =>
    t (m (Either l r)) -> m { left :: t l, right :: t r }

Unfoldable

Usage

Plain English names:

  • Generator
  • FP version of a generic "while" loop
  Given
    a function, `f`,
      that uses the next `b` value to generate
        either the case that ends the while loop, `Nothing`
        or the case that continues the while loop, `Just`
          which wraps a `Tuple` so that it can return
            the first or next value, `a`, that gets "put into" the `t` container
              and
            the next `b` value that is then used by `f`
              to run the next iteration in the loop
  and
    the first `b` value
return a container/collection, `t`, that stores all the `a` values
  that were generated by the `f` function,
    which may "contain" 0 or many `a` values.

It enables:

  • a way to generate a List of Intss where each Int in the List is one greater than the previous Int
  • a way to run the same Effect/Aff computation multiple times until a given condition is true

Definition

Code Definition

Don't look at its docs until after looking at the visual overview in the next section: Unfoldable

-- We'll ignore the `Unfoldable1` superclass for now..
class Unfoldable1 t <= Unfoldable t where
  unfoldr :: forall a b. (b -> Maybe (Tuple a b)) -> b -> t a

Visual Overview

unfoldable-visaulized

Examples

We'll implement an instance for List a.

List's Instance

data List a = Nil | Cons a

instance Unfoldable List where
  unfoldr :: forall a b. (b -> Maybe (Tuple a b)) -> b -> List a
  unfoldr f initialB = case f initialB of
    Nothing -> Nil
    Just (Tuple a nextB) -> Cons a (unfoldr f nextB)

Laws

None

Derived Functions

Overview of Possible Functions for f

The only part of unfoldr f initialB we can hard-code in a derived function is the f function. There are three different functions we could use for f:

-- We'll use this type alias in the next couple of sections
type UnfoldrFunction a b = b -> Maybe Tuple a b

-- case 1
alwaysReturnJust :: forall a b. a -> b -> UnfoldrFunction a b
alwaysReturnJust a b = \initialB -> Just (Tuple a b)

-- case 2
alwaysReturnNothing :: forall a b. UnfoldrFunction a b
alwaysReturnNothing _ = Nothing

-- case 3
itDepends :: forall a b. UnfoldrFunction a b
itDepends = -- possibilities are shown later
Case NameDerived Function NameResult
case 1!!! 😱😱😱 !!!Runtime Error: Infinite loop!
case 2noneProduces an empty t
case 3replicate, replicateA, and fromMaybesee below overviews of each function

Case 1 functions always produce a runtime error due to creating an infinite loop. They will appear in two ways:

-- obvious because Just is always returned
example1 = unfoldr (const Just (Tuple 0 0)) 0

-- Not as obvious in situations where
-- there are a lot of possible conditions
example2 =
  unfoldr f 0
  where
  f nextB
    | nextB < 0  = Just (Tuple 1 2)
    | nextB > 0  = Just (Tuple 2 -4)
    | nextB == 0 = Just (Tuple 8 4)
    | otherwise  = Nothing
      -- ^ this case never occurs

Case 3 Functions

Using a countdown or countup function to do something n-many times

countUp :: forall a. Int -> UnfoldrFunction a Int
countUp limit = \nextInt ->
  if nextInt >= limit then Nothing
  else Just (Tuple aValue (nextInt + 1))

countDown :: forall a. UnfoldrFunction a Int
countDown = \nextInt ->
  if nextInt <= 0 then Nothing
  else Just (Tuple aValue (nextInt - 1))

These kinds of functions are used in the following derived functions:

  • replicate - Add the same a value to a t container x number of times.
  • replicateA - Run the same applicative-based computation x number of times and store their results in the t container.

Converting a Maybe into another type that has an Unfoldable instance

Unfoldable1

This is the same as Unfoldable except the returned t value must always have at least 1 a value. As a result, it's actually harder to find a data type that can only implement Unfoldable1 but can't implement Unfoldable.

Definition

Code Definition

class Unfoldable1 t where
  unfoldr1 :: forall a b. (b -> Tuple a (Maybe b)) -> b -> t a

The only difference between Unfoldable and Unfoldable1 is the type signature for f. In both cases, the b value is inside of a Maybe:

-- Unfoldable
f :: forall a b. b -> Maybe (Tuple a b)

-- Unfoldable1
f :: forall a b. b -> Tuple a (Maybe b)

Laws

None

Derived Functions

Parallels to Unfoldable

ConceptUnfoldable
(t can be empty)
Unfoldable1
(t can't be empty)
Produce a t valuenonesingleton
Add an a n-many times to a t containerreplicatereplicate1
Run an applicative-based computation n-many times and store the results in a t containerreplicateAreplicate1A
Convert Maybe a to t afromMaybepossible, but not implemented

Unfoldable1 does not have a version of fromMaybe included, but I believe it is possible if one places a Monoid constraint on a and uses mempty when receiving a Nothing case.

Specific to Unfoldable1

Closing Thoughts

Stack-Safe Recursive Functions

At this point, you should read through the Design Patterns/Stack Safety.md file. That file covers the two other kinds of loops: recursive loops and Effect-based loops like whileE, forE, and untilE.

When to Use It? Array vs List

Hello. When should I use Array and when List?

It depends on which operations you want to do. Any update on an array is Ө(n) but random lookup is O(1). Lists have efficient operations at the front, and everything else is O(n). Array updates copy all other elements, resulting in twice the memory usage, while for most list operations the result and the source share part of the list. However, lists use a less compact layout in memory.

Rules of thumb: if you create the sequence once and read it many times, use an array. Folds over arrays are faster, length is constant time and lookup is constant time. If you want to constantly manipulate the structure, especially at the front, use a list.

If your number of elements is very small (i.e. n is bounded), try both and measure. Array might in many cases be faster for few elements, even with random updates. Mostly because cloning of arrays is implemented directly in V8, whereas rebuilding lists is done with many many PureScript operations.

See Typeclass abstractions to use for data structures. For algebraic graphs, consider using purescript-alga.

Application Structure

Prerequisites:

  • You should understand what "smart constructors" are (see Design Patterns folder) and how they work

The upcoming folders will explain

  • a small explanation on the onion architecture / 3-Layer Haskell Cake concepts
  • an overview of what MTL and Free/Run are and how they work conceptually
  • how to structure an FP program via MTL and Free/Run
  • folders starting with 1 contain heavily-commented examples of basic programs using these application architecture styles. When learning one particular style, you should look at these programs for what an 'end-result' looks like using that style as well as how it compares to other styles. We'll start with the simplest "hello world" program that uses only one effect and write programs that use more and more effects/capabilities.

These will later be used to write programs in the Projects folder that run in Node via the console and/or in the Browser via Halogen.

In the functional paradigm, programs are structured in such a way that they look very similar to something called the "onion architecture." The below videos are optional watching. Watch them for a clearer idea of what "onion architecture" is:

Another optional video to watch: Functional Architecture - The Pits of Success. It explains that FP naturally pushes developers towards this architecture whereas other languages push developers away from it.

When we structure our code according to the below table, it provides a number of benefits

  • top-down domain-driven design: your data types and your function's type signatures are often your always-up-to-date documentation
  • "impure" computations (i.e. computations that do things like state manipulation, reading from a file, network activities) are expressed as a "pure" computation, making them much easier to test
  • "Platforms" (i.e. frameworks, databases, etc.) can easily be swapped out with other newer platforms without changing any "business logic" code or potentially introducing regressions
Layer LevelOnion Architecture TermGeneral idea
Layer 4CoreStrong types with well-defined properties and their pure, total functions that operate on them
Layer 3Domainthe "business logic" code which uses "effects," impure computations that are expressed in a pure way
Layer 2APIthe "production" or "test" monad which "links" these pure effects/capabilties to their impure implementations
Layer 1Infrastructurethe platform-specific framework/libraries we'll use to implement some special effects/capabilities (i.e. Node.ReadLine (terminal-based programs), Halogen/React (web-based UIs))
Layer 0Machine Code
(no equivalent onion term)
the "base" monad that runs the program (e.g. production: Effect/Aff; test: Identity/Trampoline)

To get a general idea for the concept this folder is going to try to teach:

Another learning resource that is still a work-in-progress but which will explain more than this work is 'Functional Design and Architecture':

A Word of Thanks

While trying to learn this myself, I benefited from looking at the code in stepchownfun's BSD-3 licensed project, effects, as a guide when I did not completely understand something myself.

00-A-Bad-Program.purs

module BadProgram where

import Prelude

import Effect (Effect)
import Effect.Console (log)
import Effect.Random (randomInt)

main :: Effect Unit
main = do
  log "This is an example of a program that is written in an FP language, \
      \but which is terribly structured. Why? Because it's impossible to test \
      \or otherwise prove that the code works correctly. All of its pure \
      \business logic is intermixed with impure code that makes the \
      \program work."

  initialState <- randomInt 10 20
  int2 <- randomInt 5 20
  int3 <- randomInt 5 30

  let nextState = initialState + int2 * int3 / initialState

  int4 <- randomInt 200 900
  int5 <- randomInt 45 80
  int6 <- randomInt 2 3

  let finalState = nextState * int6 * int5 - int4 + initialState

  log $ "The final output of the program was: " <> show finalState

{-
There's a few issues with this program as it is currently written:

First, it's hard to test. How would you test this program to ensure
    it works properly?

Maybe we want to guarantee that the code will always produce
    a value that is even. Since all of our functions (i.e. the stuff
    we do before storing the next state) receive random integers,
    how could we ensure these functions are defined correctly?

Second, this program does not tell us whether an error can occur
    and, if so, where such an error would appear.

Third, what if we wanted to use a different random number generator
    than the one provided via `Effect.Random (randomInt)`? Let's say
    the first time we run the program, we do want to use 'randomInt.'
    If so, then this works. However, let's say the next time we run
    this program, we want it to use something more complex. Well,
    we don't have an easy way to quickly swap out a random number generator
    without changing our business logic.

Such problems as these will be fixed/improved if we structure our FP
programs using Modern FP Architecture that is covered in this folder.
-}

Monads, Effects, and Capabilities

Monads represent sequential computation via bind/>>=: "do X, and once finished, do Y". In our previous example/explanation from Hello World/Prelude/Control-Flow/How the Computer Executes FP Programs.md, we implied that Box could be used to "compute" something. In that example, however, it merely acted as a wrapper around values and functions. When we covered Effect and Aff in their respective folder, we saw that conceptually they operated very similar to Box.

Still, our Box/Effect/Aff examples did not make it clear how Monads could be a "computation." While bind/>>= insures that one computation occurs before another (i.e. sequential computation), it does not define what kinds of computation are done. Thus, we must explain what "effects"/"capabilities" are.

For the rest of this folder, we'll use the terms, "effects" and "capabilties," interchangeably. Lowercased "effect" refers to something different than the Effect monad.

Effects / Capabilities

To understand what we mean by "effects" (and due to license-related things), read through the first two sentences of Monad transformers, free monads, mtl, laws and a new approach, then continue reading this page.

Examples of Effects / Capabilities

Capabilities can be grouped together into type class functions that define computations which bind executes in a sequential manner. These type classes are specialized; they are designed to do one thing very very well.

So what kind of "capabilities" can we have? Let's now give some examples via the table below:

When we want a type of computation (effect) that...... we expect to use functions named something like ...... which are best abstracted together in a type class called...
Provides for later usage a read-only value/function that may change between different program runs
(e.g. "settings" values; dependency injection)
  • getSettingValue
  • getConfigValueStoredOnFile
  • getNumberOfPlayersInGame
MonadAsk
Modifies the state of a data structure
(e.g. changing the nth value in a list)
  • pop stack
  • replaceAt index treeOfStrings "some value"
  • (\int -> int + 1)
MonadState
Returns a computation's output and additional data that is generated during the computationMonadTell
Stops computation because of a possible error
(e.g. "file does not exist")
--MonadThrow
Deals with "callback hell"
(e.g. de-invert inversion of control)
--MonadCont
When we want to extend the functionality of...... with the ability to...... we can use its extension type class called...
MonadAskModify the read-only value for one computation
(e.g. makeFontSizeMoreAccessible getFontSize displayPage)
MonadReader
MonadTellModify or use the additional non-output data before completing a computationMonadWriter
MonadThrowCatch and handle the error that was thrown
(e.g. create the missing file)
MonadError

Modeling Effects

In this folder, we'll only cover MTL/ReaderT Design Pattern and Free/Run. The article to which we referred above overviews more ideas, but that is not our current focus. It might be worth returning to it after one has read through the rest of this folder.

Composing Monads

As we explained previously, bind/>>='s type signature forces one to only return the same Box-like monad type that is used in bind:

bind :: forall a. f   a -> ( a    -> f   b)           -> f    b
bind :: forall a. Box a -> ( a    -> Box b         )  -> Box  b
bind             (Box 4)   (\four -> Box (show four)) == Box "4"

In other words, if we use one monad, we cannot use any other monads. So, how do we get around this limitation?

We saw the same problem earlier when we wanted to run an Effect monad inside of an Aff monad. We fixed it by "lifting" the Effect monad into the Aff monad via a NaturalTransformation/~>. This was abstracted into a type class specific for Effect ~> someOtherMonad in MonadEffect.

MTL and Free use different approaches to solving this problem and its solution is what creates the Onion Architecture-like idea we mentioned before. As we saw earlier in Nate's video in this folder's ReadMe, mtl is the "Final Encoding" style and Free/Run is the "Initial Encoding" style.

The following ideas are quick overviews of each approach. Their terminology and exact details will be explained in their upcoming folder. It is not meant to be clearly understandable at first.

MTL Approach

Monad Transformers are thus named because they "transform" some other monad by augmenting it with additional functions. One monad (e.g. Box) can only use bind and pure to do sequential computation. However, we can "transform" Box, so that it now has state-manipulating functions like get/set/modify. "MTL" refers to the original "Monad Transformers Library" (I believe).

In the MTL-approach, one models the above effects using functions (we'll show how later). Since monad transformers augment some "base" monad, it creates a stack-like picture (read from bottom to top):

BaseMonad
      ^
      | augments
      |
Pure MonadState

Since each function is a different monadic type, they cannot be used within the same monadic context in do notation. Thus, one gets around this monad-composition problem by creating a "stack" of nested monad transformers (i.e. functions). Following the previous idea, we can define something similar to a NaturalTransformation that "lifts" the effects of one monadic function (e.g. a function that implements MonadReader) into another monadic function (e.g. a function that implements MonadState), which augments the base monad. Using a visual, it produces this diagram (read from bottom to top):

BaseMonad
      ^
      | augments
      |
Pure MonadState_TargetMonad
      ^
      | gets lifted into
      |
Pure MonadReader_SourceMonad

Generalizing this idea, we must ultimately create a "stack" of these function-based monad-transformers. Using a visual, it produces this diagram (read from bottom to top):

BaseMonad
      ^
      | augments
      |
Pure MonadState
      ^
      | gets lifted into
      |
Pure MonadWriter
      ^
      | gets lifted into
      |
Pure MonadReader

This idea is at the heart of the type class called MonadTrans. Again, you should feel somewhat confused right now and a bit overwhelmed. However, we'll refer to these ideas later to help explain why we make some of the design choices that we do. By the end of this folder, this will all make sense.

Free

In the Free-approach, one models the above effects using data structures (again, we'll show how later). Essentially, one uses domain-specific languages (DSLs) created via data structures to define an Abstract Syntax Tree (AST). Such trees describe computations but do not run them. Later on, an AST is "interpreted" (via a NaturalTransformation/~>) into a final base monad that actually runs the computation. Using a visual, it produces this diagram (read top to bottom):

Pure High-Level Language
      |
      | gets interpreted into
      |
     \ /
Base Monad

Due to how interpreters work, one can define high-level ASTs that are interpreted into lower-level ASTs before being run by a base monad. Using a visual, it produces this diagram (read from bottom to top):

Pure AST via High-Level Language
      |
      | gets interpreted into
      |
     \ /
Pure AST via Medium-Level Language
      |
      | gets interpreted into
      |
     \ /
Pure AST via Low-Level Language
      |
      | gets interpreted into
      |
     \ /
Base Monad

MTL

This folder does 5 things:

  • walks the reader through the Function monad and how to read its do notation and how functions can become monad transformers. At the end, we'll describe the whole point of using monad transformers.
  • walks the reader through the problems that led us to define the MonadState type class and explains why its function's type signature is defined that way. We'll use this idea to teach the general idea behind all the other monad transformers
  • overviews each monad transformer
  • explains what problem MonadTrans solves and how the 'monad transformer stack' works
  • explains the limitations of monad transformers
  • overviews the ReaderT design pattern. At the end, we'll clarify when to use "monad stacks" and when to use the ReaderT design pattern.

Explaining the Name

Monad Transformers are thus named because they "transform" some other monad by augmenting it with additional capabilities. One monad (e.g. Box) can only use bind and pure to do sequential computation. However, we can "transform" Box, so that it now has state-manipulating functions like get/set/modify. Purescript has defined all of these in the library called purescript-transformers.

Foundations

Many people find it very difficult to understand how Monad Transformers actually work. I believe it's because new learners are exposed to too many new things at once, so that they get overwhelemed and likely don't understand why they don't understand.

Rather than jumping into an explanation of what monad transformers are, I'm going to build the necessary scaffolding to make understanding them easier. At the very end, I'll begin to show what problem they solve.

In short, monad transformers make it easier to adhere to the Onion Architecture, so that one can easily test and run their business logic by swapping out the "infrastructure" used. For example, my random number game can use the Terminal environment to allow the player to input their guesses when I am actually running the program. If I want to test my program's business logic, I can "simulate" the user's guesses in a test environment.

In both cases, the same business logic is used, and adjusting it will affect both the real-world use of it and the tests. In other words, there is no 'syncing' problem here between the real business logic that runs my code and the business logic I test.

Folder's Contents

In this folder, we'll show that a Function can be a Monad and then show how to convert it into a Monad Transformer.

At the very end, we'll summarize why monad transformers are useful and give an overview of how they work.

The Function Monad

A Refresher on Monads

Via the Box type, we originally learned what a Monad even is. To refresh our memory, a data type can be called a Monad if it can implement a law-abiding instance for the Monad type class. We then used the Box type to introduce "do notation," which desugars into nested bind/>>= calls.

We later showed that using other monadic types like Maybe, Either, and List led to different control flows. Maybe led to a nested if-then-else statement. Either was similar but returned something when an error occurred. List produced a nested for loop.

However, we never stopped to consider the data type, Function. When we re-examine the Function data type, we'll see that it's naturally a Monad.

Reviewing Function as a Data Type

Putting it into syntax, Function is defined like this:

data Function a b = -- implementation

infix ? Function as ->

-- Thus, when we write this:
intToString :: Int -> String
intToString _ = "a string"

-- It desguars to this:
intToString :: Function Int String
intToString _ = "a string"

Implementing the Monad Type Class Hierarchy's Functions

Let's start implement instances for these type classes. For now, take my word for it that these implementations satisfy the laws of their respective type classes.

Functor

Initial Problems

Let's look at Functor. It's type signature looks like this.

class Functor f where
  map :: forall a b. (a -> b) -> f a -> f b

This creates the first problem: Functor expects a higher-kinded type, f, that only takes one type. For example, Box a only takes one type. However, Function a b takes two types. So, how can this be resolved? We must assume that Function a b already has its first type. For example...

data Function a b = -- implementation

noTypesDefined :: forall a b. Function a   b
noTypesDefined = -- implementation

oneTypeDefined :: forall   b. Function Int b
oneTypeDefined = -- implementation

allTypesDefined ::            Function Int Int
allTypesDefined = -- implementation

To make Function higher-kinded by only one type, and not two, we should use something like oneTypeDefined above:

class Functor (Function inputType) where

Implementing map

Getting back to the problem at hand, here's the type signature for Function's map implementation with very helpful names:

class Functor (Function inputType) where
  map :: forall originalOutputType newOutputType.
         (originalOutputType -> newOutputType) ->
         Function inputType originalOutputType -> Function inputType newOutputType

It should seem pretty obvious how this gets implemented. Let's walk through this slowly.

  1. map returns a new function whose input is input. So, let's use an inline function to do that:
class Functor (Function inputType) where
  map :: forall originalOutputType newOutputType.
         (originalOutputType -> newOutputType) ->
         Function inputType originalOutputType -> Function inputType newOutputType
  map originalToNew f = (\input -> {- remaining body of function -} )
  1. Since f is the only function that can "receive" a value of type, input, we have to pass that value into f. f will produce originalOutput, so let's store that in a let binding:
class Functor (Function inputType) where
  map :: forall originalOutputType newOutputType.
         (originalOutputType -> newOutputType) ->
         Function inputType originalOutputType -> Function inputType newOutputType
  map originalToNew f = (\input ->
    let originalOutput = f input
    in {- remaining body of function -} )
  1. Since originalToNew is the only function that can "receive" a value of type, originalOutput, we have to pass the value outputted by f into that function. originalToNew produces a value of the type, newOutput, which gives us the return value of our created function:
class Functor (Function inputType) where
  map :: forall originalOutputType newOutputType.
         (originalOutputType -> newOutputType) ->
         Function inputType originalOutputType -> Function inputType newOutputType
  map originalToNew f = (\input ->
    let originalOutput = f input
    in originalToNew originalOutput)

As we can see, the types guided us on how to implement this function. If we look at this closer, we can see that it's just function composition.

class Functor (Function inputType) where
  map :: forall originalOutputType newOutputType.
         (originalOutputType -> newOutputType) ->
         Function inputType originalOutputType -> Function inputType newOutput
  map originalToNew f = (\input -> originalToNew $ f input)
  -- or
  map originalToNew f = (originalToNew <<< f)
  -- or even
  map = (<<<)

In real code, normally we use a, b as type variable. Therefore, the above snippet will be simplfied to

class Functor (Function i) where
  map :: forall a b. (a -> b) -> Function i a -> Function i b
  map = (<<<)

Takeaways

Our first example taught us two things:

  • we have to make Function higher-kinded by one less type by specifying its first type (the input) and let the a and b arguments refer to its second type (the output).
  • to implmement the instance, we have to create a new function by using lambda syntax: \argument -> body

Apply

Initial Problems

Let's now look at Apply's apply function. It's type signature looks like this.

class (Functor f) <= Apply f where
  apply :: forall a b. f (a -> b) -> f a -> f b

Again, let's take this slowly. Notice first the first argument, what should the full type signature of f (a -> b) be if f is Function? Since the f has to be the same for both situations, then f has to be Function input. In other words, the first argument is a function that returns another function:

class (Functor (Function inputType)) <= Apply (Function inputType) where
  apply :: forall originalOutputType newOutputType.
           Function inputType (originalOutputType -> newOutputType) ->
           Function inputType  originalOutputType ->
           Function inputType  newOutputType

Implementing apply

Let's see how to implement this function.

  1. Since apply returns a new function, let's start creating one using lambda syntax:
class (Functor (Function inputType)) <= Apply (Function inputType) where
  apply :: forall originalOutputType newOutputType.
           Function inputType (originalOutputType -> newOutputType) ->
           Function inputType  originalOutputType ->
           Function inputType  newOutputType
  apply functionInFunction f = (\input -> {- body of function -})
  1. At this point, both f and functionInFunction can receive an value of type, input. For right now, let's do what we did last time and only pass it into f. We'll store the output in a let binding:
class (Functor (Function inputType)) <= Apply (Function inputType) where
  apply :: forall originalOutputType newOutputType.
           Function inputType (originalOutputType -> newOutputType) ->
           Function inputType  originalOutputType ->
           Function inputType  newOutputType
  apply functionInFunction f = (\input ->
    let originalOutput = f input
    in {- body of function -})
  1. At this point, the only way to get map originalOutput into newOutput is to pass it into the function that's hidden in functionInFunction. How do we get that out? We can pass input into that function. Again, we'll store that output in a let binding:
class (Functor (Function inputType)) <= Apply (Function inputType) where
  apply :: forall originalOutputType newOutputType.
           Function inputType (originalOutputType -> newOutputType) ->
           Function inputType  originalOutputType ->
           Function inputType  newOutputType
  apply functionInFunction f = (\input ->
    let
      originalOutput = f input
      originalToNew = functionInFunction input
    in {- body of function -})
  1. We now have all the pieces we need to return newOutput. Let's pass originalOutput into originalTonew:
class (Functor (Function inputType)) <= Apply (Function inputType) where
  apply :: forall originalOutputType newOutputType.
           Function inputType (originalOutputType -> newOutputType) ->
           Function inputType  originalOutputType ->
           Function inputType  newOutputType
  apply functionInFunction f = (\input ->
    let
      originalOutput = f input
      originalToNew = functionInFunction input
    in originalToNew originalOutput)

Great! Can we clean it up now?

class (Functor (Function inputType)) <= Apply (Function inputType) where
  apply :: forall originalOutputType newOutputType.
           Function inputType (originalOutputType -> newOutputType) ->
           Function inputType  originalOutputType ->
           Function inputType  newOutputType
  apply functionInFunction f = (\input -> (functionInFunction input) (f input))

Takeaways

Our second example taught us the following:

  • to get all the pieces necessary to implement a type class' function, we sometimes need to pass the input value into multiple functions.

Applicative

Let's now look at Applicative's pure function. It's type signature looks like this.

class (Apply f) <= Applicative f where
  pure :: forall a. a -> f a

Converting f into Function input, we get this type signature:

class (Apply (Function inputType)) <= Applicative (Function inputType) where
  pure :: forall outputType. outputType -> Function inputType outputType

Let's see how to implement it.

  1. Since pure returns a new function, let's start creating one using lambda syntax:
class (Apply (Function inputType)) <= Applicative (Function inputType) where
  pure :: forall outputType. outputType -> Function inputType outputType
  pure value = (\input -> {- body of function -})
  1. Since the function must return value as its output, let's ignore the argument and just return that value.
class (Apply (Function inputType)) <= Applicative (Function inputType) where
  pure :: forall outputType. outputType -> outputType
  pure value = (\input -> value)

Let's clean this one up:

class (Apply (Function inputType)) <= Applicative (Function inputType) where
  pure :: forall outputType. outputType -> Function inputType outputType
  pure value = (\_ -> value)

Bind

Implementing bind

Let's now look at Bind's bind function. It's type signature looks like this.

class (Functor m) <= Bind m where
  bind :: forall a b. (a -> m b) -> m a -> m b

Converting m into Function input, we get this type signature:

class (Apply (Function inputType)) <= Bind (Function inputType) where
  bind :: forall originalOutputType newOutputType.
          (originalOutputType -> Function inputType newOutputType) ->
          Function inputType originalOutputType ->
          Function inputType newOutputType

Let's see how to implement it.

  1. Since bind returns a new function, let's start creating one using lambda syntax:
class (Apply (Function inputType)) <= Bind (Function inputType) where
  bind :: forall originalOutputType newOutputType.
          (originalOutput -> Function inputType newOutputType) ->
          Function inputType originalOutputType ->
          Function inputType newOutputType
  bind originalToFunction f = (\input -> {- body of function -})
  1. Since f is the only function that can "receive" the input value, let's pass it into f and store the output:
class (Apply (Function inputType)) <= Bind (Function inputType) where
  bind :: forall originalOutputType newOutputType.
          (originalOutputType -> Function inputType newOutputType) ->
          Function inputType originalOutputType ->
          Function inputType newOutputType
  bind originalToFunction f = (\input ->
    let originalOutput = f input
    in {- body of function -})
  1. Since originalToFunction can "receive" the originalOutput value, let's pass that into originalToFunction and store its result in a let binding:
class (Apply (Function inputType)) <= Bind (Function inputType) where
  bind :: forall originalOutputType newOutputType.
          (originalOutputType -> Function inputType newOutputType) ->
          Function inputType originalOutputType ->
          Function inputType newOutputType
  bind originalToFunction f = (\input ->
    let
      originalOutput = f input
      inputToNewOutput = originalToFunction originalOutput
    in {- body of function -})
  1. Since inputToNewOutput is the only function that can produce the newOutput value, let's pass input into it to get that value:
class (Apply (Function inputType)) <= Bind (Function inputType) where
  bind :: forall originalOutputType newOutputType.
          (originalOutputType -> Function inputType newOutputType) ->
          Function inputType originalOutputType ->
          Function inputType newOutputType
  bind originalToFunction f = (\input ->
    let
      originalOutput = f input
      inputToNewOutput = originalToFunction originalOutput
    in inputToNewOutput input)

Let's now clean it up. First we'll get rid of that inputToNewOutput binding:

class (Apply (Function inputType)) <= Bind (Function inputType) where
  bind :: forall originalOutputType newOutputType.
          (originalOutputType -> Function inputType newOutputType) ->
          Function inputType originalOutputType ->
          Function inputType newOutputType
  bind originalToFunction f = (\input ->
    let
      originalOutput = f input
    in (originalToFunction originalOutput) input)

Second, we'll get rid of that originalOutput binding:

class (Apply (Function inputType)) <= Bind (Function inputType) where
  bind :: forall originalOutputType newOutputType.
          (originalOutputType -> Function inputType newOutputType) ->
          Function inputType originalOutputType ->
          Function inputType newOutputType
  bind originalToFunction f = (\input -> (originalToFunction (f input)) input)

As can be seen, this example was a slightly more complicated version of apply in that we needed to pass input to multiple functions.

Summary of Our Takeaways

  • map example:
    • we have to make Function higher-kinded by one less type by specifying its first type (the input) and let the a and b arguments refer to its second type (the output).
    • to implmement the instance, we have to create a new function by using lambda syntax: \argument -> body
  • apply/bind example:
    • to get all the pieces necessary to return the b/newOutput value, we sometimes need to pass the input value into multiple functions.

Resugaring Function

In our code above, we desugared (a -> b) into Function a b. What would happen if we resugared our type class instances above back into ->? How would we write it then?

class Functor ((->) inputType) where
  map :: forall originalOutputType newOutputType.
         (originalOutputType -> newOutputType) ->
         (inputType -> originalOutputType) -> (inputType -> newOutputType)
  map originalToNew f = (\input ->
    let originalOutput = f argument
    in originalToNew originalOutput)

class (Functor ((->) inputType)) <= Apply ((->) inputType) where
  apply :: forall originalOutputType newOutputType.
           (inputType -> (originalOutputType -> newOutputType)) ->
           (inputType -> originalOutputType) ->
           (inputType -> newOutputType)
  apply functionInFunction f = (\input ->
    let
      originalOutput = f input
      originalToNew = functionInFunction input
    in originalToNew originalOutput)

class (Apply ((->) inputType)) <= Applicative ((->) inputType) where
  pure :: forall outputType. outputType -> (inputType -> outputType)
  pure value = (\_ -> value)

class (Apply ((->) inputType)) <= Bind ((->) inputType) where
  bind :: forall originalOutputType newOutputType.
          (originalOutputType -> (inputType -> newOutputType)) ->
          (inputType -> originalOutputType) ->
          (inputType -> newOutputType)
  bind originalToFunction f = (\input ->
    let
      originalOutput = f input
      inputToNewOutput = originalToFunction originalOutput
    in inputToNewOutput input)

Monadic Function Examples

This file will help you learn how to read a monadic function's "do notation." We'll take some very simple examples and do a graph reduction on them to show how a series of bind/>>= calls are evaluated into a final value.

Function Implementations

To help us evaluate these examples manually, we'll include our verbose "not cleaned up" solutions from the previous file here (except for the Applicative one):

class Functor (Function inputType) where
  map :: forall originalOutputType newOutputType.
         (originalOutputType -> newOutputType) ->
         Function inputType originalOutputType ->
         Function inputType newOutputType
  map originalToNew f = (\input ->
    let originalOutput = f input
    in originalToNew originalOutput)

class (Functor (Function inputType)) <= Apply (Function inputType) where
  apply :: forall originalOutputType newOutputType.
           Function inputType (originalOutputType -> newOutputType) ->
           Function inputType  originalOutputType ->
           Function inputType  newOutputType
  apply functionInFunction f = (\input ->
    let
      originalOutput = f input
      originalToNew = functionInFunction input
    in originalToNew originalOutput)

-- Since pure ignores its argument, I'll use the cleaned up version
-- here because it's easier to understand
class (Apply (Function inputType)) <= Applicative (Function inputType) where
  pure :: forall outputType. outputType -> Function inputType outputType
  pure value = (\_ -> value)

class (Apply (Function inputType)) <= Bind (Function inputType) where
  bind :: forall originalOutputType newOutputType.
          (originalOutputType -> Function inputType newOutputType) ->
          Function inputType originalOutputType ->
          Function inputType newOutputType
  bind originalToFunction f = (\input ->
    let
      originalOutput = f input
      inputToNewOutput = originalToFunction originalOutput
    in inputToNewOutput input)

Example 1: pure

Let's say I have the following code using "do notation"

someComputation = do
  pure 1

Let's break it down:

pure 1

-- replace `pure` with implementation
(\_ -> 1)

This reveals the first issue with learning how to read "do notation" for monadic functions: the entire thing is one massive function. someComputation is not a value; it's a function that expects an input.

To actually use it, we'd need to write something like this:

produceAValue = someComputation "example input"
  where
  someComputation = do
    pure 1

Example 2: single bind

Let's say I have the following code using "do notation"

produceValue = someComputation 4
  where
  someComputation = do
    value <- \four -> 1 + four
    pure (value + 5)

Let's break it down:

produceValue = someComputation 4
  where
  someComputation = do
    value <- \four -> 1 + four
    pure (value + 5)

-- hide the "produceValue" part and focus only on the 'someComputation' part
do
  value <- \four -> 1 + four
  pure (value + 5)

-- desugar do notation into nested >>= calls
(\four -> 1 + four) >>= (\value ->
  pure (value + 5)
)

-- desguar >>= into bind
bind (\four -> 1 + four) (\value ->
  pure (value + 5)
)

-- replace `bind` with definition
(\input ->
  let
    originalOutput = (\four -> 1 + four) input
    originalToFunction = (\value -> pure (value + 5))
    inputToNewOutput = originalToFunction originalOutput
  in inputToNewOutput input
)

-- replace `pure` with definition
(\input ->
  let
    originalOutput = (\four -> 1 + four) input
    originalToFunction = (\value -> (\_ -> value + 5))
    inputToNewOutput = originalToFunction originalOutput
  in inputToNewOutput input
)

-- uncurry the curried function due to `pure` definition replacement
(\input ->
  let
    originalOutput = (\four -> 1 + four) input
    originalToFunction = (\value _ -> value + 5)
    inputToNewOutput = originalToFunction originalOutput
  in inputToNewOutput input
)

-- apply argument to `originalOutput` (`four` becomes `input`)
(\input ->
  let
    originalOutput = (\input -> 1 + input)
    originalToFunction = (\value _ -> value + 5)
    inputToNewOutput = originalToFunction originalOutput
  in inputToNewOutput input
)

-- evaluate `originalOutput`
(\input ->
  let
    originalOutput = 1 + input
    originalToFunction = (\value _ -> value + 5)
    inputToNewOutput = originalToFunction originalOutput
  in inputToNewOutput input
)

-- replace `originalOutput` with its implementation
(\input ->
  let
    originalToFunction = (\value _ -> value + 5)
    inputToNewOutput = originalToFunction (1 + input)
  in inputToNewOutput input
)

-- inline `originalToFunction`'s definition
(\input ->
  let
    inputToNewOutput = (\value _ -> value + 5) (1 + input)
  in inputToNewOutput input
)

-- apply the first argument to the function
(\input ->
  let
    inputToNewOutput = (\(1 + input) _ -> (1 + input) + 5)
  in inputToNewOutput input
)

-- Remove the applied argument
(\input ->
  let
    inputToNewOutput = (\            _ -> (1 + input) + 5)
  in inputToNewOutput input
)

-- inline `inputToNewOutput`
(\input ->
  (\_ -> (1 + input) + 5) input
)

-- apply the `input` argument, which gets ignored
(\input ->
         (1 + input) + 5)
)

-- finish cleaning up the code
(\input -> (1 + input) + 5))
(\input ->  1 + input  + 5)

-- re-reveal the "produceValue" part
produceValue = someComputation 4
  where
  someComputation = (\input -> 1 + input + 5)

-- inline `someComputation`
produceValue = (\input -> 1 + input + 5) 4

-- apply the argument
produceValue = (\4 -> 1 + 4 + 5)

-- remove the lambda argument
produceValue = 1 + 4 + 5

-- Evaluate it
produceValue = 10

Example 3: multiple bind

I'll leave this up to the reader to reduce, but the syntax should make it clear how it works (4 is always the initial input in each function below):

produceValue = someComputation 4
  where
  someComputation = do
    five <- (\four -> 1 + four)
    three <- (\fourAgain -> 7 - fourAgain)
    two <- (\fourOnceMore -> 13 + fourOnceMore - five * three)
    (\fourTooMany -> 8 - two + three)

Special Output

Previously, we wrote the instances for a normal (a -> b) function. But this is only one possible function! What if we modified that function, so that it outputted something different than just b? The reader might ask, "But if map, apply, pure, and bind all require the output/b type to be polymorphic (i.e. it should work for all bs), what type could we possibly return?"

Since b must still be polymorphic, why don't we wrap it in a higher-kinded type? For example, why not change (a -> b) to (a -> Box b)? How would we implement those type class instances?

Newtyping our Function

If our goal is to write instances for the function, (a -> Box b), how can we ensure this function's type signature never changes? We can wrap the function in a newtype:

newtype OutputBox a b = OutputBox (a -> Box b)

This creates a new problem. When we originally evaluated a monadic function, we could pass the value to the function without problem: produceValue = someComputation 4.

Since our above function is now wrapped in a newtype, we need a way to easily unwrap the newtype and pass the argument to the function. Why don't we create a function called runOutputBox to do just that?

runOutputBox :: forall a b. OutputBox a b -> a -> Box b
runOutputBox (OutputBox function) argument = function argument

Now we're ready to implement instances for OutputBox

Functor

Implementing map

Let's look at Functor again.

class Functor f where
  map :: forall a b. (a -> b) -> f a -> f b

Following the same idea as before, we can convert m into OutputBox input and get this type signature:

instance Functor (OutputBox input) where
  map :: forall originalOutput newOutput.
         (originalOutput -> newOutput) ->
         OutputBox input originalOutput ->
         OutputBox input newOutput
  map originalToNew (OutputBox f) = -- implementation

Let's see how to implement it.

  1. Since map returns an OutputBox type, let's start by first creating a newtype constructor:
instance Functor (OutputBox input) where
  map :: forall originalOutput newOutput.
         (originalOutput -> newOutput) ->
         OutputBox input originalOutput ->
         OutputBox input newOutput
  map originalToNew (OutputBox f) = OutputBox
  1. The type, OutputBox, wraps a function, so let's use lambda syntax to create a new one:
instance Functor (OutputBox input) where
  map :: forall originalOutput newOutput.
         (originalOutput -> newOutput) ->
         OutputBox input originalOutput ->
         OutputBox input newOutput
  map originalToNew (OutputBox f) = OutputBox (\input -> {- body of function -})
  1. Since f is the only argument that can receive the input value, let's pass input into f and store its output in a let binding:
instance Functor (OutputBox input) where
  map :: forall originalOutput newOutput.
         (originalOutput -> newOutput) ->
         OutputBox input originalOutput ->
         OutputBox input newOutput
  map originalToNew (OutputBox f) = OutputBox (\input ->
    let boxStoringOriginalOutput = f input
    in {- body of function -})

We have a problem. Do you know what it is? OutputBox a b is really (a -> Box b). So, f in the code above produces a value of the type, Box originalOutput.

Hmm... When we defined map for Function input, we could pass originalOutput directly into originalToNew at this point. However, originalOutput is currently stuck inside of Box. So, we have two questions.

  1. How do we get originalOutput out of the Box?
  2. Once we get a value of the desired type, newOutput, how do we stick it back into the Box?

In other words, our situation can be expressed in a type signature:

someFunction :: Box originalOutput -> Box newOutput

Wait! Doesn't that look very similar to Functor's map?

map :: (originalOutput -> newOutput) -> Box originalOutput -> Box newOutput

And isn't originalToNew a function with this exact type signature: (originalOutput -> newOutput)? And doesn't Box itself have an instance for Functor.

  1. Use Box's map to finish implementing the function.
instance Functor (OutputBox input) where
  map :: forall originalOutput newOutput.
         (originalOutput -> newOutput) ->
         OutputBox input originalOutput ->
         OutputBox input newOutput
  map originalToNew (OutputBox f) = OutputBox (\input ->
    let boxStoringOriginalOutput = f input
    in map originalToNew boxStoringOriginalOutput
    )

Great! Let's clean it up by inlining boxStoringOriginalOutput.

instance Functor (OutputBox input) where
  map :: forall originalOutput newOutput.
         (originalOutput -> newOutput) ->
         OutputBox input originalOutput ->
         OutputBox input newOutput
  map originalToNew (OutputBox f) = OutputBox (\input ->
      map originalToNew (f input)
    )

Takeaways

Lessons we learned in this example:

  • to implement Functor for (a -> Box b), we needed to use Box's Functor instance.

Apply

Let's now look at Apply.

class (Functor f) <= Apply f where
  apply :: forall a b. f (a -> b) -> f a -> f b

Our function will have this type signature:

instance (Functor (OutputBox input)) <= Apply (OutputBox input) where
  apply :: forall originalOutput newOutput.
           OutputBox input (originalOutput -> newOutput) ->
           OutputBox input originalOutput ->
           OutputBox input newOutput
  apply (OutputBox inputToFunction) (OutputBox f) = -- implementation
  1. As before, let's implement the shell of our return type: a newtype wrapper around a function created using lambda syntax:
instance (Functor (OutputBox input)) <= Apply (OutputBox input) where
  apply :: forall originalOutput newOutput.
           OutputBox input (originalOutput -> newOutput) ->
           OutputBox input originalOutput ->
           OutputBox input newOutput
  apply (OutputBox inputToFunction) (OutputBox f) = OutputBox (\input ->
      {- body of function -}
    )
  1. Let's pass input into f:
instance (Functor (OutputBox input)) <= Apply (OutputBox input) where
  apply :: forall originalOutput newOutput.
           OutputBox input (originalOutput -> newOutput) ->
           OutputBox input originalOutput ->
           OutputBox input newOutput
  apply (OutputBox inputToFunction) (OutputBox f) = OutputBox (\input ->
      let boxStoringOriginalOput = f input
      in {- body of function -}
    )
  1. Let's pass input into inputToFunction to expose function:
instance (Functor (OutputBox input)) <= Apply (OutputBox input) where
  apply :: forall originalOutput newOutput.
           OutputBox input (originalOutput -> newOutput) ->
           OutputBox input originalOutput ->
           OutputBox input newOutput
  apply (OutputBox inputToFunction) (OutputBox f) = OutputBox (\input ->
      let
        boxStoringOriginalOutput = f input
        boxStoringOriginalToNew = inputToFunction input
      in {- body of function -}
    )

Hmm... This seems similar to what happened before. We have two boxes that are both storing values. When we tried implementing the Functor instance for our function, we used Box's Functor definition. If we look at the types of our Boxes, we'll see that this coincidence applies here, too. boxStoringOriginalToNew has type, Box (originalOutput -> newOutput) while boxStoringOriginalOutput has type, Box originalOutput. So, let's use Box's Apply instance to finish the funciton!

instance (Functor (OutputBox input)) <= Apply (OutputBox input) where
  apply :: forall originalOutput newOutput.
           OutputBox input (originalOutput -> newOutput) ->
           OutputBox input originalOutput ->
           OutputBox input newOutput
  apply (OutputBox inputToFunction) (OutputBox f) = OutputBox (\input ->
      let
        boxStoringOriginalOutput = f input
        boxStoringOriginalToNew = inputToFunction input
      in apply boxStoringOriginalToNew boxStoringOriginalOutput
    )

Takeaways

Lessons we learned in this example:

  • to implement Apply for (a -> Box b), we needed to use Box's Apply instance.

Applicative

You've probably noticed a pattern by now. To implement a type class for our function, we need to use Box's corresponding instance for that type class.

We'll do this one quickly.

class (Apply f) <= Applicative f where
  pure :: forall a. a -> f a


instance (Apply (OutputBox input)) <= Applicative (OutputBox input) where
  pure :: forall a. a -> (OutputBox input) a
  pure value = OutputBox (\_ -> pure value {- Box's `pure` -})

Bind

We'll do this one a bit more slowly.

class (Apply m) <= Bind m where
  bind :: forall a b. m a -> (a -> m b) -> m b

-- convert `f` into `OutputBox input`
instance (Apply (OutputBox input)) <= Bind (OutputBox input) where
  bind :: forall originalOutput newOutput.
          OutputBox input originalOutput ->
          (originalOutput -> OutputBox input newOutput) ->
          OutputBox input newOutput
  bind (OutputBox inputToOriginal) originalToFunction = -- implementation

-- Write the initial newtype constructor wrapping a function created
-- via lambda syntax
instance (Apply (OutputBox input)) <= Bind (OutputBox input) where
  bind :: forall originalOutput newOutput.
          OutputBox input originalOutput ->
          (originalOutput -> OutputBox input newOutput) ->
          OutputBox input newOutput
  bind (OutputBox inputToOriginal) originalToFunction = OutputBox (\input ->
      {- body of function -}
    )

-- expose the `boxOriginalOutput` value
instance (Apply (OutputBox input)) <= Bind (OutputBox input) where
  bind :: forall originalOutput newOutput.
          OutputBox input originalOutput ->
          (originalOutput -> OutputBox input newOutput) ->
          OutputBox input newOutput
  bind (OutputBox inputToOriginal) originalToFunction = OutputBox (\input ->
      let boxOriginalOutput = inputToOriginal input
      in {- body of function -}
    )

-- use `Box`'s `bind` to reveal `originalOutput`
instance (Apply (OutputBox input)) <= Bind (OutputBox input) where
  bind :: forall originalOutput newOutput.
          OutputBox input originalOutput ->
          (originalOutput -> OutputBox input newOutput) ->
          OutputBox input newOutput
  bind (OutputBox inputToOriginal) originalToFunction = OutputBox (\input ->
      let boxOriginalOutput = inputToOriginal input
      in bind boxOriginalOutput (\originalOutput ->
           {- body of function -}
         )
    )

-- pass `originalOutput` into `originalToFunction`
-- and use pattern matching to expose the function wrapped in the `OutpuBox`
instance (Apply (OutputBox input)) <= Bind (OutputBox input) where
  bind :: forall originalOutput newOutput.
          OutputBox input originalOutput ->
          (originalOutput -> OutputBox input newOutput) ->
          OutputBox input newOutput
  bind (OutputBox inputToOriginal) originalToFunction = OutputBox (\input ->
      let boxOriginalOutput = inputToOriginal input
      in bind boxOriginalOutput (\originalOutput ->
           let (OutputBox inputToNew) = originalToFunction originalOutput
           in {- body of function -}
         )
    )

-- pass `input` into `inputToNew`, which produces `Box newOutput` and
-- satisfies the type signature of `bind`.
instance (Apply (OutputBox input)) <= Bind (OutputBox input) where
  bind :: forall originalOutput newOutput.
          OutputBox input originalOutput ->
          (originalOutput -> OutputBox input newOutput) ->
          OutputBox input newOutput
  bind (OutputBox inputToOriginal) originalToFunction = OutputBox (\input ->
      let boxOriginalOutput = inputToOriginal input
      in bind boxOriginalOutput (\originalOutput ->
           let (OutputBox inputToNew) = originalToFunction originalOutput
           in inputToNew input
         )
    )

If we were to clean up the finished code above, we would write this:

instance (Apply (OutputBox input)) <= Bind (OutputBox input) where
  bind :: forall originalOutput newOutput.
          OutputBox input originalOutput ->
          (originalOutput -> OutputBox input newOutput) ->
          OutputBox input newOutput
  bind (OutputBox inputToOriginal) originalToFunction =
    OutputBox (\input -> do
      originalOutput <- inputToOriginal input
      let (OutpuBox inputToNew) = originalToFunction originalOutput
      inputToNew input
    )

Generalizing Box to any Monad

If we look at our final instances below (they were copied from above), we'll see that we never once used Box explicitly. Rather, we could have replaced Box with any monadic data type and we would still be able to implement instances of these type class' functions that satisfy their type signatures.

instance Functor (OutputBox input) where
  map :: forall originalOutput newOutput.
         (originalOutput -> newOutput) ->
         OutputBox input originalOutput ->
         OutputBox input newOutput
  map originalToNew (OutputBox f) = OutputBox (\input ->
      map originalToNew (f input)
    )

instance (Functor (OutputBox input)) <= Apply (OutputBox input) where
  apply :: forall originalOutput newOutput.
           OutputBox input (originalOutput -> newOutput) ->
           OutputBox input originalOutput ->
           OutputBox input newOutput
  apply (OutputBox inputToFunction) (OutputBox f) = OutputBox (\input ->
      let
        boxStoringOriginalOutput = f input
        boxStoringOriginalToNew = inputToFunction input
      in apply boxStoringOriginalToNew boxStoringOriginalOutput
    )

instance (Apply (OutputBox input)) <= Applicative (OutputBox input) where
  pure :: forall a. a -> (OutputBox input) a
  pure value = OutputBox (\_ -> pure value)

instance (Apply (OutputBox input)) <= Bind (OutputBox input) where
  bind :: forall originalOutput newOutput.
          OutputBox input originalOutput ->
          (originalOutput -> OutputBox input newOutput) ->
          OutputBox input newOutput
  bind (OutputBox inputToOriginal) originalToFunction =
    OutputBox (\input -> do
      originalOutput <- inputToOriginal input
      let (OutpuBox inputToNew) = originalToFunction originalOutput
      inputToNew input
    )

If we were to generalize our function to a monad, it would look like this:

newtype OutputMonad a m b = (a -> m b)

runOutputMonad :: forall m a b. Monad m => OutputMonad a m b -> a -> m b
runOutputMonad (OutputMonad function) arg = function arg

Introducing Monad Transformers and Capabilities

Comparing Function input output to OutputBox input output

When we initially covered the original monadic function, (a -> b), we discovered that it's "do notation" could be read like this:

produceValue = someComputation 4
  where
  someComputation = do
    five <- (\four -> 1 + four)
    three <- (\fourAgain -> 7 - fourAgain)
    two <- (\fourOnceMore -> 13 + fourOnceMore - five * three)
    (\fourTooMany -> 8 - two + three)

The above computation demonstrates something important: the argument, 4, passed to someComputation is always available in its do notation despite the argument, 4, never appearing as an argument in someComputation's definition.

-- In other words, we have this...
someComputation = do
  -- code...

-- ... not this...
someComputation argumentThatIsFour = do
  -- some code

The normal (a -> b) function allows us to reference the argument we pass into the function at any point in its do notation. Unfortunately, since the return value of that function is b, we are limited to only writing pure code. In other words, someComputation is a computation that can never interact with the outside world.

But what happens if we use the OutputMonad/(a -> monad b) function? Since it outputs a monadic data type, what if that monadic data type was Effect or Aff? If so, the resulting code would be very hard to read:

produceComputation = runOutputEffect someComputation 4
  where                                                                     {-
  someComputation :: OutputMonad a   m      b                               -}
  someComputation :: OutputMonad Int Effect String
  someComputation = do
    five <- (\four -> pure $ 1 + four)
    three <- (\fourAgain -> pure $ 7 - fourAgain)
    intBetween0And12 <- (\fourOnceMore -> randomInt 0 $ 13 + fourOnceMore - five)
    (\fourTooMany -> log $ show $ 8 - intBetween0And12)

But what if we expressed the same idea using capabilities via type class constraints? This is the same code as above:

produceComputation = runOutputMonad someComputation 4
  where
  someComputation :: forall m. Monad m =>
                     MonadReader Int m =>
                     MonadEffect m =>
                     OutputMonad Int m String
  someComputation = do
    four <- ask
    let five = 1 + four
    let three = 7 - four
    intBetween0And12 <- liftEffect $ randomInt 0 $ 13 + four - five
    liftEffect $ log $ show $ 8 - intBetween0And12

Introducing ReaderT

To see this from a slighty different angle, we'll cover a few things before linking to an example showing how a computation "evolves" into the Reader monad.

OutputMonad is better known as ReaderT:

  • when ReaderT's monadic type is a real monad, we call it ReaderT
  • when ReaderT's monadic type is Identity (placeholder monadic type), we call it Reader.

It's corresponding type class is MonadReader.

Watch a computation "evolve" into the Reader monad in the Monad Reader Example.

Monad Transformers Summarized

The Main Idea

This is the whole point of using monadic functions that output monadic data types: they allow us to encode all of our business logic as one massive pure function.

If the underlying outputted monadic data type is...then running our code will ...
Effect/Aff
("impure" monads)
execute a program
Identity^/Trampoline^^
("pure" monad)
allow us test our business logic

^ Identity is what you get if Box was a newtype rather than data. In other words, newtype Identity a = Identity a. identity is a function that returns as output its input. Thus, it's often used as a "placeholder" / mempty function value. Similarly, Identity a is a type that reduces to the a type at runtime. Thus, it's often used as a "placeholder" / mempty-like monadic type. ^^ Trampoline is a monad that we haven't introduced yet.

With one monad, we can prove that our business logic works as expected and does not have any bugs, and with another monad, we can execute that same business logic as a useful program.

This helps us understand the name behind "Monad Transformers". Monadic functions that return other monadic data types (i.e. a -> m b) are called "Monad Transformers" because they transform (or augment) the base monad with additional capabilities. They are the "implementation" that makes all of this work.

However, we use type classes like MonadReader, MonadState, MonadWriter, etc. to express that a given computation can only be run if their implementation can satisfy the required capabilities.

In short, we use type classes above to "write" our business logic and monad transformers to "run" our business logic.

Breaking It Down

First, there is a type class that indicates that some underlying monad has the capability to do some effect (e.g. state manipulation via MonadState).

Second, there is a default implementation for that class via a monadic newtyped function (e.g. ReaderT). As we will see later, such functions add in specific effects, reduce some of the syntax boilerplate one might write, and make impossible states impossible. When we wish to transform some other monad, we use the newtyped monadic functions that end with T as in "transformer" (e.g. ReaderT). However, if we don't want to transform a monad (i.e. just use Identity to act as a placeholder monadic type), then we remove that T (e.g. Reader).

Third, the general pattern (there are exceptions!) that we will see reappear when overviewing the other Monad Transformers:

  • There is a type class called Monad[Word] where [Word] clarifies what functions the type class provides. This type class indicates that some underlying monad has some capability.
  • There is a single default implementation for Monad[Word] called [Word]T. When the monad type is specialized to Identity, it's simply called [Word].
  • To run a computation using Monad[Word], we must use either run[Word] computation arg (i.e. the monad is Identity) or run[Word]T computation arg (i.e. a non-Identity monad).

Putting it into a table, we get this.

Type ClassSole Implementation
(m is real monad)

function that runs it
Sole Implementation
(m is Identity)

function that runs it
Monad[Word]
(General Pattern)
[Word]T

run[Word]T
[Word]

run[Word]
MonadStateStateT

runStateT
State

runState
MonadReaderReaderT

runReaderT
Reader

runReader
MonadWriterWriterT

runWriterT
Writer

runWriter
MonadContContT

runCont
Cont

runCont
MonadErrorExceptT

runExceptT
Except

runExcept

To summarize each monad transformer, we'll use another table. The below terms for "pure" and "impure" refer to whether the computations can interact with the real world.:

A basic function...... is used to run a pure computation ...can be "upgraded" to a monad transformer...... which is used to run an impure computation ...
input -> outputthat depends on its inputglobalValue -> monad outputThatUsesGlobalValue

ReaderT
that depends on some global configuration
state -> Tuple output statethat does state manipulationoldState -> monad (Tuple (output, newState))

StateT
that does state manipulation
function $ argonce an argument is fully computed\function -> output

ContT
and periodically use a function passed in as an argument to compute something

Some monad transformers just specify what their output type will be:

A basic return value...... that is used to run a pure computation ...... can be put into a Monad and become a monad transformer...... which is used to run an impure computation ...
Tuple output additionalOutputthat also produces additional outputmonad (Tuple (output, accumulatedValue)

WriterT
that produces accumulated data as additional output
Either e athat handles partial functionsmonad (Either e a)

ExceptT
that may fail and the error matters
Maybe athat might not return a valuemonad (Maybe a)

MaybeT
that might not return a value
List athat produces 0 or more valuesmonad (List a)

ListT
that produces 0 or more values

Once again, the "base monad" that usually inhibits m in the "stack" of nested monad transformers is usually one of two things:

  • Effect/Aff: impure monads that actually make our business logic useful
  • Identity/Free: pure monads that test our business logic.

Implementing a Monad Transformer

This folder will show the thought process one would take to implement a monad transformer (e.g. the type class and its implementation) for manipulating state in a monad stack. The lessons learned/demonstrated in this folder will make it easier to understand the standard monad transformers covered in the rest of this MTL folder.

Looking at OO for a Pattern

We'll look at three examples of OO code to help us understand it's equivalent in FP code.

Incrementing an Integer

Given this code:

a = 0;
x = a++;
y = a++;
z = a++;
// which we'll rewrite to use a function that receives an argument
a = 0;
x = getAndIncrement(a);
y = getAndIncrement(a);
z = getAndIncrement(a);

getAndIncrement is an example of an impure function because it does not return the same value each time it is called. Moreover, a's value changes over time, so that a /= 0 at the end of our program. How might we write the same thing using pure functions? We'll demonstrate a few attempts and explain their problems before showing the final working solution

// we'll make the function pure
// and call it "add1"
var add1 = (i) => i + 1;

a = 0;
x = add1(a);
y = add1(a);
z = add1(a);

// Values end states are:
a == 0
x == y == z == 1

The problem is add1 receives the wrong state as an argument. If we pass the returned state from our previous call into the next call, we can resolve this problem:

a = 0;
x = add1(a);
y = add1(x);
z = add1(y);

// Values end states are:
a == 0
x == 1
y == 2
z == 3

At this point, we could do state manipulation using a recursive function...

runNTimes :: forall a. Int -> (a -> a) -> a
runNTimes 0 _ output = output
runNTimes count func arg = runNTimes (count - 1) func (func arg)

... but state manipulation is more complicated than that. What if we wanted to add 1 at one point and add 2 at another? What if we want to subtract 5 as well? In short, this approach does not work when we increase the complexity of the state manipulation. The next two examples will focus on a different kind of state manipulation.

Random Number Generators

Given this code:

x = random.nextInt
y = random.nextInt
z = random.nextInt

// rewritten to use "function arg" syntax
x = nextInt(random);
y = nextInt(random);
z = nextInt(random);

nextInt is an impure function because it does not return the same value each time it is called. How might we write the same thing using pure functions? We'll demonstrate a few attempts and explain their problems before showing the final working solution

// Assume that  `nextInt` is now pure...
x = nextInt(random);
y = nextInt(random);
// ... then 'x' ALWAYS equals 'y'
// A random number can sometimes be the same one as before,
// but this shouldn't always be true

// To make `x /= y`, we need a new `random` value, something like:
x = nextInt(random1);
y = nextInt(random2);

The solution is to make nextInt return two things via the Tuple a b type

  • the random int value
  • a new value of random
(Tuple x random2) = nextInt(random1);
(Tuple y random3) = nextInt(random2);

where Tuple a b is just a box that holds two values of the same/different types:

data Tuple a b = Tuple a b

Popping Stacks

We'll explain this idea once more using a different context: Stacks. In OO, we can write the following code:

// assuming we have a non-empty stack:
//   (top) [1, 2, 3, 4, 5] (bottom)
x = stack.pop // x == 1
y = stack.pop // y == 2
z = stack.pop // z == 3

// rewritten using "function arg" syntax
x = pop(stack);
y = pop(stack);
z = pop(stack);

pop is an impure function as calling it will not return the same value each time it is called. How might we write the same thing using pure functions?

// Assume that  `nextInt` is now pure...
x = pop(stack);
y = pop(stack);
// ... we just popped the same value twice off of the stack
// so that 'x' is always the same value/object as 'y'
// In other words
pop(stack) == x == y == 1

// To make `y` == 2, we need a version of `stack` that will return `2`
// as its next value to `pop`. In other words, something like...
x = pop(originalStack);
y = pop(originalStack_withoutX);

The solution is to make pop return two things via the Tuple a b type:

  • the popped value
  • a new version of stack without the popped value
(Tuple x originalStack_withoutX)    = pop(originalStack);
(Tuple y originalStack_withoutXorY) = pop(originalStack_withoutX);

Identifying the Pattern

Here's the solution we came up with:

(Tuple x random2) = randomInt(random1);
(Tuple y random3) = randomInt(random2);

(Tuple x originalStack_withoutX)    = pop(originalStack);
(Tuple y originalStack_withoutXorY) = pop(originalStack_withoutX);

// and generalizing it to a pattern, we get
(Tuple value1,  value2        ) = stateManipulation(value1);
(Tuple value2,  value3        ) = stateManipulation(value2);
(Tuple value3,  value4        ) = stateManipulation(value3);
// ...
(Tuple value_N, value_N_plus_1) = stateManipulation(valueN);

Turning this into Purescript syntax, we get:

state_manipulation_function :: forall state value. (state -> Tuple value state)

Implementing the Pattern

Here's the solution we came up with:

(Tuple x random2) = randomInt(random1);
(Tuple y random3) = randomInt(random2);

(Tuple x originalStack_withoutX)    = pop(originalStack);
(Tuple y originalStack_withoutXorY) = pop(originalStack_withoutX);

// and generalizing it to a pattern, we get
(Tuple value1,  value2        ) = stateManipulation(value1);
(Tuple value2,  value3        ) = stateManipulation(value2);
(Tuple value3,  value4        ) = stateManipulation(value3);
// ...
(Tuple value_N, value_N_plus_1) = stateManipulation(valueN);

Turning this into Purescript syntax, we get:

state_manipulation_function :: forall state value. (state -> Tuple value state)

Syntax Familiarity

Starting with a simple example written using meta-language, we can simulate the state manipulation syntax when it's only run once. Unlike the "add 1 to integer" problem from before, this will return the integer state as a String, not an Int:

type State = Int
type Value = String

initialState :: State
initialState = 0

add1 :: (State -> Tuple Value State)
add1 oldState   =
  let theNextState = oldState + 1
  in Tuple (show theNextState) theNextState

main :: Effect Unit
main =
  case (add1 initialValue) of
    Tuple theValue theNextState -> do
      log $ "Value was: " <> theValue               -- "1"
      log $ "next state was: " <> show theNextState --  1

Why We Need a Monad

What if we want to run add1 four times?

Knowing that we have more complicated state manipulation ahead of us (e.g. Stacks), we should follow the pattern we identified above:

  1. Pass an initial state value into add1, which outputs Tuple nextStateAsString nextState
  2. Extract the nextState part of the Tuple
  3. Pass nextState into another add1 call
  4. Loop a few times
  5. Pass the last state into add and return its output: Tuple lastStateAsString lastState.

In code, this looks like:

type State = Int
type Value = String
type Count = Int

add1 :: (State -> Tuple Value State)
add1 oldState   =
  let theNextState = oldState + 1
  in Tuple (show theNextState) theNextState

add1_FourTimes :: State -> Tuple Value State
add1_FourTimes initialState = runNTimes 4 add1 initialState

runNTimes :: Count -> (State -> Tuple Value State) -> State -> Tuple Value State
runNTimes 1 add1_ nextState = add1_ nextState
runNTimes count add1_ nextState =
  runNTimes (count - 1) add1_ (getSecond $ add1_ i)

  where
  getSecond :: Tuple Value State -> Int
  getSecond (Tuple _ state) = state

This works but only because it's so simple. Let's say we want to call add1 on the first state, then call times2 on the second state, and then return the output of calling add1 on the third state. How would we update our code to do that?

We could try to specify a stack of functions (using an array or some other stack-like data structure) that are used to recursively evaluate the next state outputted by the previous function. Below is not a working example of how one would write that, but merely demonstrates the heart behind it:

type Stack a = Array a
type State = Int
type Value = String

-- This code doesn't type check!
-- It exists for teaching purposes only!
runUsingFunctions :: Stack (State -> Tuple Value State) -> State -> Tuple Value State
runUsingFunctions [last] state = last state
runUsingFunctions [second, last] = runUsingFunctions [last] (getSecond $ second state)
runUsingFunctions [first, second, last] state =
  runUsingFunctions [second, last] (getSecond $ first state)

  where
  getSecond :: Tuple Value State -> State
  getSecond (Tuple _ state) = state

Conceptually, there are two problems with the above code.

  1. If we change the value type for one function so that it's different from all other function in the stack (e.g. toNumber :: Int -> Tuple Number Int), the above code will no longer compile.
  2. We cannot use a function that receives the next state AND value(s) produced by previous function(s) as its arguments.

As an example for the second point, how could we use these two functions in the same state manipulation workflow:

firstFunction :: State1 -> Tuple Value1 State2

fourthFunction :: State4 -> Value1 -> Tuple Value4 State5

The following function, crazyFunction, demonstrates both of these problems without the intermediary second and third functions:

  1. Take some initialState value
  2. Pass that value into add1 :: State -> Tuple Int Int, which returns Tuple value1 state2
  3. Pass value and state2 into addValue1StringLengthTo :: Int -> Int -> Tuple String Int where
    • value will be converted into a String, called valueAsString
    • the length of valueAsString will be added to state2, which produces state3
    • state3 is converted into a String, called value2
    • the function returns Tuple value2 state3
  4. Return addStringLengthTo's output: Tuple value2 nextState3

To write crazyFunction, we need something more like sequential computation, which implies bind/>>=. However, bind requires a Monad to work. With these clues, we need a function whose type signature looks something like this:

someFunction :: forall state monad value
              . Monad monad
             => (state -> Tuple value state) -- the state manipulation function
             -> state                        -- the initial state
             -> monad (Tuple value state)    -- the monad that makes `bind` work
someFunction function state = pure $ function state

Putting this all together, we get this:

someFunction :: forall state monad value
              . Monad monad

             -- the state manipulation function...
             => (state -> Tuple value state)

             -- (the initial/next state)
             -> state

             -- whose output gets lifted into a Monad that makes `bind` work,
             -- so we can compose multiple state manipulating functions
             -- together into one function
             -> monad (Tuple value state))
someFunction function initialOrNextState =
    let tuple = function initialOrNextState

      -- lift it into the Monad to
      -- enable sequential computation via bind
    in pure tuple
  )

-- our Monad type
data Box a = Box a

unwrapBox :: forall a. Box a -> a
unwrapBox (Box a) = a

addStringLengthTo :: Int -> Int -> Tuple String Int
addStringLengthTo value state =
  let valueAsString = show value
      state3 = state + (length valueAsString)
  in Tuple (show state3) state3

-- Uses `someFunction` to compose multiple state functions together into one
crazyFunction :: Int -> Box (Tuple Int Int)
crazyFunction initial = do                                                   {-
  Tuple value  state  <- pure $ function state
  Tuple value  state  <- (\s -> pure $ function s) state
  Tuple value  state  <- (someFunction function) state                      -}
  Tuple value1 state2 <- (someFunction add1) initial
  (someFunction (\s -> addStringLengthTo value1 s) state2

main :: Effect Unit
main =
  case (unwrapBox $ crazyFunction 0) of
    Tuple theString theInt -> do
      log $ "theString was: " <> theString  -- "2"
      log $ "theInt was: " <> show theInt   --  2

There's two problems with the above approach, which the next sections will refine.

The Identity Monad

Box is a literal runtime Box. So, using it here as our monad type means we'll be runtime boxing and unboxing the result of our functions, thereby slowing down our code needlessly. We only need Box so we can use a Monad for sequential computation, not because we need the type, Box, specifically (we could use Box2 and our code wouldn't change). Why don't we get rid of this needless runtime overhead by using a type that only exists at compile-time? This implies using newtype because the type still needs to implement an instance for Monad.

Since we have a "placeholder" function called identity, let's reuse this name for our compile-time-only type:

-- placeholder for a function!
identity :: forall a. a -> a
identity x = x

-- runtime type!
data    Box      a = Box      a

-- placeholder for a monad!
-- compile-time-ONLY type!
newtype Identity a = Identity a

The Syntax Problem

crazyFunction showed an issue with our current approach: we have to pass the previous state result back into the next function. If the developer passes in the wrong state value, the code will no longer work as expected:

crazyFunction :: Int -> Identity (Tuple Int Int)
crazyFunction initial = do
  -- Computation 1: here we calculate what state2 is
  Tuple value1 state2 <- (someFunction add1) initial

  -- Computation 2: here we calculate what state3 is
  Tuple value2 state3 <- (someFunction add1) state2

  -- Computation 3: here we pass in `state2` when we should pass in `state3`
  (someFunction (\s -> addStringLengthTo value2 s) state2

In short, we need to hide the state value entirely from the function so that developers cannot pass in the wrong value. Thus, we must also get rid of the Tuple value state notion in our function. Putting those two concepts together, we imagine syntax that looks like this: nextValue <- someFunction (\nextState -> stateManipulatingFunction nextState)

This syntax... nextValue <- someFunction (\initialState -> stateManipulatingFunction initialState) ... looks very similar to OO's syntax: var nextValue = initialState.stateManipulatingFunction();

Another benefit: it gets rid of the boilerplate-y noise-y Tuples

What would we need to change to get this syntax? This gets tricky.

First, initialState should now be located outside crazyFunction and appear in another function, runSomeFunction. runSomeFunction should pass the initialState value into the final composition of all the state manipulating functions:

runSomeFunction :: forall state value.
                   (state -> Identity (Tuple value state)) ->
                   state ->
                   Tuple value state
runSomeFunction stateFunctionsComposedIntoOne initialState =
  let (Identity tuple) = stateFunctionsComposedIntoOne initialState
  in tuple

addStringLengthTo :: Int -> Int -> Tuple String Int
addStringLengthTo value state =
  let valueAsString = show value
      state3 = state + (length valueAsString)
  in Tuple (show state3) state3

-- Using our new syntax...
crazyFunction :: (state -> Identity (Tuple Int state))
crazyFunction = do
  -- Computation 1
  value1 <- someFunction (\initialState -> add1 initialState)

  -- Computation 2
  -- `bind` will produce `Tuple value2 state3`
  someFunction (\state2 -> addStringLengthTo value1 state2)

Second (and as the above example shows), someFunction must somehow return just value and not Tuple value state.

From these clues, we get this new type signature:

someFunction :: forall state monad value.
                Monad monad =>
                (state -> Tuple value state) -> monad value
someFunction function = -- ???

runSomeFunction :: forall state value.
                   (state -> Identity (Tuple value state)) ->
                   state ->
                   Tuple value state
runSomeFunction stateFunctionsComposedIntoOne initialState =
  let (Identity tuple) = stateFunctionsComposedIntoOne initialState
  in tuple

It would seem that this idea is not possible. We'll reveal how in the next file. For now, we'll abstract this concept into a type class

Abstracting the Concept into a Type Class

We want to use someFunction for numerous state manipulating functions on numerous data structures (e.g. add1, popStack, replaceElemAtIndex). This implies that we need to convert someFunction into a type class, so we can use someFunction in other situations via a type class constraint. Let's attempt to define it and call the type class MonadState. Its function, state, should be the same as someFunction's type signature:

someFunction :: forall state monad value
              . Monad monad
             => (state -> Tuple value state)
             -> monad value
someFunction function = -- ???

class MonadState ??? where
  state :: forall s m a
             .  (s -> Tuple a s )
             -> m a

Because we know we need bind, let's add a Monad constraint, m, to ???:

class (Monad m) <= MonadState m where
  state :: forall s a
             .  (s -> Tuple a s )
             -> m a

We need to make sure the state type does not change, so we'll also define a functional dependency from m to s

class (Monad m) <= MonadState s m | m -> s where
  state :: forall a
             .  (s -> Tuple a s )
             -> m a

Combining this definition with its corresponding runSomeFunction, we get this (where runSomeFunction is now called runStateFunction)

class (Monad m) <= MonadState s m | m -> s where
  state :: forall a. (s -> Tuple a s) -> m a

runStateFunction :: forall s a. (s -> Identity (Tuple a s)) -> s -> Tuple a s
runStateFunction stateManipulation initialState =
  let (Identity tuple) = stateManipulation initialState
  in tuple

Ok. Now let's see how this seemingly impossible syntax is actually possible.

A Magical Monad

We reached these conclusions previously:

  • we need Monad's bind/>>= to enable multiple different state-manipulating functions to work
  • we need to hide the state from the actual function, so that developers can't pass in the wrong state value accidentally (i.e. make impossible states impossible). This came with two implications:
    • Calling bind/>>= should return just value, not Tuple value state
    • Running the computation via runSomeFunction should return Tuple value state.

In short, we need to implement the following two functions with these type signatures:

class (Monad m) <= MonadState state monad | monad -> state where
  state :: forall value. (state -> Tuple value state) -> monad value

runStateFunction :: forall state value.
                   (state -> Identity (Tuple value state)) ->
                    state ->
                    Tuple value state
runStateFunction stateManipulation initialState =
  let (Identity tuple) = stateManipulation initialState
  in tuple

Introducing the Function Monad

What if Function was a Monad? This might sound surprising at first, but it's actually true.

Recall that a Monad is any type that has lawful instances for the Functor, Apply, Applicative, Bind, and Monad (FAABM) type classes. As long as a type can successfully implement lawful functions for them, the type can be called monadic.

How might this be possible?

First, a Monad has kind Type -> Type whereas a Function has kind Type -> Type -> Type.

We can make Function's kind one less by specifying either the input type or the output type:

  • Function Int a/(Int -> a) has kind Type -> Type
  • Function a Int/(a -> Int) has kind Type -> Type

In other words, we need to turn Function into a completely new type (data, type, or newtype) that should only exist at compile time to reduce runtime overhead (e.g. type or newtype) that can also implement type classes (i.e. only newtype). Using a newtyped version of Function, we can specify all the types in the function:

newtype TypedFunction input output =
  TypedFunction (input -> output)

specifiesInput :: forall a. TypedFunction Int b -- Kind: Type -> Type

specifiesOutput :: forall a. TypedFunction a Int  -- Kind: Type -> Type

Second, since Function can refer to any function, what should our newtyped function's type signature be? We'll use the state-manipulating function's type signature itself! (state -> monad (Tuple value state))

We will call this the StateT monad. The T part of the name will become clearer later.

Monadic Instances

Let's now implement the FAABM type classes by using pattern matching to expose the inner function. The value type will be left undefined (i.e. it's the a in everything), making StateT have the necessary kind, Type -> Type:

Functor

newtype StateT state monad value =
  StateT (state -> monad (Tuple value state))

-- Let's follow the types. We'll need to return a `StateT` value
-- so we'll start by doing that:
instance (Monad monad) => Functor (StateT state monad) where
  map :: forall a b
       . (a -> b)
      -> StateT state monad a
      -> StateT state monad b
  map f (StateT g) = StateT -- todo

-- Since StateT wraps a function whose only argument
-- is state, we'll add that now:
instance (Monad monad) => Functor (StateT state monad) where
  map :: forall a b
       . (a -> b)
      -> StateT state monad a
      -> StateT state monad b
  map f (StateT g) = StateT (\state ->
      -- todo
    )

-- We need to use that function, but it only takes an `a`
-- argument. So, we need to get that `a` by using `g`
-- Thus, we'll pass the returning StateT's state argument into `g`
-- Then we get a `monad (Tuple a state)`
instance (Monad monad) => Functor (StateT state monad) where
  map :: forall a b
       . (a -> b)
      -> StateT state monad a
      -> StateT state monad b
  map f (StateT g) = StateT (\state ->
      let
        ma = g state
      in
        -- todo
    )
-- So we can use `bind/>>=` to expose the Tuple within this monad
instance (Monad monad) => Functor (StateT state monad) where
  map :: forall a b
       . (a -> b)
      -> StateT state monad a
      -> StateT state monad b
  map f (StateT g) = StateT (\state ->
      let
        ma = g state
      in
        ma >>= (\(Tuple value state2) ->
          -- todo
        )
    )

-- Great. Now let's pass `value` into the `f` function
instance (Monad monad) => Functor (StateT state monad) where
  map :: forall a b
       . (a -> b)
      -> StateT state monad a
      -> StateT state monad b
  map f (StateT g) = StateT (\state ->
      let
        ma = g state
      in
        ma >>= (\(Tuple value state2) ->
          let 
            b = f value
          in 
            --todo
        )
    )

-- Now we have our `b`. However, the returned `StateT` needs
-- to wrap a function that returns `monad (Tuple value state)`
-- Let's do that now and finish implementing Functor for StateT
instance (Monad monad) => Functor (StateT state monad) where
  map :: forall a b
       . (a -> b)
      -> StateT state monad a
      -> StateT state monad b
  map f (StateT g) = StateT (\state ->
      let
        ma = g state
      in
        ma >>= (\(Tuple value state2) ->
          let
            b = f value
          in
            pure (Tuple b state2)
        )
    )

Apply

Since Apply is very similar to Functor (actually the exact same, but we just unwrap the f now, too), we'll just show the code.

instance (Monad monad) => Apply (StateT state monad) where
  apply :: forall a b
        -- (state -> Tuple (a -> b) state)
         . StateT state monad (a -> b)
        -> StateT state monad a
        -> StateT state monad b
  apply (StateT f) (StateT g) = StateT (\s1 ->
    let
      (Tuple value1 s2) = g s1
    in
      let
        (Tuple function s3) = f s2
      in
        let
          mappedValue = function value1
        in
          pure $ Tuple mappedValue s3
        )
      )
    )

Applicative

The Applicative instance is actually quite straight forward:

instance (Monad monad) => Applicative (StateT state monad) where
  pure :: forall a. a -> StateT state monad a
  pure a = StateT (\s -> pure $ Tuple a s)

Bind & Monad

instance (Monad monad) => Bind (StateT state monad) where
  bind :: forall a b
        . StateT state monad a
       -> (a -> StateT state monad b)
       -> StateT state monad b
  bind (StateT g) f = StateT (\s1 ->
    let
      (Tuple value1 s2) = g s1
    in
      let
        (State h) = f value1
      in
        h s2
      )
    )

-- The Monad instance is just declared since there is nothing to implement.
instance (Monad m) => Monad (StateT state monad)

MonadState

instance (Monad m) => Monad (StateT state monad) where
  state :: forall value. (state -> Tuple value state) -> StateT state monad value
  state f = StateT (\s -> pure $ f s)

FAABM Using Bind

Notice, however, that the above let ... in syntax is really just a verbose way of doing bind/>>=. If we were to rewrite our instances using bind, they now look like this:

instance (Monad monad) => Functor (StateT state monad) where
  map :: forall a b
       . (a -> b)
      -> StateT state monad a
      -> StateT state monad b
  map f (StateT g) = StateT (\s1 ->
      (g s1) >>= (\(Tuple value1 s2) ->
        pure $ Tuple (function value1) s2
      )
    )

instance (Monad monad) => Apply (StateT state monad) where
  apply :: forall a b
        -- (state -> Tuple (a -> b) state)
         . StateT state monad (a -> b)
        -> StateT state monad a
        -> StateT state monad b
  apply (StateT f) (StateT g) = StateT (\s1 ->
      (g s1) >>= (\(Tuple value1 s2) ->
        (f s2) >>= (\(Tuple function s3) ->
          pure $ Tuple (function value1) s3
        )
      )
    )

instance (Monad monad) => Applicative (StateT state monad) where
  pure :: forall a. a -> StateT state monad a
  pure a = StateT (\s -> pure $ Tuple a s)

instance (Monad monad) => Bind (StateT state monad) where
  bind :: forall a b
        . StateT state monad a
       -> (a -> StateT state monad b)
       -> StateT state monad b
  bind (StateT g) f = StateT (\s1 ->
      (g s1) >>= (\(Tuple value1 s2) ->
        let (StateT h) = f value1 in h s2
      )
    )

instance (Monad m) => Monad (StateT state monad)

Reviewing StateT's Bind Instance

Let's look in particular at StateT's bind implmentation as this is crucial to understanding how it enables the syntax we desire:

instance (Monad monad) => Bind (StateT state monad) where
  bind :: forall a b
        . StateT state monad a
       -> (a -> StateT state monad b)
       -> StateT state monad b
  bind (StateT g) f = StateT (\s1 ->
      (g s1) >>= (\(Tuple value1 s2) ->
        let
          (StateT h) = f value1
        in
        -- h :: (state -> monad (Tuple value state))
           h s2
      )
    )

Behind the scenes, StateT is still using Tuple value state as normal. However, the value that is passed to f is the value type (i.e. a) and not Tuple value state. This is what enables the syntax we desire.

In other words, recall that

bind (Box 4) (\four -> body)
-- converts to
(Box 4) >>= (\four -> body)
-- which in 'do notation' looks like
four <- (Box 4)
body four

In the next file, we'll show how this actually works via a graph reduction.

Proving the Syntax

This file will prove that the syntax works by doing a graph reduction of

  • StateT's do notation
  • running a StateT and insuring it type checks.

Reading StateT Do Notation

newtype StateT state monad output = StateT (state -> monad (Tuple output state))

stateT_do_notation :: StateT StateType MonadType ValueType
stateT_do_notation = do

    -- This is what do notation looks like using a StateT monad

    value1 <- state (\initialState -> Tuple value1 state2)
    value2 <- state (\state2       -> Tuple value2 state3)
    value3 <- state (\state3       -> Tuple value3 state4)
    state (\state4 -> Tuple value4 state5)

Reducing a StateT's Do Notation

It's now time to reduce a simple StateT's do notation expression into its final result. Here's the simple expression:

f = do
  value1 <- state (\initialState -> Tuple value1 state2)
  state (\state2 -> Tuple value1 state3)

It gets ugly pretty quickly, but we present it in a manner that reduces the information overload:

-- Start!
f = do
  value1 <- state (\initialState -> Tuple value1 state2)
  state (\state2 -> Tuple value1 state3)

-- Turn the "do notation" back to ">>="
f =
  state (\initialState -> Tuple value1 state2) >>= (\value1 ->
    state (\state2 -> Tuple value1 state3)
  )

-- Convert ">>=" back into "bind"
f =
  bind (state (\initialState -> Tuple value1 state2)) (\value1 ->
    state (\state2 -> Tuple value1 state3)
  )

-- Take the function that is passed to bind, call it "func",
-- and put it into a 'where' clause:
f = bind (state (\initialState -> Tuple value1 state2)) func
  where
  func = (\value1 -> state (\state2 -> Tuple value2 state3))

-- hide everything but "func"
func = (\value1 -> state (\state2 -> Tuple value2 state3))

-- Recall what StateT's MonadState implementation is...
instance Monad m => MonadState s (StateT s m) where
  state f = StateT (\sA -> pure $ f sA)
-- ... and use it to replace `state`'s LHS with its RHS
func = (\value1 -> StateT (\sA -> pure $ f sA))
  where
  f = (\state2 -> Tuple value2 state3)

-- bump `f` into `func`
func = (\value1 -> StateT (\sA -> pure $ (\state2 -> Tuple value2 state3) sA ))

-- Rename `pure` to `pureID` to show that it's Identity's "pure"
func = (\value1 -> StateT (\sA -> pureID $ (\state2 -> Tuple value2 state3) sA ))

-- replace "pureID"'s LHS with RHS
func = (\value1 -> StateT (\sA -> Identity ((\state2 -> Tuple value2 state3) sA)))

-- Re-expose main function
f = bind (state (\initialState -> Tuple value1 state2)) func
  where
  func = (\value1 -> StateT (\sA -> Identity ((\state2 -> Tuple value2 state3) sA)))

-- Omit the "where" clause for now,
-- but still keep `func` in the first line so we can come back to it
f = bind (state (\initialState -> Tuple value1 state2)) func

-- Recall what StateT's MonadState implementation is...
instance Monad m => MonadState s (StateT s m) where
  state f = StateT (\s -> pure $ g s)
-- ... and use it to replace `state`'s LHS with its RHS
f = bind (StateT (\s -> pure $ g s)) func
  where
  g = (\initialState -> Tuple value1 state2)

-- Bump `g` into `f`
f = bind (
      StateT (\sZ -> pure $ (\initialState -> Tuple value1 state2) sZ)
    ) func

-- Rename the "pure" to "pureID" to help us remember that it's Identity's pure
f = bind (
      StateT (\sZ -> pureID $ (\initialState -> Tuple value1 state2) sZ)
    ) func

-- apply "pureID" to its argument
f = bind (
      StateT (\sZ -> Identity ((\initialState -> Tuple value1 state2) sZ))
    ) func

-- Recall what StateT's bind instance is...
instance (Monad m) => Bind (StateT s m) where
  bind :: forall a b. StateT s m a -> (a -> StateT s m b) -> StateT s m b
  bind (StateT g) f = StateT (\sY -> (g sY) >>= func2)
    where
    func2 = (\(Tuple value1 sX) -> let (StateT h) = f    value1 in h sX)
-- ... and replace its LHS with its RHS
f =
    StateT (\sY -> (g sY) >>= func2)
    ) func
    where
    g = (\sZ -> Identity ((\initialState -> Tuple value1 state2) sZ)
    func2 = (\(Tuple value1 sX) -> let (StateT h) = func value1 in h sX)

-- Re-expose "func" argument
f = StateT (\sY -> (g sY) >>= func2)
    where
    g = (\sZ -> Identity ((\initialState -> Tuple value1 state2) sZ))
    func2 = (\(Tuple value1 sX) -> let (StateT h) = func value1 in h sX)
    func = (\value1 -> StateT (\sA -> Identity ((\state2 -> Tuple value2 state3) sA)))

-- Finished!
finalFunction = StateT (\sY -> (g sY) >>= func2)
    where
    g = (\sZ -> Identity ((\initialState -> Tuple value1 state2) sZ))
    func2 = (\(Tuple value1 sX) -> let (StateT h) = func value1 in h sX)
    func = (\value1 -> StateT (\sA -> Identity ((\state2 -> Tuple value2 state3) sA)))

Running a StateT with an Initial Value

To run a StateT, we just need to unwarp the StateT newtype wrapper.:

runStateT :: forall s m a. StateT s m a -> s -> m (Tuple a s)
runStateT (StateT f) initS = f initS

Reducing a runStateT Call

We'll run the de-sugared do-notation function from above on some initial state, initS. This won't produce anything important, but shows why/how it still type checks:

-- From above
f = StateT (\sY -> (g sY) >>= func2)
    where
    g = (\sZ -> Identity ((\initialState -> Tuple value1 state2) sZ))
    func2 = (\(Tuple value1 sX) -> let (StateT h) = func value1 in h sX)
    func = (\value1 -> StateT (\sA -> Identity ((\state2 -> Tuple value2 state3) sA)))

-- Now call "runState" with "f" and some initial state
-- where 'm' is Identity
runStateT :: forall s m a. StateFunction s m a -> s -> m (Tuple a s)
runStateT (StateT f) initS = f initS

-- which now becomes
runStateT = (\sY -> (g sY) >>= func2) initS
    where
    g = (\sZ -> Identity ((\initialState -> Tuple value1 state2) sZ))
    func2 = (\(Tuple value1 sX) -> let (StateT h) = func value1 in h sX)
    func = (\value1 -> StateT (\sA -> Identity ((\state2 -> Tuple value2 state3) sA)))

-- hide `func2` and `func`
runStateT = (\sY -> (g sY) >>= func2) initS
    where
    g = (\sZ -> Identity ((\initialState -> Tuple value1 state2) sZ))

-- apply initialState to the argument
runStateT = (g initS) >>= func2
    where
    g = (\sZ -> Identity ((\initialState -> Tuple value1 state2) sZ))

-- bump "g" to the main function
runStateT = ((\sZ -> Identity ((\initialState -> Tuple value1 state2) sZ)) initS) >>= func2

-- Apply `initS` to the `(\sZ -> body)` function
runStateT = (Identity ((\initialState -> Tuple value1 state2) initS)) >>= func2

-- Apply `initS` to the `(\initialState -> body)` function
runStateT = (Identity (Tuple value1 state2)) >>= func2

-- call Identity's ">>="
runStateT = func2 (Tuple value1 state2)

-- Re-expose `func2`
runStateT = func2 (Tuple value1 state2)
    where
    func2 = (\(Tuple value1 sX) -> let (StateT h) = func value1 in h sX)

-- bump "func2" into main function
runStateT =
  (\(Tuple value1 sX) ->
        let (StateT h) = func value1
        in h sX
  ) (Tuple value1 initS)

-- apply Tuple argument to the function
runStateT =
  let (StateT h) = func value1
  in h initS

-- Re-expose `func`
runStateT =
  let (StateT h) = func value1
  in h initS

  where
  func = (\value1 -> StateT (\sA -> Identity ((\state2 -> Tuple value2 state3) sA)))

-- bump "func" into main function
runStateT =
  let (StateT h) =
      (\value1 ->
        StateT (\sA -> Identity ((\state2 -> Tuple value2 state3) sA))
      ) value1
  in h initS

-- apply "value1" to its function
runStateT =
  let (StateT h) =
        StateT (\sA -> Identity ((\state2 -> Tuple value2 state3) sA))
  in h initS)

-- Use pattern matching to extract the function bound to "h"
runStateT =
  let h = (\sA -> Identity ((\state2 -> Tuple value2 state3) sA))
  in h initS)

-- and replace the "h" binding with its definition
runStateT =
          (\sA -> Identity ((\state2 -> Tuple value2 state3) sA)) initS

-- Apply "initS" to `(\sA -> body)` function
runStateT =
                  Identity ((\state2 -> Tuple value2 state3) initS))

-- apply "initS" to `(\state2 -> body)` function
runStateT = Identity (Tuple value2 initS)

-- End result!
runStateT :: forall s m a. m (Tuple a s)
runStateT = Identity (Tuple value2 initS)

Overview

Monad Reader

MonadAsk is used to expose a read-only value to a monadic context. It's default implmentation is ReaderT:

             -- r        m     a
newtype ReaderT readOnly monad finalOutput =
  ReaderT (\readOnly -> monad finalOutput)

-- Pseudo-syntax for combining the type class and ReaderT's instance together
class (Monad m) <= MonadAsk r (ReaderT r m) where
  ask   :: forall a. ReaderT r m a
  ask a = ReaderT (\_ -> pure a)

Do Notation

When writing StateT's do notation, we have a function called get that does not take any argument but still returns a value:

stateManipulation :: State Int Int
stateManipulation =
  get
-- which reduces to
  state (\s -> Tuple s s)
-- which reduces to
  StateT (\s -> Identity (Tuple s s))

-- When we run `stateManipulation` with `runState`...
  runState (StateT (\s -> Identity (Tuple s s))) 0
-- it reduces to...
  unboxIdentity $ (\s -> Identity (Tuple s s)) 0
-- which reduces to
  unboxIdentity (Identity (Tuple 0 0))
-- and finally outputs
  Tuple 0 0

MonadAsk's function works similarly:

type Settings = { editable :: Boolean, fontSize :: Int }
useSettings :: Reader Settings Settings
useSettings = ask
-- which reduces to...
  ReaderT (\r -> Identity r)

-- When we run `useSettings` with `runReader`
  runReader useSettings { editable: true, fontSize: 12 }
-- it reduces to
  unwrapIdentity $ (\r -> Identity r) { editable: true, fontSize: 12 }
-- which reduces to
  unwrapIdentity (Identity { editable: true, fontSize: 12 })
-- which reduces to
  { editable: true, fontSize: 12 }

MonadReader

MonadReader extends MonadAsk by allowing the read-only value to be modified first before being used in one computation.

class MonadAsk r m <= MonadReader r m | m -> r where
  local :: forall a. (r -> r) -> m a -> m a

Derived Functions

MonadReader does not have any derived functions.

MonadAsk has one derived function:

  • asks: apply a function to the read-only value (useful for extracting something out of it, like a field in a settings object)

Laws, Instances, and Miscellaneous Functions

For the laws, see

To see how ReaderT implements its instances

To handle/modify the output of a reader computation:

02-Monad-Ask-Example.purs

module ComputingWithMonads.MonadAsk where

import Prelude
import Effect (Effect)
import Effect.Console (log)
import Control.Monad.Reader.Class (ask, asks)
import Control.Monad.Reader (Reader, runReader)

main :: Effect Unit
main = log $ runReader useSettings { editable: true, fontSize: 12 }

type Settings = { editable :: Boolean, fontSize :: Int }

                  --   r                 a
            -- ReaderT Settings Identity String
useSettings :: Reader  Settings          String
useSettings = do
  entireSettingsObject <- ask
  specificField <- asks (_.fontSize)

  pure ("Entire Settings Object: " <> show entireSettingsObject <> "\n\
        \Specific Field: " <> show specificField)

03-Monad-Reader-Example.purs

module ComputingWithMonads.MonadReader where

import Prelude
import Effect (Effect)
import Effect.Console (log)
import Control.Monad.Reader.Class (ask, local)
import Control.Monad.Reader (Reader, runReader)

main :: Effect Unit
main = log $ runReader useSettings { editable: true, fontSize: 12 }

type Settings = { editable :: Boolean, fontSize :: Int }

                  --   r                 a
            -- ReaderT Settings Identity String
useSettings :: Reader  Settings          String
useSettings = do
  original <- ask

  output <- local
    -- a function that modifies the read-only value...
    (\settings -> settings { fontSize = 20 })
      -- which is used in only one computation
      ask

  original_ <- ask

  pure (
        "Original: " <> show original <> "\n\
        \Output of computation that uses modified value: " <> show output <> "\n\
        \Back to normal: " <> show original_
        )

MonadState

MonadState is used to run state manipulating functions. Since only one type implements the class, we'll combine the class' definition and instance into one block:

newtype StateT state monad output =
  StateT (\state -> monad (Tuple output state))

-- Pseudo syntax: combines class and instance into one block:
class (Monad m) <= MonadState s (StateT s m) where
  state :: forall a. (s -> Tuple a s) -> StateT s m a
  state f = StateT (\s -> pure $ f s)

Reading Its Do Notation

stateT_do_notation :: StateT State Value
stateT_do_notation = do
    value1 <- state (\initialState -> Tuple value1 state2)
    value2 <- state (\state2       -> Tuple value2 state3)
    value3 <- state (\state3       -> Tuple value3 state4)
    state (\state4 -> Tuple value4 state5)

Derived Functions

As we saw above, whenever we wrote state function, function always had to wrap our output into a Tuple type:

(\state -> {- do stuff -} Tuple output nextState)

This gets tedious really fast. Fortunately, MonadState's derived functions remove that boilerplate and emphasize the developer's intent:

  • get: returns the state
  • gets: applies a function to the state and returns the result (useful for extracting some value out of the state)
  • put: overwrites the current state with the argument
  • modify: modify the state and return the updated state
  • modify_: same as modify but return unit so we can ignore the binding <- syntax
sideBySideComparison :: State Int String
sideBySideComparison = do
  state1  <- state (\s -> Tuple s s)
  state2  <- get

  shownI1 <- state (\s -> Tuple (show s) s)
  shownI2 <- gets show

  state (\s -> Tuple unit 5)
  put 5

  added1A <- state (\s -> let s' = s + 1 in Tuple s' s')
  added1B <- modify (_ + 1)

  state (\s -> Tuple unit (s + 1))
  modify_ (_ + 1)

  -- to satisfy the type requirements
  -- in that the function ultimately returns a `String`
  pure "string"

Returning to our previous example, crazyFunction was implemented like so:

  1. Take some initialState value
  2. Pass that value into add1 :: State -> Tuple Int Int, which returns Tuple value1 state2
  3. Pass value and state2 into addValue1StringLengthTo :: Int -> Int -> Tuple String Int where
    • value will be converted into a String, called valueAsString
    • the length of valueAsString will be added to state2, which produces state3
    • state3 is converted into a String, called value2
    • the function returns Tuple value2 state3
  4. Return addStringLengthTo's output: Tuple value2 nextState3

With MonadState, we would now write:

crazyFunction :: State Int String
crazyFunction = do
  value1 <- modify (_ + 1)
  modify_ (_ + (length $ show value1))
  gets show

main :: Effect Unit
main =
  case (runState crazyFunction 0) of
    Tuple theString theInt -> do
      log $ "theString was: " <> theString  -- "2"
      log $ "theInt was: " <> show theInt   --  2

runState :: forall s a. StateT s Identity a -> s -> Tuple a s
runState stateT initialState =
  let (Identity tuple) = runStateT stateT initialState
  in tuple

runStateT :: forall s m a. StateT s m a -> s -> m Tuple a s
runStateT (StateT f) initialState = f initialState

Laws, Instances, and Miscellaneous Functions

For the laws, see MonadState's docs

For its instances, see:

To handle/modify the output of a state computation:

02-Monad-State-Example.purs

module ComputingWithMonads.MonadState where

import Prelude
import Effect (Effect)
import Effect.Console (log)
import Data.Tuple (Tuple(..))
import Data.String.CodePoints (length)
import Control.Monad.State.Class (state, get, gets, put, modify, modify_)
import Control.Monad.State (State, runState)

main :: Effect Unit
main =
  case (runState sideBySideComparison 0) of
    Tuple s i -> do
      log $ "s was: " <> s
      log $ "i was: " <> show i

      case (runState crazyFunction 0) of
        Tuple theString theInt -> do
          log $ "theString was: " <> theString  -- "2"
          log $ "theInt was: " <> show theInt   --  2

crazyFunction :: State Int String
crazyFunction = do
  value1 <- modify (_ + 1)
  modify_ (_ + (length $ show value1))
  gets show

sideBySideComparison :: State Int String
sideBySideComparison = do
  state1  <- state (\s -> Tuple s s)
  state2  <- get

  shownI1 <- state (\s -> Tuple (show s) s)
  shownI2 <- gets show

  state (\s -> Tuple unit 5)
  put 5

  added1A <- state (\s -> let s' = s + 1 in Tuple s' s')
  added1B <- modify (_ + 1)

  state (\s -> Tuple unit (s + 1))
  modify_ (_ + 1)

  -- to satisfy the type requirements
  -- in that the function ultimately returns a `String`
  pure "string"

Overview

Monad Tell

MonadTell is used to return additional non-output data that is generated during a computation. For example, it can provide some sort of analysis of the computation we have just performed. It's default implementation is WriterT.

Since we can only return one object and we want to return something in addition to the output, we'll need to return a Tuple that wraps the output and additional data. In cases where we already have non-output data and need to "store" another value of non-output data, we'll need to combine the two together, which implies a Semigroup. Lastly, to implement Applicative, we will need an "empty" value of that data, which implies Monoid.

Putting this into code, we get this:

             -- w               m     a
newtype WriterT non_output_data monad output =
  WriterT (monad (Tuple output non_output_data))

-- Pseudo-syntax: combines the type class and instance into one block
class (Monoid w, Monad m) <= MonadTell w (WriterT w m) where
  tell :: w -> m Unit
  tell w = WriterT (pure (Tuple unit w))

Do Notation

Since tell returns an m Unit, which will be discarded in do notation, we'll only be writing:

useReader :: Reader NonOuputData Output
useReader = do                                                          {-
  unit <- tell nonOuputData                                             -}
          tell nonOuputData
  -- without indentation
  tell nonOuputData

Monad Writer

MonadWriter extends MonadTell by enabling a computation's non-output data to be

  • appended via tell and then exposed in the do notation for later usage (listen)
  • appended via tell after it is modified by a function (pass)

Derived Functions

MonadTell does not have any derived functions.

MonadWriter has two:

  • listens: same as listen but modifies the non-output data before exposing it to the do notation
  • censor: modifies the non-output data returned by a computation before appending it via tell.

Laws, Instances, and Miscellaneous Functions

For their laws, see

For WriterT's instances:

To handle/modify the output of a writer computation:

02-Monad-Tell-Example.purs

module ComputingWithMonads.MonadTell where

import Prelude
import Effect (Effect)
import Effect.Console (log)
import Data.Tuple (Tuple(..))
import Control.Monad.Writer.Class (tell)
import Control.Monad.Writer (Writer, runWriter)

type Output = Int
type OtherUsefulData = String

main :: Effect Unit
main = case runWriter writeStuff of
  Tuple output otherUsefulData -> do
    log $ "Computation's output: " <> show output
    log $ "Other useful data:    " <> otherUsefulData

           -- WriterT w                        a
           -- WriterT String          Identity Int
writeStuff :: Writer  OtherUsefulData          Output
writeStuff = do
  tell "first string! "
  -- some computation happens here
  tell "second string! "
  -- some computation happens here
  tell "third string!"

  pure 5 -- final output of using MonadTell

03-Monad-Writer-Example.purs

module ComputingWithMonads.MonadWriter where

import Prelude
import Effect (Effect)
import Effect.Console (log)
import Data.Identity (Identity(..))
import Data.Tuple (Tuple(..))
import Control.Monad.Writer.Class (tell, listen, pass, listens, censor)
import Control.Monad.Writer (Writer, runWriter)
import Control.Monad.Writer.Trans (WriterT(..))

main :: Effect Unit
main = case runWriter writeStuff of
  Tuple output otherUsefulData -> do
    log $ "Computation's output: " <> show output
    log $ "Other useful data:\n\n\t" <> otherUsefulData

type Output = Int
type OtherUsefulData = String

                 --   w                 a
           -- WriterT String Identity Int
writeStuff :: Writer  String          Int
writeStuff = do
  (Tuple output1 usefulData) <- listen $ WriterT (
        -- A computation that...
        Identity (
          -- returns this entire object as output,
          -- so that one can use both the computed output
          -- and the OtherUsefulData instance in later computations
          Tuple
            -- the output
            1
            -- the instance, which is appended via `tell` to
            -- the current instance of OtherUsefulData
            "useful data"
        )
    )
  tell $ "\n\n\t" <> "(Listen) 'output1' was: " <> show output1
  tell $ "\n\n\t" <> "(Listen) 'usefulData' was: " <> usefulData
      -- needed to improve reading:
      --  turns "usefulDatasomeData" into
      -- "usefulData" <> "\n\n\t" <> "someData"
      <> "\n\n\t"

  Tuple output2 modifiedData <- listens

    -- 2) ... a modified version of the non-output data
    --          which is not appended via `tell`
    --        before exposing it to the do notation
    (\someData -> "Modified (" <> someData <> ")") $
    WriterT (
        -- 1) A computation...
        Identity (
          -- 2) that returns this entire object as output...,
          -- so that one can use both the computed output and...
          Tuple
            -- the output (this is `output2`)
            2
            -- the instance, which is appended via `tell` to
            -- the current instance of OtherUsefulData
            "someData"
        )
    )
  tell $ "\n\n\t" <> "(Listens) two: " <> show output2
  tell $ "\n\n\t" <> "(Listens) modified Data: " <> show modifiedData

  output3 <- pass $ WriterT (
      -- A computation...
      Identity (

        -- that returns 3 things using a nested Tuple
        --    nested tuple: (Tuple (Tuple one three) two)
        Tuple
          (Tuple
            -- 1) the computation's output (this is `output3`)
            4
            -- 3) a function which modifies the non-output data
            --    before appending it to the current non-output data
            --    instance
            (\value -> "\n\n\t" <> "(in `pass`) Value is: " <> value)
          )
          -- 2) the non-output data
          "value"
        )
    )

  tell $ "\n\n\t" <> "(Pass) output2 was: " <> show output3
    -- needed to improve reading:
    --  turns "4someData" into
    -- "4" <> "\n\n\t" <> "someData"
    <> "\n\n\t"

  output4 <- censor
      -- 3) a function which modifies the non-output data
      --    before appending it to the current non-output data
      --    instance
      (\value -> "(in `censor`) Value is: " <> value) $ WriterT (
        -- 1) A computation...
        Identity (
          -- ...that returns two things
          Tuple
            -- a) the computation's output (this is `output4`)
            2
            -- b) the non-output data, which is...
            "someData"
        )
      )

  tell $ "\n\n\t" <> "(censor) output3 was: " <> show output4

  pure 0

Overview

MonadThrow

MonadThrow is used to immediately stop bind's sequential computation and return a value of its error type because of some unforeseeable error (e.g. error encountered when connecting to a database, file that was supposed to exist did not exist, etc).

It's default implmentation is ExceptT:

             -- e     m     a
newtype ExceptT error monad output =
  ExceptT (monad (Either error output))

-- Pseudo-syntax: combines class and instancee together:
class (Monad m) => MonadThrow e (ExceptT e m) where
  throwError :: forall a. e -> ExceptT e m a
  throwError a = ExceptT (pure $ Left a)

ExceptT: Before and After

Before using ExceptT, we would write this ugly verbose code:

getBestCompany :: Industry -> Effect (Either Error Company)
getBoss :: Company -> Effect (Either Error Name)

main :: Effect Unit
main = do
  eitherCompany <- getBestCompany ComputerIndustry
  case eitherCompany of
    Left error -> log $ "Error: " <> show error
    Right bestCompany -> do
      eitherName <- getBoss bestCompany
      case eitherName of
        Left error -> log $ "Error: " <> show error
        Right name -> do
          log $ "The name of the best company is: " <> name

After using ExceptT, we would write this clear readable code:

getBestCompany :: Industry -> Effect (Either Error Company)
getBoss :: Company -> Effect (Either Error Name)

main :: Effect Unit
main = do
  eitherResult <- runExceptT do
    bestCompany <- getBestCompany ComputerIndustry
    getBoss bestCompany
  case eitherResult of
    Left error -> log $ "Error: " <> show error
    Right name -> do
      log $ "The name of the best company is: " <> name

MonadError

MonadError extends MonadThrow by enabling a monad to catch the thrown error, attempt to handle it (by changing the error type to an output type), and then continue bind's sequential computation. If catchError can't handle the error, bind's sequential computation will still stop at that point and return the value of the error type.

newtype ExceptT e m a = ExceptT (m (Either e a))

class (Monad m) => MonadError e (ExceptT e m) where
  catchError :: forall a. ExceptT e m a -> (e -> ExceptT e m a) -> ExceptT e m a
  catchError (ExceptT m) handleError =
    ExceptT (m >>= (\either_E_or_A -> case either_E_or_A of
      Left e -> case handleError e of ExceptT b -> b
      Right a -> pure $ Right a))

For example,

getFileContents :: forall m.
                   MonadError m =>
                   String ->
                   m String
getFileContents pathToFile = do
  readFileContents pathToFile `catchError` \fileNotFound ->
    pure defaultValue

  where
    defaultValue = "foo"

Derived Functions

MonadThrow does not have any derived functions.

MonadError has 3 functions:

  • catchJust: catch only the errors you want to try to handle and ignore the others
  • try: expose the error value (if computation fails) for usage in the do notation
  • withResource: whether a computation fails or succeeds, clean up resources after it is done

Do Notation

Since MonadThrow/MonadError are error-related, we'll show the do notation in meta-language here since it will be harder to do so in the code examples:

-- MonadThrow
stopped <- throwError e
value1 <- otherComputation stopped
value2 <- otherComputation value1

-- MonadError
mightRun <- computationThatMayFail `catchError` computationWhenPreviousFailed

left_Error   <- try computationThatFails
right_Output <- try computationThatSucceeds

output <- withResource getResource cleanup computationThatUsesResource

Laws, Instances, and Miscellaneous Functions

For its laws, see

For ExceptT's instances, see

To handle/modify the output of an error computation:

02-Monad-Throw-Example.purs

module ComputingWithMonads.MonadThrow where

import Prelude
import Effect (Effect)
import Effect.Console (log)
import Data.Identity (Identity(..))
import Data.Either (Either(..))
import Control.Monad.Error.Class (throwError)
import Control.Monad.Except (Except, runExcept)
import Control.Monad.Except.Trans (ExceptT(..))

compute :: Except String Int -> Effect Unit
compute theComputation =
  case runExcept theComputation of
    Left error   -> log $ "Failed computation! Error was:  " <> error
    Right output -> log $ "Successful computation! Output: " <> show output

main :: Effect Unit
main = do
  compute $ ExceptT (
    -- A computation
    Identity (
      -- that was successful and produced output
      Right 5
    )
  )

  compute $ ExceptT (
    -- A computation
    Identity (
      -- that failed and produced an error
      Left "Example error!"
    )
  )

  compute (
    -- a successful computation
    (ExceptT (Identity (Right 5))) >>= (\right_five ->
      (throwError "the next bind will never run!") >>= (\leftE_or_RightA ->
        case leftE_or_RightA of
          Left e -> ExceptT (pure $ Left "This is a different error message!")
          Right a -> ExceptT (pure $ Right $ a + 5)
      )
    )
  )

03-Monad-Error-Example.purs

module ComputingWithMonads.MonadError where

import Prelude
import Effect (Effect)
import Effect.Console (log)
import Data.Identity (Identity(..))
import Data.Either (Either(..))
import Data.Maybe (Maybe(..))
import Control.Monad.Error.Class (catchError, catchJust, try, withResource)
import Control.Monad.Except (Except, runExcept)
import Control.Monad.Except.Trans (ExceptT(..))

main :: Effect Unit
main = do
  runMainFunction
  log "=== Derived Functions ==="
  example_catchJust
  example_try
  example_withResource

computationThatFailsWith :: forall e. e -> Except e Int
computationThatFailsWith error = ExceptT (
    -- A computation
    Identity (
      -- that failed and produced an error
      Left error
    )
  )

computationThatSucceedsWith :: forall e a. a -> Except e a
computationThatSucceedsWith a = ExceptT (
    -- A computation
    Identity (
      -- that succeeded and produced the output
      Right a
    )
  )

compute :: forall e a. Show e => Show a => Except e a -> Effect Unit
compute theComputation =
  case runExcept theComputation of
    Left error   -> log $ "Failed computation! Error was:  " <> show error
    Right output -> log $ "Successful computation! Output: " <> show output

runMainFunction :: Effect Unit
runMainFunction = do
  log "catchError:"
  compute (
    catchError
      (computationThatFailsWith "An error string")

      -- and a function that successfully handles the error
      (\errorString -> ExceptT (pure $ Right 5))
  )

  compute (
    catchError
      (computationThatFailsWith "An error string")

      -- and a function that cannot handle the error successfully
      (\errorString -> ExceptT (pure $ Left errorString))
  )

-------------------

data ErrorType
  = FailedCompletely
  | CanHandle TheseErrors

data TheseErrors
  = Error1
  | Error2

example_catchJust :: Effect Unit
example_catchJust = do
  log "catchJust:"
  -- fail with an error that we ARE NOT catching...
  compute
    (catchJust
      ignore_FailedCompletely
      (computationThatFailsWith FailedCompletely)

      -- this function is never run because
      -- we ignore the "FailedCompletely" error instance
      handleError
    )

  -- fail with an error that we ARE catching...
  compute
    (catchJust
      ignore_FailedCompletely
      (computationThatFailsWith (CanHandle Error1))

      -- this function is run because we accept the
      -- error instance. It would also work if we threw `Error2`
      handleError
    )


ignore_FailedCompletely :: ErrorType -> Maybe TheseErrors
ignore_FailedCompletely FailedCompletely  = Nothing
ignore_FailedCompletely (CanHandle error) = Just error

handleError :: TheseErrors -> Except ErrorType Int
handleError Error1 = ExceptT (pure $ Right 5)
handleError Error2 = ExceptT (pure $ Right 6)

instance Show ErrorType where
  show FailedCompletely  = "FailedCompletely"
  show (CanHandle error) = "CanHandle2 (" <> show error <> ")"

instance Show TheseErrors where
  show Error1 = "Error1"
  show Error2 = "Error2"

-------------------

example_try :: Effect Unit
example_try = do
  log "try: "
  compute' (try $ computationThatSucceedsWith 5)
  compute' (try $ computationThatFailsWith "an error occurred!")

-- In `try`, both the error and output isntance is returned,
-- thereby exposing it for usage in the do notation. To account for this,
-- we've modified `compute` slightly below.
-- Also, since we only specify either the error type or the output type above,
-- type inference can't figure out what the other type is. So,
-- it thinks that the unknown type doesn't have a "Show" instance
-- and the compilation fails.
-- Thus, we also specify both types below to avoid this problem.
compute' :: Except String (Either String Int) -> Effect Unit
compute' theComputation =
  case runExcept theComputation of
    Left error   -> log $ "Failed computation! Error was:  " <> show error
    Right e_or_a -> case e_or_a of
      Left e  -> log $ "Exposed error instance in do notation: "  <> show e
      Right a -> log $ "Exposed output instance in do notation: " <> show a

-------------------

data Resource = Resource
instance Show Resource where
  show x = "Resource"

example_withResource :: Effect Unit
example_withResource = do
  log "withResource: "
  compute (
    withResource
      getResource
      cleanupResource
      computationThatUseResource
  )

getResource :: Except String Resource
getResource = computationThatSucceedsWith Resource

cleanupResource :: Resource -> Except String Unit
cleanupResource r =
  -- resource is cleaned up here
  -- and when finished, we return unit
  ExceptT (pure $ Right unit)

computationThatUseResource :: Resource -> Except String Int
computationThatUseResource r = -- do
  -- use resource here to compute some value
  ExceptT (pure $ Right 5)

An Explanation

The following explanation builds upon and modifies this article's explanation:

  • Name of original author: Gabriel Gonzalez
  • License: CC 4.0 International License
  • Changes:
    • Convert code examples to Purescript
    • Renamed attackUnit to attack to reduce characters per line in code sections
    • Omitted section on Algebraic Data Types
    • Omitted section on Kleisli Composition

Why Callbacks Exist

When writing a library (e.g. GUI toolkit, game engine, etc.), one may want the end-developer to be able to run their own custom code at some point. In the example below, the custom code is represented by the type hole, ?doSomething:

attack :: Target -> Effect Unit
attack target = do
  valid <- isTargetValid target
  if valid
  then ?doSomething target
  else ignoreAttack

Since the library developer does not know how the end-developer will use this function, they can convert ?doSomething into a callback function that the end-developer supplies:

-- library developer's code
attack :: Target -> (Target -> Effect Unit) -> Effect Unit
attack target doSomething = do
  valid <- isTargetValid target
  if valid
  then doSomething target
  else ignoreAttack

-- end-developer's code
attack orc reduceLifeBy50

This makes life easy for the library-developer, but not for the end-developer as a large number of nested callbacks can make code very unreadable. (We only need to look at Node.js for an example)

The Continutation Solution

The problem is not the callback function; there is no other solution for the library-developer. The problem is "where" that function appears in attackUnit. In short, the type appears in the argument part of the function (attackUnit_arg) instead of in the return part of the function (attackUnit_return):

-- original version (curried function)
attack ::           Target ->  (Target -> Effect Unit)  -> Effect Unit

-- original version (uncurried function)
attack ::          (Target ->  (Target -> Effect Unit)) -> Effect Unit

-- desugar the last "->" into "Function"
attack :: Function (Target ->  (Target -> Effect Unit))    Effect Unit

-- make function appear in return type
attack :: Function  Target    ((Target -> Effect Unit)  -> Effect Unit)

-- resugar "->"
attack ::           Target -> ((Target -> Effect Unit)  -> Effect Unit)

Effect is a monad, so we can chain multiple sequential computations like that together using bind/>>=. If we can do that for Effect, why not do so for every monad? This changes our type signature of attack:

attack_no_monad ::  Target -> ((Target -> Effect Unit) -> Effect Unit)
attack_monad    ::  Target -> ((Target -> monad  Unit) -> monad  Unit)

That's a lot of code to write each time, so it can be converted into a newtype:

newtype ContT return monad input =
  ContT ((input -> monad return) -> monad return)

attack_no_monad ::  Target -> ((Target -> Effect Unit) -> Effect Unit)
attack_monad    ::  Target -> ((Target -> monad  Unit) -> monad  Unit)
attack_cont     ::  Target -> ContT Unit Effect Target

To create a ContT, we create a function whose only argument is the callback function:

ContT (\callbackFunction -> do
  -- everything else we did beforehand...
  callbackFunction arg
  -- everything else we did afterwards...
)

Let's implement it for attack and compare the two approaches:

attack_original :: Target -> (Target -> Effect Unit) -> Effect Unit
attack_original target doSomething = do
  valid <- isTargetValid target
  if valid
  then doSomething target
  else ignoreAttack

attack_contT ::  Target -> ContT Unit Effect Target
attack_contT target = ContT (\doSomething -> do
    valid <- isTargetValid target
    if valid
    then doSomething target
    else ignoreAttack
  )

And if we didn't want to use a monad, we could use Identity as a placeholder monad:

type Cont return input = ContT return Identity input

Comparing ContT to Another Function

Let's play with ContT for a bit and see what it reminds us of:

-- Original version
newtype ContT return monad input =
  ContT ((input -> monad return) -> monad return)

-- Let's newtype a version of `ContT` when `Identity` is its monad:
newtype ContIdentity return input =
  ContIdentity ((input -> Identity return) -> Identity return)

-- Since `Identity` is merely a placeholer monad,
-- let's remove it, and see what the resulting function's signature is:
contDesugared :: forall input return. ((input -> return) -> return)

-- and if we wanted to run `contDesugared`,
-- we'd need an initial `input` value:
runCont :: forall i r. ((i -> r) -> r) -> (i -> r) -> r
runCont contDesugared callbackFunction = contDesugared callbackFunction

-- Hmm... Doesn't that function's type and body look familiar?
--                   ((i -> r) -> r) -> (i -> r) -> r
apply :: forall a b. (a        -> b) -> a        -> b
apply function arg = function arg

infixr 0 apply as $

Exactly. ContT is just a monad transformer for the apply/$ function. Let's compare them further:

print   10
-- ==
print $ 10
-- ==
apply                print (10)
-- ==
runCont (Cont (\f -> f     (10))) print
-- which reduces to
              (\f -> f     (10))  print
-- which reduces to
                     print (10)

When You Need Two or More Callback Functions

If attack is modified, so that it requires two callback functions, ?doSomethingWithDamage and doSomethingWithBoth, it would seem that our nice solution from above would no longer work since we can only specify one of the two functions:

attackWith :: Target -> Weapon -> ContT Unit Effect Target
attackWith target weapon = ContT (\only1CallbackFunction -> do
    damage <- calculateDamageFor weapon (modifiedBy 1.5)
    ?doSomethingWithDamage damage
    valid <- isTargetValid target
    if valid
    then ?doSomethingWithBoth target damage
    else ignoreAttack
  )

The solution is to pass in a callback function that takes a sum type as its argument. When using ContT/Cont, the callback function is usually called k, so we'll do that here, too:

data AllPossibleInputs
  -- where each constructor wraps the arguments
  -- that will be used in a function
  = DoSomethingWithDamage Damage
  | DoSomethingWithBoth Target Damage

-- Note: This approach requires the callback function to return the same
-- `monadType returnType` type for each output.
callbackFunction :: AllPossibleInputs -> Effect Unit
callbackFunction (DoSomethingWithBoth t d) = doSomethingWithBoth t d
callbackFunction (DoSomethingWithDamage d) = doSomethingWithDamage d

doSomethingWithBoth :: Target -> Damage -> Effect Unit
-- implementation

doSomethingWithDamage :: Target -> Effect Unit
-- implementation

attackWith :: Target -> Weapon -> ContT Unit Effect Target
attackWith target weapon = ContT (\callback -> do
    damage <- calculateDamageFor weapon (modifiedBy 1.5)
    callback (DoSomethingWithDamage damage)
    valid <- isTargetValid target
    if valid
    then callback (DoSomethingWithBoth target damage)
    else ignoreAttack
  )

(I think one might be able to get around the runtime box overhead imposed by AllPossibleInputs by using type-level programming and Variant, the open Either type.)

Consider Your Perspective

If you are...... and you come across a situation where...... then you should...
a library developeryou want to use a callback functionmove it from the function's LHS to its RHS using ContT. If it should take different kinds of input, define a sum type as demonstrated above.
a developer using a libraryyou need to use a function that requires or returns a ContTunderstand that you need to specify what to do at specific points by passing in a callback function via runCont/runContT. You may also need to write a callback function that takes a sum type like that demonstrated above as its input

MonadCont

MonadCont, the Continuation Monad, is used to handle callback hell among other things.

           -- r      m     a
newtype ContT return monad input =
  ContT ((input -> monad return) -> monad return)

-- Pseudo-Syntax: combine class and instance into one block
-- and "n" represents ContT:
class (Monad m) <= MonadCont r (ContT r m) where
  callCC :: forall a. (forall b. (a -> n b) -> n a) -> n   a
  callCC :: forall a. (forall b. ContT b m a)       -> ContT r m a

Derived Functions

None!

Laws, Instances, and Miscellaneous Functions

There aren't any laws!

Instances:

To handle/modify the output of a continuation computation:

03-Monad-Cont-Example.purs

module ComputingWithMonads.MonadCont where

import Prelude
import Effect (Effect)
import Effect.Console (log)
import Control.Monad.Cont.Trans (ContT(..), runContT)

-- The sum type example we mentioned beforehand got long
-- and introduces a lot of noise. So, we'll do a very simple
-- example using Effect and `log` instead.
main :: Effect Unit
main = do
  runContT (exampleCallCC 5) log
  log "\n\n"
  runContT (exampleCallCC 20) (\s -> do
    log $ "Indenting the string"
    log $ "\t\t" <> s
    )

exampleCallCC :: Int -> ContT Unit Effect String
exampleCallCC arg = ContT $ \k -> do
  log "do some computations here..."
  k $ "callback with arg: " <> show arg
  log "do some other computations here..."
  k $ "callback with arg: " <> show 1.5
  log "do some other computations here..."

Other Monad Transformers

Usable Now

RWS

RWS a convenience monad that combines ReaderT, WriterT, and StateT into the same monad type

MaybeT

MaybeT, a computation the returns a Maybe a. The below code snippet shows why you'd use it and when:

Before (ugly verbose code):

getName :: Effect (Maybe String)
getAge :: Effect (Maybe Int)

main :: Effect Unit
main = do
  maybeName <- getName
  case maybeName of
    Nothing -> log "Didn't work"
    Just name -> do
      maybeAge <- getAge
      case maybeAge of
        Nothing -> log "Didn't work"
        Just age -> do
          log $ "Got name: " <> name <> " and age " <> show age

After (clear readable code):

getName :: Effect (Maybe String)
getAge :: Effect (Maybe Int)

main :: Effect Unit
main = do
  result <- runMaybeT do
    name <- MaybeT getName
    age <- MaybeT getAge
    pure { name, age }
  case result of
    Nothing -> log "Didn't work"
    Just rec -> do
      log $ "Got name: " <> rec.name <> " and age " <> show rec.age

You can refer to Monday Morning Haskell's post on MaybeT for more context. Replace IO with Effect and you'll get the idea.

ListT

ListT, a monad that returns a List a. In addition, it provides the regular list functions you'd expect

It follows the same idea as MaybeT above.

MonadGen

MonadGen (not included in the purescript-transformers library): generates random data. (This is used in the Testing library later. We'll cover it when we get there.)

MonadRec

MonadRec (not included in the purescript-transformers library): guarantees stack safety for monad transformers. For a tutorial on this type class, see Design Patterns/Stack Safety.md.

Sketches of Monadic Control Flow

I'd like to clean this up and provide more, but I don't know what it's copyright is. Thus, I'm only linking to it here. The below link is a visual idea as to what occurs when one uses some of these monad transformers: Monads, a Field Guide

Requires More Understanding

We previously mentioned that all lawful type classes that find their roots in Category Theory have duals. The following are monad transformers for the duals of some of the monad transformers we covered here.

To quickly summarize how they work, mapping a monadic function would change its output type whereas mapping a comonadic function would change its input type. If the definition of Functor's map for a monadic function is compose/<<<, the definition for a comonadic function is composeFlipped/>>>. See the CoMonads by Example repository for a better overview of these type classes and their implementations.

State-like Transformers

ReaderT, StateT, and WriterT can all be used to simulate state manipuation effects.

ReaderT can "modify state" in a top-down direction via MonadReader's local function:

foo :: Reader Int Int
foo = do
  local (_ + 1) do
    local (_ + 1) do
      local (_ + 1) do
        local (_ + 1) do
          stateAtThisPoint <- ask
          pure stateAtThisPoint

bar = foo 0 -- 0 + 1 + 1 + 1 + 1 == 4

WriterT can "modify state" in a bottom-up direction via MonadTell's tell function

foo :: Int -> WriterT (Array (Array String)) Int
foo i
  | i == 3 = pure 3
  | otherwise = do
      y <- foo (i + 1)
      tell [[ y ]]

bar = foo 0 -- Tuple 1 [ [3], [2], [1] ]
{-
which breaks down to...

bar = do
  one <- do
    two <- do
      three <- pure 3
      tell [ [ three ] ]
    tell [ [ two ] ]
  tell [ [ one ] ]
-}

StateT can "modify state" in a top-down and/or bottom-up direction via MonadState's get/put/modify functions.

See also What is the difference between ST effect / Reader / Writer ?

How MonadTrans Works

Reviewing Old Ideas

Thus far, we've overviewed individual Monad Transformers. However, we have not yet combined them into a "stack" that allows us to write anything useful. It's now time to reveal this. It will look similar to something we've seen before:

class LiftSourceIntoTargetMonad sourceMonad targetMonad where {-
  liftSourceMonad :: forall a. sourceMonad a -> targetMonad a -}
  liftSourceMonad ::           sourceMonad   ~> targetMonad

instance LiftSourceIntoTargetMonad Box2 Box1 where {-
  liftSourceMonad :: forall a. Box2 a -> Box1 a                      -}
  liftSourceMonad ::           Box2   ~> Box1
  liftSourceMonad (Box2 a) = Box1 a

When we introduced LiftSourceIntoTargetMonad, we mentioned that implementing this idea for two monads might be much more complicated than the above implementation. Why? Because we were referring to the newtyped function monads we explained in this folder (not exactly something you want to introduce to a new learner immediately).

We stated beforehand that MonadState's default implmenetation is StateT. bind only returns the same monad type it originally received. Thus, we have a problem if we want to use two effects at once. For example, if we to write a program that uses state manipulation effects (i.e. uses MonadState) and some other effects in the same computation (i.e. another type class introduced in this folder like MonadReader).

However, because our monadic newtyped functions serve only to "transform" the base monad by handling all the 'behind the scenes' stuff, it's actually possible to support all of these effects within the same monadic type. One monadic type is nested in another. This is where the "stack" idea comes from. In other words, we need to define a way to "lift" a monadic newtyped function into another. However, the direction should go both ways (former lifted into latter and latter lifted into former).

Since this idea is an abstraction that will repeat, will define it as a type class called, MonadTrans:

class MonadTrans t where
  lift :: forall m a. Monad m => m a -> t m a

-- using clearer type names, one should read it as
class MonadTrans transformerCloserToBaseMonad where
  lift :: forall transformerFartherFromBaseMonad a.
          Monad transformerFartherFromBaseMonad =>
          transformerFartherFromBaseMonad a ->
          transformerCloserToBaseMonad transformerFartherFromBaseMonad a

Explaining Its Process

  1. Define a type class (e.g. MonadState) that has a default implementation via (e.g. StateT)
  2. Define a type class (e.g. MonadTrans) that enables one monad to be lifted into another monad
  3. To enable multiple monadic newtyped functions to run in another one (e.g. StateT), implement MonadTrans for that monad (e.g. StateT's instance)
  4. To grant multiple monadic newtyped functions the capabilities of one type class (e.g. MonadState via StateT), make those other monads implement the one monad's type class (e.g. MonadState) in a special way (see below for a pattern):

Looking at those implementations above, we can see a general pattern (there are some exceptions to this due to how the types need to be handled, but this is generally true):

  • Require one of the types in the instance context to be an instance of MonadState
  • Implement the type class by lifting the instance into the monad and delegate that monad's implementation to that instance. Again, the monadic newtyped functions are merely handling the "behind the scenes" stuff of the effects. At the end of the day, it's still the base monad that actually makes the whole thing work.

Thus, [Word]T provides a default implementation for Monad[Word] and makes it possible to grant the base monad its capability.

In short, this type class enables us to use all of the functions from each type class explained in this folder

Using MonadTrans

When we wrote code for our MonadState example, we had something that looked like this:

type Output = Int
type StateType = Int
computation :: State StateType Output
computation = do
  modify_ (_ + 1)
  modify_ (_ * 10)
  modify_ (_ + 1)

main :: Effect Unit
main = case runState computation 0 of
  Tuple output state -> do
    log $ "Result of computation: " <> show output
    log $ "End state of computation: " <> show state

The above code works because we're using MonadState behind the scenes via StateT's instance. However, this function's type signature restricts us to only using StateT for computations. If we want to define computation, so that it can use functions from MonadWriter, we'll need to use a different approach. Let's fix this one step at a time.

First, we'll abstract our State type into MonadState by using a type constraint:

type Output = Int
type StateType = Int
computation :: forall m => MonadState StateType m => m Output
computation = do
  modify_ (_ + 1)
  modify_ (_ * 10)
  modify_ (_ + 1)

-- use a helper function to tell the type inferer that
-- `computation`'s `m` type is `StateT`
runProgram :: State StateType Output -> Tuple Output StateType
runProgram s = runState s 0

main :: Effect Unit
main = case runProgram computation of
  Tuple string state -> do
    log $ "Result of computation: " <> string
    log $ "End state of computation: " <> show state

Second, we'll add another type class constraint for MonadAsk to expose it's tell function:

type Output = Int
type StateType = Int
type NonOutputData = String
computation :: forall m
          . MonadState StateType m
         => MonadAsk NonOuputData m
         => m Output
computation = do
  modify_ (_ + 1)
  tell "Modified state by adding 1"
  currentState <- modify (_ * 10)
  tell $ "Modified state by multiplying by 10. It is now "
    <> show currentState
  modify_ (_ + 1)

Great! We now have a single computation that can do both state manipulation and use tell. However, how does that affect runProgram?

runProgram :: StateT_and_WriterT -> StateT_and_WriterT_Output
runProgram s = -- ???

When we used a monad (e.g. WriterT) to run a computation, we didn't need to specify the monad type being used. So, we used Identity as a placeholder monad and used the type alias, Writer, to make it easier to write. To use another computational monad (e.g. StateT) inside of Writer, we now need to specify what that monad is by re-exposing the T part of WriterT and replacing Identity with StateT. Putting it differently, WriterT is now transforming the monad, StateT with additional effects, which is likewise transforming the base monad, Identity with additional effects:

-- simple writer computation
writer :: Writer NonOutputData Output -> Tuple NonOuputData Output
writer w = runWriter w

-- re-expose the T part of WriterT
writer :: WriterT NonOutputData Identity Output -> Tuple NonOuputData Output
writer w = runWriter w

-- swap `Identity` with a type alias called Computation
type Computation = Identity
writer :: WriterT NonOutputData Computation Output -> Tuple NonOuputData Output
writer w = runWriterT w

-- Since the types will get long soon, break up the type signature
type Computation = Identity
writer :: WriterT NonOutputData Computation Output
       -> Tuple NonOuputData Output
writer w = runWriterT w

-- StateT with its T exposed but set to Identity still
state :: StateT State Identity Output -> Tuple Output State
state s = runState s initialState

-- re-alias Computation to StateT
-- and use `runWriterT` instead of `runWriter`
type Computation = StateT State Identity Output
writer :: WriterT NonOutputData Computation Output
       -> Tuple NonOuputData Output
writer w = runWriterT w

-- getting rid of the type alias and inlining its type
-- and rename the function's name to 'runProgram'
runProgram :: WriterT NonOutputData (StateT State Identity stateOutput) Output
           -> finalOutput
runProgram ws = ???

-- Realizing that `StateT` with all three of its types specified
-- now has kind "Type" and is thus no longer a monad ("Type -> Type"),
-- we remove the `stateOutput` type to increase
-- StateT's kind from "Type" to "Type -> Type", making it a monad again
-- so that it satisfies WriterT's monadic type requirement
runProgram :: WriterT NonOutputData (StateT State Identity) Output
           -> finalOutput
runProgram ws = ???

-- To run StateT, we also need an `initialState` argument. Let's add it
runProgram :: WriterT NonOutputData (StateT State Identity) Output
           -> State
           -> finalOutput
runProgram ws initialState = ???

A few questions arise as we do this:

  1. What should finalOutput's type be if we combine the two monad transformers together?
  2. How should program's body be implemented?

The types give us a few clues for a top-down explanation. First (answering question 2), we realize that State's monad type is still Identity since no other monad type is inside of StateT. Thus, we know that we'll need to use runState to unpack its results. Using this line of reasoning, we'll also need to use runWriterT instead of runWriter because the WriterT type is using a non-Identity monad.

That leaves us with two possible options:

  • runWriterT (runState ws initialState)
  • runState (runWriterT ws) initialState

Second (answering question 1), we know that running a StateT returns Tuple stateOutput state and running a WriterT returns Tuple writerOutput nonOutputData. That means we'll likely get something close to one of these options:

  1. Both outputs are returned using a Tuple that groups them together:
    • Tuple (Tuple stateOutput state) (Tuple writerOutput nonOutputData) (or vice versa in its order)
  2. The output of one monad is the stateOutput (a) or writerOutput (b) of the other:
    • a: Tuple (Tuple writerOutput nonOutputData) state
    • b: Tuple (Tuple stateOutput state ) nonOutputData

Since we're running one monad inside of another, the second option seems more likely.

The question is, which one is correct?

Let's continue by examining runStateT/runState and runWriterT/runWriter. We know that runMonad is just a wrapper around runMonadT when the monad is Identity:

newtype StateT s m a =
  StateT (s -> m (Tuple a s))

runState :: StateT state Identity output -> state -> Tuple output state
runState s initialState = unwrapIdentity $ runStateT s initialState

runStateT :: StateT state monad output -> state -> monad Tuple output state
runStateT (StateT function) initialState = function initialState

newtype WriterT w m a =
  WriterT (m (Tuple a w))

runWriter :: WriterT nonOutputData Identity output -> Tuple output nonOutputData
runWriter w = unwrapIdentity $ runWriterT w

runWriterT :: WriterT nonOutputData monad output -> monad Tuple output nonOutputData
runWriterT w = w

They key takeaway here is that run[Monad]T returns the same monad that is specified in [Monad]T. Looking at our function again...

runProgram :: WriterT NonOutputData (StateT State Identity) Output
           -> State
           -> finalOutput
runProgram ws initialState = ???

... running the WriterT NonOuputData monad output via runWriterT will return its monad type. Since that monad type is StateT State Identity Output, we will take the output of running WriterT (which outputs a StateT) and run the output via runState since StateT's monad type is Identity:

runProgram :: WriterT NonOutputData (StateT State Identity) Output
           -> finalOutput
runProgram ws = runState (runWriterT ws) initialState

That answers the second question (how to implement runProgram), but it still leaves us wondering what finalOutput is. This is easier to determine if we just look at runState and runWriterT again:

runWriterT :: WriterT nonOutputData monad output -> monad Tuple output nonOutputData

-- reducing `monad Tuple output nonOutputData` to something easier, `m a`
type A = Tuple output nonOutputData
runWriterT :: WriterT nonOutputData monad output -> monad A

-- specializing `monad` to `StateT State identity`
type A = Tuple output nonOutputData
runWriterT :: WriterT nonOutputData (StateT state Identity) output
           -> (StateT state Identity A)

-- re-exposing the `a` in `StateT`
runWriterT :: WriterT nonOutputData (StateT state Identity) output
           -> (StateT state Identity (Tuple output nonOutputData))

-- Looking at what `runState` returns, we see
runState :: StateT state Identity output -> state -> Tuple output state

-- Replacing StateT's `output` type with `Tuple output NonOuputData`
-- we get this:
runState :: StateT state Identity (Tuple writerOutput nonOutputData)
         -> state
         -> Tuple (Tuple writerOutput nonOutputData) state

-- Thus, runProgram's type signature is:
runProgram :: WriterT NonOutputData (StateT State Identity Output) Output
           -> State
           -> Tuple (Tuple output NonOuputData) State
runProgram ws initialState =
  runState (runWriterT ws) initialState

-- hidng `StateT`'s monad type, `Identity`, gets us this:
runProgram :: WriterT NonOutputData (State state) Output
           -> state
           -> Tuple (Tuple Output NonOuputData) state
runProgram ws initialState =
  runState (runWriterT ws) initialState

Reordering the Monad Stack

What happens, however, if we flip the order of the stack? We'll see that the arguments get flipped and the output gets flipped.

runProgram :: StateT state (Writer NonOutputData) Output
           -> state
           -> Tuple (Tuple Output state) NonOuputData
runProgram computation initialState =
  runWriter (runStateT computation initialState)

While the computation's definition did not change, how the code gets run does change.

This creates one problem with "monad stacks:" the order of how the monad transformers are run can change how the computation is evaluated. We'll cover this in more detail later.

Monad Transformer Stacks

You will want to bookmark this page.

Generalizing the idea we discovered in the previous file into a pattern, we get something like this:

program :: forall m
         . MonadState StateType m
        => MonadWriter NonOutputData m
     -- => other needed type classes here in any order...
        => m ProgramFinalOutputType
program = do
  -- use all the functions from the type classes

--  StateT state         monad output
-- "IndexN possibleInput monad typeClassOutput"
runProgram :: Index3 input3 (       -- top of the stack
                Index2 input2 (
                  Index1 input1 (
                    Index0 input0   -- =
                      Identity      -- | bottom of the stack (the base monad)
                    output0         -- =
                  ) output1
                ) output2
              ) output3
        -- -> input0      -- =
        -- -> input1      -- | all needed initial args to
        -- -> input2      -- | `run[Word]T`/`runWord` go here
        -- -> input3      -- =
           -> Tuple (
                Tuple (
                  Tuple (
                    Tuple (
                      computationOutput
                      output3
                    )
                    output2
                  )
                  output1
                )
                output0
              )
runProgram program {- args -} =
  runIndex0 (                               -- bottom of the stack
    runIndex1T (
      runIndex2T (
        runIndex3T program index3Args   -- top of the stack
      ) index2Args
    ) index1Args
  ) index0Args

Note: when using ExceptT, MaybeT, or ListT, the outputs won't necessarily be Tuples.

Monad Trans

MonadTrans enables one computational monad to run inside another, thereby exposing multiple type class' functions for usage in bind/>>= / do notation in the same function. It enables one to write an entire program via newtyped function monads (or functions with monadic syntax).

class MonadTrans t where
  lift :: forall m a. m a -> t m a

Laws

See its docs

Instances

02-Monad-Trans-Example.purs

module ComputingWithMonads.MonadTrans where

import Prelude
import Effect (Effect)
import Effect.Console (log)

import Data.Tuple (Tuple(..))

-- State
import Control.Monad.State.Class (class MonadState, get, gets, modify_)
import Control.Monad.State (State, runState)

-- Writer
import Control.Monad.Writer.Class (class MonadWriter, tell)
import Control.Monad.Writer.Trans (WriterT, runWriterT)

program :: forall m.
           MonadState Int m =>
           MonadWriter String m =>
           m String
program = do
    currentState <- get
    tell $ "Current State is now: " <> show currentState
    modify_ (_ + 10)
    gets show

run :: forall a. WriterT String (State Int) a
    -> Int
    -> Tuple (Tuple a String) Int
run function initialState = runState (runWriterT function) initialState

main :: Effect Unit
main = case run program 0 of
  Tuple (Tuple output nonOutputData) nextState -> do
    log $ "Finished!"
    log $ "(Computation) final output: " <> show output
    log $ "(Writer)   non-output data: " <> show nonOutputData
    log $ "(State)         next state: " <> show nextState

    let (Tuple (Tuple o _ ) _ ) = run program 8
    log $ "Using pattern matching to get the computation's output: " <> show o

Drawbacks of MTL

The following lists some of the issues one will face when using MTL.

Note:

  • Some of the below flaws may be specific only to Haskell and not Purescript.
  • In the below sources, any mention of Haskell's IORef is equivalent to Purescript's Ref: a global mutable variable.

MonadState Allows Only One State Manipulation Type

First, due to the functional dependency from m to s in MonadState's definition, it's impossible to do two different state manipulations within the same function. For example...

f :: forall m ouput.
  => MonadState Int m
  => MonadState String m
  -> m output
f = do
  whichValue <- get

The compiler will complain because it doesn't know which value it should 'get'. See the answer to Haskell -- Chaining two states using StateT monad transformer

One solution to this is to store all states in one larger state type and then use a Lens to access/change it:

type IntAndString = { i :: Int, s :: String }
f :: forall m output.
  => MonadState IntAndString m
  -> m output

The second solution is to use type-level programming to specify which MonadState we are referring to via an id Symbol. This would force us to change MonadState's definition to:

class (Monad m) <= MonadState (id :: Symbol) state m | m -> state
  state :: forall a. Proxy id -> (s -> m (Tuple a s)) -> m a

_i :: Proxy "i"
_i = Proxy

_s :: Proxy "s"
_s = Proxy

f :: forall m ouput.
  => MonadState "i" Int m
  => MonadState "s" String m
  -> m output
f = do
  theInt <- get _i
  theString <- get _s

However, I'm not sure what are the pros/cons of this approach, but this is similar to how Run (explained in the Free folder) enables two different state manipulations.

MonadState & MonadWriter lose their state on a runtime error

If a runtime error occurs in a computation that uses MonadState or MonadWriter, then the states in both MonadState and MonadWriter are lost (because the computation halts).

WriterT & RWST has a "space leak" problem

This is largely due to WriterT's usage of Monoid. The 'fix' is to drop some of its features and use a StateT instead. See Writer Monads and Space Leaks - Infinite Negative Utility

Since RWST also encodes things via WriterT, it also suffers from this problem.

N-squared-ish Monad Transformer Instances

Whenever one wants to define a new monad transformer (e.g. MonadAuthenticate) to encode some effect, one must define ~n^2 instances:

  • 1 MonadAuthenticate instance for each [Word]T type via MonadTrans to lift the monadic newtyped AuthenticateT function.
-- Given this stack of monad transformers
runCode :: AuthenticateT Credentials (StateT state (ReaderT value Identity Unit))

-- Each monadic function type (e.g. StateT, ReaderT, etc.) must
-- have an instance for MonadAuthenticate so it can lift the
-- AuthenticateT computation into the next monad.
  • n instances for the monadic newtyped AuthenticateT function, so that it can lift its computation into all the other monad transformer type classes (e.g. AuthenticateT -> MonadState, MonadWriter, etc.)
-- Given this stack of monad transformers
runCode :: ReaderT Value (StateT state (AuthenticateT Credentials Identity Unit))

-- AuthenticateT must lift ReaderT and StateT into an AuthenticateT
-- monadic type.

In short, we define that many instances so that the order of the monad stack does not matter as much. If our stack has an ExceptT somewhere in there, where that type occurs will change the final output.

Note: I say roughly ~n^2 because apparently there are some cases where "lifting" a function would break a law (or something).

Monad transformer stacks' type signatures get complicated quickly

Related to the previous point, but the type signatures start getting crazy very quickly. For new beginners who are just learning about monad transformers, this can be quite offsetting:

-- as an example using pseudo-syntax...
f :: StateT State (ReaderT reader (WriterT writer (ExceptT error Effect output) output))

The Order of the Monad Transformer Stack Matters

We mentioned this previously when covering how to use a monad transformer:

type Output = Int
type StateType = Int
type NonOutputData = String
computation :: forall m
          . MonadState StateType m
         => MonadAsk NonOuputData m
         => m Output
computation = do
  modify_ (_ + 1)
  tell "Modified state by adding 1"
  currentState <- modify (_ * 10)
  tell $ "Modified state by multiplying by 10. It is now "
    <> show currentState
  modify_ (_ + 1)

-- Both `program1` and `program2` support the necessary
-- capabilities to run `computation`.
runProgram1 :: WriterT NonOutputData (State state) Output
            -> state
            -> Tuple (Tuple Output NonOuputData) state
runProgram1 initialState =
  runState (runWriterT computation) initialState

runProgram2 :: StateT state (Writer NonOutputData) Output
            -> state
            -> Tuple (Tuple Output state) NonOuputData
runProgram2 initialState =
  runWriter (runStateT computation initialState)

Imagine if one of these was ExceptT. That monad transformer's location in the stack can affect how the computation works and whether it works as expected.

The ReaderT/Capability Design Pattern

The ReaderT and Capability Design Patterns

Some of the drawbacks of MTL (though not all of them) are what led to the ReaderT Design Pattern from which I originally got many of the above problems.

This design pattern was interpreted by others in a different way, so that it led to the Capability Design Pattern post.

The main point of the Capability Design Pattern is that the Monad[Word] type classes define what effects will be used in some function, not necessarily how that will be accomplished. This key insight is what makes testing our business logic code much simpler.

For a clearer picture of this idea, see the Three Layer Haskell Cake.

Looking at the above from a top-down perspective, we get this:

Layer LevelOnion Architecture TermGeneral idea
Layer 4CoreStrong types with well-defined properties and their pure, total functions that operate on them
Layer 3Domainthe "business logic" code which uses effects
Layer 2APIthe "production" or "test" monad which "links" these effects/capabilties to their implementations: (i.e. a newtyped ReaderT and its instances)
Layer 1Infrastructurethe platform-specific framework/monad we'll use to implement some special effects/capabilities (i.e. Node.ReadLine/Halogen/StateT)
Layer 0Machine Code
(no equivalent onion term)
the "base" monad that runs the program (i.e. production: Effect/Aff; test: Identity/Trampoline)

Putting it into code, we would get something that looks like this:

-- Layer 4

newtype Name = Name String

getName :: Name -> String
getName (Name s) = s

-- Layer 3

-- Capability type classes:
class (Monad m) <= LogToScreen m where
  log :: String -> m Unit

class (Monad m) <= GetUserName m where
  getUserName :: m Name

-- Business logic that uses these capabilities
-- which makes it easier to test
program :: forall m.
          LogToScreen m =>
          GetUserName m =>
          m Unit
program = do
  log "What is your name?"
  name <- getUserName
  log $ "You name is" <> (getName name)

-- Layer 2 (Production)

-- Environment type
type Environment = { someValue :: Int } -- mutable state, read-only values, etc. go in this record

-- newtyped ReaderT that implements the capabilities
newtype AppM a = AppM (ReaderT Environment Effect a)
derive newtype instance functorTestM    :: Functor AppM
derive newtype instance applyAppM       :: Apply AppM
derive newtype instance Applicative AppM
derive newtype instance bindAppM        :: Bind AppM
derive newtype instance monadAppM       :: Monad AppM
derive newtype instance monadEffect     :: MonadEffect AppM

runApp :: AppM a -> Environment -> Effect a
runApp (AppM reader_T) env = runReaderT reader_T env

-- Layer 1 (the implementations of each instance)
instance LogToScreen AppM where
  log = liftEffect <<< Console.log

instance GetUserName AppM where
  getUserName = liftEffect do
    -- some effectful thing that produces a string
    pure $ Name "some name"

-- Layer 0 (production)
main :: Effect Unit
main = do
  let globalEnvironmentInfo = -- global stuff
  runApp program globalEnvironmentInfo

-----------------------
-- Layer 2 (test)

-- newtyped ReaderT that implements the capabilities for testing
newtype TestM a = TestM (Reader Environment a)
derive newtype instance functorTestM     :: Functor TestM
derive newtype instance applyTestM       :: Apply TestM
derive newtype instance Applicative TestM
derive newtype instance bindTestM        :: Bind TestM
derive newtype instance monadTestM       :: Monad TestM


runTest :: TestM a -> Environment -> a
runTest (TestM reader) env = runReader reader env

-- Layer 1 (test: implementations of instances)
instance LogToScreen TestM where
  log _ = pure unit -- no need to implement this

instance GetUserName TestM where
  getUserName = pure (Name "John") -- general idea. Don't do this in real code.

-- Layer 0 (test)
main :: Effect Unit
main = do
  let globalEnvironmentInfo = -- mutable state, read-only values, etc.
  assert $ (runTest program globalEnvironmentInfo) == correctValue

When to Use it: ReaderT Design Pattern vs Monad Transformer Stack?

Scope of CodeExampleUse
Programming in the large
(e.g. Application Structure)
Connecting impure effects to their pure type classes via an API layerReaderT
Programming in the small
(e.g. a single complicated computation)
Doing one particular computation that uses a number of effects that others in the surrounding context do not useMonad Transformer Stack

Free

Overview

This folder will do 4 things:

  • explain what the Free monad is
  • explain how it can be used to create a pure abstract syntax tree (AST) and interpret that AST into an impure but useful computation
  • explain why one should use Run instead of Free
  • explain the limitations of Free/Run.

Free monads are another way to structure the architecture of your program. However, I wouldn't recommend using this particular way of structuring your program. See this folder's "Drawbacks of Free" for some examples.

Moreover, the gist of Free monads is clearly explained by Nate Faubion in his overview of Free and CoFree: Unrolling Free & Cofree (stop at 1:19:23) (Actual YouTube video name is "PS Unscripted - Free from Tree & Halogen VDOM"). If you watch that video, you do not need to read through the "Why Use the Free Monad" folder's content.

The Free approach deals with the "bind forces us to return the same monad type it receives" restriction by using only one monad. Rather than building a large function that is composed of smaller functions that runs once the initial arguments are given to it (i.e. MTL), the Free approach will create an Abstract Sytax Tree (AST) that describes the desired computation in a pure way. This tree is later "interpreted" via a NaturalTransformation into a base monad (i.e. Effect) that runs those computations in an inpure way. In other words, something akin to

type DSL = DomainSpecificLanguage
type AbstractSyntaxTree output = Free DSL output

defineProgram :: AbstractSyntaxTree
defineProgram = -- implementation

runProgram :: AbstractSyntaxTree ~> Effect
runProgram = -- implementation

What Are "Free" SomeTypeClass Types

When we first introduced type classes, we explained that they are an encapsulation of 2-3 things:

  1. (always) A definition of 1 or more functions/values' type signatures
  2. (almost always) Laws to which a concrete type's implementation of said type class must adhere
  3. (frequently) Functions that a type obtains for free once the core defintion/values are implemented

Moreover, some type classes combine two or more type classes together

Thus, SomeTypeClass isn't so much a 'thing' as much as it is an expectation. We don't say that f is a SomeTypeClass (for it could implement it in various ways); rather, we are really saying that f has an instance that implements SomeTypeClass's specialFunction function in such a way that it adheres to SomeTypeClass's laws. As we saw from the MTL folder, even StateT, a newtyped function, can be called a Functor because it meets all of these requirements.

However, whenever we had a type that we wanted to use in a Functor-like way, we needed to define its Functor instance before we could use it in that way. In other words, we have to write a lot of boilerplate code.

What if we could grant Functor-like capabailities for any type without implementing such an instance? That is the idea behind "free" type classes.

Essentially, a "free" TypeClassName is a box-like type, Wrapper, that grants TypeClassName-capabilities to some other type, A, by providing the necessary structure for implementing a law-abiding TypeClassName instance for Wrapper.

In short, to create a "free" SomeTypeClass, we do 2 things:

  1. Translate the type class into a higher-kinded type
  2. Do the following for each of the type class' functions, starting with the easiest function:
    • Translate one type class' function into a constructor for the new type
    • Try to implement all required instances using the constructor
    • Fix problems that arise

A "Free" Monoid

When we look at Monoid, we see this type class:

class Monoid a where                                                    {-
  append :: a -> a -> a       -- include Semigroup's function           -}
  mempty :: a

-- "hello" <> "world" == "helloworld"
-- "hello" <> mempty  == "hello"
-- mempty  <> "hello" == "hello"

Let's follow the instructions from above: First, we'll translate the type class into a data type that can take any type:

data FreeMonoid a

Second and starting with the easier function mempty, we'll translate it into a constructor for FreeMonoid. mempty is easy, since it translates into a placeholder constructor:

data FreeMonoid a
  = Mempty

instance Semigroup (FreeMonoid a) where
  append a Mempty = a
  append Mempty a = a

instance Monoid (FreeMonoid a) where
  mempty = Mempty

append is a bit harder. We need to store a value of type a, so we can try this:

data FreeMonoid a
  = Mempty
  | Append a

However, if we try to implement this as (Append a1) <> (Append a2), we can't combine a1 and a2. Rather, we need to store both an a1 and an a2 in a single Append:

data FreeMonoid a
  = Mempty
  --       a1 a2
  | Append a  a

-- since `Mempty` is our placeholder instance, we can use it
-- to fill the a2's spot

instance Semigroup (FreeMonoid a) where
  append Mempty Mempty = Mempty
  append a Mempty = a
  append Mempty a = a
  append (Append a1 Mempty) (Append a2 Mempty) = Append a1 a2

-- Works!
(Append a1 Mempty) <> (Append a2 Mempty) == Append a1 a2
-- ...well, not quite!
(Append a1 a2) <> (Append a3 Mempty) == -- ???

Our previous solution doesn't work either. If the failure case above is just another append, we get something like this: ((Append a1 Mempty) <> (Append a2 Mempty)) <> (Append a3 Mempty) Rather than defining a second a for Append, what if we nested the types together? This approach makes our code finally work:

data FreeMonoid a
  = Mempty
  --       a1  Mempty / Append a
  | Append a  (FreeMonoid a     )

instance Semigroup (FreeMonoid a) where
  append Mempty Mempty = Mempty
  append a Mempty = a
  append Mempty a = a
  append (Append a memptyOrAppend) otherAppend =
    Append a (memptyOrAppend <> otherAppend)

instance Monoid (FreeMonoid a) where
  mempty = Mempty

The above code is the exact same thing as a familiar data type, List:

data List a
  = Nil
  | Cons a (List a)

instance Semigroup (List a) where
  append Nil Nil = Nil
  append a Nil = a
  append Nil a = a
  append (Cons head tail) otherList =
    Cons head (tail <> otherList)

instance Monoid (List a) where
  mempty = Nil

Thus, we say that List is a "free" monoid because by wrapping some type (e.g. Fruit) into a List, we get a monoid instance for Fruit for free:

data Fruit
  = Apple
  | Orange
  | Banana

(Cons (Apple Nil)) <> (Cons Banana Nil) == Cons (Apple (Cons Banana Nil))

This idea can be useful for when we have types that can't implement specific type classes.

What Is and Is Not The Free Monad

Since List was the free monoid type, what would be the free monad type?

-- monad class fully expanded...
class Monad f where
  map   :: forall a. (a -> b) -> f a -> f b
  apply :: forall a. f (a -> b) -> f a -> f b
  bind  :: forall a. f a -> (a -> f b) -> f b
  pure  :: forall a. a -> f a

Using our previous instructions...

In short, to create a "free" SomeTypeClass, we do 3 things:

  1. Translate the type class into a higher-kinded type
  2. Do the following for each of the type class' functions, starting with the easiest function:
    • Translate one type class' function into a constructor for the new type
    • Try to implement all required instances using the constructor
    • Fix problems that arise

We'll start with the simplest function, pure:

data FreeMonad a
  = Pure a

instance Applicative FreeMonad where
  pure a = Pure a

instance Functor FreeMonad where
  map f (Pure a) = Pure (f a)

instance Apply FreeMonad where
  apply (Pure f) (Pure a) = Pure (f a)

instance Bind FreeMonad where
  bind (Pure a) f = f a

Well, that was easy... Wasn't this the same implementation as Identity from before? You are correct.

data Identity a = Identity a

instance Applicative Identity where
  pure a = Identity a

instance Functor Identity where
  map f (Identity a) = Identity (f a)

instance Apply Identity where
  apply (Identity f) (Identity a) = Identity (f a)

instance Bind Identity where
  bind (Identity a) f = f a

We can see here that Identity is a free Functor, Apply, Applicative, Bind, and therefore Monad for any type with kind Type.

However, that's not what others mean when they talk about the free Monad type. The Free monad makes any Functor (i.e. type with kind Type -> Type) a monad.

There are other "free" type classes (mentioned below). AFAIK, these types were not discovered at the same time by the same people. Rather, they were discovered over time as solutions to specific problems. See below for these types:

  • Coyoneda - docs & source code - a free Functor for any type of kind Type -> Type.
  • FreeAp - docs & source code - a free Applicative for any type of kind Type -> Type. Here's the related paper, which will likely make more sense once we explain how the Free monad works.
  • Free (the original version) - a free Monad for any Functor. Its implementation suffers from big performance problems when run.
  • Free (reflection without remorse) - docs & source code - a free Monad for any Functor. Its implementation removes the performance penalties of the original version and includes all of the optimizations discovered by Oleg Kiselyov (as stated by someone in PureScript's chatroom).

At this time, Coyoneda and FreeAp will not be discussed in this folder. Rather, the upcoming files will focus entirely on the Free monad.

The Original Free Monad

Rather than explaining how one can eventually reason their way through defining what the type is for the original Free monad (a bottom-up approach), we'll simply show its definition, its instances, and demonstrate why it has to work that way (a top-down approach).

data Free f a
  = Pure a
  | Impure (f (Free f a))

Let's say that Identity is our f/Functor type. What does a concrete value of the Free data type look like?

Impure ( Identity (
  Impure ( Identity (
    Impure ( Identity (
      Pure a
    ))
  ))
))

In other words, Free is just a tree-like data structure of nested Identity values (the branches in our tree) that eventually wrap a final value (the leaf in our tree). In our current example, the tree is unbalanced, so that it appears more like a linked-list than a tree:

{- Impure ( -} Identity (
  {- Impure ( -} Identity (
    {- Impure ( -} Identity (
      {- Pure -}     a
    {- ) -}        )
  {- ) -}        )
{- ) -}        )

The only difference is that Identity itself is wrapped in another type. So how do we change a value that is wrapped in a box-like type? We use Functor's map, of course! We'll use map in most of Free's instances for the needed type classes:

-- easiest one!
instance Applicative (Free f) where
  pure a = Pure a

-- a <#> f == mapFlipped a f == map f a
instance (Functor f) => Functor (Free f) where
  map f (Pure a) = Pure (f a)
  map f (Impure f_of_Free) =
    Impure (f_of_Free <#> (
      -- recursively call `map` on nested `Impure` values
      -- until we get a `Pure` value of Free
      \pure_A -> map f pure_A
      -- which applies the function to the `a`
      -- and then rewraps the `Impure` values
    ))

Let's see map in action via a graph reduction:

-- Start!
map f (
  Impure ( Identity (
    Impure ( Identity (
      Pure 5
    ))
  ))
)
-- Recursively apply `map` until we get a `Pure` value
-- 1.
      (
  Impure ( map f Identity (
    Impure ( Identity (
      Pure 5
    ))
  ))
)
-- 2.
       (
   Impure ( Identity (
     map f Impure ( Identity (
       Pure 5
     ))
   ))
 )
-- 3.
      (
   Impure ( Identity (
     Impure ( map f Identity (
       Pure 5
     ))
   ))
 )
-- 4.
       (
   Impure ( Identity (
     Impure ( Identity (
       map f (Pure 5)
     ))
   ))
 )
-- Now apply the function to pure's value
        (
   Impure ( Identity (
     Impure ( Identity (
       Pure (f 5)
     ))
   ))
 )
-- End definition
map f (
  Impure ( Identity (
    Impure ( Identity (
      Pure a
    ))
  ))
)
==
  Impure ( Identity (
    Impure ( Identity (
      Pure (f a)
    ))
  ))

Let's look at the Apply instance now:

instance (Functor f) => Apply (Free f) where
  apply (Pure f) (Pure a) = Pure (f a)
  apply (Impure f_of_Free_F) pure_A =
    Impure (f_of_Free_F <#> (
      -- recursively call `apply` on nested `Impure` values
      -- until we get a `Pure` value of Free
        \pure_F -> apply pure_F pure_A
      -- apply the function and then rewrap `Impure` values
    ))
  apply pure_F (Impure f_of_Free)  =
    Impure (f_of_Free <#> (
      -- recursively call `apply` on nested `Impure` values
      -- until we get a `Pure` value of Free
        \pure_A -> apply pure_F pure_A
      -- apply the function and then rewrap `Impure` values
    ))

Let's see apply in action via a graph reduction:

-- Reminder: function arg == arg # function

-- Start
--    "Left" Impure            "Right" Impure
apply (Impure (Identity (Pure f))) (Impure (Identity (Pure a)))
-- Use `map` to recursively call `apply` on the left Impure until we get the
-- Pure value
Impure ((Identity (Pure f)) <#> (\pure_F  -> apply pure_F (Impure (Identity (Pure a)))))
Impure (Identity ((Pure f)   #  (\pure_F  -> apply pure_F (Impure (Identity (Pure a)))))
-- apply `Pure f` to the function
Impure (Identity (             (\(Pure f) -> apply (Pure f) (Impure (Identity (Pure a)))))
Impure (Identity (                           apply (Pure f) (Impure (Identity (Pure a)))))
-- Remove the extra whitespace
Impure (Identity (apply (Pure f) (Impure (Identity (Pure a)))))

-- Now use `map` to recursiveely call `apply` on the right Impure until we get
-- Pure value
Impure (Identity (Impure ((Identity (Pure a) <#> (\pure_A  -> apply (Pure f) pure_A))))
Impure (Identity (Impure (Identity ((Pure a)  #  (\pure_A  -> apply (Pure f) pure_A))))
-- apply `Pure a` to the function
Impure (Identity (Impure (Identity (            (\(Pure a) -> apply (Pure f) (Pure a))))))
Impure (Identity (Impure (Identity (                          apply (Pure f) (Pure a) ))))
-- Remove thee extra whitespace
Impure (Identity (Impure (Identity (apply (Pure f) (Pure a)))))
-- Look up the instance
--    apply (Pure f) (Pure a) = Pure (f a)
-- and replace the LHS with the RHS
Impure (Identity (Impure (Identity (Pure (f a)))))

Now let's define Bind, again using the map recursively:

instance (Functor f) => Bind (Free f) where
  bind (Pure a) f = f a
  bind (Impure f_of_Free) f =
    Impure (f_of_Free <#> (
      -- recursively call `bind` on nested `Impure` values
      -- until we get a `Pure` value of Free
      \pure_A -> bind pure_A f
      -- apply the function and then rewrap `Impure` values
    ))

Let's see bind in action via a graph reduction:

-- Start!
bind (Impure ( Identity (Pure a))) f

-- Recursively call `bind` via `map` until reach a `Pure` value:
bind (Impure ( Identity  (Pure a))) f
      Impure ((Identity  (Pure a)) <#> (\pure_a -> bind pure_a f) )
      Impure ( Identity ((Pure a)   #  (\pure_a -> bind pure_a f)))
-- Apply `Pure a` to the function
      Impure ( Identity (              (           bind (Pure a) f)))
      Impure ( Identity (                          bind (Pure a) f))
-- remove extra white space
Impure ( Identity (bind (Pure a) f))
-- Look up the instance
--    bind (Pure a) f = f a
-- and replace the LHS with the RHS
Impure ( Identity (Pure (f a)))

Definition of Free Monad

Putting it all together, we get this:

data Free f a
  = Pure a
  | Impure (f (Free f a))

instance Applicative (Free f) where
  pure a = Pure a

instance (Functor f) => Functor (Free f) where
  map f (Pure a) = Pure (f a)
  map f (Impure f_of_Free) =
    Impure (f_of_Free <#> (
      -- recursively call `map` on nested `Impure` values
      -- until we get a `Pure` value of Free
      \pure_A -> map f pure_A
      -- which applies the function to the a
      -- and then rewraps the `Impure` values
    ))

instance (Functor f) => Apply (Free f) where
  apply (Pure f) (Pure a) = Pure (f a)
  apply (Impure f_of_Free_F) pure_A =
    Impure (f_of_Free_F <#> (
      -- recursively call `apply` on nested `Impure` values
      -- until we get a `Pure` value of Free
        \pure_F -> apply pure_F pure_A
      -- apply the function and then rewrap `Impure` values
    ))
  apply pure_F (Impure f_of_Free)  =
    Impure (f_of_Free <#> (
      -- recursively call `apply` on nested `Impure` values
      -- until we get a `Pure` value of Free
        \pure_A -> apply pure_F pure_A
      -- apply the function and then rewrap `Impure` values
    ))

instance (Functor f) => Bind (Free f) where
  bind (Pure a) f = f a
  bind (Impure f_of_Free) f =
    Impure (f_of_Free <#> (
      -- recursively call `bind` on nested `Impure` values
      -- until we get a `Pure` value of Free
      \pure_A -> bind pure_A f
      -- apply the function and then rewrap `Impure` values
    ))

The next file will explain why this implementation has performance problems.

The Remorseless Free Monad

What follows is a quick summary of the Reflection without Remorse paper. This summary:

  • explains what's at the heart of the original Free's performance problem
  • explains using a very high-level "read the paper if you want to understand the 'type magic'" explanation for how the "reflection without remorse" version of Free fixes that performance problem.

Similar Shapes: The Free Monoid and the Free Monad

Surpisingly, these two "free" types bear a similar resemblence:

data List a   = Nil    | Cons    a (List a  )
data Free f a = Pure a | Impure (f (Free f a))

A list is just an unbalanced tree. Thus, Free is essentially the same as List, except that it stores a higher-kinded type (kind Type -> Type) rather than a concrete type (kind Type)

This similarity will be used to explain why Free has performance problems.

The Direction Matters

Let's talk about Semigroup:

class Semigroup a where
  append :: a -> a -> a

infix 4 append as <>

instance Semigroup Int where
  append i1 i2 = i1 + i2

Semigroup requires its implementation to adhere to the law of association, meaning that, when append is used on the output of a previous append and some other value, the location of the parenthenses don't matter:

1 <> 2 <> 3 <> 4 <> 5 <> 6 <> 7 <> 8

-- no association: Tree-like structure
-- Start
  1 <> 2  <>  3 <> 4   <>   5 <> 6 <>   7 <> 8
-- Add parentheesis, starting from the 'leaves'
 (1 <> 2) <> (3 <> 4)  <>  (5 <> 6) <> (7 <> 8)
((1 <> 2) <> (3 <> 4)) <> ((5 <> 6) <> (7 <> 8))

-- Left association: List-like structure
-- Start
      1 <> 2  <> 3  <> 4  <> 5  <> 6  <> 7  <> 8
-- Add parenthesis, starting from the left
     (1 <> 2) <> 3  <> 4  <> 5  <> 6  <> 7  <> 8
    ((1 <> 2) <> 3) <> 4  <> 5  <> 6  <> 7  <> 8
   (((1 <> 2) <> 3) <> 4) <> 5  <> 6  <> 7  <> 8
  ((((1 <> 2) <> 3) <> 4) <> 5) <> 6  <> 7  <> 8
 (((((1 <> 2) <> 3) <> 4) <> 5) <> 6) <> 7  <> 8
((((((1 <> 2) <> 3) <> 4) <> 5) <> 6) <> 7) <> 8
-- Finish:
((((((1 <> 2) <> 3) <> 4) <> 5) <> 6) <> 7) <> 8

-- Right association: List-like structure
-- Start
1 <> 2 <> 3 <> 4 <> 5 <> 6 <> 7 <> 8
-- add parenthesis, starting from the right
1 <>  2 <>  3 <>  4 <>  5 <>  6 <> (7 <> 8)
1 <>  2 <>  3 <>  4 <>  5 <> (6 <> (7 <> 8))
1 <>  2 <>  3 <>  4 <> (5 <> (6 <> (7 <> 8)))
1 <>  2 <>  3 <> (4 <> (5 <> (6 <> (7 <> 8))))
1 <>  2 <> (3 <> (4 <> (5 <> (6 <> (7 <> 8)))))
1 <> (2 <> (3 <> (4 <> (5 <> (6 <> (7 <> 8))))))
-- Finish:
1 <> (2 <> (3 <> (4 <> (5 <> (6 <> (7 <> 8))))))

We can see that the output of adding up all these integers, regardless of where we put the parenthesis, will still be the same output value (that's the law of associativity).

However, functions that are "associative" sometimes take longer to output that value depending on which "direction" it goes. As an example, consider the Semigroup instance for List:

data List a
  = Nil
  | Cons a (List a)

infix 4 Cons as :
-- Nil == []
-- 1 : Nil == [1]
-- 1 : 2 : 3 : Nil == [1, 2, 3]

-- Right associative: Start
(1 : Nil) <>  (2 : Nil) <>  (3 : Nil) <>  (4 : Nil) <> (5 : Nil)        -- 0
(1 : Nil) <>  (2 : Nil) <>  (3 : Nil) <> ((4 : Nil) <> (5 : Nil))       -- 0
(1 : Nil) <>  (2 : Nil) <> ((3 : Nil) <> ((4 : Nil) <> (5 : Nil)))      -- 0
(1 : Nil) <> ((2 : Nil) <> ((3 : Nil) <> ((4 : Nil) <> (5 : Nil))))     -- 0
append (1 : Nil) ((2 : Nil) <> ((3 : Nil) <> ((4 : Nil) <> (5 : Nil)))) -- 1
1 : (append Nil ((2 : Nil) <> ((3 : Nil) <> ((4 : Nil) <> (5 : Nil))))) -- 2
1 : ((2 : Nil) <> ((3 : Nil) <> ((4 : Nil) <> (5 : Nil))))              -- 3
1 : (append (2 : Nil) ((3 : Nil) <> ((4 : Nil) <> (5 : Nil))))          -- 4
1 : (2 : (append Nil ((3 : Nil) <> ((4 : Nil) <> (5 : Nil))))           -- 5
1 : (2 : ((3 : Nil) <> ((4 : Nil) <> (5 : Nil))))                       -- 6
1 : (2 : (3 : (append Nil <> ((4 : Nil) <> (5 : Nil)))))                -- 7
1 : (2 : (3 : ((4 : Nil) <> (5 : Nil)))))                               -- 8
1 : (2 : (3 : (4 : (append Nil (5 : Nil)))))                            -- 9
1 : (2 : (3 : (4 : (5 : Nil))))                                         -- 10
1 : (2 : (3 : (4 : 5 : Nil)))                                           -- 11
1 : (2 : (3 : 4 : 5 : Nil))                                             -- 12
1 : (2 : 3 : 4 : 5 : Nil)                                               -- 13
1 : 2 : 3 : 4 : 5 : Nil                                                 -- 14

-- Left associative: Start
--  List1         List2         List3         List4        List5
   (1 : Nil) <>  (2 : Nil) <>  (3 : Nil) <>  (4 : Nil) <> (5 : Nil)       -- 0
  ((1 : Nil) <>  (2 : Nil)) <>  (3 : Nil) <>  (4 : Nil) <> (5 : Nil)      -- 0
 (((1 : Nil) <>  (2 : Nil)) <>  (3 : Nil)) <>  (4 : Nil) <> (5 : Nil)     -- 0
((((1 : Nil) <>  (2 : Nil)) <>  (3 : Nil)) <>  (4 : Nil)) <> (5 : Nil)    -- 0
(((append (1 : Nil) (2 : Nil)) <>  (3 : Nil)) <>  (4 : Nil)) <> (5 : Nil) -- 1
(((1 : (append Nil (2 : Nil))) <>  (3 : Nil)) <>  (4 : Nil)) <> (5 : Nil) -- 2
(((1 : (2 : Nil)) <>  (3 : Nil)) <>  (4 : Nil)) <> (5 : Nil)              -- 3
(((1 :  2 : Nil ) <>  (3 : Nil)) <>  (4 : Nil)) <> (5 : Nil)              -- 3
-- At this point, we will need to iterate
-- through the List1 all over again!
((append (1 : 2 : Nil) (3 : Nil)) <>  (4 : Nil)) <> (5 : Nil)             -- 4
((1 : (append (2 : Nil) (3 : Nil))) <>  (4 : Nil)) <> (5 : Nil)           -- 5
((1 : 2 : (append Nil (3 : Nil))) <>  (4 : Nil)) <> (5 : Nil)             -- 6
((1 : 2 : (3 : Nil)) <>  (4 : Nil)) <> (5 : Nil)                          -- 7
((1 : 2 :  3 : Nil) <>  (4 : Nil)) <> (5 : Nil)                           -- 7
-- At this point, we will need to iterate
-- through the List1 AND List2 all over again!
(append (1 : 2 : 3 : Nil) (4 : Nil)) <> (5 : Nil)                         -- 8
(1 : (append (2 : 3 : Nil) (4 : Nil))) <> (5 : Nil)                       -- 9
(1 : 2 : (append (3 : Nil) (4 : Nil))) <> (5 : Nil)                       -- 10
(1 : 2 : 3 : (append Nil (4 : Nil))) <> (5 : Nil)                         -- 11
(1 : 2 : 3 : (4 : Nil)) <> (5 : Nil)                                      -- 12
(1 : 2 : 3 : 4 : Nil) <> (5 : Nil)                                        -- 12
-- At this point, we will need to iterate
-- through the List1 AND List2 AND List3 all over again!
append (1 : 2 : 3 : 4 : Nil) (5 : Nil)                                    -- 13
1 : (append (2 : 3 : 4 : Nil) (5 : Nil))                                  -- 14
1 : 2 : (append (3 : 4 : Nil) (5 : Nil))                                  -- 15
1 : 2 : 3 : (append (4 : Nil) (5 : Nil))                                  -- 16
1 : 2 : 3 : 4 : (append Nil (5 : Nil))                                    -- 17
1 : 2 : 3 : 4 : (5 : Nil)                                                 -- 18
1 : 2 : 3 : 4 : 5 : Nil                                                   -- 18

The law of associativity guarantees that the output of a non/left/right-associative function will always be the same. However, the above code demonstrates that one direction of function application (right association) can be faster than another (left association).

So, let's think about why this occured. Due to the way the type is defined, List must use recursion to get to its tail Nil before it can replace that tail, Nil, with the list to which it is being appended. Since List and Free are structured similarly, then Free will also suffer the same performance costs if it uses a left-associative function to reach its tail, Pure. So, which function does Free use that acts just like append? The bind function. Every bind/>>= call will iterate through the entire Free structure, apply the function to its Pure a value and then rewrap everything in an Impure value:

-- Thus, this code...
freeMonad >>= f >>= g >>= h >>= ...
-- is synonymous with the runtime performance hit as this code...
(((list <> f) <> g) <> h) <> ...

When we call freeMonad >>= f, we iterate through freeMonad's entire structure. When we take that output and bind/>>= it to g, we iterate through freeMonad's entire structure plus any new nesting values that f added to it. When we take that output and bind/>>= it to h, the total cost is freeMonad + f's additional structure + g's additional structure. As a result, Free's performance suffers because of its recursive nature.

Is recursion by itself bad? No, recursion can be quite helpful; it's not the problem. Rather, the problem with List's left-associative <> function is the slow recursive-time access to List's tail. Since List represents a sequence of values, we can fix the append problem by using a different sequence-like data structure that grants fast constant-time access to its tail. Similarly, to fix the problem with Free, we should define it differently, so that the "data structure" to which it is similar also has constant-time tail access (i.e. constant time access to its Pure value).

This sounds easy until you remember what bind's type signature allows: bind :: forall a b. m a -> (a -> m b) -> m b. In other words, bind must work "for all a and b types" where these types can differ in-between multiple bind calls.

Fortunately, the paper's authors figured out how to do this using a FingerTree (data structure with constant time head and tail access) that stores a special type that represents a bind's type signature and which can be composed just like multiple bind functions. This "type magic" won't be explained here; you'll need to read the paper (see Section 4 and 5) on your own to understand it fully.

To quote from their documentation, Purescript's Free monad is "the Free monad implemented in the spirit of [that] paper."

Now that we understand what the Free monad is, let's see why it's useful.

The Expression Problem

Brief Summary

The Expression Problem can be summarized into three ideas:

The goal is to define a data type by cases, where

  1. one can add new cases to the data type and
  2. (one can add) new functions over the data type,
  3. without recompiling existing code, and
  4. while retaining static type safety.
Pre-existing compiled functionFunction is added
Pre-existing compiled data typeAll languagesFP: Add a function in a new file
OO: ???
Data type is addedFP: ???
OO: Add a subclass in a new file
Problem's Solution^^

^^ This cannot be defined until the corresponding FP/OO issue (marked as ???) is resolved

A Solution to the Expression Problem

The paper, Data Types à la carte, synthesized a number of known-at-the-time-of-writing ideas into a solution to the Expression problem by figuring out a way to compose complicated data types. When they applied their findings to the Free monad, they found that they could "run" multiple monads inside of one monad.

This folder is a summmary and commentary on the paper linked above. It is meant to be read alongside of or after you read the paper. While you, the reader, could just read the paper and ignore the rest of this folder's contents, there are some advantages to reading through this folder's contents alongside of the paper:

  • Sometimes, the above paper will state that something is true, but not show why. This folder will explore that more and show why it's true.
  • Sometimes, the paper may use unfamiliar terminology or use symbolic data types. This folder will explain the terminology and use alphabetical names to refer to some data types.
  • The last file in this folder (i.e. Embedded Compilers.md) explains one of the key features one obtains by using the Free monad approach

Reading the Paper and This Folder Side-by-Side

Rather than follow the paper exactly, this folder will define and solve a simpler version of the Expression Problem to demonstrate the basic idea of the solution. With that foundation, the paper's real problem will be explored and solved.

In addition, the paper explains how to write a show function that pretty prints the mathetmatical expression. This folder's contents will not overview that part of the paper. The full reasons will be explained when we get to that part.

Contents of The Paper

The paper above has 8 sections:

  1. Introduction (what is the expression problem and an example of it?)
  2. Fixing the Expression Problem (how do we compose data types?)
  3. Evaluation (how do we evaluate composed data types?)
  4. Automating Injections (how do we reduce boilerplate when working with composed data types?)
  5. Examples (Provide a full example of a solution to our problem)
  6. Monads for Free (Why is this relevant to Free monads?)
  7. Applications (Using this approach to define extensible effects by composing Free monads)
  8. Discussion

This folder has 8 files:

  1. Seeing and Solving a Simple Problem
  2. Reducing boilerplate via Either
  3. Seeing and Solving a Harder Problem
  4. Writing the Evaluate Function
  5. Writing the Show function (optional)
  6. From Expression to Free
  7. Defining Modular Monads
  8. Embedded Compilers

Correspondance Table

This FolderGeneral IdeaCorresponding Paper sectionGeneral idea
1 (File)Prep work: Defining and solving a simple version of the problem by composing data typesSections 1/2/3/5 (ish)Laying a foundation
2 (File)Prep work: Abstract data type composition via EitherSection 4 (ish)Laying a foundation
3 (File)Showing why the paper's problem is hard to solve, but still solvable; reveal CoproductSections 1/2/4/5 (ish)-
4 (File)-Sections 3/5Writing the evaluate function
5 (File)Optional readingSections 3/5Writing the show function
6 (Folder)Show that Expression is really FreeSection 6-
7 (File)Using 'languages' to model effectsSection 6Simulating the State monad
8 FileDefining abstract syntax trees via FreeSections 6/7Its relevance to and application for Free monads

Seeing and Solving the Problem in FP Code

The goal is to define a data type by cases, where

  1. one can add new cases to the data type and
  2. (one can add) new functions over the data type,
  3. without recompiling existing code, and
  4. while retaining static type safety.

A Very Simple Example of The Problem

Given this code

data Fruit
  = Apple
  | Banana

showFruit :: Fruit -> String
showFruit Apple = "apple"
showFruit Banana = "banana"

We can easily add a new function to our code without needing to recompile our existing code

-- in another file...
intFruit :: Fruit -> Int
intFruit Apple = 0
intFruit Banana = 1

However, if we want to add another data constructor to Fruit, we can only do so by updating Fruit to include Orange and then updating all of our functions to include Orange as well:

data Fruit
  = Apple
  | Banana
  | Orange

showFruit :: Fruit -> String
showFruit Apple = "apple"
showFruit Banana = "banana"
showFruit Orange = "orange"

Since Fruit has already been compiled, we will need to recompile our code with the updated version of Fruit. Moreover, if we do not update showFruit/intFruit, then we no longer have an exhaustive pattern match. Thus, these functions are no longer pure but are now partial functions.

The Solution

The solution, then, is to be able to define data types in such a way that they "compose". The best way to compose data types is to group two types into one type via a type wrapper:

-- original file
data Fruit
  = Apple
  | Banana

-- new file
data Fruit2
  = Orange

data FruitGrouper = -- ???

How should FruitGrouper be defined? A value of FruitGrouper should only be one of 3 values:

  1. FruitGrouper Apple
  2. FruitGrouper Banana
  3. FruitGrouper Orange

We can define it using this approach:

data FruitGrouper
  = Fruit_  Fruit
  | Fruit2_ Fruit2

This approach will enable showFruit and intFruit to continue to work as expected. If we wanted to define a new function that uses both, we would pass in FruitGrouper instead:

-- original file. This cannot change once written!
data Fruit
  = Apple
  | Banana

showFruit :: Fruit -> String
showFruit Apple = "apple"
showFruit Banana = "banana"

-- new file
data Fruit2
  = Orange

data FruitGrouper
  = Fruit_  Fruit
  | Fruit2_ Fruit2

showAllFruit :: FruitGrouper -> String
showAllFruit (Fruit_  appleOrBanana) = showFruit appleOrBanana
showAllFruit (Fruit2_ Orange) = "orange"

Great! We have now seen how to solve a very simple version of this problem. Now, let's refine this approach a bit as preparation for a future harder problem.

Reducing Boilerplate

There are two problems with our current approach that we want to raise.

First, if we want to add another data constructor Cherry, we now need to nest that type even further using another type wrapper:

-- original file. This cannot change once written!
data Fruit
  = Apple
  | Banana

showFruit :: Fruit -> String
showFruit Apple = "apple"
showFruit Banana = "banana"

-- File 2. This cannot change once written!
intFruit :: Fruit -> Int
intFruit Apple = 0
intFruit Banana = 1

data Fruit2
  = Orange

data FruitGrouper
  = Fruit_  Fruit
  | Fruit2_ Fruit2

showAllFruit :: FruitGrouper -> String
showAllFruit (Fruit_  appleOrBanana) = showFruit appleOrBanana
showAllFruit (Fruit2_ Orange) = "orange"

-- File 3.
data Fruit3 = Cherry

data FruitGrouper2
  = FruitGrouper_ FruitGrouper
  | Fruit3_       Fruit3

showMoreFruit :: FruitGrouper2 -> String
showMoreFruit (FruitGrouper_ a) = showAllFruit a
showMoreFruit (Fruit3_ Cherry)  = "cherry"

We see that we keep nesting types inside more type wrappers. If we were to abstract this away into a more general type, we basically have nested Eithers:

data Either a b
  = Left a
  | Right b

Either Fruit (Either Fruit2 Fruit3)

Anytime we want to add a new data constructor, we need to nest it in another Either: Either first (Either second (Either third (Either fourth ... (Either _ last)))) If we were to visualize this data structure, it looks like this:

   Either                    Either
   /   \                     /   \
  /     \                   /     \
first  Either         Left first  Right
        /  \                      /  \
       /    \                    /    \
  second   Either       Left second   Right
            /  \                      /  \
           /    \                    /    \
      third    Either        Left third   Right
                /  \                      /  \
               /    \                    /    \
           fourth   last        Left fourth   Right last

Thus, to access last, we need to call Right (Right (Right (Right last)))

Second, using a value that is wrapped in a nested data structure leads to boilerplate.

Here's an example of putting a value of one of the nested types into the data structure. One needs to write a variant for each type position in our data structure:

putInsideOf :: forall first second third fourth last
             . last
            -> Either first (Either second (Either third (Either fourth last)))
putInsideOf last = Right (Right (Right (Right last)))

If we want to extract a value of a type that is in our nested Either value, we need to return Maybe TheType because the value may be of a different type in the nested Either value. Using Maybe makes our code pure:

extractFrom :: forall first second third fourth last
           . Either first (Either second (Either third (Either fourth last)))
          -> Maybe Result
extractFrom (Right (Right (Right (Right last)))) = Just last
extractFrom _ = Nothing

Once again, we need to write a variant of this function that works for every type position (e.g. first, second, and third) in our data structure.

In short, this boilerplate gets tedious. However, boilerplate usually implies a pattern we can use. Here's two ways we could make this easier:

  1. Rather than using a nested Either type, why not define a type for this specific purpose?
  2. Rather than using extractFrom and putInsideOf , why not define a type class with two functions for this specific purpose?

Defining NestedEither

Looking at our example of a nested Either below, what is the common structure?

Either first   (      Either second   (      Either third         last))
Left   first | Right (Left   second | Right (Left   third | Right last))
Left   first | Right (NestedEither second theRemainintTypes            )

So we come up with this idea, but it doesn't work...

data NestedEither a b
  = Left a
  | Right (NestedEither b c)

...because we enter an infinte loop:

  1. To define c in the Right value's NestedEither b c argument, we need the type declaraction, data NestedEither a b to include the third type, c. Thus, we go to step 2.
  2. We update the type to data NestedEither a b c. However, now the NestedEither b c in Right value has only two types, not three. Thus, it no longer adheres to its own declaration (i.e. NestedEither b c ? vs NestedEither a b c). To add the type, we need it to be different than the others to enable the recursive idea of a nested Either, so we'll call it d. Thus, we return to step 1 except c is now d in that example.

Still, we can clean up the verbosity/readability of nested Eithers by creating an infix notation for it:

infixr 6 type Either as \/

Either Int String == Int \/ String

-- As an example, the first line below reduces to the last line below
first \/  second \/  third \/  fourth \/ last       -- first
first \/  second \/  third \/ (fourth \/ last)
first \/  second \/ (third \/ (fourth \/ last))
first \/ (second \/ (third \/ (fourth \/ last)))    -- last

Defining InjectProject

When we have to write the same function again and again for different types in FP languages, we convert it into a type class (e.g. Semigroup, Monoid, Functor, etc.). Likewise, when we look at our code below...

putInsideOf :: forall first second third fourth last
             . last
            -> Either first (Either second (Either third (Either fourth last)))
putInsideOf last = Right (Right (Right (Right last)))

... we want putInsideOf to mean "if I have a data structure with nested types, take one of those types values and put it into that data structure." In other words, we want to inject some value into a data structure:

inject :: forall theType theNestedEitherType
        . theType
       -> theNestedEitherType

When we look at the other code we had...

extractFrom :: forall first second third fourth last
             . Either first (Either second (Either third (Either fourth last)))
            -> Maybe last
extractFrom (Right (Right (Right (Right last)))) = Just (f last)
extractFrom _ _ = Nothing

... we want extractFrom to mean "If I have a data structure with nested types, I want to extract the value of a specific type out of that structure via Just or get Nothing if the value does not exist." In other words, we want to project some type's value from the data structure into the world:

project :: forall nestedType theType
         . nestedType
        -> Maybe theType

Turning this idea into a type class, we get this:

class InjectAndProject someType nestedType where
  inject :: someType -> nestedType
  project :: nestedType -> Maybe someType

Purescript-Either

Indeed, the ideas we've proposed have already been defined by purescript-either:

The library provides convenience functions and types for nested Eithers, but only up to 10 total types:

  • Convenience functions for dealing with injection and projection
  • Convenience functions for running code that handles every possible type in the nested Either:
  • Convenience types for writing them in a function's type signature without the \/ syntax

Seeing and Solving a Harder Example

Failing to Solve a Harder Version

Let's try using our Either-based solution to solve a harder problem. We'll solve the same problem the paper to which we referred beforehand solved.

They want to define a program that can evaluate an expression that consists of adding and multiplying integers. However, they want to define this program in such a way that it also solves the Expression Problem.

To start, we'll define the Value type that wraps an Int and the Addition type in the "file 1". An Addition should work in all of these cases:

  1. Adds two ints
  2. Adds an int and another add expression
  3. Adds two add expressions.

To achieve this result, we'll define our type like so:

data Expression
  = Value Int
  | Add Expression Expression

evaluate :: Expression -> Int
evaluate (Value i) = i
evaluate (Add x y) = (evaluate x) + (evaluate y)

show2 :: Expression -> String
show2 (Value i) = show i
show2 (Add x y) = "(" <> show x <> " + " <> show y <> ")"

Now, if we want to add Multiply in "file 2" without changing or recompiling "file 1", how does that fare? If we define our data type for Multiply like this...

data Multiply = Multiply Expression Expression

... then Multiply will work similarly to Add:

  1. Multiplies 2 Int values
  2. Multiply an int value and an add expression
  3. Multiply two add expressions

The problem with our above definition is that it does not account for two other cases that should work:

  1. Multiply two multiply expressions
  2. Add two multiply expresions

Exploring Our Options

Let's model these types differently by separating them all into their own types that we can later compose. We won't include Multiply yet:

data Value = Value Int
data Add = Add Expression Expression

type Expression = Value \/ Add

value :: Int -> Expression
value i = inj (Value i)

add :: Expression -> Expression -> Expression
add x y = inj (Add x y)

show2 :: Expression -> String
show2 (Left (Value i)) = show i
show2 (Right (Add x y)) = "(" <> show2 x <> " + " <> show2 y <> ")"

evaluate :: Expression -> Int
evaluate (Left (Value i)) = i
evaluate (Right (Add x y)) = (evaluate x) + (evaluate y)

To achieve the capability to add and multiply Multiply expressions, what would we need to do, even if it means breaking the constraints of the Expression Problem?

-- File 1
data Value = Value Int
data Add = Add Expression Expression

-- File 2
data Multiply = Multiply Expression Expression

-- The problem: this is the final type we need
type Expression = Value \/ Add \/ Multiply

There are two places where we could define Expression, each with its own problems:

  • If we define it in File 1, then its definition cannot include the Multiply field because File 1 doesn't know about that type.
  • If we define it in File 2, then File 1 will not compile because the compiler does not know what the type, Expression, is.

Hmm... The Expression Problem is more nuanced than first thought. Still, the above refactoring helps shed light on what needs to be done. Let's look back at Add:

data Add = Add Expression Expression

Add must add 2 Expressions. However, we've hard-coded what Expression means. Since the location declaration of Expression causes a problem, we must turn this hard-coded type into a generic type to enable us to define it at a later time. The same goes for Multiply:

data Add expression = Add expression expression
-- or a less verbose version
data Multiply e = Multiply e e

We also know from our previous simpler problem that we will need to eventually compose our data types together into a big Expression type. In other words, we should get something like this:

--        L             RL  RR
-- Either Value (Either Add Multiply)

              Right ( Left ( Add (
                (Left (Value 1))
                (Right ( Right ( Multiply
                  (Left (Value 1))
                  (Left (Value 1))
                ))
              ))

To summarize, we need to

  • define the expression type in Add/Multiply generically so we can define it at a later time as a way (avoids the location problem)
  • define an Expression type that composes data types together but somehow prevents them from knowing about one another. We need something like a "buffer zone" but for our types.

Solving the Problem

We'll show you how the paper solved this, starting with the type's value and then showing the actual type declaration/definition:

-- Value of our Expression type with Placeholder commented out
{- Placeholder ( -} Right ( Left ( Add (
  {- (Placeholder -} ( Left  (Value 1))   -- )
  {- (Placeholder -} ( Right ( Right ( Multiply
              {- (Placeholder -} (Left (Value 1))  -- )
              {- (Placeholder -} (Left (Value 1))  -- )
                      )))  -- )
                    )))  -- )

-- A full value
Placeholder ( Right ( Left ( Add (
  (Placeholder ( Left (Value 1)))
  (Placeholder ( Right ( Right ( Multiply
    (Placeholder (Left (Value 1)))
    (Placeholder (Left (Value 1)))
  ))))
))))

-- Define a special "placeholder" type that hides the next
-- `expression` type from the actual data type
-- `f` is a higher-kinded type
data Expression f = Placeholder (f (Expression f))

-- Make the types all higher-kinded by one
data Add e = Add e e
data Multiply e = Multiply e e
-- `e` not used here
-- but needed to make the next type work
data Value e = Value Int

{-
The following is pseudo-code. I don't konw whether it compiles.

Use Either to compose these higher-kinded types:
             Either (Value e) (Either (Add e)    (Multiply e))
type AMV e =        (Value e) \/      (Add e) \/ (Multiply e)
newtype AMV_Expression = AMV_Expression (Expression AMV)

Revealing Coproduct

Above, we wrote this:

Either (Value e) (Either (Add e)    (Multiply e))
       (Value e) \/      (Add e) \