Idea: A Lisp-like language that compiles to Elm

Hi all!

I’m looking for feedback for an idea I’ve been thinking about / working on for
the past few weeks: a Lisp-like (actually more Clojure-like) language that
compiles to clean Elm code.

The “clean” part is important – I want the user to be able to stop using the
language and simply adopt the generated Elm code when desired.

My main motivation is to reduce the boilerplate needed to write programs. I want
to spend more time thinking about the logic of my application and not typing
error-prone code like a fromString : String -> Maybe MyType or
all : List MyType.

Below is a sketch of what I’d like the language to be like. The inspirations for
various features is:

  • The syntax is a subset of Clojure.
  • The “derive” part is inspired by Rust and the excellent serde Rust crate.
  • The macro system is very similar to Clojure and other Lisps.
  • The ability to define multiple modules inside a single file using defmodule
    is inspired by Elixir.
; A file can define any number of modules. These are compiled to the correct
; path inside `/src/`, `/src/App/Direction.elm` in this case.
(defmodule App.Direction
  (deftype
    {:derive
      ; This will generate `toString : Direction -> String`.
      ToString
      ; This will generate `fromString : String -> Maybe Direction`.
      FromString
      ; This will generate both
      ; `decoder : Json.Decode.Decoder Direction`,
      ; and `encode : Direction -> Json.Encode.Value`.
      ; To derive only one of them, you can derive `Json.Decode` and
      ; `Json.Encode`.
      Json
      ; This will generate `all : List Direction`
      All}
    Direction North East South West)

  ; `def` defines a value.
  (def one 1)
  ; `defn` defines a function (it's just a combination of `def` and `fn`).
  (defn increment [x] (+ x one))
  ; `defn` can pattern match at the top level without explicit `case`.
  (defn invert
    [North] South
    [South] North
    [East] West
    [West] East)
  ; Both `def` and `defn` can optionally be type annotated
  (def {:type Int} two 2)
  (defn {:type (-> Int Int)} increment2 [x] (+ x two)))

(defmodule App.User
  ; Type aliases can also be defined.
  (defalias
    {:derive
      ; Derivers can take arguments. Here we rename some fields for the
      ; generated decoder and encoder to be snake_case.
      (Json {:rename {:firstName :first_name, :lastName :last_name}})}
    ; Since there's no ambiguity possible, imports are automatically inferred
    ; from the code. There's no need to do `import Direction` here.
    User {:firstName String, :lastName String, :heading Direction.Direction})

  ; We can define macros at the top level or inside `defmodule`.
  ; These don't compile to anything in the .elm source code; they're purely
  ; a compile time transform.
  (defmacro match? [pattern expr]
    `(case ~expr
      ~pattern True
      _ False))

  (defn isAlex [user] (match? {:name "Alex"} user)))

Question: Would you be interested in using this?

3 Likes

Personally I wouldn’t.

I would use Elm instead!

8 Likes

Interesting, Richard! Can you elaborate on why?

I personally wouldn’t use it mainly because I never really found Lisp aesthetically pleasing, since first programming in it some 35 years ago. I always preferred the ML style, since to me it reads more like mathematics written to explain a solution to someone else.

I see some engineering problems too.

If the types don’t line up in Lisp, then I don’t see how they’d line up in the generated Elm.

Once you start using the Elm code, your Lisp code becomes defunct. Unless you want to try round-tripping, which is a massive undertaking.

Programming in Elm means that you don’t have to deal to much with Javascript, but you do still have to be able to read it, since in any real app you’re going to have to write some sort of interop. With Lisp in the mix, you now have to be able to read and/or code Lisp, Elm and Javascript. I don’t know too many people who enjoy coding in all three of those styles.

There’s no meta-programming in Elm, so all the macro processing would probably need to be done on the Lisp side.

Regarding the type annotations you find error prone; I think it’s worth mentioning that:

  • you don’t need to write them, Elm infers them;
  • most editor plugins will generate them for you if you want, once your code is working;
  • It’s entirely possible, even desirable, to write the type declarations before you write most of the code, so you get a good idea if you’re on the right track with modelling your problem;
  • they’re great documentation!

If you’re coming from a Clojure background, you likely already know about ClojureScript; could you please give a brief explanation on what you think Lisp->Elm would give over ClojureScript?

4 Likes

I see how my post would be ambigous :slight_smile: What I meant was typing the declarations of those functions. For example:

type MyType = A | B

toString x = case x of
  A -> "A"
  B -> "B"

fromString x = case x of
  "A" -> Just A
  "B" -> Just B
   _ -> Nothing

all = [A, B]

The first one is not a huge problem but it’s easy to get the last two out of sync with the data type.

If you’re coming from a Clojure background, you likely already know about ClojureScript; could you please give a brief explanation on what you think Lisp->Elm would give over ClojureScript?

Types mainly and access to the high quality libraries Elm has.

I really like the derive part. That is something I will like to have.

Function annotations are really important for me, something that this syntax doesn’t allow. They help me validate my assumptions. e.g. this functions takes a Direction and not something else. When I have written Elm without type annotations I found that any type mismatches becomes really hard to understand and track.

2 Likes

Not much to elaborate on, just sharing a data point as requested. I prefer Elm 0.19’s design to this design; even if it were implemented flawlessly I’d still choose Elm.

I know whenever I ask nice people whether or not they’d use something, there’s a bias toward saying yes or saying nothing, which skews the information I’m trying to gather. So I try to honestly share data points when I think I wouldn’t end up using something.

8 Likes

You probably missed it because of the long example, but it does allow type annotations:

  (defn {:type (-> Int Int)} increment2 [x] (+ x two)))

And I thank you for that! I knew very few people would like this idea when I posted it. :slight_smile: Honest feedback would help me rethink my choices. Maybe it would be better to just add a @derive syntax on top of Elm as that’s what I need the most.

2 Likes

I think you’re right in breaking it down to just the deriving, since that is what’s bothering you.

I think that we would benefit from a “real” motivating example. The one you gave does not show a problem that cannot be solved elegantly with current Elm. It rather is the solution to a problem you perceive. Oftentimes, I found that even when Elm forces you to write out more stuff, it ends up being actually pretty nice in the end. So I would love to see a motivating example of a case, where deriving is really necessary. I personally can’t think of one right now.

For most people, the hard part of writing code is not typing on a keyboard and reading a greater volume of code. The hard part of writing code is managing complexity.

This language seems to aim to minimize typing/reading effort while making the development chain more complex.

I’m not (yet?) clear why I’d use it or recommend it.

4 Likes

Cue unfinished projects territory :smiley:

4 Likes

ÍMHO It would be way more easier to implement, manage and gather interest for some hackable IDE plugin, which would be able to auto generate the functions declaration you don’t want to write manually.

2 Likes

I think the number one reason Python took off and continues to grow is that the actual code you write looks so nice. It’s very easy on the eyes, and mentally very easy to “grok” what’s going on.

Personally, I think the converse applies to Lisp: the language is very confusing to look at and very difficult to mentally parse what is happening.

But hey, if that’s your thing!

2 Likes

Personally, I much prefer ML syntax to Lisp; I always found Lisp syntax hard to read.

I have a suggestion for you though - why compile this Lisp to Elm? Another route you could take is to dynamically evaluate the Lisp in Elm. This would give you a way to modify the behaviour of Elm programs at runtime.

There could also be other dynamic DSLs that could be evaluated in Elm - another example might be a spreadsheet application written in Elm, where the formula language is dynamically evaluated.

Well, maybe not the type of feedback you are asking for but I’d love to see a toy lisp implementation just to better understand elm-parser. But still I’d stick to Elm.

Why do you think I wrote dmy/elm-pratt-parser? :wink:

So who is this language for? Is this just for clojure people that don’t want to invest time in properly learning Elm? The only thing I see is multiple modules per file, but I’m not convinced that’s necessary or practical.

With that, there are also some fundamental problems I see with this as a project:

  • Compiling to clean code may not always be feasible. Even if it gets passed through Elm formatter, maybe variable names won’t like up, or functions will be produced in strange ordering.
  • It’s not clear what boilerplate you’re actually removing. If it’s just the typing, that’s optional, but definitely makes self-documenting code much easier.
  • It’s a custom language subset specifically for compiling to another language, and in turn, that compiles to javascript. People that know clojure will still have the learning curve of what is your language’s subset is, and how that relates to Elm.
  • By compiling directly to Elm rather than using the Elm compile to just produce javascript, you are giving any developer of your language an easier path, which just as others have pointed out, is to just write in Elm.
  • No clear benefits. If Elm had a shortcoming by language design, then you might get some people interested in this, especially if clojure offered some language feature that Elm could not, but that isn’t evident in what you presented.

I think you’ve put a fair amount of thought into this, and I think that’s awesome, but as far as I’m concerned, this isn’t something I would use.

I think the main benefit of this proposal is cutting on writing some code using Derive. I can see the appeal for JSON decoders and encoders.

For other things like fromString and toString, I’m happy writing these implementations myself when I need them, which is not that often.

As someone mentioned before, maybe you could explore creating some sort of code generator for your needs instead. An example is https://github.com/dillonkearns/elm-graphql which generates all the encoders / decoders for graphql types.

3 Likes

Very good example. In particular, I think this library should be compared to the ppx_graphql (GitHub - andreas/ppx_graphql: Type-safe GraphQL queries in OCaml) library in OCaml, a language which has deriving capacities. I wouldn’t be able to do a fair comparison of the two given that I haven’t yet used the elm-graphql library (only looked at it) and I have used the ppx_graphql library too little to really form a strong opinion. But my impression is still that the solution proposed with elm-graphql ends up much cleaner because once the tool has generated the required encoders / decoders, you are back to a simple language where usual, simple constructs work, and all the usual tooling can be used to help. On the contrary, ppx_graphql lets you write long strings containing actual GraphQL query and typecheck them in your code but none of the usual ways of splitting your code in small functions and the editor support tools that you are used to relying on seem to be of any help anymore.

I agree that the verbosity of ELM is a problem, and having macros is a way to reduce that. In the future when server-less is the only solution, this will be even a bigger problem. More and more programs be created and maintained inside browsers on a small screen like a tablet or so.

But remember that macros is much more than just preprocessing, since macros can call functions you just defined, and this is rather hard. See what clojurescript had to do: https://clojurescript.org/about/differences#_macros

1 Like