-
Notifications
You must be signed in to change notification settings - Fork 26
/
ErrorsExample.java
274 lines (238 loc) · 11.7 KB
/
ErrorsExample.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/*
* Copyright 2016 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.common.base;
import java.util.Arrays;
import java.util.List;
import com.diffplug.common.base.Errors;
import com.diffplug.common.base.Throwing;
// @formatter:off
@SuppressWarnings({"serial"})
public class ErrorsExample {
/** Errors is a simple little class that makes ErrorHandling a lot easier. */
public void whyItsGreat() throws Exception {
List<Food> foodOnPlate = Arrays.asList(
cook("salmon"),
cook("asparagus"),
cook("enterotoxin"));
// without Errors, we have to write this
foodOnPlate.forEach(val -> {
try {
eat(val);
} catch (Barf e) {
// get out the baking soda
}
});
// With Errors, we can succinctly
// sweep it under the rug
foodOnPlate.forEach(Errors.suppress().wrap(this::eat));
// save it for later
foodOnPlate.forEach(Errors.log().wrap(this::eat));
// make mom deal with it
foodOnPlate.forEach(Errors.rethrow().wrap(this::eat));
// ask the user deal with it
foodOnPlate.forEach(Errors.dialog().wrap(this::eat));
// We can also make our own Errorses, which can be reused across a project
Errors retryHandler = Errors.createHandling(error -> {
if (error instanceof Barf) {
Food cause = ((Barf) error).looksLikeItUsedToBe();
try {
eat(cause);
} catch (Barf barf) {
// at least we tried really hard
}
}
});
foodOnPlate.forEach(retryHandler.wrap(this::eat));
}
public void whereDoLogAndDialogComeFrom() {
// Errors.log() promises to "log", and Errors.dialog() promises to alert the user.
// Doing those things is very different depending on whether your code is running as a web
// application, console application, or desktop gui
// One way around this ambiguity is to avoid using Errors.log() and Errors.dialog()
// entirely, and only use your own custom Errors. But if you are shipping a framework,
// and your users might end up using Errors.log() and .dialog(), then you might want to
// specify what those do.
// By default, Errors.log() is just Throwable.printStackTrace, and Errors.dialog()
// opens a JOptionPane. You can modify this behavior with the following:
DurianPlugins.register(Errors.Plugins.Log.class, error -> {
// log to twitter
});
DurianPlugins.register(Errors.Plugins.Dialog.class, error -> {
// Headless application: email the sysadmin and exit
// Web application: ajax an alert() to the user
});
// The trick is, you have to call these methods BEFORE Errors.log() or Errors.dialog()
// are used anywhere in your whole application. Once log() or dialog() have been used, they are
// fixed for the duration of the runtime. If you're writing a library, then you probably shouldn't
// try to change them. If you're writing an application or framework, then you probably should.
// If you're running in a JUnit environment, then you probably want any call to log() or dialog()
// to kill the test. Setting the following system properties (again, before log() or dialog() are
// called) will cause any errors to get wrapped and thrown as a java.lang.AssertionError.
System.setProperty("durian.plugins.com.diffplug.common.base.Errors.Plugins.Log",
"com.diffplug.common.base.Errors$Plugins$OnErrorThrowAssertion");
System.setProperty("durian.plugins.com.diffplug.common.base.Errors.Plugins.Dialog",
"com.diffplug.common.base.Errors$Plugins$OnErrorThrowAssertion");
}
@Override
public Object clone() {
// We'd like to return a Food,
// but the only way to get it is the cook() method,
// which throws a checked exception
try {
return cook("spaghetti");
} catch (IAmOnFire e) {
// TODO: water()
return CEREAL;
}
// Our superclass doesn't let us propagate the exception,
// so we've got to either
// A) return a default value
// B) rethrow the checked exception, wrapped in a RuntimeException
//
// If we decide to take the "default value" route, we might want to suppress
// the memory of being on fire, or we might want to log it to twitter
}
@Override
public void finalize() {
// If we're taking the "default value" route, Errors has you covered
Food logged = Errors.log().getWithDefault(() -> cook("spaghetti"), CEREAL); // log to twitter
Food suppressed = Errors.suppress().getWithDefault(() -> cook("spaghetti"), CEREAL); // suppress to insecurity buffer
// If we're taking the "rethrow RuntimeException" route, then specifying a
// default value would be nonsensical, so we don't do it
Food rethrow = Errors.rethrow().get(() -> cook("spaghetti"));
// I'm stressed, don't judge me
Errors.suppress().run(() -> {
eat(logged);
eat(suppressed);
eat(rethrow);
});
}
public void finalizeAdvanced() {
// The previous example uncovered one of Errors's secrets. There are two
// subclasses of Errors: Errors.Handling, and Errors.Rethrowing.
// An instance of Errors.Rethrowing is an Errors that guarantees by construction
// to always handle errors by throwing a RuntimeException. This means that when it is returning
// a value from a fallible function, it doesn't need a default value.
Errors.Rethrowing rethrowing = Errors.createRethrowing(error -> {
// Note that we're returning the exception, not throwing it.
// The Errors will throw it for us, thus "guaranteed by construction".
// It'd be okay if we threw it ourselves too, but it's not as pretty.
return new RuntimeException("AHHHHHHHHHHHHHHHHHH!!!!");
});
Food thrown = rethrowing.get(() -> cook("spaghetti")); // no default needed
// An instance of Errors.Handling is an Errors that doesn't make this guarantee.
// Since it isn't going to handle errors by throwing an exception (well, it might, but it isn't
// promising to, so it might not), a default value is required.
Errors.Handling handling = Errors.createHandling(error -> {
if (error instanceof RuntimeException) {
throw (RuntimeException) error;
} else {
System.err.println("Hot! Hot! Hot! Hot!");
}
});
Food handled = handling.getWithDefault(() -> cook("spaghetti"), CEREAL); // gotta have that default
// A plain-old Errors can deal with functions that don't return values (namely Runnable and Consumer).
// But to work with functions that do return a value (namely Supplier and Function), you're gonna
// need to have it in its true Handling / Rethrowing form. In a modern IDE with autocomplete,
// The Right Thing will just automatically happen for you.
if (thrown.equals(handled)) {
// I guess I wasted my time with the two subclasses, because it didn't matter in the end.
} else {
// Time well spent.
}
}
@SuppressWarnings("unused")
public void wrapping() {
// If your functional interface has 0 or 1 outputs, and 0 or 1 inputs, then Errors can wrap it into its standard Java 8 form
Throwing.Runnable marathon = () -> { throw new IAmOnFire(); };
java.lang.Runnable marathonSafe = Errors.log().wrap(marathon);
Throwing.Consumer<Food> eat = this::eat;
java.util.function.Consumer<Food> eatSafe = Errors.log().wrap(eat);
Throwing.Supplier<Food> cookSpatula = () -> cook("spatula");
java.util.function.Supplier<Food> cookSpatulaOrGetCereal = Errors.log().wrapWithDefault(cookSpatula, CEREAL);
java.util.function.Supplier<Food> cookSpatulaOrBurn = Errors.rethrow().wrap(cookSpatula);
Throwing.Function<String, Food> cookAnything = this::cook;
java.util.function.Function<String, Food> cookAnythingOrGetCereal = Errors.log().wrapWithDefault(this::cook, CEREAL);
java.util.function.Function<String, Food> cookAnythingOrBurn = Errors.rethrow().wrap(this::cook);
// If your function has more than 1 input, you can either
// A) Make a wrapper function that calls Errors.get() to return a value (recommended)
// B) Make a "wrapper wrapper" (see https://github.com/diffplug/durian/blob/master/test/com/diffplug/common/base/ErrorsMultipleInputs.png)
// If your function has more than 1 output, see https://github.com/diffplug/durian/blob/master/test/com/diffplug/common/base/ErrorsMultipleOutputs.png
}
@SuppressWarnings("unused")
public void throwingSpecfic() {
// Throwing.Specific lets you express functional interfaces which throw a specific exception
Throwing.Specific.Consumer<Food, Barf> eatSignature = this::eat;
Throwing.Specific.Function<String, Food, IAmOnFire> cookSignature = this::cook;
// This can be helpful for writing generic code for working with a specific kind of exception, but
// it isn't helpful for writing code for generic exceptions (because type erasure).
//
// The only way to know the type of List<T> is to grab an element out of the
// list and call getClass() on it. And then you still mostly don't know the type of the list.
// This same limitation ripples through exception handling in the following way:
class BarfHarness {
// If you knew it was Barf at compile time then you can catch it
void exceptionKnownAtCompileTime(Throwing.Specific.Runnable<Barf> eatAndThen) {
try {
eatAndThen.run();
} catch (Barf e) {
// and on Janitor's day too!
}
}
// If the exception was generic at compile time, then you have to catch the most-general possible exception, which is Throwable
<E extends Throwable> void exceptionGenericAtCompileTime(Throwing.Specific.Runnable<E> eatAndThen) {
try {
eatAndThen.run();
// You can try "catch (E e)", but you'll get: "Cannot use the type parameter E in a catch block"
// You can try "catch (Barf e)", but you'll get: "Unreachable catch block for ErrorsExample.Barf. This exception is never thrown from the try statement body"
} catch (Throwable e) {
// Looks like we're stuck catching Throwable. Why should we bother having the exception be generic?
}
}
}
// Well, we probably shouldn't. It didn't do us any good.
// Throwing.Specific.* is most useful when it's implementing plain-old Throwing.*
Throwing.Runnable runnableNonSpecific = () -> { throw new IAmOnFire(); };
Throwing.Specific.Runnable<Throwable> runnable = runnableNonSpecific;
// Here's the voluminous source code for Throwing:
// public interface Runnable extends Specific.Runnable<Throwable> {}
// public interface Supplier<T> extends Specific.Supplier<T, Throwable> {}
// public interface Consumer<T> extends Specific.Consumer<T, Throwable> {}
// public interface Function<T, R> extends Specific.Function<T, R, Throwable> {}
// public interface Predicate<T> extends Specific.Predicate<T, Throwable> {}
// Now you know how to cook spaghetti and eat salmon using Java >= 8 and generic exceptions using Errors!
}
/** Immutable, unfortunately. */
private class IAmOnFire extends Exception {}
/** A monoid over the group "food". */
class Food {}
/** Needs another day or two to flesh out some implementation details. */
private class Barf extends Exception {
public Food looksLikeItUsedToBe() {
// TODO: Computer vision
return null;
}
}
/** Endofunctor of the monoid over the food. */
void eat(Food food) throws Barf {}
/** Returns the Hamming distance between the ingredients. */
Food cook(String ingredients) throws IAmOnFire {
return null;
}
/** This is obtained after stamping the natural numbers in ascending order on individual grains. */
private static final Food CEREAL = null;
}
//@formatter:on