Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Union Types in Flow and Reason (jez.io)
109 points by jez on April 22, 2018 | hide | past | favorite | 53 comments


The TypeScript comment is wrong, the author needs to use a const enum: https://www.typescriptlang.org/docs/handbook/enums.html#cons...

Then it generates code that's better than Flow. It's not as aggressive as Reason for sure, but it's still pretty good.

https://www.typescriptlang.org/play/#src=const%20enum%20Scre...


Not just that, the initial Flow example can actually be translated 1-to-1 to TypeScript (without using enums at all):

https://www.typescriptlang.org/play/index.html#src=type%20Ap...


Ah! Thanks for pointing this out, I'll be sure update the post. (I haven't used TypeScript all that extensively, so I didn't even know about `const enum`.)


FYI here's a variant that eliminates the `impossible` function and instead uses an assignment to the same effect:

https://www.typescriptlang.org/play/#src=const%20enum%20Scre...

This is what uglify makes of that:

    var needsCancelButton = function (e) {
      switch (e) {
        case 0:
        case 1:
          return!0;
        case 2:
          return!1
      }
      return e
    };
EDIT: Just using `return screen` without the reassignment also works because the return type of the function and the type of the argument are incompatible, but that seems a lot more error prone and less widely applicable that explicitly reassigning to a `never` variable and returning that.


I hope the author adds this correction to his article because there’s no way to comment on it directly.


ReasonML is by far my favorite emerging JavaScript tech. Not only is its JS code output small, it's also performant. For immutable updates it generates code faster[1] that Object.assign! :)

[1] https://medium.com/@javierwchavarri/performance-of-records-i...


Would love to see a guide on using bucklescript with VueJS.


As someone who 1) knows a few functional programming concepts, 2) hasn't programmed extensively in a functional language, 3) would like to get productive fast (in web development), but also 4) wants a language for the long term (think general purpose, decent momentum), including for academic interests... Is there a language that fits all these requirements, or am I asking too much?

Elm is too limited, Haskell seems too convoluted, unsure about F# and OCaml, don't know about good alternatives.

I've been considering PureScript because it seems to strike a balance between features, JS interoperability and skill transferability, but I'm still a little afraid of it being unnecessarily complex for my next short-term projects.

I've only now started considering ReasonML, but am I wrong to think it's much closer to the "get productive fast" side than the "long-term investment" one? Not to mention its JS-like syntax, which I admit I have a distaste for.


I’m going to echo the Haskell recommendations. Just get started.

I was also overwhelmed by how convoluted the language seemed but eventually used it for a project, and my opinion completely changed. Haskell is a simple, beautiful language that enjoys great tooling.

Start out by learning just enough IO and monad stuff so that you can read input and pass it to your functions. This will not take long. Then you can gradually build up your knowledge of the language by creating modules that will become more and more idiomatic with time. You will discover parametrized types, laziness, and user-defined operators. At times you will struggle with how to use the type system to achieve your goal, but through practice, you will learn how it limits you, so you will know what approach to take.

Eventually you might notice that your code could be easier to read and write, and that’s where monads come in.

Somebody in this thread also recommended Clojure, which is a fine language that I would have also recommended a few months ago—it was my go-to language for spinning up servers or doing some computation. Now that I have tried Haskell though, I feel like I might not need to use Clojure again because Haskell is much more fun. So I’d hate for someone else to miss out on the fun like I did for years because they think they have to read a 600-page treatise just to get started! It’s very far from the truth.


Considering you mention not having programmed extensively in a functional language, I'd say you should pick a language with extensive documentation for beginners. I know you dismissed it, but many people have put a lot of time and thought into designing materials for learning Haskell. In particular, I think most people recommend the Haskell book[1].

That being said, all the choices you list (Elm, Haskell, F#, OCaml, PureScript) are solid languages with good communities. The core languages are similar enough that if you start learning one, the things you learn will transfer decently enough to all the others.

[1]: Haskell Programming from First Principles (http://haskellbook.com/)


A 1228 page introduction to the language? Wow.


It is definitely much more than an introduction. There's a lot of examples and exercises, and it goes into a lot of detail on some topics. The chapter about parser combinators is 70 pages by itself. It's only an "introduction" in the sense that it starts off assuming no Haskell experience.

In a sense, it feels like the textbook for a course combined with transcripts of the lectures... although it flows better than that, of course.


There's a lot of white space.


You can check with Clojure and ClojureScript to target JS specifically. You can write your backend in Clojure and the front part in ClojureScript. Having the possibility to use the same language for both is great.

It's a mature functional language with interesting properties, it's practical, data oriented. Libraries quality are top notch, you'll find no big framework but libraries working together to achieve what you want.

Some ClojureScript libraries are awesome, for example if you're on React you have Reagent or Rum that are fantastic libraries to work with React.

OP post could be written like this in Clojure (if you want to be explicit with all the screens):

    (defn needs-cancel-button [screen]
        (case screen
            :screens/loading true
            :screens/code-entry true
            :screens/success false))
You'll notice that this is not typed but you can use Clojure spec to have some validation (this way being less explicit just returning false for the Success Screen, and true otherwise):

    (def screens #{:screens/loading :screens/code-entry :screens/success})	
    (defn needs-cancel-button 
        [screen]
        {:pre [(s/valid? screens screen)]}
        (case screen
            :screens/success false
            true))
Union types can be mimicked using spec, you'll find how here: https://lambdaisland.com/blog/25-09-2016-union-types

OCaml is also great, IMO you won't lose your time by learning these languages, you'll find some way to improve your current code

Whether you learn Clojure, OCaml, F#, Haskell, Elm or Haskell, you'll end up being a better developer.


All your points (1 through 4) matched with what was looking for. I chose Clojure. It's Lispy, focuses on immutable data (but not necessarily pure functions), has a version (cljs) that compiles to javascript (if you're writing web-apps, its great to write both your front-end and backend code in one langauage). I've broadly found clojure to be a "practical" functional language to work in.

(If it helps, I'm primarily a python/js developer before picking Clojure).


For someone coming from JS/Python, I think a major familiar property of Clojure is the dynamic nature. You can quickly try something out or iterate and it runs right away, without asking you to specify the program completely type-wise.

Clojure's data checking and testing stories are good too, with spec and the various test libs - including generative tests from specs. And of course you can share the same specs on front and back end, demonstrating a payoff of the front/back code sharing.


The core of haskell is relatively straightforward, believe it or not -- it's just that a lot of what is blogged about is bleedong edge, either language-wise or in concepts they explain.

PureScript is pretty much haskell, it simply compiles to javascript -- wouldn't say there is much skill transferability in terms of language when coming from js, but it might make sense if you develop front-end.

Don't know about the other languages.


There's one huge difference between Haskell and PureScript: strictness. PureScript is strict, whereas Haskell is lazy. So be careful when you write algorithms which rely on laziness.


Thanks! Should have added that I actually haven't really done anything serious in PureScript either.


I went through all of the languages you mention, and OCaml/ReasonML are what you're looking for (if you want a typed FP language). More than 50% of FB messenger is written in ReasonML so it's clearly a good long term investment. If you don't like the ReasonML syntax, then you can just use OCaml because they're 100% interoperable. It's also a very practical language, which unlike the others doesn't militantly enforce purity, and lets you directly write imperative code.


I wanted the answer to a similar question for myself to be f#/fable - but it turned out that getting up and running on Linux with dot.net core and cli tooling was a bit convoluted.

I did manage to bundle a ~32mb hello world with dependencies/runtime (with cross-compilation, yay!) - but I couldn't get giraffe or any other server (framework) running in a reasonable amount of time. (Side-note, I'd appreciate a pointer to a f# Web server stack that works in the above scenario - something light that's appropriate to build a json-api server in).

Many tutorials appeared to be a year out of date, with wrong details - leading to code snippets not working, things not building, dependencies not installing.

Since I know ocaml is solid, I now am a strong believer in reasonml (which fixes some of the oddities with ocaml, making it more pleasant - in a way a little more like standard ml).

Reasonml has a strong Web app side, and compiles to sane js.

The other great option is clojure/clojure script. It's been dogged a bit by jvm (startup) overhead - not sure how/if graal/truffle AOT compilation will improve that.


Are you specifically looking for an introduction to functional programming, or to more generally modern language features? The strong static typing of Haskell or OCaml is orthogonal to their functional properties, but they tend to go together because they were designed by academic types in the last 20-ish years. Assuming you're interested in all of the above:

For immediate productivity, I would recommend Scala - it's run on the JVM, and lets you fall back to a fairly Java-like subset. It's ugly in some ways, but has a more mature web programming ecosystem than a lot of other functional programming languages - a decent backend framework in Play and Scala.js for maybe writing a few critical browser-side libraries - and has decent and growing adoption.

If you're not looking for quick productivity in web development, and don't care all that much about learning a language that's immediately applicable to existing job opportunities, I'd probably recommend the more "pure" modern languages like Haskell or OCaml; they're niche, but it's a stable niche, especially in more academic circles, and will give you a cleaner mental model of all the features that are hackily being tacked on in JS and Scala.

I've heard good things about Clojure (think LISP on the JVM), but haven't personally tried it and don't know how vibrant its community is. Having access to JVM libraries, as with Scala, is a great plus for immediate productivity.


The purescript example in the post shows one of Purescript’s biggest problems, and that’s non-optimal codegen.

The series of if checks (O(n)) it generates as opposed to a switch statement can really add up in loops with bigger enums.

Of the langs listed, ReasonML/OCaml have the best codegen/ffi story, it’s relatively easy to drop them into existing JS projects.


apologies for recommending to extend your scope of interest. But I would highly recommend to add to your list of 'targets': mobile development. That's because you mentioned: longterm.

Longerm, in my views, means being able to write software for web, desktop, multiple smart-phone/table platforms, with a backend in, ideally, the same language/tooling as front end.

It seems like ReasonML is the only one that is targeting the above [1] (but I could be wrong). And that's because of its integration/support of ReactNative.

[1] https://medium.com/@peterpme/your-first-reasonml-pr-into-an-...


Haskell supports this via GHCJS and cross compilation support. One cross platform library for front-end coding is Reflex-Dom


I'd go for F#, OCaml, Scala, or possibly Rust.

Elm, Purescript and ReasonML feel like much smaller communities that I wouldn't trust to necessarily make for a good long-term investment; Haskell is a bit too much of a jump to get productive in immediately. If you learn like I do - incrementally - then you want a strictly-evaluated, mainstream-ey language that allows you to write side effects without tracking them, just to start with.


How about Kotlin? First class support on Android. Great tooling and compiles to JS


Scala.js is a great, stable, mature tool. Scala is a sharp sword, but it fits your criteria quite well. I'm certainly enjoying Scala.js very much.


How is the uglify.js example correct? It replaces 'status !== 2' with '!(status>=2)'

   > needsCancelButton(2)
   false
Surely it should be:

   function needsCancelButton(n){ return 2 === n }
Also, more generally, uglify.js has no idea about the range of integers that could be passed in so how is a greater than optimization like that ever valid?


Ah, that's my mistake! The generated Reason output got copy/pasted incorrectly. (It's correct if you click through to the Try Reason link.)

In fact, Reason generates the `>=` comparison, and it knows it can do this because it know that a value greater than 2 can never happen.

The post should be updated to reflect this in a second!


I believe this is because it takes an enum, not an integer. Perhaps Reason is communicating this to uglify?


My understanding is that Uglify is just a JS to JS transform, and there isn't a way for Reason to supply type information. At least, I don't see anything about it in the README: https://github.com/mishoo/UglifyJS

Also note that, at least with Flow and TypeScript, type information isn't guaranteed to be correct, so it's a bit dangerous to do type-based optimizations (although tools certainly could try, with the caveat that if your types are wrong, your code may break even more than it otherwise would).


Your second paragraph is important here. Flow, TypeScript, and PureScript all have two-way interop with JavaScript code as a design goal. PureScript also has generating "fast, understandable code" as a design goal. It might be falling short on "fast" here, but it's much more understandable in isolation than the Reason output.

This explains pretty much everything the OP notices in the comparison.


I believe reason is a different flavor than typescript and friends, though I could be mistaken.


Nitpicky but critical detail: union types are not the same as "sum" types such as what you get in Reason, Purescript, and Elm. Union types are convenient for adding atop a dynamic language or modeling records (although, Purescript's row types are better), but they are less capable of abstraction.

The key example to differentiate union and sum types is `int union int == int` whereas `int sum int` is something unique and different from `int`. A sum type keeps track of the "left side" versus the "right side".

So, all said and done not only is Reason producing faster, tinier code... it's also managing a more sophisticated and powerful abstraction!


This doesn't sound right

Sum types are not more capable of an abstraction than union types. If we consider them in isolation, they are strictly less capable. For example, its possible to model sum types using union types in TypeScript by simply adding a tag field:

  type Either<T, U> = { kind: 'left',  left: T } 
                    | { kind: 'right', right: U }

If you do this, typescript will also be able to do exhaustiveness checks provided its configured with noImplicitAny and strictNullChecks:

For example, try removing a case item in the below code (paste it on http://www.typescriptlang.org/play/ and in options turn on the above mentioned checks):

  type Stringifiable = { toString(): string };

  function stringify<T extends Stringifiable, U extends Stringifiable>(thing: Either<T, U>):string {
    switch (thing.kind) {
      case 'left': return 'Left ' + thing.left.toString()
      case 'right': return 'Right ' + thing.right.toString()
    }
  }
edit: additionally, PureScript row types are not strictly better. While row polymorphic records are a better tool to model records (they can model open or closed records and subtype relations are not needlessly complex), AFAIK PureScript still lacks advanced transformation tools such as mapped and conditional types which really make TS records shine (see https://www.typescriptlang.org/docs/handbook/release-notes/t... for example)

Motivating example (given a record type, returns a record type containing only the properties that are not functions):

  type NonFunctionPropertyNames<T> = { 
    [K in keyof T]: T[K] extends Function ? never : K 
  }[keyof T];

  type NonFunctionProperties<T> = { [P in NonFunctionPropertyNames<T>]: T[P] };


You can encode sums using unions as you note, but you need to do this in order to have good abstraction. For instance, an option type `type Option<T> = null | T` has the weird property of being idempotent `Option<Option<T>> == Option<T>` which means that operations working generically over `Option` cannot exploit free theorems over T.

Mapped and conditional types add power to Typescript's repertoire, surely. On the other hand, this isn't a deficiency of _row_ types but instead the total position of Purescript's type system as opposed to Typescript. I just posit that row types are a nicer formulation for handling records than union/intersection atop sets of key mappings.


Depends on what you mean by "good abstraction". If I wish to use sum types to represent discriminated unions and parametric polymorphic types then yes its better to make sure the type "sets" don't intersect.

I wouldn't really consider this encoding special though - its standard practice e.g see Redux. The usage syntax could be better, but its very much a native, commonly accepted practice that works well.

If however I want to use unions for other things, like simple unions of `string | number` I can do that too. For example, I can also describe overlapping records of "options" where specifying one option means you have to specify another, and I can model that without nesting the option subsets. I can't do that with sum types.

Its unfortunate but lots of JS libraries do mix parametric types with their parameters e.g. `T | Promise<T>` means that Promise<Promise<T>> is the same as Promise<T>, however at least we can model that in the type system.

TypeScript is worse at all things involving parametric polymorphism, thats true. Mainly due to lack of higher kinded types, but also due to the bad modeling habits of parametric things built into the JS language (not just promises - look at array concat). But thats why I qualified my statement with "if you look at sum types in isolation"

With records, though, its just way ahead - so much so that I'm bothered by the primitive built in records in most other languages. Its great that row polymorphism is a nicer way to handle records, but I need the power too, and TS gives me that.


I'm not arguing that unions aren't a good choice for Typescript with its given aim to stay close to normal JS semantics. These choices make it difficult to abstract in raw JS as well (see all the Promises-aren't-monads noise).

And yeah, type-level programming is super powerful. You could install it into PS if you wanted, but I don't think that's the goal there.


The difference between Reason's and PureScript's output is easily explained if you take into account the differing goals of the two projects. PureScript has an emphasis on [JS interop](https://leanpub.com/purescript/read#leanpub-auto-polyglot-we...) going both ways. Being able to add some PureScript code to a JS codebase, or even to intentionally layer a JS frontend on top of a PureScript core, is an explicit design goal.

You can actually create instances of PureScript types in JS and pass them into functions defined in PureScript. That explains why there's a JS class for each data constructor. Similarly, the generated PureScript code can't actually "know[] the match is exhaustive", since it could be called from anywhere.

The article was interesting, but I think the comparison would benefit from providing an example of the code generated for calling the function as well as its definition.


From the section on TypeScript:

> enums, which are basically like Java’s enums.

They really aren't. Java enums are somewhat unique in that they are full-fledged classes, not just glorified constants. TS enums are much closer to those of C#.


Haxe also produces pretty nice JS output:

var needsCancelButton = function(screen) { switch(screen) { case 0:case 1: return true; case 2: return false; } };

The Haxe code can be found here: https://try.haxe.org/#79e40


Meanwhile in dynamic land:

  if(cancelButton == undefined) throw new Error("Specify if there should be a cancel button or not")


If the Flow type checker ensures exhaustiveness, why does the switch statement require a default case and an `impossible` function?


Read the linked previous post[1]. Flow doesn't ensure exhaustiveness unless you ask it to, by telling it that any case that goes to "default" is invalid (impossible).

[1] https://blog.jez.io/flow-exhaustiveness/


Miserable hack.


You can also just cast screen to empty in the default case by adding a line that says (screen: empty). This is how the flow docs recommend doing it in Redux reducers.


I think the root of the problem is overzealous use of OOP patterns like inheritance. If you would do the most naive implementation you would just do one view, copy it, and when needed a fourth, just copy it again, instead of entangling your code with yet more paths.


This is a nice insight into Reason's handling of sum types. Here's the Reason code as we'd typically write it:

    let needsCancelButton = fun
    | LoadingScreen | CodeEntryScreen => true
    | SuccessScreen => false
    };


Are union types the new lambdas?


They seem overused and overpraised. They have their uses, but largely you don't want to mash together different things in a program if you can avoid it. Because it leads to unmaintainable switching code. Alternatives: Have n containers of simple types instead of one container with elements of union types with n alternatives. Sometimes that's not possible because you need to process them in a specific order, but even then you can just keep an additional reference table that references into the n containers. Another alternative would be to make the type an ADT (of course, not always applicable, either).


For the Appendix: Idris is also a functional language that can compile to JS. I wouldn't say the output is very readable, but except for a couple of JS interop issues, I haven't had too look at it at all :)


I wonder how the dart to js compiler would do




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: