1 /++ 2 Error handling that bundles exceptions with return values. 3 4 The design of this module is based on C++'s proposed 5 [std::expected](https://wg21.link/p0323) and Rust's 6 [std::result](https://doc.rust-lang.org/std/result/). See 7 ["Expect the Expected"](https://www.youtube.com/watch?v=nVzgkepAg5Y) by 8 Andrei Alexandrescu for further background. 9 10 License: MIT 11 Author: Paul Backus 12 +/ 13 module expectations; 14 15 /// $(H3 Basic Usage) 16 @safe unittest { 17 import std.exception: assertThrown; 18 19 Expected!int charToDigit(char c) 20 { 21 int d = c - '0'; 22 if (d >= 0 && d < 10) { 23 return expected(d); 24 } else { 25 return missing!int( 26 new Exception(c ~ " is not a valid digit") 27 ); 28 } 29 } 30 31 auto goodResult = charToDigit('7'); 32 auto badResult = charToDigit('&'); 33 34 assert(goodResult.hasValue); 35 assert(goodResult.value == 7); 36 37 assert(!badResult.hasValue); 38 assertThrown(badResult.value); 39 assert(badResult.error.msg == "& is not a valid digit"); 40 } 41 42 /** 43 * An exception that represents an error value. 44 */ 45 class Unexpected(T) : Exception 46 { 47 /** 48 * The error value. 49 */ 50 T value; 51 52 /** 53 * Constructs an `Unexpected` exception from a value. 54 */ 55 pure @safe @nogc nothrow 56 this(T value, string file = __FILE__, size_t line = __LINE__) 57 { 58 super("unexpected value", file, line); 59 this.value = value; 60 } 61 } 62 63 /** 64 * An `Expected!(T, E)` contains either an expected value of type `T`, or an 65 * error value of type `E` explaining why the expected value couldn't be 66 * produced. 67 * 68 * The default type for the error value is `Exception`. 69 * 70 * A function that returns an `Expected` object has the following advantages 71 * over one that may throw an exception or return an error code: 72 * 73 * $(LIST 74 * * It leaves the choice between manual error checking and automatic stack 75 * unwinding up to the caller. 76 * * It allows error handling to be deferred until the return value is 77 * actually needed (if ever). 78 * * It can be easily composed with other functions using [map] and 79 * [flatMap], which propagates error values automatically. 80 * * It can be used in `nothrow` code. 81 * ) 82 * 83 * An `Expected!(T, E)` is initialized by default to contain the value `T.init`. 84 * 85 * $(PITFALL Unlike a thrown exception, an error returned via an `Expected` 86 * object will be silently ignored if the function's return value is discarded. 87 * For best results, functions that return `Expected` objects should be marked 88 * as `pure`, so that the D compiler will warn about discarded return values.) 89 */ 90 struct Expected(T, E = Exception) 91 if (!is(T == E) && !is(T == void)) 92 { 93 private: 94 95 import sumtype; 96 97 SumType!(T, E) data; 98 99 public: 100 101 /** 102 * Constructs an `Expected` object that contains an expected value. 103 */ 104 this(T value) 105 { 106 data = value; 107 } 108 109 /** 110 * Constructs an `Expected` object that contains an error value. 111 */ 112 this(E err) 113 { 114 data = err; 115 } 116 117 /** 118 * Assigns an expected value to an `Expected` object. 119 */ 120 void opAssign(T value) 121 { 122 data = value; 123 } 124 125 /** 126 * Assigns an error value to an `Expected` object. 127 */ 128 void opAssign(E err) 129 { 130 data = err; 131 } 132 133 /** 134 * Checks whether this `Expected` object contains a specific expected value. 135 */ 136 bool opEquals(T rhs) 137 { 138 return data.match!( 139 (T value) => value == rhs, 140 (E _) => false 141 ); 142 } 143 144 /** 145 * Checks whether this `Expected` object contains a specific error value. 146 */ 147 bool opEquals(E rhs) 148 { 149 return data.match!( 150 (T _) => false, 151 (E err) => err == rhs 152 ); 153 } 154 155 /** 156 * Checks whether this `Expected` object and `rhs` contain the same expected 157 * value or error value. 158 */ 159 bool opEquals(Expected!(T, E) rhs) 160 { 161 return data.match!( 162 (T value) => rhs == value, 163 (E err) => rhs == err 164 ); 165 } 166 167 /** 168 * Checks whether this `Expected` object contains an expected value or an 169 * error value. 170 */ 171 bool hasValue() const 172 { 173 return data.match!( 174 (const T _) => true, 175 (const E _) => false 176 ); 177 } 178 179 /** 180 * Returns the expected value if there is one. Otherwise, throws an 181 * exception 182 * 183 * Throws: 184 * If `E` inherits from `Throwable`, the error value is thrown. 185 * Otherwise, an [Unexpected] instance containing the error value is 186 * thrown. 187 */ 188 inout(T) value() inout 189 { 190 return data.match!( 191 (inout(T) value) => value, 192 delegate T (inout(E) err) { 193 static if(is(E : Throwable)) { 194 throw error; 195 } else { 196 throw new Unexpected!E(error); 197 } 198 } 199 ); 200 } 201 202 /** 203 * Returns the error value. May only be called when `hasValue` returns 204 * `false`. 205 */ 206 inout(E) error() inout 207 in { assert(!hasValue); } 208 do { 209 import std.exception: assumeWontThrow; 210 211 return data.tryMatch!( 212 (inout(E) err) => err 213 ).assumeWontThrow; 214 } 215 216 deprecated("Renamed to `error`") 217 inout(E) exception() inout 218 { 219 return error; 220 } 221 222 /** 223 * Returns the expected value if present, or a default value otherwise. 224 */ 225 inout(T) valueOr(inout(T) defaultValue) inout 226 { 227 return data.match!( 228 (inout(T) value) => value, 229 (inout(E) _) => defaultValue 230 ); 231 } 232 } 233 234 // Construction 235 @safe nothrow unittest { 236 assert(__traits(compiles, Expected!int(123))); 237 assert(__traits(compiles, Expected!int(new Exception("oops")))); 238 } 239 240 // Assignment 241 @safe nothrow unittest { 242 Expected!int x; 243 244 assert(__traits(compiles, x = 123)); 245 assert(__traits(compiles, x = new Exception("oops"))); 246 } 247 248 // Self assignment 249 @safe nothrow unittest { 250 Expected!int x, y; 251 252 assert(__traits(compiles, x = y)); 253 } 254 255 // Equality with self 256 @system unittest { 257 int n = 123; 258 Exception e = new Exception("oops"); 259 260 Expected!int x = n; 261 Expected!int y = n; 262 Expected!int z = e; 263 Expected!int w = e; 264 265 assert(x == y); 266 assert(z == w); 267 assert(x != z); 268 assert(z != x); 269 } 270 271 // Equality with T and Exception 272 @system unittest { 273 int n = 123; 274 Exception e = new Exception("oops"); 275 276 Expected!int x = n; 277 Expected!int y = e; 278 279 () @safe { 280 assert(x == n); 281 assert(y != n); 282 assert(x != 456); 283 }(); 284 285 assert(x != e); 286 assert(y == e); 287 assert(y != new Exception("oh no")); 288 } 289 290 // hasValue 291 @safe nothrow unittest { 292 Expected!int x = 123; 293 Expected!int y = new Exception("oops"); 294 295 assert(x.hasValue); 296 assert(!y.hasValue); 297 } 298 299 // value 300 @safe unittest { 301 import std.exception: collectException; 302 303 Expected!int x = 123; 304 Expected!int y = new Exception("oops"); 305 306 assert(x.value == 123); 307 assert(collectException(y.value).msg == "oops"); 308 } 309 310 // error 311 @system unittest { 312 Exception e = new Exception("oops"); 313 Expected!int x = e; 314 315 assert(x.error == e); 316 } 317 318 // valueOr 319 @safe nothrow unittest { 320 Expected!int x = 123; 321 Expected!int y = new Exception("oops"); 322 323 assert(x.valueOr(456) == 123); 324 assert(y.valueOr(456) == 456); 325 } 326 327 // const(Expected) 328 @safe unittest { 329 const(Expected!int) x = 123; 330 const(Expected!int) y = new Exception("oops"); 331 332 // hasValue 333 assert(x.hasValue); 334 assert(!y.hasValue); 335 // value 336 assert(x.value == 123); 337 // error 338 assert(y.error.msg == "oops"); 339 // valueOr 340 assert(x.valueOr(456) == 123); 341 assert(y.valueOr(456) == 456); 342 } 343 344 // Explicit error type 345 @safe unittest { 346 import std.exception: assertThrown; 347 348 Expected!(int, string) x = 123; 349 Expected!(int, string) y = "oops"; 350 351 // haValue 352 assert(x.hasValue); 353 assert(!y.hasValue); 354 // value 355 assert(x.value == 123); 356 assertThrown!(Unexpected!string)(y.value); 357 // error 358 assert(y.error == "oops"); 359 // valueOr 360 assert(x.valueOr(456) == 123); 361 assert(y.valueOr(456) == 456); 362 } 363 364 /** 365 * Creates an `Expected` object from an expected value, with type inference. 366 */ 367 Expected!(T, E) expected(T, E = Exception)(T value) 368 { 369 return Expected!(T, E)(value); 370 } 371 372 // Default error type 373 @safe nothrow unittest { 374 assert(__traits(compiles, expected(123))); 375 assert(is(typeof(expected(123)) == Expected!int)); 376 } 377 378 // Explicit error type 379 @safe nothrow unittest { 380 assert(__traits(compiles, expected!(int, string)(123))); 381 assert(is(typeof(expected!(int, string)(123)) == Expected!(int, string))); 382 } 383 384 /** 385 * Creates an `Expected` object from an error value. 386 */ 387 Expected!(T, E) missing(T, E)(E err) 388 { 389 return Expected!(T, E)(err); 390 } 391 392 @safe nothrow unittest { 393 Exception e = new Exception("oops"); 394 assert(__traits(compiles, missing!int(e))); 395 assert(is(typeof(missing!int(e)) == Expected!int)); 396 } 397 398 @safe nothrow unittest { 399 auto x = missing!int("oops"); 400 assert(__traits(compiles, missing!int("oops"))); 401 assert(is(typeof(missing!int("oops")) == Expected!(int, string))); 402 } 403 404 deprecated("Renamed to `missing`") 405 Expected!(T, E) unexpected(T, E)(E err) 406 { 407 return missing!T(err); 408 } 409 410 /** 411 * Applies a function to the expected value in an `Expected` object. 412 * 413 * If no expected value is present, the original error value is passed through 414 * unchanged, and the function is not called. 415 * 416 * Returns: 417 * A new `Expected` object containing the result. 418 */ 419 template map(alias fun) 420 { 421 /** 422 * The actual `map` function. 423 * 424 * Params: 425 * self = an [Expected] object. 426 */ 427 auto map(T, E)(Expected!(T, E) self) 428 if (is(typeof(fun(self.value)))) 429 { 430 import sumtype: match; 431 432 alias U = typeof(fun(self.value)); 433 434 return self.data.match!( 435 (T value) => expected!(U, E)(fun(value)), 436 (E err) => missing!U(err) 437 ); 438 } 439 } 440 441 @safe unittest { 442 import std.math: approxEqual; 443 444 Expected!int x = 123; 445 Expected!int y = new Exception("oops"); 446 447 double half(int n) { return n / 2.0; } 448 449 assert(__traits(compiles, () nothrow { 450 x.map!half; 451 })); 452 453 assert(x.map!half.value.approxEqual(61.5)); 454 assert(y.map!half.error.msg == "oops"); 455 456 alias mapHalf = map!half; 457 458 assert(mapHalf(Expected!int(123)).value.approxEqual(61.5)); 459 } 460 461 @safe unittest { 462 Expected!(int, string) x = 123; 463 Expected!(int, string) y = "oops"; 464 465 assert(x.map!(n => n*2).value == 246); 466 assert(y.map!(n => n*2).error == "oops"); 467 } 468 469 /** 470 * Forwards the expected value in an `Expected` object to a function that 471 * returns an `Expected` result. 472 * 473 * If the original `Expected` object contains an error value, it is passed 474 * through to the result, and the function is not called. 475 * 476 * Returns: 477 * The `Expected` object returned from the function, or an `Expected` object 478 * of the same type containing the original error value. 479 */ 480 template flatMap(alias fun) 481 { 482 /** 483 * The actual `flatMap` function. 484 * 485 * Params: 486 * self = an [Expected] object 487 */ 488 auto flatMap(T, E1)(Expected!(T, E1) self) 489 if (is(typeof(fun(self.value)) == Expected!(U, E2), U, E2) 490 && is(E1 : E2)) 491 { 492 import sumtype: match; 493 494 alias ExpectedUE2 = typeof(fun(self.value)); 495 alias E2 = typeof(ExpectedUE2.init.error()); 496 497 return self.data.match!( 498 (T value) => fun(value), 499 (E1 err) => ExpectedUE2(cast(E2) err) 500 ); 501 } 502 } 503 504 @safe unittest { 505 import std.math: approxEqual; 506 507 Expected!int x = 123; 508 Expected!int y = 0; 509 Expected!int z = new Exception("oops"); 510 511 Expected!double recip(int n) 512 { 513 if (n == 0) { 514 return missing!double(new Exception("Division by zero")); 515 } else { 516 return expected(1.0 / n); 517 } 518 } 519 520 assert(__traits(compiles, () nothrow { 521 x.flatMap!recip; 522 })); 523 524 assert(x.flatMap!recip.value.approxEqual(1.0/123)); 525 assert(y.flatMap!recip.error.msg == "Division by zero"); 526 assert(z.flatMap!recip.error.msg == "oops"); 527 528 alias flatMapRecip = flatMap!recip; 529 530 assert(flatMapRecip(Expected!int(123)).value.approxEqual(1.0/123)); 531 } 532 533 @safe unittest { 534 import std.math: approxEqual; 535 536 Expected!(int, string) x = 123; 537 Expected!(int, string) y = 0; 538 Expected!(int, string) z = "oops"; 539 540 Expected!(double, string) recip(int n) 541 { 542 if (n == 0) { 543 return missing!double("Division by zero"); 544 } else { 545 return expected!(double, string)(1.0 / n); 546 } 547 } 548 549 assert(__traits(compiles, () nothrow { 550 x.flatMap!recip; 551 })); 552 553 assert(x.flatMap!recip.value.approxEqual(1.0/123)); 554 assert(y.flatMap!recip.error == "Division by zero"); 555 assert(z.flatMap!recip.error == "oops"); 556 } 557 558 deprecated("Renamed to `flatMap`") 559 template andThen(alias fun) 560 { 561 alias andThen = flatMap!fun; 562 }