Skip to content

Syntax Changes #503

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

Merged
merged 5 commits into from
Jan 25, 2023
Merged

Syntax Changes #503

merged 5 commits into from
Jan 25, 2023

Conversation

gdotdesign
Copy link
Member

@gdotdesign gdotdesign commented Oct 14, 2021

This PR implements a syntax changes that I wanted to do before 1.0.0. These changes hopefully will make the language more understandable and more welcoming to newcomers.

The main change stems from the issue of allowing only one expression per code block (function body, if / else branches, case branches, etc...) #394, also I wanted to simplify the language a bit.

Code Blocks

This PR makes it possible to write multiple statements in a code block without a try like this:

fun format (prefix : String, number : Number) : String {
  string =
    Number.toFixed(2, number)

  parts =
    String.split(".", string)

  digits =
    parts[0]
    |> Maybe.withDefault("")
    |> String.lchop("-")
    |> String.split("")
    |> Array.groupsOfFromEnd(3)
    |> Array.map(String.join(""))
    |> String.join(",")

  decimals =
    parts[1]
    |> Maybe.withDefault("")
    |> String.rchop("0")

  if (String.isEmpty(decimals)) {
    prefix + digits
  } else {
    prefix + digits + "." + decimals
  }
}

Code blocks can also be used by themselves as an expression:

value = {
  a = "Hello"
  b = "World"

  "#{a} #{b}!"
}

I hope this will make code less verbose and reduce overall boilerplate.

Await and Promises

Previously, the way to handle promises was through using sequence or parallel. From the feedback over the years, I found that it's hard for people to understand how they work, so this PR removes those constructs and adds an await keyword which can be placed before a statement:

fun makeRequest : Promise(String) {
  result = 
    await Http.send(Http.get("https://www.example.com/"))

  case (result) {
    Result::Ok(response) => response.body
    Result::Err => "Something went wrong!"
  }
}

There are two rules to keep in mind when working with await:

  • The await keyword awaits the given Promise(a) and returns its value a
  • If there is an awaited statement in the block, the return type of the block will be a Promise (since it's asynchronous).

Other things:

  • await can be used without a variable:

    await someFunction()
    otherStatment
    
  • the Promise type changed from Promise(error, value) to just Promise(value)

Removed language features

  • where - since we can add statements directly before the expression this is removed
  • with - it was not used and not documented
  • try - was replaced by code blocks
  • sequence - was replaced by code blocks and await
  • parallel - was not used widely, and it's partially replaced with code blocks and await
  • catch, finally, then - these were constructs used in try, sequence and parallel

Standard library update

The standard library https://github.com/mint-lang/mint/tree/code-blocks/core was updated as well and all tests are passing. You can check how these changes looks as well.

Call for comments and questions

I would like to have feedback to these changes, so I ask you to share your feelings / questions about this in the comments 🙏

@gdotdesign gdotdesign changed the title Code blocks Syntax Changes Oct 14, 2021
@Sija Sija added enhancement New feature or request language Language feature labels Oct 14, 2021
@morpatr
Copy link

morpatr commented Oct 15, 2021

As always thanks for your work. Looking forward to arriving at 1.0.0 in the future. Is this going to be deprecated first? I like the potential for less verbose code, but I do like the explicit nature of try, sequence, and parallel. How would the following be rewritten to conform with the changes above?

  fun search : Promise(Never, Void) {
    sequence {

      params =
        SearchParams.empty()
        |> SearchParams.append("search", input)
        |> SearchParams.toString()

      response =
        "/api/products?" + params
        |> Http.get()
        |> Http.send()

      case (response.status) {
        400 => next { /* do something */ }
        401 => next { /* do something */ }
        403 => next { /* do something */ }
        404 => next { /* do something */ }
        =>
          try {
            object =
              response.body
              |> Json.parse()
              |> Maybe.toResult("")

            decoded = decode object as Stores.Products

            next { /* do something */ }
          } catch Object.Error => errorCode {
            next { /* do something */ }
          } catch String => errorCode {
            next { /* do something */ }
          }
      }
    } catch Http.ErrorResponse => network {
      next { /* do something */ }
    } finally {
      next { /* do something */ }
    }
  }

@gdotdesign
Copy link
Member Author

@morpatr Thanks for the feedback!

This is what that code look like:

fun search : Promise(Void) {
  params =
    SearchParams.empty()
    |> SearchParams.append("search", input)
    |> SearchParams.toString()

  await result =
    "/api/products?" + params
    |> Http.get()
    |> Http.send()

  case (result) {
    Result::Err => next { /* do something */ }
    Result::Ok(response) =>
      case (response.status) {
        400 => next { /* do something */ }
        401 => next { /* do something */ }
        403 => next { /* do something */ }
        404 => next { /* do something */ }

        => 
          case (Json.parse(response.body)) {
            Maybe::Nothing => next { /* do something */ }
            Maybe::Just(object) =>
              case (decode object as Stores.Products) {
                Result::Ok(decoded) => next { /* do something */ }
                Result::Err => next { /* do something */ } 
              }
          }
    }
  } 

  /* Finally here. */
}

@jansul
Copy link
Contributor

jansul commented Oct 16, 2021

Overall a fan! As much as I also like the explicitness (and cool factor) of sequence, parallel etc this does seem to simplify things quite a bit.

I like the change to Promise, makes sense to just use Result instead 🙂

Is there an example of how you do something like parallel but using async?

@gdotdesign
Copy link
Member Author

@jansul I'm still trying to figure out how can parallel with these changes, but I have an idea.

Let's say we have two requests to make, previously it would look like this:

parallel {
  a = requestA
  b = requestB
} then {
  a.body + b.body
}

Now it could just be this:

{
  await a = requestA
  await b = requestB

  a.body + b.body
}

The compiler has all the information to decide to do a and b automatically in parallel because they are only referenced in the last statement together.

It's not implemented yet though.

@jansul
Copy link
Contributor

jansul commented Nov 12, 2021

I played around with migrating mint-realworld to use this branch - mostly to get a feel for the new syntax. Overall was fairly painless - and as pleasant to use as mint always is 🥰

There were a couple of places where sequence is used, but there wasn't an explicit reference between each promise (for example with the following snippet, you probably do want Window.navigate("/") only to run after the others). Naively converting these to use await probably wouldn't do what you expected, but I'm sure with a bit of refactoring could work fine.

fun logout : Promise(Never, Void) {
  sequence {
    Storage.Local.remove("user")

    resetStores()

    next { user = UserStatus::LoggedOut }

    Window.navigate("/")
  } catch Storage.Error => error {
    Promise.never()
  }
}

One bug I did notice was that if you had a trailing assignment, something like this:

fun toggleUserFollow (profile : Author) : Promise(Api.Status(Author)) {
  url =
    "/profiles/" + profile.username + "/follow"

  request =
    if (profile.following) {
      Http.delete(url)
    } else {
      Http.post(url)
    }

  status = 
    Api.send(Author.fromResponse, request)
}

It would generate JS similar too this (with a const in the return) which would give me a SyntaxError in Firefox.

ni(zx) {
  const zy = `/profiles/` + zx.username + `/follow`;
  const zz = (zx.following ? ES.ud(zy) : ES.ul(zy));
  return const aaa = CJ.pg(CX.nu, zz);
}

I removed status = and it worked fine. Unfortunately I don't know enough about the way mint works to debug it any further sorry.

@gdotdesign gdotdesign added this to the 0.16.0 milestone Dec 13, 2021
@DavidBernal
Copy link

{
  await a = requestA
  await b = requestB

  a.body + b.body
}

The compiler has all the information to decide to do a and b automatically in parallel because they are only referenced in the last statement together.

I think this could be a problem. For example, I'm a javascript developer and I understand how async/await works on JS, so I could do:

{
  ...
  await r = AddLikeToServer()
  await likes = CountLikesFromServer()
  ...
}

Suppose that is the first like on the server, so I would expect that likes will be 1, but if this run parallely could received 0 likes. With parallel/sequence we can choice what is the best method on each use case. I recognize that for JS developers parallel/sequence could be strange, but it is only at the beginning. When I understood it, I found it better than async/await

Anyway, I am a super noob with mint, so my opinions could be erroneous

@gdotdesign
Copy link
Member Author

I think this could be a problem. For example, I'm a javascript developer and I understand how async/await works on JS, so I could do:

After thinking about it some, I came to the same conclusion. I might reintroduce the parallel keyword if there is need (please 👍 on this comment if you need it)

@DavidBernal
Copy link

I still thinking sequence/parallel are two great tools and shouldn't remove. Sorry to bother you. It's a big language and I hope it grow

@gdotdesign
Copy link
Member Author

This PR is kind of ready, here are the final list of changes:

  • Promises changed to take a single parameter instead of two Promise(value)

  • Removed try, parallel, sequence, with, where, catch, finally and then language features.

  • Removed partial application language feature (conflicting with default arguments) until we can figure out a solution for the ambiguity.

  • Removed safe operators &. and &(.

  • Statements are using : instead of = to make them unambiguous from records: { name = "Joe" } can be either a block or a record, also using : look like labels which reflects their purpose more clearly.

  • The name of the enum is now optional and the variant can be used alone as a reference.

  • Added block expressions.

  • Added optional await keyword to statements.

  • Added optionla await keyword to the condition of case expressions.

  • Added the ability to define default values for function arguments.

  • Added the ability to create decoder functions usign the decode feaute by omitting the decodable object: decode as Array(String)

  • Added here document support:

    <<#MARKDOWN
    Renders markdown content to Html
    MARKDOWN
    
    <<-TEXT
    Text content which leaves leading indentation intact.
    TEXT
    
    <<~TEXT
    Text content which leaves trims leading indentation to the first line.
    TEXT
    

@gdotdesign gdotdesign marked this pull request as ready for review December 1, 2022 14:51
@Sija Sija self-requested a review December 1, 2022 17:19
Copy link
Member

@Sija Sija left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks awesome! 🤩
Can't wait to see the new wave of fresh 🌿 !

Couple of suggestions:

  • Revert "The name of the enum is now optional and the variant can be used alone as a reference." since it will most likely cause problems in the future
  • Reverse the : and = usage, so the records will be defined with : ({ foo: "bar" })
  • Switch the order of arguments, so the pipe receiver will always comes first

- Records now use `:` instead of `=`
- Statements now use `let` and `=` instead of `:`
@gdotdesign
Copy link
Member Author

* Revert "The name of the enum is now optional and the variant can be used alone as a reference." since it will most likely cause problems in the future

* Reverse the `:` and `=` usage, so the records will be defined with `:` (`{ foo: "bar" 

These were implemented in this PR, should be ready for merging 🚀

* Switch the order of arguments, so the pipe receiver will always comes first

This is a separate PR #571

@gdotdesign gdotdesign merged commit c479228 into master Jan 25, 2023
@gdotdesign gdotdesign deleted the code-blocks branch January 25, 2023 07:45
@@ -200,12 +185,11 @@ module File {
input.multiple = true
input.type = 'file'

document.body.appendChild(input)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gdotdesign Was that removed intentionally?
I'm asking since there's still document.body.removeChild(input) left beneath 🤔

@gdotdesign gdotdesign mentioned this pull request Mar 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request language Language feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants