Authors: Denis Redozubov, Catherine Galkina
Today we are going to tell you why we write frontend in Haskell and compile it to JavaScript. As a matter of fact, such a process is called transpilation:
Transpilation is the process of transforming the program written in language X into the equivalent program in language Y. In contrast to compilation, languages X and Y have roughly the same level of abstraction.
Why Do We Need Transpilation?
In general, transpilation can serve the following main purposes:
- Migration between different versions of the same language. Programming languages don’t stand still. They are actively developing and acquire new convenient and attractive features with each new version. Unfortunately, it may be the case that the new language features are not supported everywhere and right away, that’s why the issue of the versions backwards compatibility arises. In this case, such version-to-version transpiler does a sort of the expression desugaring into older and usually less expressive versions. Babel is an example of the transpiler translating JS code into its subset supported by browsers. Backward transformation is also possible when it is necessary to translate the project into a newer version of the language, but you are pressed for time and too lazy to do this manually. For example, you can use 2to3 to transpile Python 2.x code into Python 3.
- Translation from one programming language into another based on the runtime system requirements and/or developers’ wishes. For example, running in a browser requires the code in JS (which is used most often today) or WASM (which is less widespread for the present). Development, on the other hand, must meet other requirements, which are better fulfilled in another language. This source language may support unique mechanisms such as automatic parallelization or be related to an entirely different paradigm. The code generated by transpilers can either look almost identical to the source code (which simplifies debugging) or be transformed beyond recognition as compared with the source code. There are utilities that allow matching the transpiled code to the original code (for instance, SourceMap for JS).
Let’s give some examples:
- Languages used for frontend development and translated into JS:
- TypeScript is a JavaScript superset with optional type annotations checked during transpilation.
- CoffeeScript is a more expressive – as compared to JS – language supplemented with Python- and Haskell-style syntactic sugar.
- Elm is a purely functional language that features static typing (and generally looks much like Haskell) and allows creating web applications in the declarative style called The Elm Architecture (TEA).
- PureScript is also a purely functional and statically typed language with a Haskell-like syntax.
- ClojureScript is an extension of Clojure language (which, in its turn, is a Lisp dialect) used for web programming on the client side.
- Hardware description languages:
- Bluespec -- is a high-level hardware description language that initially came up as a Haskell extension and is transpiled into Verilog.
- Clash is also functional and uses Haskell-like syntax, generates VHDL, Verilog or SystemVerilog.
- Verilator, unlike the previous two languages, operates the other way, converting Verilog subset into C++ or SystemC.
- Transpilers of the assembler languages for various architectures or different processors in one architecture system (for example, between 16-bit Intel 8086 and 8-bit Intel 8080).
Why Not Develop in Pure JS?
As you can see from the examples above, the discussion of transpilation in general inevitably brings up the subject of translation into JS. Let’s consider its purposes and potential benefits in more detail:
- Transpilation to JS allows running the application in web browsers.
- Developers use the same tools as for the backend development, so you don’t need to learn other library infrastructures, package managers, linters etc.
- It becomes possible to use the programming language which is more in line with the team’s preferences and project requirements. You can also obtain such mechanisms as the strong static typing that is foreign to the classic frontend stack.
- The logic common for the frontend and backend can be arranged separately and reused. For example, calculating the total order cost can be a nontrivial task due to the domain specifics. On the client side, it is necessary to display the order total cost, and during the server request processing, everything has to be rechecked and recalculated again. You can write the business logic used to calculate the total order cost only once in one language and use it in both cases.
- The code generation mechanisms and generics are used, that allow you to make sure that JSON serialization and deserialization or even binary representation will function smoothly. We used this approach to speed up parsing for requests that needed a large amount of processing, which improved performance in a number of situations.
- The process of tracking API compatibility between the client and the server becomes easier. When the client and server applications are deployed synchronously and the browser caches are used correctly, there must be no incompatibility issues which may arise during asynchronous deployments. For instance, if one part of the application addresses another part using API, and the API changes, there is a chance of forgetting about the changes on the client side and losing request parameters, or sending the request body in an invalid format. This can be avoided if the client application is written in the same language. Ideally, the application won't even be compiled if the client function doesn’t correspond to the current API version.
- Developers with the same skills participate both in backend and frontend tasks, which provides the teams with additional organizational flexibility and improves the bus factor. In this way it becomes easier to assign the tasks and load to each of the team members. This is also important when an urgent fix is required – the least occupied team member takes on the task irrespective of the project part it relates to. The same person can correct the field validation in the frontend, a DB query, and the handler logic on the server.
Our Experience with JS Transpilation
We selected the frontend development tools considering the following factors:
- We wanted to use a language with strong static typing.
- We already had a fairly large code base for the Haskell backend.
- Most of our employees have a significant experience in commercial development in Haskell.
- We wanted to enjoy the benefits of one stack.
At present, here at Typeable we develop frontend in Haskell and use the web framework Reflex and the functional reactive programming (FRP). The source code in Haskell is transpiled into the JavaScript code using GHCJS.
The TypeScript and other JS extensions don’t work well for us as they offer weaker typing and their type system is not sufficiently developed as compared with Haskell. In general, these languages differ too drastically from those our team got accustomed to.
We’ve opted for Reflex instead of such alternatives as Elm and PureScript – first of all because we wanted to use the same development stack as for the backend. Moreover, Reflex saves you the trouble of following a specific application architecture and, to some extent, is more flexible and “low-level”. A detailed comparison of Elm and Reflex can be found in our post on the subject.
Conclusions
We were able to gain the benefits of JS transpilation we described above:
- All parts of the project are developed using the same stack, and the team members are “all-purpose” programmers.
- Simplistically, the project structure consists of a number of packages: API description, business logic description, backend and frontend. The first two packages are the parts shared by the frontend and backend, with the major portion of the code reused.
- We use
servant
library that allows us to describe API at the type level and check during the compilation whether both the server handlers and the client functions use correct parameters of the required types and correspond to the current API version (if you forgot to change the client function at the frontend, it just won’t be built). - JSON serialization and deserialization functions, CSV, binary representation etc. are generated automatically and identically in the backend and frontend. There’s almost no need to think of the API level.
Surely, some difficulties do exist:
- You still have to use pure JS FFI to work with external plug-ins.
- Debugging becomes more complicated, especially in the step-by-step mode. However, this is needed in very rare cases; most errors are found in the implementation logic.
- Less documentation is available as compared with JS frameworks.
Top comments (0)