ReasonML : A Brief Introduction

ReasonML : A Brief Introduction

Contrary to popular opinion, JavaScript isn’t the worst thing to happen to humankind. If it were truly the worst, it wouldn’t have exploded in popularity. While quality and success are often not perfectly correlated, a product of abysmal quality would never succeed. JavaScript is, like many programming languages, an admixture of good and bad, with the good outweighing the bad. That said, there are true, fundamental problems with the language that are sadly getting worse as time goes on because of the community's refusal to institute breaking changes. TypeScript went some way to fixing a few of those problems, but after seeing pages of thick, TypeScript-specific syntax, I started to think that the halfway house of a JavaScript superset was the wrong course of action.

Obviously, it was the right course for Microsoft; TypeScript has proven to be an enormous success. It is wildly more popular than the previous “Better JS” language of CoffeeScript, and has absolutely buried PureScript, Dart, and Elm. The ability to gradually transition from a JavaScript codebase to a TypeScript codebase was, from a marketing perspective, gold. A great many packages on NPM are now being written with TypeScript bindings and the numbers continue to grow. While I do not think that TypeScript will ultimately surpass vanilla JavaScript, it will become a powerful force in its own right.

Ironically, I suspect that this success has laid the seeds for its eventual downfall. Basically, as TypeScript’s growth continues, the very thing that allowed it to explode—the reticence of developers to accept another language—will disappear, leaving behind a population that will be ever more likely to reject the shortcomings of TypeScript and instead look to a wholesale new language.

Brendan Eich gave a famous talk in 2015 called “Rise of The Compilers,” where he talked about how the newest abilities of JavaScript are available now compliments of transpilers, such as Babel, that can turn modern code into backwards compatible code. There are two ways to interpret this. The positive interpretation is that JavaScript will grow as developers no longer worry about cross-browser issues. But the negative interpretation, which I think more far-sighted, is that training a population to rely on transpilers trains that population to not care about the underlying language. JavaScript developers are systematically being demotivated to use JavaScript.

This new, fertile ground is being sown just as Web Assembly is providing new frontiers for front-end engineers and Go provides a similar back-end experience to Node but with significantly more power. An analysis of WebAssembly is outside the scope of this article, but bytecode for the web has been a desire of the community for at least a decade. This means that front-end engineering is going to fracture quickly, with code targeting both JavaScript and WA. The desire for a high-quality language that can target both JavaScript and WA is going to grow, and the winners of that competition have not yet been decided.

Enter Reason.

What Is ReasonML?

ReasonML, or often just Reason, is a syntax layer on top of OCaml. You may be thinking that it is similar to Elixir vis-a-vis Erlang, or Scala vis-a-vis Java, but in those cases, the alternative language actually compiles down to the bytecode used by the virtual machine. Since OCaml does not rely on a virtual machine, Reason actually compiles down to a standard OCaml abstract syntax tree, which can then be turned into JavaScript or into a native executable.

Conversely, Reason is not like TypeScript vis-a-vis JavaScript in that it is a superset of OCaml. It is a language unto itself and unlike JavaScript into TypeScript, where all valid JavaScript is also valid TypeScript, valid OCaml is not valid Reason. That said, anyone who knows OCaml will very easily be able to shift into Reason. There are many more similarities than differences.

Reason is very similar to OCaml in its structure but has a number of syntactical niceties to make the transition from JavaScript easier. You could conceivably write plain OCaml and transpile to JavaScript and the experience would be more-or-less the same. The benefit is that Reason’s syntax is arguably cleaner.

Reason was created because OCaml is an excellent language and it was discovered that most of OCaml maps to JavaScript very easily. As such, instead of starting with JavaScript and trying to create syntactic structures based on those semantics, Reason starts with OCaml and converts its structures to JavaScript. Some JavaScript functionality is lost, but these losses are mostly invisible to the developer.

Why Bother With ReasonML?

“Why Bother?” should be the mantra of any JavaScript developer. The amount of noise and junk in the JS world is overwhelming, and any shiny new thing must make an excellent case for itself. As you can likely suppose from my writing of this article, I think that ReasonML makes an excellent case for itself.

First off, what TypeScript showed with its gradual growth and adoption was that interoperation with existing code was critical for success in the open market. The majority of work is either maintenance or product growth. A small percentage of work is truly greenfield, meaning that for any tool or language to succeed, it must play nicely with what is out there.

That said, other languages have tried and failed, even with good interop, such as Dart and CoffeeScript. Neither are complete failures, of course. CoffeeScript’s weekly NPM downloads have increased from around 400k to around 700k in the past twelve months. But just because these languages do not suffer from poor interop doesn’t mean that they make a sufficient value proposition beyond that. CoffeeScript is fundamentally nothing more than syntactic sugar over an entire language. Meanwhile, Dart acts more like a refinement of JavaScript, while eliminating the functional parts that many developers enjoy and on which React, along with many other frameworks, was built.

But that is about the competition and its weaknesses, not about Reason’s strengths. Reason is, I think, best described as JavaScript done right. Or perhaps more charitably, it is what JavaScript would have been if Netscape had taken more than two weeks to design the language. It completely fulfills Brendan Eich’s desire to make Scheme for the web, while also including object-oriented functionality that fulfills Netscape’s desire to make the language align to Java-style objects. Further, it brings an algebraically guaranteed type system that accelerates development at scale while providing security beyond any other JavaScript-targeting language or tool chain.

The requirements of that type system include a number of syntactic structures and underlying semantic concepts that are different from traditional object oriented and procedural languages. These strictures are somewhat alien to developers coming from other languages and are a major reason for the failure of functional languages to achieve greater uptake. Once learned, though, they force developers to think and work in a very formalized way. Basically, it is more difficult to do something quick-n-dirty in a functional paradigm. There are positives and negatives to that, but as JavaScript increasingly becomes a “very serious tool” for “very serious people,” that restriction is almost undeniably a good thing.

Furthermore, Reason is different. It feels different. It looks different. It functions differently. It embraces its status as a higher-level language, instead of the odd, halfway house that is JavaScript or Java, where the language is high level but pretends at operating as though it were C. There is a great satisfaction to working on something at a more symbolic level then the old, hardware-rooted paradigm of the original “portable assembly.”

It is perhaps for these reasons that Reason’s Github star history is climbing faster than any other JavaScript compile language in history according to https://star-history.t9t.io. Not Dart, nor CoffeeScript, nor GopherJS, nor PureScript. Only the mighty TypeScript grew at a faster pace. While there are no guarantees, I believe that Reason stands an excellent chance at bursting into the mainstream and, along with OCaml, becoming the de rigeur language for garbage-collected WebAssembly development. It is everything we are already doing, only better, more pure, and more satisfying.

The Semantics

As stated, ReasonML and its underlying cousin, OCaml, are functional languages. While there is a lot of variance in what constitutes functional programming, it usually has three components important to the developers: functions as values, immutable data, and constant returns.

Functions as values is often called functions as first-class language components. That is to say that functions act like any other value and can be passed, returned, extended, and decomposed. This ability has been adopted by most other mainstream languages today and should be familiar to JavaScript developers.

Immutable data is a restriction that flies in the face of procedural and C-style operation. Maximum performance is had by directly changing a value sitting in memory. In functional programming, this never occurs. Instead, existing data is used as input into a function and the returned value is then stored in memory. Unsurprisingly, functional languages do not usually implement memory pointers.

Constant return means that a function always returns the same type. If a function can return nothing in one formulation, it must also return nothing in every other formulation. I focus on nothing because null errors are so common that language-level protections against them are noteworthy.

Along with those three principles functional languages have a strong focus on function purity. That is to say a function will always return the same output for the same input. Again, this is a lesson often deployed in other languages. The primary purpose of this is to isolate so-called side effects. Side effects are a change to application state. OCaml is not as rigid as some other languages as regards side effect management but is far better than most common languages, especially JavaScript.

The Syntax

OCaml’s syntax is somewhat alien to those accustomed to C’s dominating style. Lines are always terminated by double semicolons. Math operators are non-polymorphic save for minus. Lists and arrays are delimited by semicolons instead of commas. There are others, but this suffices.

Thus is the purpose of Reason: to take OCaml and make its syntax more similar to existing languages. This is not the vanity project that it may first appear. Language aesthetics play a large role in language success, which is precisely why JavaScript introduced the pure-sugar class constructor. It is hoped that the new aesthetics combined with the ability to target native binaries, JavaScript, and anything in-between will give Reason an advantage in the Darwinian competition that defines the language landscape.

So an OCaml list written as

let myList = [42; 2001; 3.14] ;;

Becomes the much more common

let myList = [42, 2001, 3.14];

Just as with JavaScript, the semicolon is not required but it is considered best practice to include. Unexpected behaviors caused by semicolon insertion are avoided, though.

All of the syntactic peculiarities are best covered in a complete language overview and are outside the scope of a simple introduction. Suffice it to say, it won’t look strange to most people.

The Party Pieces

ReasonML has four truly fantastic semantic structures that make it a language worthy of investigation: pattern matching, options, variants, and currying. Combined, they provide novel ways of expression logic while enforcing near-perfect operational security, giving developers the satisfaction of code that, if it compiles, it just works.

Pattern Matching

Pattern matching is at the root of functional languages. Since the language itself views computation as an equation to be run, determining if one “thing” is like another “thing” is likewise an algebraic process of symbol manipulation. This usually involves a recursive function that takes inputs and decomposes them into simpler and simpler symbols until a difference is discovered.

Since comparisons are not mere comparisons between two things sitting in memory, very complex structures composed of many primitives can be put through a pattern matching algorithm. As such, Reason allows the developer to determine if entire expressions are equivalent! It also means that Reason comes with the ability to do a deep comparison of structures built in.

type newRecord = {

x: int,

   y: int

};

let t = {x:23, y:42} < {x:33, y:42};

The above comparison will return true because the x value in each record is different, and 33 is greater than 23. This power needs to be wielded carefully, since deep comparisons of large objects can be expensive, but to have that power right in the language, along with all of the optimizations that language-level tools bring, is attractive.

Pattern matching takes the place of JavaScript’s switch semantics but adopts the switch syntax. In OCaml, what is happening is apparent by the usage of the keyword match, but since the ultimate behavior is so similar to switch, Reason’s designers simply decided to grab JS’s terminology. Again, aesthetics are important.

Options

Options are one of Reason’s conceptual building blocks that are different from more procedural languages. Basically, an option is the state of there possibly being a value. If there is possibly a value, then the entire program knows to account for the possibility of there not being a value. This is Reason’s front line against null and undefined errors and highlights the symbolic and philosophical nature of Reason’s conception. Possibility has nothing to do with registers and circuits. It is a higher-level concept that translates down to machine operations in a way that is invisible to the developer. It is the perfect argument for linguistic abstraction.

Options have two states, Some and None. Those who have worked with other functional languages before will recognize this immediately.

let anOption = Some(42);

The above example is highly simplified, but it suffices to communicate the concept. In this situation, the compiler knows that anOption may be an integer, but it may also be nothing. Thus, all later code that references anOption is forced to account for both states.

Options also communicate an important structure in functional programming, the monad. A monad is basically a super-simple wrapper for something. In the above example, Some() is the wrapper, and integer 42 is the value. This adds security because the compiler knows that it needs to handle the wrapper and not the contents and the wrapper’s behavior is fixed and known.

In the world of JavaScript, this sort of behavior is mimicked by “boxing” values. In this way of programming, values are never free-floating. Instead, they are wrapped in an array of only one index. This provides some of the same security, but since it is an operation created by the programmer and not the language, the strength of the guarantee is much weaker.

Variants

Options are a special manifestation of another structure in Reason, the Variant. Variants are a type with multiple possible states. Options have only two, Some and None. Variants can have an arbitrary number. But just as with Options, code that references a Variant must take into account its many possible states.

The possible states of a Variant are called Constructors because they can themselves be composed of multiple types. This is where the aforementioned pattern matching reveals its true power, as types built from types built from types can be checked and compared easily and reliably.

let myVariant =

   | State1

   | State2

   | State3(String);

let aThing = State2;

The compiler infers that aThing is of type myVariant and State2. But the compiler also knows that aThing could have been State1 or State3, so by combining variants with switch statements lets the compiler confirm whether a comparison is exhaustive or not.

let myState =

switch (aThing)

   | State1 => “In state 1”

   | State2 => “In state 2”

   | State3(str) => “In state 3 with “ ++ str;

All possible states of myVariant are covered. This goes far beyond a non-nullable type and gives us a flexible form of application construction that is nonetheless strictly constructed.

Currying

Currying has less to do with security and more to do with novel forms of idea expression. Basically, currying is taking a process that requires more than one argument and decomposing it into a series of functions that each take one argument.

I use the term process since the underlying conception is separate from the functions. For example, if I want to add three numbers, the concept I am decomposing is the adding of numbers of which there are three. A function is merely one way to achieve that.

The engineer can manually curry a process, thinking through the steps required to achieve a task and building only single-argument functions, but currying is usually handled on the language level, where an engineer is only concerned with the higher-level process while the language manages the single-argument functions under the covers. There are tools to achieve this in JavaScript, such as Ramda and Lodash, and JavaScript itself allows currying, but since the process is not truly a first-class process, there is significant performance overhead.

Reason has no performance overhead. Furthermore, the automatic currying of functions is the default functionality, meaning that it is not something that a developer can regret not implementing earlier.

let divide = (denom, numr) => numr / denom;

let divideBySix = divide(6);

let divideByTwo = divide(2);

In the above example, divideBySix() and divideByTwo() can be created by calling divide() with only one argument. We now have three distinct conceptual units of functionality but only had to write one unit of logic. This is a highly simplified example, but a slightly more complex one perhaps better illustrates the power of this.

let append = (appendString, baseString) => baseString ++ “ “ ++ appendString;

let appendSignature = append(“Keep in touch,\nAaron“);

The append function is written once, but can be easily decomposed to create a new function that appends my signature to any string. There are many more details that are outside the scope of this article, such as whether the first or last argument is passed and ability to label arguments, so further research for those interested is highly encouraged.

Astute readers may have noticed a similarity to object-oriented inheritance with this, and that is accurate. Both are ways to reduce code duplication and extend the abilities of a single piece of logic. And just as with inheritance, there is a classic “fragile base class” problem when being too curry-happy. An explanation of how to best write code to avoid these issues is outside the scope of this article. I mention this not to say that currying is fundamentally bad, but to show how the underlying considerations of functional programming and object-oriented programming are very similar. There is no reason to see functional practices as alien.

The General Language

I mentioned the party pieces first because they are the uniquely competitive tools that the language brings to the table. Reason also brings many other benefits that are less specifically granular and more about the total experience.

First, since Reason is mostly just OCaml, it inherits a large, powerful ecosystem of tools and knowledge. It is not nearly as large as other, more popular languages, but I argue that is a benefit. Any ecosystem is going to have garbage in it. The percentage is not fixed, though. Small ecosystems, since they are often the playthings of highly skilled programmers, are disproportionately high-quality. They may lack features, which explains their lack of adoption in Fortune 500 environments, but the features that are there are finely crafted. As a language grows, the percentage of garbage explodes as developers of all skill levels pile into the tool chain. NPM is perhaps the greatest example in history, with over half-a-million packages available, where tens of thousands are more like left-pad than Lodash.

OCaml is unique in this regard since it is a small language but has achieved significant uptake in academia and industry. It was created in 1996 by the French National Institute for Research in Computer Science and Automation, aka INRIA. It is itself based on Caml and ML. And except for Erlang, no other niche language has achieved as much acceptance in major companies. And just like Erlang, the companies that do use it consider it a competitive advantage over other companies wedded to the industrial giant squids that are Java and .NET. Because of this, unlike other niche languages, OCaml and Reason have libraries of extremely high quality modules that actually contain many of the features that industrial consumers require. Indeed, it seems that the only thing keeping major corporations from adopting OCaml is its conceptual difference from industrial languages.

Learning those conceptual differences offers great value. For example, extremely large Reason projects transpile to JavaScript in milliseconds. Meanwhile, TypeScript transpilation times can enter multiple seconds rapidly. This is not because the transpiler developers for Reason are better than Microsoft. It is because of the structure of the language itself and the guarantees that the structures provide. As stated earlier, Reason is quite superior to JavaScript, and the speed with which the code can be analyzed is a low-level manifestation of that superiority.

A more high-level manifestation of this is simple lines of code. Functional languages strongly encourage developers to decompose code into discrete units of functionality and then compose applications from them in declarative ways. This means that the language itself is driving authors to applications with fewer total lines of code. Obviously, good developers in any language can achieve this, but the fact that the language itself is encouraging it should provide architects with increased confidence.

Reason is also notably superior because it implements naturally many things that JavaScript developers are using regardless. Functional paradigms are growing in use. Immutable data is de rigueur for large scale applications, notably in React and Redux. And while TypeScript does not provide runtime type safety, many applications are leveraging third-party packages to achieve that. Reason does all of these things on its own. Basically, Reason is where JavaScript development is going. Why not just jump straight to the finish line?

The Future

Whether you have been convinced by my arguments or not, I think it undeniable that the JavaScript world is at a crossroads. What will the future of our industry look like? We as developers have an opportunity to make that choice before mere industrial concerns dictate technological direction as happened with JavaScript. We can proactively choose a language and toolchain like Google did with Go, where they set out to create the perfect language for their problem space. We need not be held hostage by the vicissitudes of fate.

There are three primary motivators. Server-side is continuing to grow and will demand better performance and reliability. Front-end application complexity is increasing. And WebAssembly will become a major player sooner than we think. The languages and tools that we choose must account for these motivators by enabling developers and sweeping away the detritus of technologies past. I see TypeScript, Redux, CoffeeScript, et al as an abdication of that opportunity, instead attempting to delicately construct an unstable edifice upon the pile of trash.

I argue that ReasonML is perhaps our best choice to achieve those ends. Only Go, with its robust community and ecosystem, is in the same ballpark. But while Go provides rigid structural safety, it is compositionally inferior to both JavaScript and Reason.

I don't mean to imply that Reason is perfect. Just as the OCaml on which it is built, Reason does not yet have a good implementation for handling multiple cores or scaling across an arbitrary number of servers. For the time being, if targeting Node and JavaScript, this is unimportant. But for the future of the language, where WebAssembly and native binaries are generated, this is a significant problem.

I think that most of Reason's problems are either technical challenges that will be met, like the above, or they will be solved with greater uptake by the community. Unlike JavaScript, there are no fundamental problems with the basic conception of the language. The fabric of Reason is not attempting to tear itself in two. The challenges ahead are the challenges that any developer would face in any language. But the benefits, of which there are many, are unique to Reason.

We have spent the past fifteen years crashing forward. We are covered in bruises and weighed down by a million, tiny bad decisions. We need not continue. We can choose a better life, liberated as much as we can be from the dictates of ignorant industry. I believe that Reason stands the best chance of becoming the language of the JavaScript intelligentsia—the domain of the elite—separate from the corporate-driven chaos of NPM-fueled feature factories. We can build a better language, for better teams, at better companies. Or we can grind out another agile ticket for a bank.

What would you rather do?

Alfie Long

President - Lumicity, part of the G2V Group.

4 年

Great article Aaron. I recruit for Front End Engineers in Boston. I would love to chat more with you to see how i can help, now or in the future.?

回复

要查看或添加评论,请登录

社区洞察

其他会员也浏览了