Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Operators as syntax sugar #219

Open
robinheghan opened this issue Sep 4, 2023 · 26 comments
Open

Operators as syntax sugar #219

robinheghan opened this issue Sep 4, 2023 · 26 comments
Labels
accepted The issue has been reviewed and accepted for implementation language proposal proposal of a major feature to the language

Comments

@robinheghan
Copy link
Member

robinheghan commented Sep 4, 2023

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:

  • + => plus
  • - => minus
  • / => divide
  • * => multiply

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:

  • == => equals
  • != => notEqual
  • ++ => append
  • |> => applyRight
  • <| => applyLeft
  • < => lessThan
  • <= => lessThanOrEqual
  • > => greaterThan
  • >= => greaterThanOrEqual

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.

@robinheghan robinheghan added the language proposal proposal of a major feature to the language label Sep 4, 2023
@boxed
Copy link

boxed commented Sep 4, 2023

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
Copy link
Member Author

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
Copy link

dbj commented Sep 4, 2023

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
Copy link
Member Author

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

@boxed
Copy link

boxed commented Sep 4, 2023

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

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

@z5h
Copy link

z5h commented Sep 4, 2023

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
Copy link
Contributor

avh4 commented Sep 4, 2023 via email

@boxed
Copy link

boxed commented Sep 4, 2023

++ 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
Copy link
Contributor

avh4 commented Sep 4, 2023 via email

@robinheghan
Copy link
Member Author

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

@dbj
Copy link

dbj commented Sep 4, 2023

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
Copy link

dbj commented Sep 4, 2023

+1 on adding string interpolation / templating for strings.

@laurentpayot
Copy link

laurentpayot commented Sep 5, 2023

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
Copy link

joakin commented Sep 8, 2023

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
Copy link
Member Author

robinheghan commented Sep 8, 2023

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 robinheghan changed the title Operators as syntax sugar, and no more currying Operators as syntax sugar Sep 9, 2023
@robinheghan
Copy link
Member Author

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
Copy link
Member Author

@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
Copy link
Member Author

robinheghan commented Sep 11, 2023

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
Copy link

joakin commented Sep 11, 2023

@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
Copy link

joakin commented Sep 11, 2023

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
Copy link
Member Author

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
Copy link

joakin commented Sep 11, 2023

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
Copy link

boxed commented Sep 11, 2023

Why is == different?

@robinheghan
Copy link
Member Author

@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
Copy link

joakin commented Sep 11, 2023

@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
Copy link
Member Author

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. =)

@robinheghan robinheghan added the accepted The issue has been reviewed and accepted for implementation label Sep 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted The issue has been reviewed and accepted for implementation language proposal proposal of a major feature to the language
Projects
None yet
Development

No branches or pull requests

7 participants