gren-lang / compiler

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

Operators as syntax sugar #219

Open robinheghan opened 1 year ago

robinheghan commented 1 year ago

Operators are easy if you already know their meaning. I would argue that most people learn the basic operators for mathematics (+, -, /, *) and tend to find their presence in programming languages unsurprising. In fact, I believe most people would find it an unecessary burden to learn how to do maths in a way that doesn't correlate to the same syntax they learned in school. In other words, I believe many would consider Gren a worse language for not having operators.

On the other hand, operators are hard if you have no idea what they mean. Most people prefer actual functions with actual names, than to read cryptic symbols with different precedense rules and inline semantics. With very few exceptions, I believe operators can make code easier to read, at the expense at making code harder to understand.

One problem with Gren's current operators, is their lack of flexibility. If someone was to create a BigNumber type, it would be impossible to use it with +, even though it would make sense to use the same DSL with a BigNumber.

So, here is what I propose:

Any operator which isn't already well known or can greatly enhance the readability of Gren code, should be removed.

The operators that remain, will become syntax sugar. This means that it will no longer be possible to define operators in Gren code, even when limited to core API's.

The mathmatical operators will desugar to regular function calls:

The implementation used, depends on the functions in scope. Since Gren plans to remove default imports, and replace type classes with parametric modules, this will give users the ability use these operators with custom types. Future Int and Float modules will implement the above functions.

There are more aliases:

The boolean operators && and || will remain builtins, due to their short-circuiting mechanics.

Operators not mentioned above will be removed.

Using operators as function references (map (+) [1, 2, 3]) will be a compile error. Use the named function reference instead.

boxed commented 1 year ago

I just have to say that I love this.

What about precedence? I have long argued that importing math precedence into programming was a mistake and we should always require parenthesis, I guess this is implied by this proposal?

robinheghan commented 1 year ago

I haven't come to a decision on that, yet. I personally wrap every math expression with parens because I can't remember the precedense rules in my head. I'll have to see how it turns out.

dbj commented 1 year ago

I agree with your rationale on the operators. I have never liked operator overloading for that reason.

However, I use currying a ton with iterators. For example:

a = List.map (add 5) [1, 2, 3, 4, 5]

Is your plan to have to wrap add in a lamda or perhaps provide some magic for that too?

robinheghan commented 1 year ago

@dbj For your example, add would have to be wrapped in a lambda.

boxed commented 1 year ago

One could imagine a specific partial operator/keyword. So like:

a = List.map (partial add 5) [1, 2, 3, 4, 5]
z5h commented 1 year ago

I'm no expert, but it isn't clear to me what the type a -> b -> c means if passing in an a doesn't give me a b -> c. In any case, is there something that should prevent us from writing our own "partial application helpers"?

partial1of2 : (a -> b -> c) -> a -> (b -> c)
partial1of2 f a =
    \b -> f a b
avh4 commented 1 year ago

Imo, ++ => concat should also be removed. Personally I find it's rarely used, or when it is it doesn't really help readability at all.

Personally I'd be surprised if inequality operators (>, <, >=, <=) don't exist, and I always find it confusing to try to work with code that avoids those operators, since the naming of the functions and the argument order is never consistent from one API to another. But also, I guess in a lot of code, inequality checks are rarely used, so maybe it's fine not to have them?

On Mon, Sep 4, 2023 at 10:32 AM Mark Bolusmjak @.***> wrote:

I'm no expert, but it isn't clear to me what the type a -> b -> c means if passing in an a doesn't give me a b -> c. In any case, is there something that should prevent us from writing our own "partial application helpers"?

partial1of2 : (a -> b -> c) -> a -> (b -> c) partial1of2 f a = \b -> f a b

— Reply to this email directly, view it on GitHub https://github.com/gren-lang/compiler/issues/219#issuecomment-1705566404, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAAJRSC2AZJCVMCUBA54VTXYYGCXANCNFSM6AAAAAA4KONRTE . You are receiving this because you are subscribed to this thread.Message ID: @.***>

boxed commented 1 year ago

++ rarely used? Hmm. Is there a nice string interpolation feature in Elm/gren I am missing?

btw, look to copy Swift if you do string interpolation as their system is by far the best I've seen and learned all the lessons of the languages before it.

avh4 commented 1 year ago

Oh, I guess you're right for String concat. I was only thinking of List concat.

On Mon, Sep 4, 2023 at 12:29 PM Anders Hovmöller @.***> wrote:

++ rarely used? Hmm. Is there a nice string interpolation feature in Elm/gren I am missing?

btw, look to copy Swift if you do string interpolation as their system is by far the best I've seen and learned all the lessons of the languages before it.

— Reply to this email directly, view it on GitHub https://github.com/gren-lang/compiler/issues/219#issuecomment-1705643011, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAAJRUAGOLXCTSB3T3ZUWDXYYTZXANCNFSM6AAAAAA4KONRTE . You are receiving this because you commented.Message ID: @.***>

robinheghan commented 1 year ago

@avh4 You're right about inequality operators. Slipped from mind when I wrote this issue. They should remain.

dbj commented 1 year ago

is there something that should prevent us from writing our own "partial application helpers"?

Nothing is stoping us, but it leads to boilerplate code. If it is common to do, having a language convention is a good idea.

dbj commented 1 year ago

+1 on adding string interpolation / templating for strings.

laurentpayot commented 1 year ago

Oh, I guess you're right for String concat. I was only thinking of List concat.

In Elm I use List concatenation from time to time, mostly in complex views.

joakin commented 1 year ago

Hey @robinheghan , how would this work when you are working with float and int operations in the same module?

Or with having equality with different types in the same module?

robinheghan commented 1 year ago

Before I answer, keep in mind that Gren is implementing parametric modules as a replacement for Elm's type classes'ish thing. That means that it won't be possible for a single function or operator to work on multiple types. As such, using operators for both Ints and Floats (or even more stuff) in the same module will be a little cumbersome.

There are two ways to go about this, I suppose.

  1. expose the functions that is required for the operator aliases for the type you use the most, and use regular functions for the other type
module Foo exposing (..)

import Int exposing (plus, minus, multiply, divide, equals)

intExpr : Int.T
intExpr = 42 + 2 - 15 / 2 * 3

floatExpr : Float.T
floatExpr = Float.multiply (Float.divide (Float.minus (Float.plus 42.0 2.0) 15) 2) 3

(I'm just writing this down quickly, there are probably ways to make the above more pallatable)

  1. locally alias the functions in order to work with operators
module Foo exposing (..)

intExpr : Int.T
intExpr = 
    let
        plus = Int.plus
        minus = Int.minus
        multiply = Int.multiply
        divide = Int.divide
    in
    42 + 2 - 15 / 2 * 3

floatExpr : Float.T
floatExpr = 
    let
        plus = Float.plus
        minus = Float.minus
        multiply = Float.multiply
        divide = Float.divide
    in
    42.0 + 2.0 - 15.0 / 2.0 * 3.0

For equality, I'd just use the qualified functions.

robinheghan commented 1 year ago

I just edited the proposal.

I added a few missing operators and removed all mentions about currying. After discussions here and on the Elm slack, I'm not entirely sure about removing currying. At least, I don't think it needs to be proposed here.

robinheghan commented 1 year ago

@joakin One could imagine that operators are allowed to be qualified. If so, this would also be an option:

module Foo exposing (..)

import Int exposing (plus, minus, multiply, divide)
import Float as F

intExpr : Int.T
intExpr = 42 + 2 - 15 / 2 * 3

floatExpr : Float.T
floatExpr = 42.0 F.+ 2.0 F.- 15.0 F./ 2.0 F.* 3.0
robinheghan commented 1 year ago

Just edited the proposal.

Bitwise operators were removed. It's rare to have long chains of bitwise operations, and so the benefit of operators isn't all that appearant.

joakin commented 1 year ago

@joakin One could imagine that operators are allowed to be qualified. If so, this would also be an option:

module Foo exposing (..)

import Int exposing (plus, minus, multiply, divide)
import Float as F

intExpr : Int.T
intExpr = 42 + 2 - 15 / 2 * 3

floatExpr : Float.T
floatExpr = 42.0 F.+ 2.0 F.- 15.0 F./ 2.0 F.* 3.0

That's certainly an option, can get a bit repetitive but would work.

I'm going to mention a couple of alternatives inspired from OCaml.

First could be having float specific operators that you could use in the specific case you need to disambiguate between int and floats, like +. and -., etc. This is the approach taken by Gleam, and OCaml does the same thing for numbers. This doesn't fix it if you have == for example with different types in the same file.

Another option that I think would look nice, similar to above, would be what they call locally opening a module, specifically the expression syntax. It looks like this:

module Foo exposing (..)

intExpr : Int.T
intExpr = Int.(42 + 2 - 15 / 2 * 3)

floatExpr : Float.T
floatExpr = Float.(42.0 + 2.0 F.- 15.0 / 2.0 * 3.0)

mixedExpr : Float.T
mixedExpr = Float.(42.3 + Int.(2 - 15))

Essentially it is a new type of expression that looks like this <module-path>.(<expr>) and what it does is bring the bindings from the module to scope only for expr. That way you could reference the operators from the module and avoid a bit of repetition.

It also has a lot of other potential use cases, like for example when generating HTML, you could do:

view =
    Html.(
        div []
            [ h1 [] [ text "My Grocery List" ]
            , ul []
                    [ li [] [ text "Black Beans" ]
                    , li [] [ text "Limes" ]
                    , li [] [ text "Greek Yogurt" ]
                    , li [] [ text "Cilantro" ]
                    , li [] [ text "Honey" ]
                    , li [] [ text "Sweet Potatoes" ]
                    , li [] [ text "Cumin" ]
                    , li [] [ text "Chili Powder" ]
                    , li [] [ text "Quinoa" ]
                    ]
            ]
    )

It is a tricky problem to solve, hope these give you some ideas.

Another option that could be worth considering is what Richard did with Roc, you can read a bit here. All operators desugar to a single predictable function call, and for the numbers, they are represented with a shared type Num so that when you desugar + into Num.add it can work. Here is some info: https://github.com/roc-lang/roc/blob/main/roc-for-elm-programmers.md#numbers. And here is the operator desugaring table: https://github.com/roc-lang/roc/blob/main/roc-for-elm-programmers.md#operator-desugaring-table

joakin commented 1 year ago

Just edited the proposal.

Bitwise operators were removed. It's rare to have long chains of bitwise operations, and so the benefit of operators isn't all that appearant.

Makes sense to make removing currying its own proposal. In my opinion worth doing, but not mixing it with the operators seems wise.

robinheghan commented 1 year ago

Another option that I think would look nice, similar to above, would be what they call locally opening a module

That was a very interesting idea. Thank you for introducing it to me.

The nice thing is that it can always be introduced later, after this proposal is implemented (if it is implemented).

Another option that could be worth considering is what Richard did with Roc

I want Gren to only have a single mechanism for this kind of flexibility. I think it would be a worthwhile way to go if Gren was to retain the limited "type class" functionality it has now, but I do plan to rip that out eventually.

joakin commented 1 year ago

The nice thing is that it can always be introduced later, after this proposal is implemented (if it is implemented).

👍

Another option that could be worth considering is what Richard did with Roc

I want Gren to only have a single mechanism for this kind of flexibility. I think it would be a worthwhile way to go if Gren was to retain the limited "type class" functionality it has now, but I do plan to rip that out eventually.

I'm not sure what you are referring to. In Roc the operators are syntax sugar for an unambiguous qualified function call, no type classes. And the numbers are defined in this clever way (something like this):

module Num

type Num a

type Int32
type Float64

type alias Int = Num Int32
type alias Float = Num Float64

-- (+) always desugars to Num.a
add : Num a -> Num a -> Num a

-- (/) always desugars to Num.div
div : Num a -> Num a -> Float

As far as I can tell, there is no type classes magic in this stuff. In the case of Roc the monomorphization will generate the right function variants for all used types, and in the case of a JS target you could just use a kernel function that is the normal JS operators which can take ints or floats and wouldn't have any problem.

It solves a bit the ergonomics of the math operators, but it doesn't solve == which would still need to be magic.

boxed commented 1 year ago

Why is == different?

robinheghan commented 1 year ago

@joakin While add is unambigous, the actual implementation depends on the type. In JS, the underlying implementation would be the same. However, in a future where Gren targets WASM, you need to know the exact implementation.

The "depends on the actual type" bit is what i meant with "type classes". I assumed Roc used abilities for this, but I could be wrong.

@boxed I would guess because you'd expect == to be used for most things, while for numbers you'd only expect to use +, etc., with either Int or Float

joakin commented 1 year ago

@joakin While add is unambigous, the actual implementation depends on the type. In JS, the underlying implementation would be the same. However, in a future where Gren targets WASM, you need to know the exact implementation.

The "depends on the actual type" bit is what i meant with "type classes". I assumed Roc used abilities for this, but I could be wrong.

I believe since the Roc compiler is monomorphizing all functions, it ends up calling the right function at code generation. I don't think it is using type classes. If you plan to make Gren monomorphizing when you do WASM to do the most efficient assembly, this could work, and like I mentioned above it does work for the JS target.

I'm trying to suggest some options since the most used operators which are listed in this proposal should be easy to use in my opinion. Imagine in your module when you want to do some math on floats and you already had math on ints, how confusing it would get for a beginner that doesn't have the context, or if you have a new == comparison on a different type in the same module and it doesn't work. It would be different to most programming languages today and make Gren harder to learn.

robinheghan commented 1 year ago

I believe since the Roc compiler is monomorphizing all functions, it ends up calling the right function at code generation. I don't think it is using type classes.

I think we're both right. From an implementation standpoint, you're correct that there might not be anything dynamic at play during runtime. However, from the user standpoint, a function that works for all numbers is polymorphism.

Parametric modules is how similar functionality will be implemented in Gren (and monomorphism is one way of implementing it, though that would come at a cost of increased asset size). While it might not be the best fit for numbers in particular, I believe it is the overall better solution. I also believe Gren is better off with just one mechanism for this (I believe Roc is exploring parametric modules in addition to abilities, which is something I'd like to avoid for Gren).

I'm trying to suggest some options since the most used operators which are listed in this proposal should be easy to use in my opinion. Imagine in your module when you want to do some math on floats and you already had math on ints, how confusing it would get for a beginner that doesn't have the context, or if you have a new == comparison on a different type in the same module and it doesn't work. It would be different to most programming languages today and make Gren harder to learn.

I really appriciate that! I'm just trying to explain why the Roc approach might not be the best fit for Gren, currently. =)