4

Why Derw: an Elm-like language that compiles to TypeScript?

 2 years ago
source link: https://derw.substack.com/p/why-derw-an-elm-like-language-that
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Prelude

This post will talk about why I’ve decided to make a new typed ML language based on the TypeScript ecosystem. It’s very opinion based, so you might not agree with all I write, but that’s okay. The language is not ready for general use yet, it has several bugs, but I thought I’d share some of my motivation. Writing it down and making it public also gives myself a good reason to keep working on it.

So, what is it, in a sentence?

Write TypeScript-compatible code in an ML language, with a great tooling environment built around it.

Derw is an ML family language, written in TypeScript, with TypeScript or Javascript as an output format. Generated output is type-safe TypeScript, untyped JavaScript, or Derw itself. 

If you’ve never seen ML before, here’s what it looks like generally:

validateAge: number -> string
validateAge age =
  if age > 21 then
    "Welcome to the store."
  else
    "Sorry, you have to be over 21 to enter here."

Motivation

In my day-to-day job, I write mostly TypeScript, on both backend and frontend. While TypeScript is a great improvement over vanilla JavaScript, there’s still a few things I dislike about it. Firstly, I miss the syntax of ML. The small things, like fewer brackets, simpler function. Basically: my personal preferences. I also miss how simple defining and using types is, and pipes. 

When it comes to the ecosystem, there’s a bunch of things I miss from Elm. A straightforward testing framework with no config. Function-based pure view functions as opposed to classes or functions with localized states. The MVU loop, powered by union types instead of using strings.

No language is perfect, and TypeScript is a serious improvement over JavaScript, eliminating a lot of my concerns when writing JS. Not to mention the tooling is amazing — VS Code’s support for TS makes working with unfamiliar code bases a delight while also speeding up development on projects you know inside and out. It’s just, it’s not ML. 

Why couldn’t I use Elm? Elm, currently, is an awesome language for writing frontend code without many dependencies. But if you need to work existing codebases, or write backend code, it’s a little more complicated.

What’s there today?

You can checkout the README for a full list of functionality, or check out the tests to see how they are used in practice.

Most of the syntax is there, and some level of type checking is involved. You can write Derw today, compile it, and run it.

There’s output generation for TypeScript, Javascript and Derw. Derw output is mostly there for formatting the file. There’s a command line tool for running the compiler. Generated TypeScript can be run through TypeScript’s library to validate that the file was correctly generated.

One of the deviations from Elm is the module system. The module system figures out both global (i.e node_modules) and relative (i.e src directory) imports. You can import Derw, TypeScript or Javascript, or export things from a file. Any Derw you import will be compiled, too, in the order the files are imported in.

What’s next?

The obvious missing part is how to deal with async functions. I intend to address this with a stdlib that provides wrappers for both async/await functions, and callbacks. It will probably look quite similar to Elm, though I haven’t landed on an idea I love yet. There’s also the issue of making functions that can be used in pipes, for example List.map instead of using someArray.map. 

Types is mostly handwaving currently, so I need to unify them between Derw files and imported TS. Long term, I’d like to rewrite the codebase in Derw itself. The current codebase is mostly there for bootstrapping, so I’ve taken a number of shortcuts. During the rewrite, I’ll work on making the error messages Elm-like: friendly, human and simple.

To build the ecosystem, I’ve been working on a collection of libraries that provide some TypeScript tooling I’ve been missing. I’d like to add support for Coed, a TypeScript Elm-style MVU loop framework with Html support. Bach is a no config test runner. The goal is similar to those in Elm-land, standardize on one tool, avoid additional configuration and keep it simple.

Elm vs TypeScript vs Derw comparison

Let’s take a look at a short comparison between some Elm, TypeScript and Derw code. These follow my personal style, so it might not be the same style you’d write.

When writing TypeScript, I like to treat union types as if they were union types in Elm. That means that each tag should be a function, and it’s own separate type. 

For example, imagine you were going to define a Result type which either can be a successful value or an error value. In Elm you’d define and use it something like this:

type Result a b = Success a | Error b

isValidName: String -> Result String String
isValidName name =
  if name == "Noah" then
    Success name
  else
    Error "Only Noah as a name is supported"

handleNameError : Result String String -> String
handleNameError result =
  case result of 
     Success name -> "Hello, " ++ name
     Error message -> "Hm, I don't know you"

In TypeScript, to get a similar experience, I write something like:

// type constructors for possible tags
type Success<a> = {
  kind: "Success";
  value: a;
}

function Success<a>(value: a): Success<a> {
  return {
    kind: "Success",
    value,
  }
}

type Error<b> = {
  kind: "Error";
  value: b;
}

function Error<b>(value: b): Error<b> {
  return {
    kind: "Error",
    value,
  }
}

// the union type itself
type Result<a, b> = Success<a> | Error<b>;

// functions
function isValidName(name: string): Result<string, string> {
  if (name === "Noah") return Success(name);
  return Error("Only Noah as a name is supported");
}

function handleNameError(result: Result<string, string>): string {
  switch (result.kind) {
     case "Success": return "Hello, " + result.value;
     case "Error": return "Hm, I don't know you";
  }
}

This is of course not the only way to write TypeScript code to achieve the same functionality, but it’s the pattern that I find that represents data structure in my head, and gives the best error messages and guidance. The main benefit of having a constructor function for each type is that you no longer have to write manual objects, and you can pass the constructor as a function to things like map.

In Derw, the above code would look something like this:

type Result a b = Success { value: a } | Error { value: b }

isValidName: string -> Result string string
isValidName name =
  if name == "Noah" then
    Success { value: name }
  else
    Error { value: "Only Noah is supported as a name" }

handleNameError: Result string string -> string
handleNameError result =
  case result of
    Success { value } -> "Hello, " + value
    Error { value } -> "Hm, I don't know you"

As you can see the Derw solution is a lot closer to Elm than TypeScript. But the compiler outputs code very similar to the TypeScript — meaning you can work with TS from Derw quite easily.

Another note is that I’m not a fan of JSX. If Javascript is generating HTML, I prefer using Javascript syntax. JSX provides an abstraction that hides how the underlying code actually works. That being said, in favour of being pragmatic, I’m planning to add JSX support to Coed. I want to make it easy as possible to transition code from React to Coed — and vice versa. Derw is not going to support TSX, but it might output it.

Existing alternatives

There’s a number of ML-inspired languages at this point for writing frontend code. Elm, PureScript, Reason, LiveScript. They all approach language design a little differently. I won’t go super deep into each language, but to give you a rough summary from my perspective. Elm might be considered the most limited one — focusing on great developer experience at the cost of flexibility. PureScript is powerful at the cost of being complex. Reason is based around OCaml, whereas I prefer Haskell-style. LiveScript is untyped and Javascript-based.

My background

To give some context on me, I figured I’d give a backstory to myself, like a comic book character. I got into Elm while researching at university, with my thesis being about how modern frontend frameworks follow functional programming. Later, I worked at NoRedInk where we used Elm for as much as possible. I used to spend a bunch of time working on Elm, both in my personal time and work time, but these days TypeScript has replaced that.

Some of the highlights of what I’ve worked on in Elm land:

Part of what drives me is helping others, so at some point I was answering most questions on the Elm Slack. I had to take a break from that, but the motivation is still there. We can make things better by working together. My hope with Derw is not to replace the existing options, but to give me a way to enjoy the code I work with more.

Any questions, feel free to reach out to me on Twitter.

Why is it called Derw? Derw is the Welsh word for oaks, and I’m Welsh. You might pronounce it as Deru if you were English.

P.P.S

If you missed it, the repo can be found at https://github.com/eeue56/derw


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK