mint
mint copied to clipboard
Syntax Changes
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 givenPromise(a)
and returns its valuea
- 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 fromPromise(error, value)
to justPromise(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 intry
,sequence
andparallel
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:
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 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. */
}
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 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.
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.
{ await a = requestA await b = requestB a.body + b.body }
The compiler has all the information to decide to do
a
andb
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
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)
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
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
andthen
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 ofcase
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
* 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