Designing Crux – Methods

Crux’s foundations are rooted in the ML language family. Everything is a function. Haskell has this property too, but there is a really sucky aspect of an everything-is-a-function world.

Imports.

Here’s a common experience I have with Haskell:

let url = getCustomerUrl myCustomerId

OK, I have a string.

I need to check if it starts with “https://”

Scroll to top of file… That means I need to call the startsWith function. What module does it live in again?

Think… is this a Text or a ByteString? Or maybe a String? That determines which module I import…

This is a very common bit of friction, and it comes up all the time in Haskell, both for the standard library and your own code.

Now let’s hypothesize what reading from a data store might feel like in a world with explicit imports:

OK, my React component has an env prop.

I know the UserStore is on the env, but I need to import the Environment module to call getUserStore:

let userStore = Environment.getUserStore(env).

Now I want the list of users, so import UserStore and:

let users = UserStore.getUserList(userStore)

I can see it now: “Why can’t I just write let users = env.getUserStore().getUserList()? Crux sucks!”

In addition, assuming a roughly-one-type-per-module structure, each type gets two “names” that must be used: the module name and the type name. If types are refactored to live in different modules, all the callers have to be updated with new imports. Finally, if those names diverge, it could get confusing.

Dynamic languages like JavaScript and Python solve this usability issue by implementing all method calls with dynamic lookup. Besides the obvious performance hit, this can make analyzing the call graph nontrivial — both by humans and tools.

Languages like C++ and Java and Go take a different approach: they make methods a first-class concept. This adds some complication to the language when you want to use a method as a normal function. C++ has adapters that let you use members as functions sometimes but it’s nicer if methods are just functions.

Crux is not an object-oriented language, but we want the usability benefits of “method syntax”, sometimes known as type-directed name resolution:

  1. with no dynamic dispatch
  2. without any specialize method concept — methods should just be normal functions
  3. and, importantly, while still supporting bidirectional type inference for record types

The reason we have to worry about a conflict with type inference is because records already use dot syntax. Consider the following function:

fun distance(start, end) {
  return sqrt(sqr(end.x - start.x) + sqr(end.y - start.y))
}

Notice no type annotations! The distance function is inferred to take two records, each of which has floating point x and y properties. It returns a float.

Since we think it’s important that . syntax be used for records, Crux uses an arrow -> for method calls. Method calls only work when the type is known. Fortunately, in most programs, the type inference engine already knows the concrete types of most things. If you try to write a function like:

fun isHTTPSURL(url) {
  return url->startsWith("https://")
}

you will get a type error, since it doesn’t know what url is. The fix is easy: provide a type annotation on the argument:

fun isHTTPSURL(url: String) {
  return url->startsWith("https://")
}

In practice, most of the time, the type is known from context. Let’s go back to the Flux UserStore example. With method syntax, it would be spelled:

let users = env->getUserStore()->getUserList()

-> is mainly a way to avoid imports. a->b() can be read as “assuming the type of a is known, look up the symbol b in the module that defined a, and call b(a)”

The env example works because the type of env is known. Therefore, the location of the getUserStore() function is known, and it’s known to return a UserStore, so getUserList is looked up there. No type annotations required, there’s nothing special about member functions, and we achieve all the performance and tooling benefits of static dispatch!

One note: for simplicity, we have been applying the rule that the type must have been inferred before the method call is resolved. Technically, this is more restrictive than necessary. Consider the following example:

fun addSuffix(p) {
  if p->endsWith(".json") {
    return p
  } else {
    return p + ".json"
  }
}

The p + ".json" expression proves that p’s type is String, and therefore p->endsWith(".json") could be resolved. However, supporting this kind of backwards knowledge transfer would complicate the compiler (I think). Perhaps we’ll look into fixing it later. :)

Another note: sometimes you simply want to reference a “member function” without having a particular instance in mind. You might have the type name in scope, but don’t want to bother importing. We’ve considered support syntax like &String::startsWith to reference the startsWith function in the module that defines the String type. It could be useful in situations like ["foo", "bar", "baz"]->map(&String::length).

Two Kinds of Servers

Occasionally I overhear (or get sucked into) an argument that goes like this:

“Go is the best language for writing servers because X.”

“No, you neanderthal, Haskell is, because Y.”

“Well I’ve never needed Y! And Haskell is too out of touch with real problems.”

“But how can you tolerate such a limited and poorly-designed tool?”

“Well, I like to use it and I get stuff done, so what do you care?”

Rarely do these types of discussions change anyone’s mind. :)

People will argue all day long in a vacuum about what languages is best, but languages are tools and proper evaluation of a tool must be tied to a concrete context. Thus, the subject at hand: there are two types of servers. Specialized single-purpose servers differ from general business application servers.

Single-Purpose Infrastructure

Single-purpose infrastructure, like databases, request proxies, and messaging systems emphasize latency, throughput, reliability, and efficient use of hardware. Some examples are Redis, MySQL query proxies, and nginx. The choice of programming language matters to the extent that it facilitates the performance and reliability goals of the infrastructure. For these types of servers, Go is actually a great language. With Go, you get performance comparable to the JVM, a bunch of high-quality libraries, and the ability to plop your static binary onto any machine or container.

I’ll share an example from IMVU. IMVU’s primary content delivery system for all user-generated content, including 3D assets and user photos, was served by a pool of Perlbal proxies. This pool of machines served some of the highest traffic for the whole business, but the performance was not good and the code inside Perlbal was hard to maintain. Normally we wouldn’t have cared about the code except that we had discovered some race conditions in Perlbal. One of my coworkers finally got fed up and, within a couple weeks, replaced this entire part of our system with a replacement written in Go. Two machines (hooked up to 10g switches of course) running the Go implementation could serve all of the traffic previously allocated to the entire Perlbal pool. (We ended up running more instances for redundancy, however.) Not only was the new code faster, it was dramatically more maintainable. This was a big success story for Go.

Special-purpose servers usually have only a few skilled authors, clear goals, and eventually reach a point where they’re “done” and further maintenance is increasingly rare. (Though they might eventually be replaced with new infrastructure.)

Application Servers

On the other hand, every company has that server codebase with all the logic that makes the business go. You know the one. Where the code dates back to the founding of the company, and the programming language is whatever the founders liked at the time. :) It’s had hundreds of contributors from many backgrounds. Perhaps the original authors are no longer with the company. The needs of the code have evolved over time with the business, and after about four or five years it’s a person’s or team’s full-time job to manage large-scale refactoring.

I call these application servers, but I’ve heard the term “frontend server” used before too. Performance is a concern, but straight-line code execution is often less of an issue than efficient IO to the backend services.

Application servers are where the features live, and therefore they evolve with the needs of the business. The ability to define safe abstractions and refactor the code is more important than with special-purpose servers. Programming safety is also critical. You wouldn’t want somebody’s hack week feature to turn into a buffer overflow, runtime exception, corrupt data, or security vulnerability. Thus, type systems (or at least runtime invariant enforcement in languages like Python or PHP) are critical in application servers.

Go does not make a good language for application servers. The lack of immutable data makes it really awkward to enforce invariants. The lack of generics makes it awkward to express IO unless you generate code for each entity type in your database. Complicated arrangements of goroutines and channels are necessary to parallelize backend IO. Data structure traversals must be manually spelled out every time. Haskell, on the other hand is brilliant for application servers (assuming your team is comfortable with the learning curve). Libraries like Haxl allow specification of business rules in natural imperative style, while automatically extracting IO parallelism from the structure of the code. In Go, timeouts must be implemented manually every time, whereas in Haskell there is a single, always-correct timeout function. Go also doesn’t have anything like Haskell’s mapConcurrently, and while it can be done manually, it’s tricky to remain correct in the presence of unhandled errors. (In a high-reliability system, you probably want to transfer a runtime error like division by zero or null pointer dereference to your request handler’s error handler so it can be logged appropriately, rather than shutting down your whole server.) With Haskell, you can enforce 100% reliable unit tests or that transactions are never nested.

Even pre-generics Java was better than Go at defining invariants – with final, you could feel confident that a value is never changed after it’s initialized. This entire post by Dave Cheney could have been replaced with one sentence: “Too bad Go doesn’t support constant values in general.”

I’m not saying Go can’t be made to work for application servers, nor am I saying Haskell is necessarily the answer. Haskell has relatively poor usability characteristics, and I may write more about that later. But when you are trying to pick a language for a server, make sure you understand how it’s likely to evolve over time. A small group of people solving a focused problem is a completely different beast from hundreds of people hacking on business logic across years, so choose your language appropriately. :)

Designing Crux – Function Calls

Tupled vs. Curried Functions

Programming languages specify function argument lists in one of two ways: tuples or currying. Most mainstream languages use the former:

function add(x, y) {
    return x + y;
}

The function add takes two arguments. You can imagine the caller building up a tuple of arguments on the stack before calling the function.

Haskell and ML effect multi-argument functions with currying. For example:

add :: Int -> Int -> Int
add x y = x + y

This is equivalent to:

add :: Int -> (Int -> Int)
add x = \y -> x + y

That is, add is a function that takes an Int and returns another function which also takes an Int and returns an Int.

Currying is quite elegant. All functions take one argument, which makes certain types of metaprogramming easy. Curried function syntax is also extremely convenient: map (add 1) [1, 2, 3] returns [2, 3, 4].

However, there are some downsides to curried functions:

The first has to do with knowing when effects occur. In Haskell, where function application is always pure, this isn’t an issue. But since Crux allows unrestricted effects (more on this later), we want it to be obvious when side effects can happen. Consider this snippet of a hypothetical curried language:

add x y =
  print "adding " + toString x + " and " + toString y
  return x + y

Now consider this other definition:

add x =
  print "adding " + toString x
  return \y ->
    print " and " + toString y
    return x + y

Both functions have the same behavior when called like this:

add 1 2
add 1 3

But they would have different behavior with a partial application:

let addOne = add 1
addOne 2
addOne 3

That’s one argument against curried functions, though you could argue it’s a rare issue not worth worrying much about.

However, a more serious issue has to do with consistent performance. Curried languages don’t actually produce new functions for every argument of every call. That would be way too slow. In practice, functions take multiple arguments at the machine-code level (where the number of arguments is their arity), and when the function’s definition is not known at compile-time, some dynamic arity checks are added to decide whether to create a new partially-applied function, call straight through to the implementation, or perform oversaturation.

These dynamic checks add a bit of runtime overhead, and since we want Crux to have obvious execution semantics, we decided not to use curried functions.

Finally, I want to mention a third downside of curried functions: the error messages. At least in Haskell, if you pass too few or too many arguments, you don’t get a very obvious error message.

Prelude> let f a b = a + b
Prelude> f 1 2
3
Prelude> f 1
<interactive>:31:1:
    No instance for (Show (a0 -> a0))
      (maybe you haven't applied enough arguments to a function?)
      arising from a use of ‘print’
    In the first argument of ‘print’, namely ‘it’
    In a stmt of an interactive GHCi command: print it
Prelude> f 1 2 3
<interactive>:32:1:
    Non type-variable argument in the constraint: Num (a -> t)
    (Use FlexibleContexts to permit this)
    When checking that ‘it’ has the inferred type
      it :: forall a t. (Num a, Num (a -> t)) => t
```

Syntax

Syntax is somewhat tangential to the issue at hand, and it’s certainly possible to use the f x y z syntax with tupled functions, but my experience teaching Haskell is that people get unnecessarily hung up on the "f x y z" call syntax and we decided against spending our strangeness budget here.

Conclusion

That was a long justification for saying that Crux’s function syntax and semantics are just like every other mainstream language. :)

fun add(x, y) {
    x + y
}

let four = add(2, 2)

Designing DFPL – The Name (Update)

Previously, I’d talked about the import of a programming language’s name.

I have a quick update on that front. Andy had originally named the project Crux, but we questioned if we wanted that name in the long term. So… when in doubt, run a survey!

I put together a simple survey with some front-runners. Two questions: “Which is your favorite?” followed by “Check all that you’d be okay with.” Crux came in 2nd place for the favorite but it was the one most people were okay with, and since it was the name we already had, we stuck with it. :)

Dropbox Hack Week: GraphQL Server in Haskell

Last week was hack week at Dropbox. I took the opportunity to explore the implementation of a GraphQL server that does optimal IO batching and concurrency.

Serial IO

A common performance problem in the implementation of web services is serial IO. Let’s say you have a service that returns the names of all of your friends. It’s easy and natural to implement it like this:

me = fetch_user_info(my_user_id)
friend_names = []
for friend_id in me.friends:
  friend_names.append(fetch_user_info(friend_id).name)
return friend_names

The problem is that each user info lookup for my friends occurs sequentially. Even with TCP connection pooling, that’s still a packet round-trip in the datacenter. “But that should be faster than 10 ms right?” Even if it is, it doesn’t take many friends to blow your performance budget and send your response time across the threshold of perceptible delay.

Moreover, this problem compounds on itself. If your inner functions have serial IO and you call them in a loop, you’ve just added potentially thousands of round-trips to your backend data sources. I would hazard a guess and say serial IO is the largest contributor to service response latencies.

Manually Batched IO

One solution is to always batch IO. Every function takes a list and returns a list. This can be made to work (indeed, I’ve achieved excellent performance by carefully, manually, batching) but doesn’t compose as your services scale. Sometimes it’s just too hard to know all of the data you will need to fetch, and the dependencies between that data.

OK, so serial IO is bad. More on that in a bit. Let’s talk about REST now.

REST

At IMVU, we went all-in on REST, building a rather substantial framework to make it easy to create REST services. We anticipated the fact that REST services tend to require many round-trips from clients, so we built a response denormalization system, where the service can anticipate the other services a client will need to hit and include their responses too.

This sounded great at first, and in fact was mostly an improvement from the prior state of affairs. But, at scale, REST has some challenging performance problems. For one, REST services have a defined schema, and they always return the data they advertise. As I mentioned, if a service doesn’t return all the data a client needs, the client needs to hit more services, increasing the number of client-to-server round-trips. In addition, because the service always returns the same set of data, it must query the backend database to fetch more data than the client even cares about.

This phenomenon tends to happen most frequently with core domain objects like users. Because users are so important, they accumulate relationships with so many pieces of data (e.g. list of subscribed experiments, contact lists, shopping carts, etc.), almost all of which is irrelevant to most clients.

Why GraphQL?

This is where GraphQL comes in. In GraphQL, the client specifies the data that it wants. There is only one request, even if the data is deeply nested, and the server only has to fetch exactly what’s needed.

Consider the query:

query HeroNameQuery {
    newhope_hero: hero(episode: NEWHOPE) {
        name
    }
    empire_hero: hero(episode: EMPIRE) {
        name
    }
    jedi_hero: hero(episode: JEDI) {
        name
    }
}

It looks up the hero of each of the first three Star Wars movies, fetches any information it needs from the backend, and returns only what is requested:

"data": {
    "HeroNameQuery": {
        "jedi_hero": {
            "name": "R2-D2"
        },
        "empire_hero": {
            "name": "Luke Skywalker"
        },
        "newhope_hero": {
            "name": "R2-D2"
        }
    }
}

There are GraphQL implementations for many languages but many of them don’t solve the serial IO problem I described to start this post. In fact, a naive GraphQL implementation might issue IO per field of each object requested.

For hack week, I wanted to explore the design of a GraphQL server that issued all of its backend IO in optimally concurrent batches.

Why Haskell?

Dropbox doesn’t use Haskell, but I find it to be a great language for exploring design spaces, particularly around execution models. Also, Facebook open sourced their excellent Haxl library which converts code written with serial IO into efficient batched requests. Haxl provides an effect type that, when it can understand that two data fetches are independent, runs them both in parallel. When all Haxl operations are blocked on backend data fetches, only then does it issue the backends. My prototype GraphQL resolvers are surprisingly naive, specified with sequential code. Haxl automatically batches up the requests and hands them to the DataSource for execution.

In addition, there is nothing clever about the GraphQL request handler or graph traversal and filtering — all cleverness is handled by Haxl.

At this point, you might be thinking “So was anything challenging about this project?” On the data fetch side, no, not really. :) However, I did run into one unexpected snag when using Haxl for data updates: because Haxl tries really hard — and in a general, composable way — to run your IO in parallel, you must be careful about how writes and reads are sequenced together. If you leave out the () <- on line 105, Haskell sequences the operations with >> instead of >>=, and Haxl’s >> uses the Applicative bind operation instead of the Monad bind operation, and thus it assumes it can run them in parallel. And, as you might expect, issuing a write and read to the same data concurrently doesn’t end well. :)

Conclusions

I am very thankful for jdnavarro’s excellent GraphQL query parser. With it, in four days, I was able to get a prototype GraphQL server up and running. Using Haxl and Hedis, I have it hooked up to a Redis data source, and it correctly batches all independent IO reads and runs them concurrently.

The Star Wars hero names query above results in two batched backend requests:

fetch star wars batch of size 3:
["FetchEpisode(Jedi)","FetchEpisode(Empire)","FetchEpisode(NewHope)"]
fetch star wars batch of size 2:
["FetchCharacter(1000)","FetchCharacter(2001)"]

You can even see that it noticed that R2-D2 is the hero of both movies, and only requested its info once.

The performance is pretty good: on some AWS instances, I measured about 3500 queries per second per machine and a query latency averaging 3 ms. Of course, that could worsen as permissions checks and so on are implemented. On the other hand, the code is completely unoptimized, full of lazy data structures and an unoptimized parser without a parser cache.

The prototype code is open sourced on the Dropbox GitHub.

It’s probably possible to build something like Haxl in Python with generators, but you’d have to give up standard loops and list comprehensions, instead using some kind of parallel map operation. You also would not benefit from anything that teases concurrency out of imperative functions like GHC’s upcoming ApplicativeDo extension. There are some things that Haskell’s restricted effects are uniquely good at. :)

I’d guess it would probably be even trickier to do a good implementation in Go given that the programmer has less control over goroutines than Python’s generators and Monads in Haskell. That said, perhaps someone will discover a clean, optimal implementation. We should pay attention to efforts like this.

I think GraphQL will be a huge deal for high-performance web services. It’s harder to implement than REST or traditional RPC services, but the reduction in response latencies could be substantial. Naive implementations aren’t going to have good performance, but if you are interested in aiming straight at the finish line, Haskell and Haxl will give you a head start.

Designing DFPL – The Name

Without hard evidence, it’s hard to say how much a language’s name matters, at least beyond basic Googleability.

Marketing has an effect on any kind of product, and programming languages are no exception.  Some names convey their different from other well-known languages, like C++ (an extension of C) or TypeScript (JavaScript but with a type system).  Java, Go, Python, and Ruby, on the other hand, are short, memorable words with neutral or positive connotations.  Bonus points for names that carry a family of associated words, e.g. Ruby and Gem.

Since our language is more of a synthesis of good ideas than a modification to an existing language, a short positive word seems to be the right strategy.

Crux is the first name we tried, though neither of us is thrilled with it.  We briefly tried the name Sneak, but everyone hated it.  Fig is short, neutral-to-positive, but nobody seemed excited by that name either.

Do you have suggestions?

The name affects the file extension, which ought to be short.  (Or am I the only one perpetually annoyed by the horizontal real estate consumed by .java and .coffee?)

Designing DFPL – The Broad Strokes

Why not Python?

I love Python. It’s easy to learn, predictable, and works well for a wide variety of programs. Python’s the first language where I regularly found myself writing programs and having them work on the first try. The problem with Python is that it breaks down at scale.

Performance

In my experience, whenever a Python codebase reaches a couple hundred thousand lines of code, performance becomes an issue. It’s well-known that Python’s straight-line perf is not even close to C or even Java. In addition, Python programs don’t make efficient use of memory: objects are large and sparse — programs consist of a lot of pointer-chasing. The garbage collector’s not great, and startup and module loading times become an issue at scale, especially for short-lived processes like unit tests, build scripts, or desktop applications.

There are Python JIT compilers, but JITs don’t really help with startup time or memory usage.

Parallelism

Python has a GIL, meaning it’s hard to write programs that make efficient use of the hardware. It’s possible to move CPU-bound work into C, but any kind of coordination between Python and C has to ping-pong across the GIL, which becomes a central bottleneck.

Launching multiple processes can help, but each process pays Python’s base process memory usage overhead, and they can’t easily share in-memory caches.

As we’ve seen from Haskell and Go, the ability to have convenient, lightweight, efficient parallelism is a big deal.

Maintainability

I talked about this in The Long-Term Problem With Dynamically Typed Languages. As codebases get huge, you really want to be able to lean on the computer in order to safely make changes. Type systems are mini proof assistants for expressing guarantees like "I never mix up binary data with textual data" or "I always check the case that optional values are empty". Unit tests help, but are "proof by example". Type systems add a meaningful layer of confidence, allowing software to scale to larger teams and faster changes.

Why not Haskell/Go/Rust/Scala/F#/Whatever?

I agree 100% with Patrick Walton here:

There is room for some statically-typed language to occupy the network-services niche where performance needs to be good—maybe 2x slower than optimal (note that this is not acceptable for all network services)—and speed of development trumps most other considerations. Some sort of hybrid of Swift, Rust, and Go, perhaps, as none of them are 100% ideal for this space.

The dynamic language space is relatively saturated at this point. Rust is doing a great job in the native machine performance space. F# and Swift and, to some degree, Scala are excellent choices for the .NET, Apple, and JVM ecosystems, respectively.

But as of yet, there’s nothing great in either the web frontend space or the application server space.

Haskell’s laziness, curried functions, and nonobvious operational semantics are not suitable for frontend code. While it’s an otherwise perfect language for writing application servers, its syntax is tragically divisive, and I have no hope that Haskell as it stands today will become mainstream.

Go’s type system somehow manages to be both verbose and not expressive enough. Go is sufficient for single-purpose servers, but, from first-hand experience, the lack of generics really hurts in application servers. (I probably owe an essay on the difference between the two types of servers.) The Go community tends to rely on idioms, but it would be better if the language was general and expressive enough to allow direction expression of things like timeouts and cancellation. Also, Go is not actually memory-safe. Go is especially frustrating because, if it had, say, generics and tuples instead of the more limited multiple-return-values, it would be a truly excellent programming language.

Rust’s concurrency is not lightweight enough, and the syntax is extremely verbose.

Scala has a significant platform dependency on the JVM. Check out the size of some Scala.js programs. Also Scala’s type system is insanely complex.

F# brings along a large .NET dependency. It also suffers from the ML syntax. (Actually, it has two syntaxes.)

Maybe Swift is great, but it was only just open sourced, and I’d be worried about the Obj-C platform dependency. I’ve yet to write any real Swift programs, but what I’ve seen looks great.

All of these languages are lovely and have their uses, but none are particularly satisfactory replacements for Go or TypeScript/JavaScript. That is, lightweight code that can run at high performance in a browser and on a concurrent, scalable server.

But, hold on, what about js_of_ocaml? This is where things get interesting. Andy used js_of_ocaml for a while and found that, on paper, Ocaml has all the right properties: it’s safe, high-performance, compiles to very concise, efficient JavaScript, expressive, concurrent, established… In fact, it’s nearly perfect… if you can stomach the syntax, that is. As I mentioned previously, at this point I’m convinced that ML-family languages haven’t caught on largely for reasons of taste (or lack thereof — just look at the js_of_ocaml website’s font selection).

The Goals

So what are we trying to do with this thing?

The language doesn’t need anything new or innovative. ML gives us how bidirectional type inference, sum types with pattern matching, and soundness. Haskell gives us overloading (i.e. type classes, i.e. type-indexed values). Go shows us how important a solid experience is, both for the first time and in daily use. Python shows us how it’s possible to connect predictable syntax and solid, composable semantics.

Thus, this is largely a human factors project. Our goal is to see if we can marry all of those things into one language.

Our goals are to have

  • Memory safety.
  • Static typing without heavyweight type annotations everywhere.
  • Language features that are composable.
  • A minimal runtime, with no large platform dependency.
  • Obvious execution semantics. Like imperative languages, you should be able to look at a program and understand its runtime behavior.
  • Tight, clean compilation to JavaScript. Should be a suitable alternative to TypeScript or CoffeeScript.
  • A reasonable replacement for Go or Node on the backend.
  • Cost-free abstractions.

Non-goals

  • C- or Rust-like performance. Rust is already doing a great job there.
  • Support for restricted effects. It’s not clear the usability cost is worth it.

Next time, I’ll either dive into one of the specific design decisions we’ve made, or talk about the language’s overall gestalt or texture.

CRTs, Pixels, and Video Games

A collection of links about how CRTs rendered old video games differently than the harsh pixelation we see today in emulators and retro-style games:

Designing a Delightful Functional Programming Language

ML and Haskell have amazingly powerful type systems. Their type systems, and to some degree syntax, have widely influenced the modern crop of languages. Both can achieve machine performance. Haskell has fantastic concurrency. Both have concise, boilerplate-free syntax — some would say too concise. Both have the ability to build abstractions without impacting performance.

So why aren’t there more Haskell or ML programmers? Like, it’s fantastic that Rust and Swift borrowed some of their ideas, but why did it take two decades? Why do things like CoffeeScript and Go spread like wildfire instead? Go is especially bizarre because it’s not really very different from Java in many ways, and a lot worse in others.

I’m sure there are many reasons. Tooling is important. Marketing is important. Haskell is (or perhaps just was) largely a research language. The first-time user experience is important. I don’t claim to know every reason why some languages spread and others don’t, but I know at least one thing: syntax matters a whole lot more than we’d like to admit.

Consider all of the bracing and spacing and indentation arguments people have about C.

Remember that brilliant people have whined about the fact that Python uses whitespace to indicate nesting, as if that materially affects how much work they can get done.

Hell, CoffeeScript gained significant popularity by adding nothing but syntax to JavaScript. There were many opportunities to add actual features to CoffeeScript, like lightweight coroutine syntax, but CoffeeScript’s motto was “It’s just JavaScript.” Its popularity is based on the strength of the syntax alone.

There are two ways to interpret this phenomenon. One is “stupid humans and their quirks and biases, if only the uneducated masses would see the light and focus on what really matters”. Another is the realization that programming is a deeply human activity. A programming language lets humans take their human thoughts and map them into something a computer can execute. The aesthetics of that activity are important.

If a language is going to get adopted, at least without significant corporate backing, it needs to appeal to peoples’ tastes.

Back to Haskell and ML. They have their proponents, but it’s only until now that they’re starting to dance with the mainstream. It’s a shame, because software would be in a much better place if all the material benefits of ML and Haskell (bidirectional type inference, cheap and safe concurrency, sum types) had spread faster.

I’ll share some relevant experiences.

My personal OCaml experience occurred in university. One day I decided to play with this OCaml thing. My rationale is perhaps embarrassing, but I quit for two reasons: the addition operator for integers is + but for floats it’s +.. The other thing was the pervasive use of ;;, at least in the repl. Honestly, it felt tasteless, so I dropped it and moved on, never getting to see the real benefits (polymorphism, type inference, modules, sum types).

Now, Haskell. I was part of the effort to get Haskell adopted at IMVU. The benefits of using Haskell were simply too great to ignore. But when we tried to get everyone to learn it, a nontrivial fraction of engineers developed a visceral aversion. “I understand all the benefits but… I just hate it.” “Why do you hate it?” “The whitespace, $, the two let forms, the weird function names… I dunno, it’s just ugly and I hate it.”

It made me deeply sad that these seemingly superficial complaints prevented what should have been a slam dunk, at least when it came to safety and performance and maintainability and development speed.

When I joined Dropbox, I had some conversations with teammates about the benefits of something like Haskell (most people either don’t know about it or think it’s some academic obscurity), and I got a serious negative reaction to the idea of static types. “They’re heavyweight.” “They’re hard to learn.” “They hinder refactoring.” “They hurt development speed.”

None of these are necessarily factual. After I poked and prodded, I figured out that the resistance to the idea of static typing comes from prior exposure to things like C++ or Java (or Go), which have especially verbose syntaxes and type systems. This is similar to what happens with functional programming, where a common reaction to the idea is “Ugh, I hated my lisp class in college.”

These realizations led Andy and I to take on a project: design a programming language with all the expressive power and semantic correctness of an ML, yet with a syntax and aesthetic that would be instantly recognizable to your typical Go or JavaScript programmer. Surely it’s achievable. This is largely a human factors project – while the PL theory is important, we do not intend to significantly innovate there. Instead, the main innovation is applying familiarity and usability to a domain with the opposite reputation.

In subsequent posts, I will describe some of DFPL’s design decisions.

PL Usability: Name Fewer Things

If you follow me on Twitter or know me in person, you know I’ve been somewhat obsessed with programming languages lately. Why? In my career I’ve written high-performance desktop software, embedded controllers, 3D engines, scalable and low-latency web stacks, frontend JavaScript stacks… and at this stage in my life I’m tired of dealing with stupid human mistakes like null pointer exceptions, uninitialized data, unhandled cases, accidentally passing a customer ID where you meant an experiment ID, accidentally running some nontransactional code inside of a transaction, specifying timeouts with milliseconds when the function expects seconds…

All of these problems are solvable with type systems. Having learned and written a pile of Haskell at IMVU, I’m now perpetually disappointed with other languages. Haskell has a beautiful mix of a powerful type system, bidirectional type inference, and concise syntax, but it also has a reputation for being notoriously hard to teach.

In fact, getting Haskell adopted in the broader IMVU engineering organization was a significant challenge, with respect to both evangelization (why is this important) and education (how do we use it).

Why is that? If Haskell is so great, what’s the problem? Well, there are a few reasons, but I personally interviewed nearly a dozen IMVU engineers to get their thoughts about Haskell and a common theme arose again and again:

There are too many NAMES in Haskell. String, ByteString, Text. Functor, Applicative, Monad. map, fmap. foldl, foldl’, foldr. Pure let … in syntax vs. monadic let syntax. Words words words.

Some people are totally okay with this. They have no problem reading reference documentation, memorizing and assigning precise meaning to every name. This is just a hypothesis, but I wonder whether mathematically-oriented people tend to fall into this category. Either way, it’s definitely true that some people tolerate a proliferation of names less than others.

As someone who can’t remember anything, I naturally stumbled into a guiding principle for API design: express concepts with a minimal set of names. This necessitates that the concepts be composable.

And now I’m going to pick on a few specific examples.

Maybe

My first example is Haskell’s maybe function.

First of all, what kind of name is that. I bet that, if you didn’t know what it did, you wouldn’t be able to guess. (A classic usability metric is how accurately things can be guessed.)

Second of all, why? Haskell already has pattern matches. You don’t need a tiny special function to pattern match Maybes. Compare, for some m :: Maybe Int:

maybe (return ()) (putStrLn . show) m

with:

case m of
  Just i -> putStrLn (show i)
  Nothing -> return ()

Sure, the latter is a bit more verbose, but I’d posit the explicit pattern match is almost always more readable in context. Why? Because maybe is yet another word to memorize. You need to know the order of its arguments and its meaning. The standard library would be no weaker without this function. Multiple experienced Haskellers have told me to prefer it over explicit pattern matches, but it doesn’t optimize for readability over the long term.

Pattern matches are great! They always work the same way (predictability!), and they’re quite explicit about what’s going on.

pathwalk

For my second example, the last thing I want to do is pick on this contributor, because the use case was valid and the pull request meaningful. (In the end, we found another way to provide the same functionality, so the contributor did add value to the project.)

But what I want to show is why I rejected this pull request. pathwalk was intended to be a small, light, learnable API that anyone could drop into a project. It exposes one function, pathWalk, with three variants, one for early exit, one for accumulation of a result, and one that uses lazy IO for convenience. All of these are intended to be, if not memorizable, quite predictable. The pull request, on the other hand, added a bunch of variations with nonobvious names, as well as six different callback types. For an idea as simple as directory traversal, introducing a pile of exported symbols would have a negative impact on usability. Thus, to keep the library’s interface small and memorizable, I rejected the PR.

TypeScript

Now I’ll discuss the situation that led me to writing this post. My team at Dropbox recently adopted TypeScript to prevent a category of common mistakes. For our own sanity, we were already annotating parameters and return types — now we’re simply letting the computer verify correctness for us.

However, as we started to fill in type annotations for the program, a coworker of mine expressed some concern. "We shouldn’t write so many type definitions in this frontend code. JavaScript is a scripting language, not heavyweight like Java." That argument didn’t make sense to me. Surely it doesn’t matter whether we’re writing high-performance backend code or frontend code — type systems satisfy the same need in either situation. And all I was doing was annotating the parsed return type of a service request to prevent mistakes. The specific code looked something like this:

// at the top of the file
interface ParsedResponse {
  users: UserModel[];
  payloads: PayloadModel[];
}
 
// ...
 
// way down in the file
function makeServiceRequest(request: Request): Promise<ParsedResponse> {
  return new Promise((resolve, reject) => {
    // ...
    resolve({
      users: parsedUserModels,
      payloads: parsedPayloadModels,
    });
  });
}

At first I could not understand my coworker’s reaction. But I realized my coworker’s concerns weren’t about the types — after all, he would have documented the fields of the response in comments anyway, and, all else equal, who doesn’t want compiler verification that their code accesses valid properties?

What made him react so negatively was that we needed to come up with a name for this parsed response thing. Moreover, the definition of the response type was located far away from the function that used it. This adds some mental overhead. It’s another thing to look at and remember.

Fortunately, TypeScript uses structural typing (as opposed to nominal typing), and the advantage of structural typing is types are compatible if the fields line up, so you can avoid naming intermediate types. Thus, if we change the code to:

function makeServiceRequest(request: Request): Promise<{
  users: UserModel[],
  payloads: PayloadModel[],
}> {
  return new Promise((resolve, reject) => {
    // ...
    resolve({
      users: parsedUserModels,
      payloads: parsedPayloadModels,
    });
  });
}

Now we can have type safety as well as the concision of a dynamic language. (And if we had bidirectional type inference, it would be even better. Alas, bidirectional type inference and methods are at odds with each other.)

Other Examples

git has the concept of an "index" where changes can be placed in preparation for an upcoming commit. However, the terminology around the index is inconsistent. Sometimes it’s referred to as the "cache", sometimes the "index", sometimes the "stage". I get it, "stage" is supposed to be the verb and "index" is the noun, but how does that help me remember which option I need to pass to git diff? It doesn’t help that both stage and index are both nouns and verbs.

In the early 2000s I wrote a sound library which was praised for how easy it was to get up and running relative to other libraries at the time. I suspect some of this was due to needing to know only a couple concepts to play sounds or music. FMOD, for on the other hand, required that you learn about devices and mixing channels and buffers. Now, all of these are useful concepts, and every serious sound programmer will have to deeply understand them in the end. But from the perspective of someone coming in fresh, they just want to play a sound. They don’t want to spend time studying a bunch of foundational concepts. Audiere had the deeper concepts too, but it was useful to provide a simpler API for the simple 80% use case.

Python largely gets this right. If you want to know the size of some x, for any x, you simply write len(x). You don’t need a different function for each data type.

To summarize, whenever you provide an API or language of some kind, think hard about the names of the ideas, and whether you can eliminate any. It will improve the usability of your system.