I’m trying to create a library that can be used to build purely functional GUI programs using Scala, Scalaz, and the Java GUI toolkit, JavaFX. I was advised by a person in the Scalaz Gitter chat to look at the Purescript Halogen project and Transient since both have to manage state. I’ve read through Halogen’s docs and have looked through some of the source code.
I was wondering whether any of the Halogen contributors would care to answer some questions regarding the history of the project and give context for the design decisions they made.
Ok, here are some of my questions. (“you” refers to you and other contributors throughout the history of the project):
What are the major implementation decisions you have made when designing this project? In other words, what approaches could you have used and why did you choose your current one over others?
What are the strengths/weaknesses, pros/cons, ease-of-use/pain-points of the approach you took? How are you seeking to resolve some of those problems?
If you could redesign the project, what would you do differently?
Understanding the code
To understand the internals of the project, which files should I look at first before reading through the others?
Are there any issues/PRs that I should read through or further examine?
What is a high-level overview for how the Driver (i.e. the code that makes it possible for a developer to write declarative code that “just works”) works? It seems like the project could be used to render visuals other than HTML (which better fits my context), but having a general idea will help me understand its specifics more.
Alright, this is a bit long, but hopefully useful!
The primary motivation of the current design was to find a way to make it convenient and natural to have components that can wrap 3rd party libraries. The choices that imposed on us (having component state, being able to query components to retrieve information from them in an effectful manner) guided a great deal of the implementation and remaining design.
Halogen was essentially an entirely different project before the v0.5 release - @paf31 originally worked on the design prior to that, and we used it at SlamData for a few months - it was much more FRP-ish at that point, with no component state to speak of, no ability to query components, etc. Our experiences of trying to use it for SlamData’s needs is what led us to the radical component redesign, as an attempt to solve some really bad problems we had trying to integrate things like the Ace editor. (Originally, dealing with 3rd party components meant sending messages in at the very top level of the driver and then figuring out how to route the message to the appropriate place, plus having to always listen to every bit of state change that we might want to observe from a component, and so on. It completely destroyed any hopes of modularity when dealing with 3rd party components).
Halogen is somewhat notorious for having many type parameters and complicated types, but the state of affairs now is significantly better than the 0.5 version - we did not use any existential types at first, so all information about all components in the hierarchy was present in the type signatures, leading to some pretty fun type errors to decipher. Using existentials was a way of making components more modular and composable, and helping simplify the crazy types and errors that users used to deal with.
Pro and con: Halogen isn’t opinionated about how you use it. It leads to a difficult time for some people starting with it, since they’re not sure at what level they should split things up into components, etc.
We have no concrete answer to that - it’s something you get a feel for when working with it over time. That means there probably are heuristics that can be applied, but nobody has written much down about it yet.
Pro and con: There are also multiple ways of achieving the same thing (albeit with different tradeoffs) - component input+output values can kinda be used like queries. Values can be sent into a component via input or via query. Things like that. For these situations, the various options were added to address specific needs, but again it can be hard to figure out which is appropriate as a newcomer.
We’re not specifically doing anything to address this. There’s perhaps room to explore some of these things more in the guide, but it’s not something that an active effort has been made on.
Con: There are still quite a few type variables floating around, which can make the learning curve a little steep. Component setup stuff can be a bit boilerplatey, but on the other hand, at least it’s boilerplate that you can’t get wrong (as it won’t typecheck if it’s wrong).
This can also makes dealing with very generic/parameterised components (higher order components, etc.) quite a trial, getting types to line up and such.
I don’t know if there’s much else we can do to improve this further. We’ve made almost all the type variables that are not relevant to the component’s “interface” existential, eliminating the overhead there. In the upcoming v5 release the way child components are defined for a parent has been reworked to make it simpler for the multiple-types-of-child-component case that was previously where a bunch of boilerplate and type overhead came from.
There’s also some changes to the component internals in v5 that will improve the situation for creating higher order components, but some exploration will be needed there to figure out how to get the best out of it.
Con: The API for subscriptions (doing things like adding event listeners outside of the declarative HTML API) has always been difficult to figure out. It’s mainly for “advanced” use cases, so not a pain that everyone experiences at least.
It has been refined somewhat for the upcoming v5 release to hopefully make it more approachable.
Pro: It does succeed in its goals of making it possible to wrap 3rd-party libraries without it being massively confusing or difficult to set up, and also preserving the modularity while achieving this. A 3rd-party component is entirely indistinguishable from any other Halogen component now.
This is something we pretty much take for granted now, since there are only a handful of things we needed it for (like Ace), but in replying to this, I’ve been reminded of how much of a horror show things were originally, so it is kind of a big deal!
Pro: I’ve heard this from others as well as experienced it myself - “it just works”. There’s very little in the way of surprises when using it, making it easy to achieve what you want, but also that it’s easy to maintain later.
This is like the ultimate pro in my book - there’s a bit of a journey to get there sometimes, but when you do, the fact it lets you do what you need to without too much fuss is such a pleasure!
Nothing . We’ve taken quite a long and windy road to get to where we’re at, but along the way have been some pretty big changes. We’ve always done what we thought best at the time to improve the library for our uses, even if that meant quite drastic breakage, so the current state of the library is near enough the best thing we know how to do at the time it is released.
I’m not too sure, it depends a bit on what you’re looking for. Most of the issues and PRs discussing things are usually talking about implementation details rather than design, since a lot of the design discussion happened in chats, etc.
There’s also a lot of history that is not so relevant if you’re primarily looking at where we’re at rather than where we came from too. Here’s the issue that set the library on its current course though:
Some more recent issues on improvements to the API:
The Halogen.Component and HalogenM.Query.HalogenM modules would be high up on this list, as they have all the important aspects of the component/eval interface.
After that, pretty much everything under Halogen.Driver is where the actual nitty gritty internals exist. The internals are pretty gritty indeed too (see below).
You’re absolutely right that rendering HTML is not baked in - components themselves accept a bifunctor h value that is the render product (where the arguments are: 1. a type representing slots for child components, and 2. the type of actions/queries the render product can raise for the component to eval).
The driver is split into parts to support this too:
The Halogen.Aff.Driver.Eval just deals with taking component queries/actions, interpreting them as HalogenM, and interpreting HalogenM as Aff. Essentially it’s the interpreter for the Free HalogenF that HalogenM is a newtype of. It knows nothing at all about the rendered content.
The Halogen.Aff.Driver.State module contains types relating to the record that is associated with components for their runtime state. There are also some existential-ized versions of the types that are necessary for use at times in the driver code.
The record is basically a giant bag of mutable vars in the form of Refs, so the Eval/Driver code is really not that pleasant. We decided having a nice interface to the library was much more important that having a nice implementation though. The implementation only needs writing once, but if we don’t provide the tools we want in the interface, the same problems will need solving by everyone in every project.
The Halogen.Aff.Driver module deals with turning components into something that can actually produce a UI - it deals with all the stuff that is necessary to make a component work that Eval didn’t cover. It still is h-agnostic, but accepts a record that expects rendering to support a few operations.
Finally, Halogen.VDom.Driver has the runUI entry point that HTML based apps use. It constructs the record that the Halogen.Aff.Driver needs to deal with rendering, and then uses Halogen.Aff.Driver to do the rest.
Originally, Halogen had its own HTML representation that was translated into virtual-dom elements during this rendering stage. Eventually we came up with our own 100% PureScript virtual DOM rendering library purescript-halogen-vdom, so to avoid the overhead of changing representation again during rendering, Halogen’s internal HTML representation is now based on the purescript-halogen-vdom also.
The driver/render interface was designed before purescript-halogen-vdom was finished, but it is at least flexible enough to support purescript-halogen-vdom and a virtual-dom based renderer (although nobody actually uses the latter) despite those libraries having quite different designs. Supporting a React renderer is not out of the question (and probably React Native), but it’s not something I have the need for or time/ability to maintain.
My goal is to build a GUI program using FP concepts. It will need to use something like a textarea (albeit with some modifications), some 2D graphics, and it needs to be scalable.
Originally, I wanted to build a GUI program using Haskell, but saw that GUI was not well-supported in the language. Thus, I turned to Scala since I already had a background in Java. I’ve since learned that FP via Scala is bad (most use workarounds, tricks, and compiler plugins to deal with such issues). However, I didn’t really have an alternative AFAIK.
Decision for now…
After reading through and exploring a number of things, here’s the two options I know I have.
I could port Halogen over to Scala and try to write a VDom-like library for JavaFX (VSceneGraph) before I could then write my GUI program.
This would take a lot of time to port the library and I don’t think it’s currently possible (AFAIK, Scala/Scalaz/Scalaz-zio doesn’t currently have an AVar construct). Furthermore, writing a good VSceneGraph library would take time as I’d need to research other VDom libraries to understand how they work and then figure out what their equivalent would be in JavaFX (assuming I don’t hit any issues with package-private JavaFX API). Finally, I’d need to write tests that proved the code worked as desired.
I could use Halogen to write it in Purescript.
This would require learning web technologies (npm, pulp, bower, purescript libraries and where stuff is located, DOM stuff) before I could or as I create a GUI program.
Between the two, this second option is presumably much easier and more realistic. Moreover, Purescript’s syntax is close enough to Haskell that it feels like Haskell. So, I think going that direction makes more sense than my original one.
There are a few questions I now have, but most of them can be answered if I start Googling things and reading what they say (e.g. web technologies and how to use them).
However, I do have one question regarding Halogen. My program will need to draw some squares and lines. It seems I should use the SVG element rather than that Canvas element. However, Halogen does not include the SVG element in its Halogen.HTML module in both v4.0.0 and the current master branch. Why?