I’ve attempted this post 5 times now, and have come to the conclusion that to start off, there needs to be a version that is as neutral as possible. There are not many people who have worked on production applications in multiple frontend FP systems, and lived to tell the tale. So here goes, Fresheyeball’s guide to selecting a functional programming platform for the browser. I’m going to assume you are already sold on functional programming and it’s benefits in a UI development context.
This is about building purely functional user interfaces.
If you are looking for me to say that using Rx.js + Angular is functional programming, or endorse Reflex, or hedge in some other way, the answer is no. This is about purely functional programming, which means I’m not even going to consider a system valid without referential transparency and IO managed in some way. So yes, all flavors of Lisp are off the table as well.
- You have an existing code base, and developers on staff who refuse to learn an ML like language.
- You don’t yet have the skill to adopt Elm, and are looking for space to grow towards FP.
- Deadlines make transitioning your code base to a pure language infeasible.
- You are already comfortable with functional programming.
- Comparably poor tooling, libraries, and code quality.
Here is how:
Compile time checks
- Flow types are not even worth considering. Actual errors are missed, while petty semantics are over enforced. Beyond that, Flow types are optional, so you can not gain a guarantee of safety.
- TypeScript 2 is decent. Not only does it provide some genuine safety, but there is a big ecosystem of definition files.
You will need to find the one that works with your environment. Runtime checks are mostly good for contract enforcement with the server. If you can get runtime checks setup do so. Consider typify or sanctuary-def.
- Almost everything else
Use TypeScript 2 and definition files for type checking, both at compile time and runtime. Use definition files to ensure all outside libraries are also checked (you may need to write some of these yourself). Pass the compiled JS to eslint to check for impurities. All IO actions should take place within the Ramda Fantasy IO Monad, and runIO should appear in your code once, ever. This is the best you are likely to get, and you know what, it’s pretty reasonable.
(Full disclosure, I’ve used each of these technologies individually and in some combination, but not the specific combination in the recommendation. There may be some unforeseen challenge in mixing these tools. That said it’s the best we are likely to get.)
Elm is a unique language, with it’s own voice and abstractions. Elm is purely functional, and arguably the purest option available. Here are some reasons to choose Elm:
- You resonate with The Elm Architecture as a programming model (if you are thinking ‘oh I like Redux’, no, not that).
- You want a purely functional language but have not yet mastered type classes and Monads.
- Performance is a key consideration for your application.
- You work with people who are on board to learn, but don’t have time to tackle the curve of more advanced FP.
- You don’t trust yourself or others to maintain the virtues of The Elm Architecture without compiler enforcement.
Here are some reasons you might want to avoid Elm:
- You are accustomed to type classes and other compiler features Elm lacks.
- You want your application to have client server isomorphism, and there is zero server-side support for Elm.
- You don’t resonate with The Elm Architecture as a programming model.
Pros unique to Elm
Elm is the most mature, of the FP transpilers. This means you can expect a broad ecosystem of useful packages, and advanced tooling. Elm also enforces The Elm Architecture (or TEA) on you from the compiler level. Now there is nothing to stop you from using the TEA pattern in the language of your choosing (so long as it has type unions), and many often do, but choosing Elm means reaping the maximum benefit from the TEA pattern. Benefits including time travel debugging, incredible performance (courtesy of TEA specific compiler optimizations) and immediate code familiarity (think of the nice bits of RoR). TEA also generally yields very nice simple, non-Monadic code.
TEA enforces a separation between stateful logic and view logic. This is good or bad for development depending on how you resonate. But one thing it’s definitely good for is designers. If there are people at your company who write Html/Css only, Elm is a great pick. Most designers can quickly and easily understand Elm view code, and edit that code successfully.
IO is extremely well managed. This is part of why Elm is so incredibly pure and safe. There is nothing like
unsafePerformIO or any such thing in the language. Elm code is almost never Monadic (this should be a good smell nay saying Haskellers), and since we can’t cheat, this means IO is never performed in the middle of code. This makes it trivial to track down side effects.
Enforced TEA also means that the application has a single source of truth representing all state in the app. One weird benefit of this design decision is that the complete state of the program can be serialized, and de-serialized. Making restoring state, and QA a breeze.
The TEA specific benefits are possible outside of Elm, but are not guaranteed.
Cons unique to Elm
TEA is not composable, but through brute force. Many attempts have been made to make TEA compose, with limited success. If you want all the benefits of TEA, you are stuck building and maintaining some giant types, and writing a ton of boilerplate. The ways around this problem are fairly weak, and amount to, “don’t write components” (IE don’t intend your code to be re-usable), use lots of dictionaries for state, attempt your own code generation tools.
Elm’s type system is very limited compared to other FP options out there. Not only are there no type classes, there is no higher kinded polymorphism either, so even dictionary passing style is out of the question. This means that polymorphism on properties like Functor are just not possible. Upgrading types is a pain, and code can rot compared to languages with these features. For example, lets say we realize we can eliminate error states in our code by upgrading part of it from using a standard
List to using a
ZipList. Making this move is going to require touching alot of code, simply because polymorphic abstractions like
fmap are not possible.
Elm has a bad attitude toward learning beyond a certain level. Ideas like Applicative are in the platform, but the term Applicative is no where to be found. Meaning people who wish to learn more are left in a lurch. Instead of serving as the half-way house to Haskell that it could be, Elm seems to deliberately be producing a thought prison. Where developers can get lured in and trapped in their progress. Some people are quite happy there, but others are left wondering where to go from here, once they have mastered everything Elm has to teach them.
PureScript is a purely functional language that is very inspired by Haskell. PureScript has many of Haskell’s features, and a fresh take that removes many of Haskell’s warts. Here are some reasons you might choose PureScript:
- You like Haskell, but fear the immaturity of the GHCjs ecosystem.
- You want a strict language that is easy to reason about and debug at runtime.
- You want access to killer type system features in platforms that support NodeJS but don’t support Haskell (for example AWS Lambda).
- You want to wrap and use existing JS libraries, in an server client isomorphic application.
- You are John De Goes reading this (hi John!)
- You resonate with the possibility of PureScript actually superseding Haskell as the next great functional compiler.
Here are some reasons you might avoid PureScript:
- You want server client isomorphism and already have a Haskell backend.
- PureScript’s learning curve is too high given deadlines.
- PureScript lacks a feature from Haskell you can’t live without (type families?).
- Bower and Browserify are crap.
- Performance is super critical to your application.
PureScript is reaching maturity extremely rapidly, and while performance is not yet fantastic, it’s more than enough for production use cases.
What’s in this bag?
PureScript is the medium maturity option, not as mature as Elm, but more mature than GHCjs. Tooling for PureScript is very nice for it’s age. Any gaps that are there now, should resolve with time, as this is a community that is strongly empowered. PureScript unlike Elm, is also general purpose, and extremely powerful. As a result, there is no one best way to go about UI development in PureScript. There are two fairly DOMinant approaches:
- Halogen, an original library unique to PureScript, offering high levels of type safety, and a clean composable programming model.
- Pux, a flavor of TEA backed by Reactjs. Pux suffers from and shares many benefits from TEA in Elm, but is ultimately more flexible.
So how should you go about writing a UI in PureScript? It’s about what matters to you and what you care about. Should you go with Halogen you can expect a high degree of type level complexity and some very hard to reason about error messages. The abstractions in Halogen can be difficult to grasp, even for experienced functional programmers, but the pay off is awesome. You will gain incredibly reliable code, with few compromises.
Should you choose Pux, you will get much of the benefits of TEA, and some of the problems as well. Pux is designed to wrap React components, and those components are often of very poor quality. React is also fairly slow by itself, and so buying into Pux means buying into React as well.
Or you can roll your own framework, which is actually an attractive option here. You can mix and match, using parts of Pux and Halogen together with more fringe frameworks like Optic-UI. PureScript is fun and flexible, but there is also not a clear path to take, depending on your orientation, that is either a undesirable risk, or a highly desirable playground. Since there is a pure virtual DOM package in PureScript with great performance, I was able to roll my own version of TEA that blends TEA with Monadic programming in just a couple days. That would not have been possible in any other platform.
Programming model heterogeneity is actual worth considering. Today there is not a clear winner for how to program a purely functional UI, and working in a series of small burnable petri dishes might bring forth a better future for all.
Where is PureScript going?
Everywhere. PureScript is becoming more and more general and getting closer to competing with Haskell directly (though Phil (the author or PureScript) seems to disagree). So long as there is demand for Haskell like languages on the backend, there is room for a competitor that is strict by default, and designed to be embedded into existing platforms. With lessons learned from Haskell, PureScript boasts an ideal type class hierarchy, extensible records, and a documenting
A paradox of maturity. GHCjs is the new old kid. GHC is obviously the most powerful, flexible, and mature functional programming system for production use-cases. And not by a little bit either, Haskell has over 20 years of brilliant contribution of offer, and an incredibly expansive, principled and well-maintained ecosystem of packages. But compiling to JS is fairly new, and immature. Here as some reasons to pick GHCjs:
- It’s GHC Haskell!
- You resonate strongly with Higher Order Reactive programming.
- You want isomorphic server client code, and already have a Haskell backend.
- You can’t live without Haskell’s features and tool chain.
- Performance is not critical to your application.
- Almost total access to the vast wealth of work already done in the Haskell ecosystem.
Here are some reasons to avoid GHCjs:
- Complicated to setup and get going (unless you are using
- Performance is still an issue.
- You don’t resonate with Higher Order Reactive programming.
- Small ecosystem for JS development.
GHCjs does have a clear path forward on browser UI development with it’s single popular library, Reflex. Reflex is Higher Order Reactive programming, and is highly Monadic. Buying into GHCjs today means you are going to be buying into Reflex. Reflex is highly composable and flexible, by mixing Monadic abstractions like crazy. This means developer discipline, as IO can be done anywhere in the code, and discrete state machines are common.
There is also no separation between stateful logic and view logic, as the work of building a system in Higher Order FRP is more about specifying a network, than making expressions about change. This can make code very natural to write, but harder to follow, as there is no enforced organization. On the one hand, Reflex code feels like solid FP in the Monadic programming model (complete with MTL), but on the other hand has an imperative quality where code is describing how we accomplish our goals, instead of what the solution is.
Reflex is not backed by virtual DOM, and talks about how it makes virtual DOM unnecessary. This is technically true, but it also means more manual documenting of interactions between the state of the application and the DOM, as keeping elements in place is not automatic.
If you value highly composable UI components, but don’t want the type level complexity of Halogen, then Reflex might be for you. Reflex offers high performance streams that work together. Push streams, pull streams, and a tuple of the two, which is essentially a Signal. The one downside of this approach, is that fewer things fit together, simply by dint of there being multiple stream types. Creating steams is also effectful in some cases, though why some cases need to return a Monad, and others are more pure, can be hard to understand.
Reflex is also very new, so the ecosystem of components is tiny. By far the smallest of all options discussed (thought that will change with time). The documentation and other gaps also exist today, almost entirely as a result of it’s young age. So plan on spending some time in the IRC channel for things that you might expect live in a nice polished guide.
Will Reflex be the only framework for web based UI development in Haskell? No obviously not. Competing abstractions will come along once GHCjs adoption is wider. But for the moment, GHCjs has it’s hero, and there is a clear path forward.
(Note: Reflex is not tied to GHCjs, but this article is about programming in the browser, and I felt that GHCjs is somewhat tied to Reflex. I am aware that
react-flux is also an option, but don’t have the experience to comment. There are certainly other ways to do frontend development with GHCjs, but Reflex is the only one I’ve seen that appears to have significant traction).
I had not put Scala into the running of possibilities for purely functional programming, until @ValentinKasas corrected me. I’ve always been skeptical of Scala, ever since witnessing teams of people preaching FP while writing highly imperative code none the wiser. That said, there is clearly a ton of great FP being done in Scala, and with a little extra effort you can enforce purity.
Here are some reasons you might choose Scala JS:
- You want server client isomorphic code, and already have a Scala backend (or plan on using a Scala backend for one the reasons below).
- You resonate with Scala’s unique and wonderful type system in which ADTs and Subtypes live side by side.
- You need your backend to be in the JVM for some reason, and fear the risks involved in adopting less established alternatives.
Some reasons you might want to avoid Scala JS:
- There are better options on the JVM (Haskell will run on the JVM, along with PureScript).
- You application needs performance to be critical.
- You are more accustomed to an ML style language, and more standard type system.
IO can be managed in Scala by usage of the
Task Monad in Scalaz, which is much like the
IO Monad. Effectful actions can be wrapped in a Task, and it’s possible to localize the Task execution to a single location.
Alternatively you could write all your code in the context of
Free Monads with Cats. And treat the interpreter of some command Functor as the entry point. This should also be possible with Scalaz, but is actually the canonical approach with Cats.
(Due note: I’ve written limited Scala and no Scala JS. ValentinKasas felt it deserved a mention, and he is right. Luckily I have just enough Scala experience to justify commenting.)
Today I am doing Reflex, because thats the right fit for environment I am in, and loving it. That said, I’ve enjoyed Elm and PureScript immensely as well.
Its about fitting the situation in which you find yourself. There are no bad options here. It’s an incredible blessing, that we went from 0 viable ways to do FP in the browser to 4 in just a few years. All of these options are backed by corporate usage, so none of them are likely to die off any time soon!
I hope this helps you make a decision about which platform works best for you.
(Final note: If I forget to mention your favorite library, or felt I was mis-characterizing the subject matter in some way. Feel free to let me know! I’d love to learn more.)