gren-lang / compiler

Compiler for the Gren programming language
https://gren-lang.org
Other
379 stars 23 forks source link

Remove automatic currying #221

Closed robinheghan closed 1 year ago

robinheghan commented 1 year ago

Currying as a concept is, especially the advanced applications of it, a difficult concept for beginners to wrap their head around. In addition, currying imposes a runtime overhead that is non-trivial to optimize away.

This proposal is about removing automatic currying from the language.

Benefits

The benefits can be listed quite easily:

  1. The lack of currying makes functions work like in any other language. Not having to explain currying means the language is easier to teach.
  2. There's no need for a smart compiler in order to produce efficient code
  3. There's a limit to how smart/advanced Gren code you can write (code inevitable becomes simpler)

Disadvantages

As with the benefits section, one can briefly list the disadvantages, but I also think it's wise to explore them in depth.

  1. Loss of applicatives
  2. Operators like |> has to become builtins of the language
  3. Certain patterns require a bit more code (which isn't a dealbreaker to me, but worth noting)

The biggest downside is the loss of applicatives, which means code like the following won't be possible anymore:

type alias Person =
  { name : String
  , age : Int
  }

-- Gren doesn't automatically create constructors for type aliased records
constructor : String -> Int -> Person
constructor name age =
    { name = name, age = age }

decoder : Person
decoder =
    Json.succeed constructor
        |> Json.required "name" Json.string
        |> Json.required "age" Json.int

Instead, one would have to use a monadic style (the API listed below doesn't exist)

decoder : Person
decoder =
    Json.string "name" <| \name ->
        Json.int "age" <| \age ->
            Json.succeed { name = name, age = age }

Stylistically, I don't mind the monadic approach. Even if I did, one could always introduce syntax to make it look nicer. I also think it's harder to mess up with regards to getting the order of arguments wrong. However, the monadic approach has to stop at the first encountered error when decoding. The applicative style can catch all runtime errors at once.

So, in the case where there's something wrong with both name and age when decoding a json blob, the monadic approach will only tell you the first error it encounters, while the applicative approach will be able to tell you about all errors that was encountered.

One could also make use of map2, map3, map4 etc. type of functions, but these must be handwritten and therefore doesn't scale. It's unlikely that Gren's core package will provide a Json.map16 function, for instance.

At first glance, one would assume that the monadic approach would be more "Gren-like" anyway, as we've already removed tuples on the basis that it encourages the use of positional semantics. However, applicatives aren't as likely to be littered throughout your code as datastructures are, and so I'm not to concerned about that point here.

One could imagine that one could introduce a curry keyword so that currying could be opt-in. I am, however, wary of introducing keywords that look like functions but doesn't actually work like functions, so I'm not too keen on that idea.

Finally, the lack of currying means that operators like |> would have to become "magic" builtins of the language, as opposed to being API as it is today.

lue-bird commented 1 year ago

On "Loss of applicatives":

There have been quite a few discussions about pitfalls of elm's "applicatives" with messing up argument order which somewhat still exist even without positional record type alias constructor functions. I once proposed field mapping syntax which doesn't rely on currying and would fix the order issue. With a decoder it would look this:

decoder : Person
decoder =
    Json.succeed { name = {}, status = {}, age = {} }
        |> Json.andField !name "name" Json.string
        |> Json.andField !status "status" Json.string
        |> Json.andField !age "age" Json.int

(which the elm slack has taught me is still applicative)

Here's a solution in elm which doesn't rely on currying but isn't viable in gren because it doesn't have tuples but I wanted to mention it anyway: a style where you collect all the arguments

type alias Person =
  { name : String
  , age : Int
  }

decoder : Person
decoder =
    Json.succeed
        |> Json.andField "name" Json.string
        |> Json.andField "status" Json.string
        |> Json.andField "age" Json.int
        |> Json.map
            (\( ( ( (), name ), status ), age ) ->
                { name = name, status = status, age = age }
            )
robinheghan commented 1 year ago

@lue-bird In your first example, you're relying on the fact that !name can return a "different" record, in that the type of name changes from {} to String, right?

lue-bird commented 1 year ago

Yes, that's why I called the feature "field mapping syntax", not "field update syntax"

laurentpayot commented 1 year ago

One could also make use of map2, map3, map4 etc. type of functions, but these must be handwritten and therefore doesn't scale. It's unlikely that Gren's core package will provide a Json.map16 function, for instance.

What about andMap?

robinheghan commented 1 year ago

@laurentpayot andMap relies on automatic currying

robinheghan commented 1 year ago

This wasn't an easy decision.

Since the monadic form must be performed sequentially, it's a worse fit for decoding, validating and concurrency.

Applicatives can be used without currying, but only if mapX functions are written by hand. This isn't only tedious, but also increases code-size in the compiled application.

Leaving currying in, means that <| and |> doesn't have to be builtins, but can remain as they are.

Currying, while confusing for beginners, hasn't seemed to prevent people from learning or enjoying Elm and the performance overhead they currently represent is possible to optimize away in the common case without too much effort.