The PureScript logo by Gareth Hughes is licensed under the CC-BY 4.0 license

PureScript and Haskell at Lumi

Phil Freeman
Aug 6, 2018 · 9 min read


The need for types on the front-end

We get a lot of benefit from using Haskell on the backend at Lumi. All of our API servers and clients are written in Haskell using Servant, and we use esqueleto and persistent (with a generous helping of custom DSLs) to write our SQL queries. It is extremely rare for us to encounter a runtime error in production on our servers, and when we do, it is usually of the “business logic error” variety. It’s hard to overstate the value of having types everywhere on the backend.

  1. Everyone on the team was able to write JavaScript effectively, but not everyone was familiar with compile-to-JavaScript languages. Now we have enough developers that we don’t need to worry about that, but originally, it was useful for everyone to be able to contribute easily.
  2. JavaScript was a suitable choice for a small application initially, allowing us to iterate quickly, but is less suitable now that the project has grown in scope. It is not particularly difficult to build new code in JavaScript, but it is difficult to maintain a large JavaScript application, requiring extensive tests for things which could be covered by types.

The decision to use PureScript

I had personally used typed front-end languages (TypeScript and PureScript) extensively, and other team members had similar experience with other options like Flow and Elm. As the original developer of the PureScript compiler, I had an obvious interest in seeing it adopted at Lumi, but I was also aware that it might not be the optimal choice of front-end language for a few reasons:

  • Using a relatively uncommon programming language might make it more difficult to hire developers.
  • Without language-level support for things like JSX and CSS, we would need to find some other way to work with our design team, who previously were able to modify our React components directly if necessary.
  • The library ecosystem is relatively small, and we would need to build some things of our own in order to be productive.
  • The language should integrate smoothly with JavaScript, its libraries and its build tools.
  • The type system should be expressive, supporting things like sum types, row polymorphism, type classes and higher-kinded types.
  • It should be easy to build simple solutions, but still possible to experiment with more advanced ideas. As Justin Woo puts it, the language should have a culture of “the sky is the limit” and should not limit your creativity.
  • The development of the language itself should be open enough that we would be able to modify any part of the toolchain if it became necessary.

Getting started

Eventually, we decided to jump in and try out PureScript by replacing one of our existing JSX-based React components. We chose a simple, pure component with no side-effects or API calls. After setting up the PureScript compiler in our existing Webpack-based build, we were off to a good start.

Generating types

The first step was to generate a complete set of PureScript types to correspond to the types we used in our Haskell API. To solve this problem, we turned to GHC Haskell’s support for datatype-generic programming. We created a simplified representation of PureScript data types (PursTypeConstructor), and a type class for Haskell record types which would be converted into PureScript types (ToPursType):

data PursRecord = PursRecord
{ recordFields :: [(Maybe Text, PursType)]
}
data PursTypeConstructor = PursTypeConstructor
{ name :: Text
, dataCtors :: [(Text, PursRecord)]
}
class ToPursType a where
toPursType :: Tagged a PursTypeConstructor
default toPursType
:: ( Generic a
, GenericToPursType (Rep a)
)
=> Tagged a PursTypeConstructor
toPursType = retag $ genericToPursType @(Rep a) id

Generating API clients

The next step was to turn our Haskell API definitions into usable, safe PureScript clients. Fortunately, Servant is perfect for this task — since our API definitions are represented at the type-level, we can turn those definitions into PureScript code and know for sure that the resulting code will be compatible with the server implementation.

$(do names <- reifyManyWithoutInstances ''HasForeignType
[ ''Order
, -- ... a list of other top-level types goes here
] (const True)
let toInstance nm =
let tyCon = TH.ConT nm
nmLit = TH.LitE (TH.StringL (TH.nameBase nm))
in [d| instance HasForeignType
Purs
PursType
$(pure tyCon)
where
typeFor _ _ _ = PursTyCon $(pure nmLit)
|]
concat <$> mapM toInstance names
)
  • For each type it finds, the toInstance function turns its name into a HasForeign type instance using a Template Haskell splice. The tyCon and nmLit nodes are in scope, so we can use anti-quotation $(...) to use them in the splice.

Types as a tool

As I mentioned earlier, a good type system should increase productivity by reducing busywork. In addition to making our API clients free from boilerplate, we’ve been able to increase productivity in a number of other areas since implementing PureScript. I hope to be able to write about each of these in some detail in future:

  • We have built a combinator library for assembling forms which are compatible with our API types. By using lenses and a handful of basic functions, we are able to build type-safe forms in a fraction of the time it would take by hand.
  • We have also built a type-level DSL in the style of Servant for deriving forms from types for certain data collection tasks. Just as Servant allows us to repurpose our type-level API definitions for the generation of API clients and documentation, we can reuse our type-level form descriptions for all sorts of things like storage in the database, indexing and querying.
  • Now that our API clients are represented on the front-end, we would like to find new ways of representing our API calls, and find ways to implement features like batching and caching in a generic way.

Conclusion

While we still have a way to go, our experience with PureScript so far has been very positive. Instead of touting its benefits myself, I’ll leave you with a few quotes from the team:

The main thing I notice is that it’s easier to expand abstractions. You can take something really specific and incrementally make it more general.

— Brady

[I’ve had] such a positive experience, with little mental overhead, and total trust in the compiler. I implemented an entire page with a list of data, filters, search, and pagination which worked first time.

— Brandon

I’ve been a fan of Javascript’s “loose” style; but as the applications I’ve worked on got larger, fear of the “null” or “undefined” began to take over. Using PureScript ensures any incorrect logic is caught or contained. I can feel more confident in the code I push out.

— Kimi

After a few months in PureScript, having to edit the occasional JavaScript file fills me with anxiety — a typo or incorrect assumption anywhere could break half the site! In PureScript, even if I’ve gotten the business logic wrong somewhere, the problem is always contained.

— Madeline

Lumi

Find us at https://www.lumi.com/blog

Thanks to Madeline Trotter, Stephan Ango, and Brady Ouren.

Phil Freeman

Written by

Lumi

Lumi

Find us at https://www.lumi.com/blog