mint icon indicating copy to clipboard operation
mint copied to clipboard

Syntax Changes

Open gdotdesign opened this issue 2 years ago • 8 comments

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 :pray:

gdotdesign avatar Oct 14 '21 06:10 gdotdesign

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 */ }
    }
  }

morpatr avatar Oct 15 '21 11:10 morpatr

@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. */
}

gdotdesign avatar Oct 15 '21 11:10 gdotdesign

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?

jansul avatar Oct 16 '21 21:10 jansul

@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.

gdotdesign avatar Oct 18 '21 11:10 gdotdesign

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 :smiling_face_with_three_hearts:

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.

jansul avatar Nov 12 '21 14:11 jansul

{
  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

DavidBernal avatar Dec 30 '21 18:12 DavidBernal

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 :+1: on this comment if you need it)

gdotdesign avatar Dec 31 '21 16:12 gdotdesign

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

DavidBernal avatar Dec 31 '21 18:12 DavidBernal

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 avatar Dec 01 '22 14:12 gdotdesign

* 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 :rocket:

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

This is a separate PR #571

gdotdesign avatar Jan 24 '23 10:01 gdotdesign