Skip to content

Commit 9f4dd6f

Browse files
committed
Add Future implementation
1 parent 129bf3c commit 9f4dd6f

File tree

6 files changed

+610
-16
lines changed

6 files changed

+610
-16
lines changed

README.md

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,90 @@ A collection of primitives for event-driven programming.
99

1010
## Installation
1111

12-
1. Add the dependency to your `shard.yml`:
12+
Add the dependency to your `shard.yml` and run `shards install`:
1313

14-
```yaml
15-
dependencies:
16-
concur:
17-
github: your-github-user/concur
18-
```
19-
20-
2. Run `shards install`
14+
```yaml
15+
dependencies:
16+
concur:
17+
github: lbarasti/concur
18+
```
2119
2220
## Usage
2321
2422
```crystal
2523
require "concur"
2624
```
2725

28-
TODO: Write usage instructions here
26+
### Using Future
27+
You can use `Future` to wrap asynchronous computations that might fail.
28+
```crystal
29+
f = Future.new {
30+
sleep 2 # a placeholder for some expensive computation or for a lengthy IO operation
31+
"Success!"
32+
}
33+
34+
f.await # => "Success!"
35+
```
36+
If you want to keep on manipulating the result of a future in a separate fiber, then you can rely on `Future`'s instance methods.
37+
38+
For example, given a future `f`, you can apply a function to the wrapped value with `#map`, filter it with `#select` and recover from failure with `#recover`
39+
40+
```crystal
41+
f.map { |v| v.downcase }
42+
.select { |v| v.size < 3 }
43+
.recover { "default_key" }
44+
```
45+
46+
Here is a contrived example to showcase some other common methods.
47+
48+
You can combine the result of two Futures into one with `#zip`:
49+
50+
```crystal
51+
author_f : Future(User) = db.user_by_id(1)
52+
reviewer_f : Future(User) = db.user_by_id(2)
53+
54+
permission_f : Future(Bool) = author_f.zip(reviewer_f) { |author, reviewer|
55+
author.has_reviewer?(reviewer)
56+
}
57+
```
58+
59+
You can use `#flat_map` to avoid nesting futures:
60+
61+
```crystal
62+
content_f : Future(Content) = permission_f
63+
.flat_map { |reviewer_is_allowed|
64+
if reviewer_is_allowed
65+
db.content_by_user(1) # => Future(Content)
66+
else
67+
raise NotAllowedError.new
68+
end
69+
}
70+
```
71+
72+
And perform side effects with `#on_success` and `#on_error`.
73+
74+
```crystal
75+
content_f
76+
.on_success { |content|
77+
reviewer_f.await!.email(content)
78+
}
79+
.on_error { |ex| log_error(ex) }
80+
```
81+
82+
Check out the [API docs](https://lbarasti.com/concur/) for more details.
2983

3084
## Development
3185

32-
TODO: Write development instructions here
86+
Run `shards install` to install the project dependencies. You can then run `crystal spec` to verify that all the tests are passing.
3387

3488
## Contributing
3589

36-
1. Fork it (<https://github.com/your-github-user/concur/fork>)
90+
1. Fork it (<https://github.com/lbarasti/concur/fork>)
3791
2. Create your feature branch (`git checkout -b my-new-feature`)
3892
3. Commit your changes (`git commit -am 'Add some feature'`)
3993
4. Push to the branch (`git push origin my-new-feature`)
4094
5. Create a new Pull Request
4195

4296
## Contributors
4397

44-
- [lorenzo.barasti](https://github.com/your-github-user) - creator and maintainer
98+
- [lbarasti](https://github.com/lbarasti) - creator and maintainer

shard.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
name: concur
2-
version: 0.1.0
2+
version: 0.2.0
33

44
authors:
55
- lbarasti <[email protected]>
66

7-
crystal: 1.0.0
7+
crystal: ">=1.0.0"
88

99
dependencies:
1010
rate_limiter:

spec/future_spec.cr

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
require "./spec_helper"
2+
3+
describe Future do
4+
it "runs a computation asyncronously" do
5+
ch = Channel(Nil).new
6+
7+
Future.new { ch.send nil }
8+
9+
ch.receive
10+
end
11+
12+
it "can be awaited on" do
13+
f = Future.new { 2** 10 }
14+
res = f.await
15+
res.should eq 1024
16+
typeof(res).should eq (Int32 | Exception)
17+
end
18+
19+
it "can be awaited on multiple times" do
20+
f = Future.new { 2** 10 }
21+
10.times {
22+
f.await.should eq 1024
23+
}
24+
end
25+
26+
it "can be awaited on by multiple fibers" do
27+
n_fibers = 100
28+
results = Channel(Int32 | Exception).new(n_fibers)
29+
f = Future.new { sleep rand; 2** 10 }
30+
n_fibers.times {
31+
spawn {
32+
results.send f.await
33+
}
34+
}
35+
n_fibers.times {
36+
results.receive.should eq 1024
37+
}
38+
end
39+
40+
it "can be awaited on and raise on exception" do
41+
f = Future.new { sleep 0.4; raise CustomError.new("error"); 42 }
42+
expect_raises(CustomError) {
43+
f.await!
44+
}
45+
end
46+
47+
it "can be awaited on with timeout" do
48+
f = Future.new { sleep 0.6; 42 }
49+
f.await(0.2.seconds)
50+
.should be_a Concur::Timeout
51+
f.await
52+
.should eq 42
53+
f.await(0.1.seconds)
54+
.should eq 42
55+
end
56+
57+
it "can be awaited on with timeout and raise on exception" do
58+
f = Future.new { sleep 0.4; raise CustomError.new("error"); 42 }
59+
expect_raises(Concur::Timeout) {
60+
f.await!(0.2.seconds)
61+
}
62+
expect_raises(CustomError) {
63+
f.await!(0.3.seconds)
64+
}
65+
end
66+
67+
it "can be queried for completion" do
68+
f = Future.new { sleep 0.3; :done }
69+
f.done?.should be_false
70+
f.await
71+
f.done?.should be_true
72+
end
73+
74+
it "supports running callbacks, in order, on completion" do
75+
results = [] of Int32
76+
f = Future.new { 5 }
77+
f.on_complete { |r|
78+
results << r.as(Int32) + 1
79+
raise Exception.new("runtime exception")
80+
}.on_complete { |r|
81+
case r
82+
when Int32
83+
results << r
84+
end
85+
}.await.should eq 5
86+
87+
results.should eq [6, 5]
88+
end
89+
90+
it "supports running callbacks in order on success" do
91+
results = [] of Int32
92+
f = Future.new { 5 }
93+
f.on_success { |r|
94+
results << r.as(Int32) + 1
95+
raise Exception.new("runtime exception")
96+
}.on_success { |r|
97+
case r
98+
when Int32
99+
results << r
100+
end
101+
}.await.should eq 5
102+
103+
results.should eq [6, 5]
104+
end
105+
106+
it "will not run #on_success callbacks if the future fails" do
107+
results = [] of Int32
108+
f = Future.new { raise CustomError.new; 5 }
109+
f.on_success { |r|
110+
results << 1
111+
}.on_success { |r|
112+
raise Exception.new
113+
results << 2
114+
}.await.should be_a CustomError
115+
116+
results.should be_empty
117+
end
118+
119+
it "supports running callbacks in order on error" do
120+
results = [] of Exception
121+
f = Future.new { raise CustomError.new; 5 }
122+
123+
f.on_error { |ex|
124+
results << ex.as(CustomError)
125+
raise Exception.new("runtime exception")
126+
}.on_error { |ex|
127+
results << Timeout.new
128+
}.await.should be_a CustomError
129+
130+
results.first.should be_a CustomError
131+
results.last.should be_a Timeout
132+
end
133+
134+
it "will not run #on_error callbacks if the future succeeds" do
135+
results = [] of Int32
136+
f = Future.new { 5 }
137+
f.on_error { |r|
138+
results << 1
139+
}.on_error { |r|
140+
raise Exception.new
141+
results << 2
142+
}.await.should eq 5
143+
144+
results.should be_empty
145+
end
146+
147+
it "supports function composition" do
148+
c_1 = Future.new { rand ** 2 }
149+
c_2 = c_1.map { |x| 1 - x }
150+
151+
(c_1.await! + c_2.await!).should eq 1
152+
c_2.done?.should be_true
153+
end
154+
155+
it "doesn't propagate errors backward when composing functions" do
156+
c_1 = Future.new { 2 }
157+
c_2 = c_1.map { |x| raise CustomError.new("error"); 1 - x }
158+
159+
c_2.await.should be_a CustomError
160+
c_1.await.should eq 2
161+
end
162+
163+
it "propagates errors when composing functions" do
164+
c_1 = Future.new { raise CustomError.new("error"); rand ** 2 }
165+
c_2 = c_1.map { |x| raise Exception.new("generic error"); 1 - x }
166+
167+
c_2.await.should be_a CustomError
168+
end
169+
170+
it "can be filtered" do
171+
f = Future.new { 5 }
172+
g = f.select { |x| x % 2 == 1 }
173+
h = f.select { |x| x % 2 == 0 }
174+
g.await.should eq 5
175+
h.await.should be_a Concur::EmptyError
176+
end
177+
178+
it "supports combining two futures with #flat_map" do
179+
f = Future.new { 5 }
180+
g = Future.new { 3 }
181+
182+
f.flat_map { |x| g.map { |y| x + y } }
183+
.await.should eq 8
184+
end
185+
186+
it "will return the first encountered error on #flat_map" do
187+
f = Future.new { raise CustomError.new; 5 }
188+
g = Future.new { 3 }
189+
190+
f.flat_map { |x| g.map { |y| x + y } }
191+
.await.should be_a CustomError
192+
end
193+
194+
it "can be flattened in case of nested futures" do
195+
f = Future.new { Future.new { 5 } }
196+
f.await.should be_a Future(Int32)
197+
198+
f.flatten
199+
.await.should eq 5
200+
end
201+
202+
it "supports mapping its underlying value to a specific type" do
203+
f = Future.new { rand < 0.5 ? "hello" : 5 }
204+
205+
typeof(f.await).should eq String | Int32 | Exception
206+
v = f.map_to(Int32).await
207+
typeof(v).should eq Int32 | Exception
208+
end
209+
210+
it "raises a TypeCastError when mapping to an incompatible type" do
211+
f = Future.new { rand < 0.5 ? "hello" : 5 }
212+
g = f.map { |v| v.class == Int32 ? "hello 2" : 6 }
213+
214+
expect_raises(TypeCastError) {
215+
fv = f.map_to(Int32).await!
216+
gv = g.map_to(Int32).await!
217+
}
218+
end
219+
220+
it "can be recovered if it fails" do
221+
f = Future.new { raise CustomError.new; 42 }
222+
f.recover { |e|
223+
case e
224+
when CustomError
225+
100
226+
else
227+
raise Exception.new("Unexpected failure")
228+
end
229+
}.await.should eq 100
230+
end
231+
232+
it "does not recover successful futures" do
233+
f = Future.new { 42 }
234+
f.recover { |ex| 43 }
235+
.await.should eq 42
236+
end
237+
238+
it "can be transformed" do
239+
f = Future.new { 42 }
240+
g = Future.new { raise CustomError.new; 42 }
241+
t = -> (res : Int32 | Exception) {
242+
case res
243+
when Int32
244+
"#{res + 1}"
245+
else
246+
"0"
247+
end
248+
}
249+
f.transform(&t).await.should eq "43"
250+
g.transform(&t).await.should eq "0"
251+
end
252+
253+
it "supports zipping futures together" do
254+
f = Future.new { 1 }
255+
g = Future.new { 2 }
256+
f.zip(g) { |v1, v2| v1 + v2 }
257+
.await.should eq 3
258+
end
259+
end

0 commit comments

Comments
 (0)