Play

How Elm inspired Play
Login

How Elm inspired Play

This is an account of how the Play programming language came to be. It’s important for me that you realize that Elm is likely to be more mature than Play for several years, and that Play would not have existed if not for Elm.

That being said, there's a reason why I spend my free time developing an entirely new programming language, and that's because of how it differs from, in my opinion, the best programming language currently available: Elm.

The beginning

The first commit of the Play language was made on February 18th, 2020. At that time I had already toyed with the idea of making a programming language for several years. In fact, just six months earlier I had scrapped a project called Weblog, which was a modern implementation of Prolog, after I discovered that the logic-based paradigm didn’t suit me as well as I’d initially hoped.

It might surprise you to learn that the initial idea of Play was not to make a concatenative, or stack-oriented, programming language. The initial goal of Play, which still is an important goal of the language, was to serve as a playground (hence the name) for different ideas I had for improving Elm’s runtime.

Some of those ideas I believe are relevant to Elm’s future, like web assembly and reference counting. Others I doubt will ever make into Elm, like Play's implementation of unions.

I quickly realized that I wanted to depart from Elm’s syntax. One thing I was afraid of was someone picking up Play as an improved Elm and porting a bunch of code to it. I say afraid, because I rely on Elm for my day-job, as well as most of my hobby projects, including Play. Harming the Elm ecosystem in any way, however unlikely, would be devastating to me personally.

I stumbled across a video on YouTube where Slava Prestov introduced the Factor programming language, which was my first introduction to concatenative languages. I ended up reading much of the reference documentation of Factor, and even got two books on Forth (known as the first concatenative language). A little while later, on 20th of March, I decided that Play would use the syntax and semantics of a concatenative language, but be as pure and staticly typed as Elm.

The concatenative language syntax

While the syntax of Play was decided pretty much by accident, it does have some interesting properties. In fact, the more time I spent with the syntax, the more I became convinced that it was a perfect choice for Play. In May I wrote this tweet, which sealed the choice of syntax going forward.

Before I can tell you exactly why I like this syntax so much, I first have to give you a primer. Let’s start with this example:

deftype: Language
: name String
: paradigm String

def: functional-languages
type: (List Language) -- (List String)
: [ paradigm> "functional" = ] filter
  [ name> ] map

For comparison, here's the same code in Elm:

type alias Language =
  { name : String
  , paradigm : String
  }

functionalLanguages : List Language -> List String
functionalLanguages languages =
  languages
    |> List.filter (\lang -> lang.paradigm == "functional")
    |> List.map .name

The key differences between Elm and Play seems to be:

For me, this is what makes the concatenative syntax so intriguing. The code I've found to be the easiest to follow, is in the form of pipelines. Personally, I try to write most of my code in the pipeline-style, but depending on how my colleagues write code, or depending on the third-party libraries we use, this is not always possible.

The idea of a pipeline-first language is interesting because it requires everyone to write pipelinable (and in my opinion, easy-to-read) code. Not all code is easier to read as a pipeline (certain math equations come to mind), but I'd say that on average this is the case.

Multiple returns

Another interesting element of the concatenative syntax is that a function can return multiple values. In Elm, this can be simulated with tuples:

mostlyFunctional : List Language -> Bool
mostlyFunctional languages =
  languages
    |> List.partition (\lang -> lang.paradigm == "functional")
    |> Tuple.mapBoth List.length List.length
    |> (\(fp, nonFp) -> fp > nonFp)

And in Play:

def: mostly-functional?
type: (List Language) -- Bool
: [ paradigm> "functional" = ] partition
  [ length ] apply2
  >

While the fundamental solution is the same in both languages, working with multiple returns is a tad simpler in Play because Play has direct support for it. In Elm, if the Tuple module doesn't have a helper function for your use case, you can always use destructuring and a lambda to do what you want, but Play doesn't require any such acrobatics.

Auto-generated, overridable, helper functions

When defining a compound structure in Play, it will auto-generate several helper functions. In this example:

deftype: Language
: name String
: paradigm String

Play will automaticly generate two getter functions (name> and paradigm>) as well as two setter functions (>name and >paradigm). In the future, it's not unlikely that additional functions will be generated.

The interesting part of Play is that there is no other way to get or set properties of a compound type than using its auto-generated getter and setter functions. Another key element, which is not yet implemented, is that such functions can be overriden with a user-defined implementation.

The equivalent example in Elm will auto-generate two getter functions (.name and .paradigm). Setter functions will not be generated, but there's custom syntax in Elm for setting a property of a record:

setName : String -> Language -> Language
setName newName lang =
  { lang | name = newName }

In Elm, there is no way to define whether certain properties of a record are settable or not. Instead, if you want to lock down access to a specific type, the common approach in Elm is to wrap the record in an opaque type and define your own setters and getters for this new type.

This means that the easiest/fastest thing to do in Elm, is not necessarily the most scalable thing to do. In my experience in writing Elm libraries, I use records directly when prototyping, then re-write much of the code to use opaque types when finalizing the library.

In Play, if there are getters and setters that you do not wish to be commonly accessible from a module, you can simply choose not to export them. The easiest practice, is also the best practice.

Unions

Play's unions are different from Elm's custom types. I think it's easiest to explain the difference using examples, so let's look at a possible implementation of Elm's Maybe type:

type Maybe a
  = Just a
  | Nothing

Maybe represents a value which may or may not be available. But it's important to realize that Maybe String is a very different type than String.

If you realize that it doesn't always make sense to pass a String to a function that you're already using in many places in your application, it would be tempting to change the type to a Maybe String instead. However, this would require you to refactor every call site where you pass a String by wrapping the String in Just.

In Play, Maybe would defined using a union:

defunion: Maybe a
: a
: Nothing

deftype: Nothing

A type belongs to a union if it appears in any of its branches. A String is also a Maybe String. Changing the type of a function to accept a Maybe String instead of a String would not be a breaking change.

Another interesting property of unions and compound types being separate things in Play, is the extra flexibility you get.

For example, in Elm, a NonEmptyList is a different type than a List. Conceptually, these types could be implemented as:

type List a = Cons a | Empty

type alias NonEmptyList a = ( a, List a )

A function which accepts a NonEmptyList does not work with a List and vice versa.

In Play, these types can be implemented as:

defunion: List a
: NonEmptyList a
: EmptyList

deftype: EmptyList

deftype: NonEmptyList a
: first a
: rest (List a)

With this definition, a function that accepts a List will work with both a NonEmptyList and EmptyList. On the other hand, a function that accepts a NonEmptyList will not accept EmptyList.

This enables you to specify functions that can take in a possibly empty list, and return a list which is definitely not empty.

Conclusion

Play would not exist without Elm and many of the fundamentals (such as immutability and controlled side-effects) are the same in both languages. However, Play has certain key differences which can make your life as a developer easier.

To me, this is why I believe developing Play is a worthwhile endeavour.