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

Make promise handlers synchronous, with an async macro like JS async #2

Merged
merged 4 commits into from
Mar 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 108 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
# Promises.jl: *JavaScript-inspired async*

> #### Summary:
>
> A **`Promise{T}`** is a container for a value **that will arrive in the future**.
>
> You can **await** Promises, and you can **chain** processing steps with `.then` and `.catch`, each producing a new `Promise`.

You can use Promises.jl to run code in the background:


```julia
download_result = Promise((resolve, reject) -> begin
Let's look at an example, using Promises.jl to download data in the background:

filename = Downloads.download("https://api.github.com/users/$(username)")

# call `resolve` with the result
resolve(filename)

end)
```julia
download_result = @async_promise begin

#=> Promise{Any}( <pending> )
# This will download the data,
# write the result to a file,
# and return the filename.
Downloads.download("https://api.github.com/users/$(username)")
end

#=> Promise{Any}( <resolved>: "/var/folders/v_/fhpj9jn151d4p9c2fdw2gv780000gn/T/jl_LqoUCC" )
```

```
Expand All @@ -27,7 +33,7 @@ The result is a *pending promise*: it might still running in the background!
```julia
download_result

#=> Promise{Any}( <pending> )
#=> Promise{Any}( <resolved>: "/var/folders/v_/fhpj9jn151d4p9c2fdw2gv780000gn/T/jl_LqoUCC" )
```

You can use `@await` to wait for it to finish, and get its value:
Expand All @@ -36,7 +42,7 @@ You can use `@await` to wait for it to finish, and get its value:
```julia
@await download_result

#=> "/var/folders/v_/fhpj9jn151d4p9c2fdw2gv780000gn/T/jl_VNrh2x"
#=> "/var/folders/v_/fhpj9jn151d4p9c2fdw2gv780000gn/T/jl_LqoUCC"
```

<br>
Expand All @@ -55,7 +61,7 @@ download_result.then(
)

#=>
Promise{Any}( <resolved>: Dict{String, Any} with 32 entries:
Promise{Dict{String, Any}}( <resolved>: Dict{String, Any} with 32 entries:
"followers" => 0
"created_at" => "2011-04-21T06:33:51Z"
"repos_url" => "https://api.github.com/users/JuliaLang/repos"
Expand All @@ -72,53 +78,49 @@ Promise{Any}( <resolved>: Dict{String, Any} with 32 entries:
⋮ => ⋮ )
```

<br>
Since the original Promise `download_result` was asynchronous, this newly created `Promise` is also asynchronous! By chaining the operations `read` and `JSON.parse`, you are "queing" them to run in the background.


## Error handling: `reject` and `.catch`
<br>

A promise can finish in two ways: it can **resolve** or it can **reject**. This corresponds to the two functions in the constructor, `resolve` and `reject`:

```julia
Promise((resolve, reject) -> begin
## Error handling: rejected Promises

if condition
# Resolve the promise:
resolve("Success!")
else
# Reject the promise
reject("Something went wrong...")
end
end)
```
A Promise can finish in two ways: it can **✓ resolve** or it can **✗ reject**. In both cases, the `Promise{T}` will store a value, either the *resolved value* (of type `T`) or the *rejected value* (often an error message).

If you `@await` a promise that has rejected, the rejected value will be rethrown as an error:
When an error happens inside a Promise handler, it will reject:


```julia
oopsie_result = Promise((res, rej) -> rej("oops!"))

#=> Promise{Any}( <rejected>: "oops!" )
```

```julia
@await oopsie_result
bad_result = download_result.then(d -> sqrt(-1))

#=>
"oops!"
Promise{Any}( <rejected>:
DomainError with -1.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[1] fetch(p::Main.workspace#3.Promise{Any})
@ Main.workspace#3 ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:78
[1] throw_complex_domainerror(f::Symbol, x::Float64)
@ Base.Math ./math.jl:33
[2] sqrt
@ ./math.jl:567 [inlined]
[3] sqrt(x::Int64)
@ Base.Math ./math.jl:1221
[4] (::Main.var"#5#6"{typeof(sqrt)})(d::String)
@ Main ~/Documents/Promises.jl/src/notebook.jl#==#34364f4d-e257-4c22-84ee-d8786a2c377c:1
[5] promise_then(p::Promise{Any}, f::Main.var"#5#6"{typeof(sqrt)})
@ Main.workspace#3 ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:63
[6] #18
@ ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:175 [inlined]
)
```

In addition, when an exception occurs inside a Promise body, the Promise will reject, with the error message as rejected value:
If you `@await` a Promise that has rejected, the rejected value will be rethrown as an error:


```julia
Promise((res, rej) -> res(sqrt(-1)))
@await bad_result

#=>
Promise{Any}( <rejected>:
DomainError with -1.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
Expand All @@ -128,15 +130,53 @@ Stacktrace:
@ ./math.jl:567 [inlined]
[3] sqrt(x::Int64)
@ Base.Math ./math.jl:1221
[4] (::Main.var"#9#10"{typeof(sqrt)})(res::Main.workspace#3.var"#resolve#11"{Any, Promise{Any}}, rej::Function)
[4] (::var"#5#6"{typeof(sqrt)})(d::String)
@ Main ~/Documents/Promises.jl/src/notebook.jl#==#34364f4d-e257-4c22-84ee-d8786a2c377c:1
[5] macro expansion
@ ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:33 [inlined]
[6] (::Main.workspace#3.var"#3#10"{Any, Main.var"#9#10"{typeof(sqrt)}, Promise{Any}})()
@ Main.workspace#3 ./task.jl:423
)
[5] promise_then(p::Main.workspace#3.Promise{Any}, f::var"#5#6"{typeof(sqrt)})
@ Main.workspace#3 ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:63
[6] #18
@ ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:175 [inlined]
Stacktrace:
[1] fetch(p::Main.workspace#3.Promise{Any})
@ Main.workspace#3 ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:112
```

<br>


## The `Promise` constructor

Remember that a promise can finish in two ways: it can **✓ resolve** or it can **✗ reject**. When creating a Promise by hand, this corresponds to the two functions passed in by the constructor, `resolve` and `reject`:

```julia
Promise{T=Any}(resolve, reject) -> begin

if condition
# Resolve the promise:
resolve("Success!")
else
# Reject the promise
reject("Something went wrong...")
end
end)
```


```julia
yay_result = Promise((resolve, reject) -> resolve("🌟 yay!"))

#=> Promise{Any}( <resolved>: "🌟 yay!" )
```

```julia
oopsie_result = Promise((res, rej) -> rej("oops!"))

#=> Promise{Any}( <rejected>: "oops!" )
```

(A shorthand function is available to create promises that immediately reject or resolve, like we did above: `Promises.resolve(value)` and `Promises.reject(value)`.)


<br>


Expand Down Expand Up @@ -184,6 +224,15 @@ Promise{String}((res,rej) -> res("asdf"))
#=> Promise{String}( <resolved>: "asdf" )
```

This information is available to the Julia compiler, which means that it can do smart stuff!


```julia
Core.Compiler.return_type(fetch, (Promise{String},))

#=> String
```

Trying to resolve to another type will reject the Promise:


Expand All @@ -194,24 +243,26 @@ Promise{String}((res,rej) -> res(12341234))
Promise{String}( <rejected>:
ArgumentError: Can only resolve with values of type String.
Stacktrace:
[1] (::Main.workspace#3.var"#resolve#11"{String, Promise{String}})(val::Int64)
@ Main.workspace#3 ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:16
[2] #25
@ ~/Documents/Promises.jl/src/notebook.jl#==#9d9179de-19b1-4f40-b816-454a8c071c3d:1 [inlined]
[3] macro expansion
@ ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:33 [inlined]
[4] (::Main.workspace#3.var"#3#10"{String, Main.var"#25#26", Promise{String}})()
@ Main.workspace#3 ./task.jl:423
[1] (::Main.workspace#3.var"#resolve#20"{String, Promise{String}})(val::Int64)
@ Main.workspace#3 ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:21
[2] (::Main.var"#25#26")(res::Main.workspace#3.var"#resolve#20"{String, Promise{String}}, rej::Function)
@ Main ~/Documents/Promises.jl/src/notebook.jl#==#9d9179de-19b1-4f40-b816-454a8c071c3d:1
[3] Promise{String}(f::Main.var"#25#26")
@ Main.workspace#3 ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:38
)
```

This information is available to the Julia compiler, which means that it can do smart stuff!
#### Automatic types

Julia is smart, and it can automatically determine the type of chained Promises using static analysis!


```julia
Core.Compiler.return_type(fetch, (Promise{String},))
typeof(
Promise{String}((res,rej) -> res("asdf")).then(first)
)

#=> String
#=> Promise{Char}
```


Expand Down
4 changes: 3 additions & 1 deletion src/Promises.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ const Resolved = 💛.Resolved
const Rejected = 💛.Rejected
const PromiseSettledResult = 💛.PromiseSettledResult
const var"@await" = 💛.var"@await"
const var"@async_promise" = 💛.var"@async_promise"

export Promise,
@await
@await,
@async_promise


end
Loading