Uploaded by abanda.ludovic500

effect-oriented-programming-preview

advertisement
Effect-Oriented Programming
Creating Reliable Systems with Scala 3 and ZIO 2
Bill Frasure, Bruce Eckel and James Ward
Effect-Oriented Programming
Creating Reliable Systems with Scala 3 and ZIO 2
Bill Frasure, Bruce Eckel and James Ward
This book is for sale at http://leanpub.com/effect-oriented-programming
This version was published on 2024-01-25
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean
Publishing process. Lean Publishing is the act of publishing an in-progress ebook
using lightweight tools and many iterations to get reader feedback, pivot until you
have the right book and build traction once you do.
© 2021 - 2024 Bill Frasure, Bruce Eckel and James Ward
CONTENTS
Contents
Copyright . . . . . . . . . . . . . . .
Effect Oriented Programming
Source Code . . . . . . . . . . .
Edit This Chapter . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
2
5
Preface . . . . . . . . . . . .
Who is the book is for
Code examples . . . .
Acknowledgements .
Edit This Chapter . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6
6
6
7
7
What Are Effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Introduction . . . . . . . . . . . . .
Untangling the Chaos . . . . .
The State of Software . . . . .
The Software Crisis . . . . . .
Reliability . . . . . . . . . . . .
Dealing with unpredictability
Effects can not be un-done . .
What is an Effect? . . . . . . .
The Interpreter . . . . . . . . .
Edit This Chapter . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
9
9
10
11
12
13
14
14
16
17
Superpowers with Effects . . . . . . . . .
Building a Resilient Process in stages
Step 1: Happy Path . . . . . . . . . . .
Step 2: Provide Error Fallback Value
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
18
18
18
18
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
CONTENTS
Step 3: Retry Upon Failure . . . . . . . . . .
Step 4: Fallback after multiple failures . . .
Step 5: Timeouts . . . . . . . . . . . . . . . .
Step 6: Fallback Effect . . . . . . . . . . . . .
Step 7: Concurrently Execute Effect . . . . .
Step 8: Ignore failures in Concurrent Effect
Step 9: Rate Limit TODO . . . . . . . . . . .
Edit This Chapter . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
19
19
20
20
21
22
22
24
Why Functional . . . . . . . . . . . . . . . . . . . . .
A Different Goal . . . . . . . . . . . . . . . . . .
Reuse . . . . . . . . . . . . . . . . . . . . . . . . .
Pure Functions . . . . . . . . . . . . . . . . . . .
Composability . . . . . . . . . . . . . . . . . . . .
Effects . . . . . . . . . . . . . . . . . . . . . . . .
Core Differences Between OO and Functional
Summary: Style vs Substance . . . . . . . . . .
Edit This Chapter . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
25
25
28
29
30
31
32
32
33
Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
General/Historic discussion . . . . . . . . . . . . . . . . . . . . . . . . . . . .
What ZIO Provides Us. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
DI-Wow! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
TODO Rewrite this without . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Step 1: Effects can express dependencies . . . . . . . . . . . . . . . . . . . . .
Step 2: Provide Dependency Layers to Effects . . . . . . . . . . . . . . . . . .
Step 3: Effects can require multiple dependencies . . . . . . . . . . . . . . .
Step 4: Dependencies can “automatically” assemble to fulfill the needs of
an effect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Step 5: Different effects can require the same dependency . . . . . . . . . .
Step 6: Dependencies must be fulfilled by unique types . . . . . . . . . . . .
Step 7: Providing Dependencies at Different Levels . . . . . . . . . . . . . .
Step 8: Dependencies can fail . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Step 9: Dependency Retries . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Step 10: Fallback Dependencies . . . . . . . . . . . . . . . . . . . . . . . . . .
Step 11: Layer Retry + Fallback? . . . . . . . . . . . . . . . . . . . . . . . . . .
34
34
35
35
35
36
36
37
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
39
39
40
41
42
42
43
43
CONTENTS
Testing Effects . . .
Random . . . . . .
Time . . . . . . . .
Edit This Chapter .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
44
44
46
48
Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Historic approaches to Error-handling . . . . . .
ZIO Error Handling . . . . . . . . . . . . . . . . .
Unions AKA Sum Types AKA Enums AKA Ors
Edit This Chapter . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
49
49
55
59
61
Contract-Based Composability . . . . .
Composability Explanation . . . . . .
Alternatives and their downsides . .
Universal Composability with ZIO .
All The Thing Example . . . . . . . .
Hedging . . . . . . . . . . . . . . . . .
Repeats . . . . . . . . . . . . . . . . . .
Edit This Chapter . . . . . . . . . . . .
Automatically attached experiments.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
62
64
64
66
67
68
68
68
69
Concurrency High Level
zipPar, zipWithPar . .
validateWithPar? . . .
withParallelism . . . .
Edit This Chapter . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
75
76
76
76
76
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
path
. . . .
. . . .
77
77
77
77
77
77
79
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Concurrency Interruption . . . . . . . . . . . . . . . . . . . . . .
Why Interruption Is Necessary Throughout the Stack . . . .
Timeout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Race . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.withFinalizer . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Fork Interruption . . . . . . . . . . . . . . . . . . . . . . . . . .
Uninterruptable . . . . . . . . . . . . . . . . . . . . . . . . . . .
Future Cancellation (Contra-example. Not necessary for
explanation) . . . . . . . . . . . . . . . . . . . . . . . .
Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
happy
. . . . .
. . . . .
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
79
80
CONTENTS
State . . . . . . . . . . . .
Unreliable Counting
Reliable Counting .
Ref.Synchronized . .
Edit This Chapter . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
81
81
82
85
85
Reliability . . . . . . . . . . . . . . . . .
Caching . . . . . . . . . . . . . . .
Staying under rate limits . . . . .
Constraining concurrent requests
Circuit Breaking . . . . . . . . . .
Hedging . . . . . . . . . . . . . . .
Edit This Chapter . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
86
86
87
87
89
89
89
ZIO and ZLayer . . . .
ZIO . . . . . . . . .
ZLayer . . . . . . .
Composing . . . . .
ZIO <-> ZLayer . .
Edit This Chapter .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
90
90
91
91
92
92
Appendix Running Effects . . . . . . . . . . . . .
Building applications from scratch . . . . . .
Testing code . . . . . . . . . . . . . . . . . . . .
Interop with existing/legacy code via Unsafe
Edit This Chapter . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
93
93
94
97
97
Experiments . .
rezilience . .
performance
zio_helpers .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. 98
. 98
. 106
. 108
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Copyright
Effect Oriented Programming
By Bill Frasure, Bruce Eckel and James Ward
{{ These are the new ISBNs }}
Copyright ©2021, MindView LLC.
eBook ISBN 978-0-9818725-6-8
Print Book ISBN 978-0-9818725-7-5
The eBook ISBN covers the Leanpub eBook distribution in all formats, available
through www.EffectOrientedProgramming.com.
Please purchase this book through www.EffectOrientedProgramming.com, to
support its continued maintenance and updates.
All rights reserved. Printed in the United States of America. This publication is
protected by copyright, and permission must be obtained from the publisher prior
to any prohibited reproduction, storage in a retrieval system, or transmission in any
form or by any means, electronic, mechanical, photocopying, recording, or likewise.
For information regarding permissions, see EffectOrientedProgramming.com.
Created in Crested Butte, Colorado, USA.
Text printed in the United States
Ebook: Version 1.0, Month Year
First printing Month Year
Cover design by Daniel Will-Harris, www.Will-Harris.com¹
¹http://www.Will-Harris.com
2
Copyright
Many of the designations used by manufacturers and sellers to distinguish their
products are claimed as trademarks. Where those designations appear in this book,
and the publisher was aware of a trademark claim, the designations are printed with
initial capital letters or in all capitals.
The Scala trademark belongs to {{???}}. Java is a trademark or registered trademark
of Oracle, Inc. in the United States and other countries. Windows is a registered
trademark of Microsoft Corporation in the United States and other countries. All
other product names and company names mentioned herein are the property of their
respective owners.
The authors and publisher have taken care in the preparation of this book, but make
no expressed or implied warranty of any kind and assume no responsibility for errors
or omissions. No liability is assumed for incidental or consequential damages in
connection with or arising out of the use of the information or programs contained
herein.
Visit us at www.EffectOrientedProgramming.com.
Source Code
All the source code for this book is available as copyrighted freeware, distributed
via Github². To ensure you have the most current version, this is the official code
distribution site. You may use this code in classroom and other educational situations
as long as you cite this book as the source.
The primary goal of this copyright is to ensure that the source of the code is properly
cited, and to prevent you from republishing the code without permission. (As long
as this book is cited, using examples from the book in most media is generally not a
problem.)
In each source-code file you find a reference to the following copyright notice:
²https://github.com/EffectOrientedProgramming/EOPCode
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
3
Copyright
// Copyright.txt
This computer source code is Copyright ©2021 MindView LLC.
All Rights Reserved.
Permission to use, copy, modify, and distribute this
computer source code (Source Code) and its documentation
without fee and without a written agreement for the
purposes set forth below is hereby granted, provided that
the above copyright notice, this paragraph and the
following five numbered paragraphs appear in all copies.
1. Permission is granted to compile the Source Code and to
include the compiled code, in executable format only, in
personal and commercial software programs.
2. Permission is granted to use the Source Code without
modification in classroom situations, including in
presentation materials, provided that the book "Effect Oriented
Programming" is cited as the origin.
3. Permission to incorporate the Source Code into printed
media may be obtained by contacting:
MindView LLC, PO Box 969, Crested Butte, CO 81224
MindViewInc@gmail.com
4. The Source Code and documentation are copyrighted by
MindView LLC. The Source code is provided without express
or implied warranty of any kind, including any implied
warranty of merchantability, fitness for a particular
purpose or non-infringement. MindView LLC does not
warrant that the operation of any program that includes the
Source Code will be uninterrupted or error-free. MindView
LLC makes no representation about the suitability of the
Source Code or of any software that includes the Source
Code for any purpose. The entire risk as to the quality
and performance of any program that includes the Source
Code is with the user of the Source Code. The user
understands that the Source Code was developed for research
and instructional purposes and is advised not to rely
exclusively for any reason on the Source Code or any
program that includes the Source Code. Should the Source
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
4
Copyright
Code or any resulting software prove defective, the user
assumes the cost of all necessary servicing, repair, or
correction.
5. IN NO EVENT SHALL MINDVIEW LLC, OR ITS PUBLISHER BE
LIABLE TO ANY PARTY UNDER ANY LEGAL THEORY FOR DIRECT,
INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
INCLUDING LOST PROFITS, BUSINESS INTERRUPTION, LOSS OF
BUSINESS INFORMATION, OR ANY OTHER PECUNIARY LOSS, OR FOR
PERSONAL INJURIES, ARISING OUT OF THE USE OF THIS SOURCE
CODE AND ITS DOCUMENTATION, OR ARISING OUT OF THE INABILITY
TO USE ANY RESULTING PROGRAM, EVEN IF MINDVIEW LLC, OR
ITS PUBLISHER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE. MINDVIEW LLC SPECIFICALLY DISCLAIMS ANY
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE SOURCE CODE AND DOCUMENTATION PROVIDED
HEREUNDER IS ON AN "AS IS" BASIS, WITHOUT ANY ACCOMPANYING
SERVICES FROM MINDVIEW LLC, AND MINDVIEW LLC HAS NO
OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
ENHANCEMENTS, OR MODIFICATIONS.
Please note that MindView LLC maintains a Web site which is
the sole distribution point for electronic copies of the
Source Code, where it is freely available under the terms
stated above:
https://github.com/EffectOrientedProgramming/EOPCode
If you think you've found an error in the Source Code,
please submit a correction at:
https://github.com/EffectOrientedProgramming/EOPCode/issues
You may use the code in your projects and in the classroom (including your
presentation materials) as long as the copyright notice that appears in each source
file is retained.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
5
Copyright
Edit This Chapter
Edit This Chapter³
³https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/00_Copyright.md
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Preface
Who is the book is for
This book uses Scala 3 but the focus is not on the language. The code examples should
be relatable to programmers who are familiar with: - Chaining operations on objects
("asdf ".trim.length) - Strong static typing - Functions that take parameters, can
be named, and referenced - TODO
This book also uses Scala ZIO, an effect-oriented library for Scala, but it does not go
into depth on ZIO. To learn ZIO, see: TODO
Code examples
The code examples are available at: TODO
The code in this book uses language syntax that might be unfamiliar.
• significant indentation
• colon syntax
Teaching With This Book
You may use the examples, exercises and solutions in classroom and other educational situations as long as you cite this book as the source. See the Copyright⁴ page
for further details.
The primary goal of the copyright is to ensure that the source of the code is properly
cited, and to prevent you from republishing the code without permission. As long
as this book is cited, using examples from the book in most media is generally not a
problem.
⁴\protect\char”007B\relax\protect\char”007B\relax???\protect\char”007D\relax\protect\char”007D\relax
7
Preface
Acknowledgements
Kit Langton, for being a kindred spirit in software interests and inspiring contributor
to the open source world.
Wyett Considine, for being an enthusiastic intern and initial audience.
Hali Frasure, for dozens of dinners and facilitating our book nights generally.
Edit This Chapter
Edit This Chapter⁵
⁵https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/01_Preface.md
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
What Are Effects
Introduction
TODO: Combine with “Reliability” ?
08:07 AM January 13, 2018
Televisions, Radios, and Cell Phones across Hawaii suddenly flash an alert:
“BALLISTIC MISSILE INBOUND THREAT TO HAWAII. SEEK IMMEDIATE
SHELTER. THIS IS NOT A DRILL”
Local communities sound alarms.
Calls to 911 jam the phone lines.
Panicked internet searches overwhelm data networks.
Students sprint from classrooms to fallout shelters.
Parents say goodbye to their children.
Untangling the Chaos
Thankfully, no missiles were launched that day.
During what should have been a quiet system test, an employee at the Hawaii
Emergency Management Agency accidentally pushed the wrong button. From the
Washington Post⁶:
“He clicked the button to send out an actual notification on Hawaii’s emergency
alert interface during what was intended to be a test of the state’s ballistic missile
preparations computer program.” The employee was prompted to choose between
the options “test missile alert” and “missile alert”, had selected the latter, initiating
the alert sent out across the state.
⁶https://www.washingtonpost.com/news/post-nation/wp/2018/01/14/hawaii-missile-alert-how-one-employeepushed-the-wrong-button-and-caused-a-wave-of-panic/
10
Introduction
Here is the system’s control screen:
This cluster of inconsistently named links increased the likelihood of mistakes. Basic
changes would drastically simplify proper use of the alerts. Imagine the earlier
mishaps that moved “False Alarm” to the top of the list.
We believe the system was doomed long before the interface was created. The fatal
flaw was that both “live” and “test” alerts were available in the running application.
A safe system makes these behaviors mutually exclusive.
The effects of this system were:
• Sending messages to Cell Phones
• Playing warnings on Radio frequencies
• Displaying banners on Television stations
The State of Software
There are many other examples of carefully-built software systems failing disastrously:
• The Ariane 5 rocket self-destructed on 4 June 1996 because of a malfunction in
the control software (the program tried to stuff a 64-bit number into a 16-bit
space).
• The American Northeast Power Blackout, August 14 2003.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
11
Introduction
• The NASA Mars Climate Orbiter, September 23, 1999. The orbiter was programmed for metric but ground control software used non-metric English.
The list goes on; just search for something like “Famous Software Failures” to see
more. And consider security- all the applications you use that are constantly being
updated with security patches. What about those that aren’t? Are they that good, or
is security being ignored?
How did things get so bad?
The Software Crisis
In the 70’s and 80’s, the idea of the Software Crisis emerged. This can be summarized
as: “We can’t create software fast enough.” One of the most popular attempts to solve
this problem was Structured Analysis & Design, which was a way to understand a
problem and design a solution using existing imperative languages.
The real problem that Structured Analysis & Design set out to solve was big
monolithic pieces of code. When one programmer was able to solve the entire
problem, the structure of the program didn’t matter as much. But as software needs
grew, this approach didn’t scale. In particular, you couldn’t finish a project more
quickly by adding more programmers, because there wasn’t a way to hand off
portions of a program to multiple programmers. To do that, teams needed some way
to break down the complexity of the program into individual functions—functions
that might someday be reused. This was seen as the reason for the Software Crisis.
Structured Analysis was an attempt to discover the individual functions in a program.
But it was a top-down approach, and it assumed these functions could be determined
before any code is written. Structured Analysis & Design continued the approach of
“big up-front design.” The analyst produced the structure, and then the programmers
implemented it.
Experienced programmers know that a design that cannot evolve during development is doomed to failure: both programmers and stakeholders learn things during
development. You discover much of your structure as you’re building the program,
and not on a whiteboard. Building a program reveals things you didn’t know were
important when you designed the solution.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
12
Introduction
From this book’s perspective, the most fundamental problem with Structured Analysis & Design was that it only paid lip service to the idea of reliability. There was
nothing about reliability truly integrated into Structured Analysis & Design.
Structured Analysis & Design was motivated by a business problem: “how do we
create software faster?” Virtually every language that came out in its aftermath
focused on development speed. Not reliability. So we produced a lot of languages
to quickly create unreliable software.
Reliability
A reliable system does not break. // TODO Discuss
If you’ve been programming for a while, this sounds far-fetched or even impossible.
Most existing languages are built for rapid development. You create a system as
quickly as possible, then begin isolating areas of failure, finding and fixing bugs until
the system is tolerable and can be delivered. Throughout the lifetime of the system,
bugs are regularly discovered and fixed. There is no realistic expectation that you will
ever achieve a completely bug-free system, just one that seems to work well enough
to meet the requirements. This is the reality programmers have come to accept.
If each piece of a traditional system is unreliable, when you combine these pieces
you get a multiplicative effect – the resulting parts are significantly less reliable than
their component pieces.
What if we could change our thinking around the problem of building software
systems? Imagine building small pieces that can each be reasoned about and made
rock-solid. Now suppose there is a way to combine these reliable pieces to make
bigger parts that are just as reliable. Each time you combine smaller parts to create a
bigger part, the result inherits the reliability of its components. Instead of multiplying
unreliability, you maintain reliability. The resulting system is as reliable as any of
its components.
This is what functional programming together with effects management can achieve.
This is what we want to teach you in this book.
The biggest impact on you as a programmer is the requirement for patience. With
most languages, the first thing you want to do is figure out how to write “Hello,
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
13
Introduction
World!”, then start accumulating the other language features as standalone concepts.
In functional programming we start by examining the impact of each concept on
reliability. We then combine the smaller concepts, ensuring reliability at each step.
A reliable system isolates parts that are always the same (pure functions) from the
parts that can change (effects). This mathematical rigor produces a reliable system.
Some aspects of writing code in this style might seem onerous. Most of us are used
to the more immediate feedback and satisfaction of getting something working, so
this can be challenging. But would you rather create an unreliable system quickly?
We assume you are reading this book because you do not.
The Effect is not what happens on the external system because there is no way to
know the actual impact of what the program caused by the communication.
Dealing with unpredictability
Any real program has to interact with things outside the programmer’s control.
All external systems are unpredictable.
One of the biggest challenges in building systems that are more predictable / reliable
/ ??? is to isolate and manage the unpredictable parts.
An approach that programmers may use to handle this is to delineate the parts of the
program which use external systems.
By delineating them, programmers then have tools to handle the unpredictable parts
in more predictable ways.
The interactions with external systems can be defined in terms of “Effects” which
create a delineation between the parts of a program that interact with external
systems and those that don’t.
For example, a program that displays the current date requires something that
actually knows the current date.
The program needs to talk to an external system (maybe just the operating system)
to get this information.
These programs are unpredictable because the programmer has no control over what
that external system will say or do.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
14
Introduction
Effect Oriented systems allow us to apply strategies to mitigate the unpredictability
of using external systems.
Effects can not be un-done
Once a program has communicated with an external system, (i.e. executed an Effect),
everything that happens on that external systems is out of the program’s control.
(analogies on human communication) Imagine that a friend recently stayed in your
home. 3 days after they leave, you realize that you are missing some money that
had been stored in the guestroom. Now you have a dilemma - do you ask them if
they took the money? Simply by asking, you could permanently change, or even end,
your relationship with this person. They could immediately admit fault, and ask for
forgiveness. Now you know that they are capable of stealing from you - will you
ever trust them in your home again? They could angrily deny the accusation, and
resent you for making it. Or the conversation could go in a million different other
ways that are impossible to predict. We know one thing for certain - you will never
be able to un-ask that question. Even if you ultimately grow closer with this person
after navigating this situation, you can’t go back to a world where you never asked.
Regardless of any apology and forgiveness, your relationship is now different.
What is an Effect?
An effect is an interaction with the world outside your CPU. An application might
generate any number of effects, which fall into two categories:
• Observing the World
• Changing the World
Effects cannot be undone. If you 3D-print a figurine, you cannot reclaim that material.
Once you send a Tweet, you can delete it but people might have already read it. Even
if you provide database DELETE statements paired with INSERT statements, it must
still be considered effectful: Another program might read your data before you delete
it, or a database trigger might activate during an INSERT.
TODO {{Explain: Optionality, Asynchronicity, Blocking – In a later chapter. }}
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
15
Introduction
Observing the World
Observation can be very basic:
• Accepting user input from the console
• Getting the current time from the system clock
• Taking the output of a random number generator
Observations can also be complex and domain-specific:
• Sensing slippage in an anti-lock braking system
• Getting the current price of a stock
• Detecting the current from a pacemaker
• Checking the temperature of a nuclear reactor
We explore similar scenarios throughout the book.
Changing the World
Just as with observations, changes can be basic:
• Displaying on the console
• Writing to a file
• Mutating a variable
• Saving to a database
They can be advanced:
• 3D printing a model
• Triggering an alarm
• Stabilizing an airplane
• Detonating explosives
• ZIOs are not their result.
Effects Defined as Data
TODO
One approach to defining effects is…
The effects have not been executed when defined.
A common mistake when starting with ZIO is trying to return ZIO instances
themselves rather than their result.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
16
Introduction
println(Random.nextInt)
// Stateful(repl.MdocSession.MdocApp.res0(02_What_Are_Effects.md:8),zio\
.FiberRef$unsafe$$anon$2$$Lambda$14980/0x0000000803cea840@3701ae45)
We will not see a random number printed out; we see some inscrutable type
information.
This is a mistake because ZIO’s are not their result, they are descriptions of effects
that produce the result. The ZIO instance only describes something to be done.
ZIOs are not automatically executed. To actually run a ZIO, your program must take
the data types and interpret / run them, executing the logic . The user must determine
when/where that happens.
Consider the Option type in the standard library. An Option might have a value
inside of it, but you can’t safely assume that it does. Similarly, a ZIO might produce
a value, but you have to run it to find out.
You can think of them as recipes for producing a value. You don’t want to return a
recipe from a function, you can only return a value. If it is your friend’s birthday,
they want a cake, not a list of instructions about mixing ingredients and baking.
The Interpreter
Scala compiles code to JVM bytecodes, An interpreter steps through and executes
your code, much like the JVM interprets JVM bytecodes. The interpreter is the
hidden piece understands so much more about the meaning of your code. This
includes the ability to decide what to run concurrently and how to invisibly tune
that concurrency–all at runtime. The interpreter is responsible for deciding when to
context-switch between tasks, and is able to do this because it understands the ZIO
code that it’s executing.
The interpreter is also the mechanism that evaluates the various effects described in
the generic type parameters for each ZIO object.
The reason we have the defer directive(method?) in zio-direct is to indicate that this
code will be evaluated by the interpreter later.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
17
Introduction
Edit This Chapter
Edit This Chapter⁷
⁷https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/02_What_Are_Effects.md
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Superpowers with Effects
TODO: A couple sentences about the superpowers
Building a Resilient Process in stages
We want to evolve our process from a simple happy path to a more resilient process.
Progressive enhancement through adding capabilities
TODO: Is “Step” necessary? Some other word?
Step 1: Happy Path
runDemo:
saveUser:
"mrsdavis"
// User saved
Step 2: Provide Error Fallback Value
object DatabaseError
object TimeoutError
Superpowers with Effects
runDemo:
saveUser:
"Robert'); DROP TABLE USERS"
.orElseFail:
"ERROR: User could not be saved"
// DatabaseError
// ERROR: User could not be saved
Step 3: Retry Upon Failure
import zio.Schedule.{recurs, spaced}
val aFewTimes =
// TODO Restore original spacing when done
// editing
// recurs(3) && spaced(1.second)
recurs(3) && spaced(1.millis)
runDemo:
saveUser:
"morty"
.retry:
aFewTimes
.orElseSucceed:
"ERROR: User could not be saved"
// DatabaseError
// DatabaseError
// User saved
Step 4: Fallback after multiple failures
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
19
Superpowers with Effects
runDemo:
saveUser:
"morty"
.retry:
aFewTimes
.orElseSucceed:
"ERROR: User could not be saved"
// DatabaseError
// DatabaseError
// DatabaseError
// DatabaseError
// ERROR: User could not be saved
Step 5: Timeouts
// TODO Restore real value when done editing
val timeLimit = 5.millis
// timeLimit: Duration = PT0.005S
// 5.seconds
// first is slow - with timeout and retry
runDemo:
saveUser:
"morty"
.timeoutFail(TimeoutError)(timeLimit)
.retry:
aFewTimes
.orElseSucceed:
"ERROR: User could not be saved"
// Interrupting slow request
// Database Timeout
// User saved
Step 6: Fallback Effect
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
20
Superpowers with Effects
// fails - with retry and fallback
runDemo:
saveUser:
"morty"
.timeoutFail(TimeoutError)(timeLimit)
.retry:
aFewTimes
.orElse:
sendToManualQueue:
"morty"
.orElseSucceed: // TODO Delete?
"ERROR: User could not be saved, even to the fallback system"
// DatabaseError
// DatabaseError
// DatabaseError
// DatabaseError
// User sent to manual setup queue
Step 7: Concurrently Execute Effect
TODO Consider deleting. Uses an extension
// concurrently save & send analytics
runDemo:
saveUser:
"morty"
// todo: maybe this hidden extension method
// goes too far with functionality that
// doesn't really exist
// TODO Should we fireAndForget before the
// retries/fallbacks?
.fireAndForget:
userSignupInitiated:
"morty"
.timeoutFail(TimeoutError)(timeLimit)
.retry:
aFewTimes
.orElse:
sendToManualQueue:
"morty"
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
21
Superpowers with Effects
.orElseSucceed:
"ERROR: User could not be saved"
// User saved
Step 8: Ignore failures in Concurrent Effect
Feeling a bit “meh” about this step.
// concurrently save & send analytics, ignoring analytics failures
runDemo:
// TODO Consider how to dedup strings
saveUser:
"mrsdavis"
.timeoutFail(TimeoutError)(timeLimit)
.retry:
aFewTimes
.orElse:
sendToManualQueue:
"mrsdavis"
.tapBoth(
error =>
userSignUpFailed("mrsdavis", error),
success =>
userSignupSucceeded("mrsdavis", success)
)
.orElseSucceed:
"ERROR: User could not be saved"
// Analytics sent for signup completion
// User saved
Step 9: Rate Limit TODO
TODO Slot these in:
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
22
Superpowers with Effects
23
Time
#### Measuring Time Since there is already a .timed method available directly on
ZIO instances, it might seem redundant to have a timed TestAspect. However, they
are distinct enough to justify their existence. ZIOs .timed methods changes the result
type of your code by adding the duration to a tuple in the result. This is useful, but
requires the calling code to handle this new result type. TestAspect.timed is a
non-invasive way to measure the duration of a test. The timing information will be
managed behind the scenes, and printed in the test output, without changing any
other behavior.
Restricting Time
Sometimes, it’s not enough to simply track the time that a test takes. If you have
specific Service Level Agreements (SLAs) that you need to meet, you want your
tests to help ensure that you are meeting them. However, even if you don’t have
contracts bearing down on you, there are still good reasons to ensure that your tests
complete in a timely manner. Services like GitHub Actions will automatically cancel
your build if it takes too long, but this only happens at a very coarse level. It simply
kills the job and won’t actually help you find the specific test responsible.
A common technique is to define a base test class for your project that all of your
tests extend. In this class, you can set a default upper limit on test duration. When a
test violates this limit, it will fail with a helpful error message.
This helps you to identify tests that have completely locked up, or are taking an
unreasonable amount of time to complete.
For example, if you are running your tests in a CI/CD pipeline, you want to ensure
that your tests complete quickly, so that you can get feedback as soon as possible. you
can use TestAspect.timeout to ensure that your tests complete within a certain
time frame.
Flakiness
Commonly, as a project grows, the supporting tests become more and more flaky.
This can be caused by a number of factors:
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Superpowers with Effects
24
• The code is using shared, live services Shared resources, such as a database or
a file system, might be altered by other processes. These could be other tests in
the project, or even unrelated processes running on the same machine.
• The code is not thread safe Other processes running simultaneously might alter
the expected state of the system.
• Resource limitations A team of engineers might be able to successfully run the
entire test suite on their personal machines. However, the CI/CD system might
not have enough resources to run the tests triggered by everyone pushing to
the repository. Your tests might be occasionally failing due to timeouts or lack
of memory.
Edit This Chapter
Edit This Chapter⁸
⁸https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/03_Superpowers.md
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional
TODO: Combine with “What are Effects” ?
In your journey to this book, you might have already learned one way to
think about programming, and possibly several.
You might have succeeded creating a project using that approach.
You might also have encountered programming constructs inspired by functional
programming.
For example, many languages have introduced lambdas along with support for
streams and functional operations like map.
Some languages allow functions to be - be created anonymously - stored in variables
- passed as parameters to other functions - returned from other functions
C++, especially more recent versions, has added a number of features that support
functional-style programming.
If, however, you are coming from an imperative programming background, these
functional-style devices can seem arbitrarily complicated. Why go through the
trouble of an effect-oriented API when plain vars, for loops, and :Unit exist?
Sometimes it seems like functional programmers write code like this just to be fancy.
To understand what’s really behind this different way of thinking about programming, we will start with some history.
A Different Goal
In the early days of programming, most code was written in an assembly language
for a particular machine.
Why Functional
26
Assembly language had primitive function-like constructs called subroutines, but
they were so much work to set up and safely use that programmers often just wrote
the code inline.
Even if you wanted to create reusable code, it was often easier to just “goto” a piece
of code and use global variables, rather than bothering with passing arguments and
returning results.
In those early days, a “high level language” meant a language like C that passed
arguments and returned results for you. This suddenly made writing functions much
easier, safer, and faster.
But the habits of assembly-language programmers didn’t vanish overnight, and
people were still prone to writing obtuse monolithic programs —often a single
function for the whole program— and jumping around in their code using gotos.
Many programs were written and maintained by a small number of programmers,
or even a single programmer, for whom that code made sense. Anyone else reading
the code would be baffled.
In addition, the idea of calling code written by other people was fairly foreign. If
you didn’t write the code yourself, how would you know if it does what you want?
Documentation and testing were also primitive, if they existed at all. Thus “code
reuse” was a big goal. There were many attempts within companies to create cultures
of code reuse, because many programmers were rewriting the same functionality
from scratch, over and over within the same organization.
Monolithic programs that didn’t reuse code were also a maintenance nightmare. It
was not uncommon for such programs to be thrown away and rewritten just to add
some new features. Not surprisingly, writing everything from scratch also took a lot
longer than reusing common functionality. The new question became: “how do we
make code reuse easier?”
Languages like C and Pascal made it easy to both write and call functions. They
were a big improvement over assembly language, However, the monolithic habits
of assembly-language programmers persisted into those new languages. Libraries
grew bigger and more complex, and using those libraries was not easy. In C, for
example, you would often have to allocate memory before calling a function, and
then remember to release the memory after that function returned.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional
27
You also had to learn how to pass information from one library function to another.
You had to learn how each library reported errors, which typically varied in strategy
from one library to the next. Code could be reused, but it wasn’t easy.
At this point, object-oriented programming brought several good ideas to the world.
It allowed you to package data structures with automatic initialization and cleanup,
with all the functions that act upon that data.
If you wanted to reuse some code, you created an object with some initialization
values, then sent messages to that object to produce the desired results. This did
make code reuse easier, and helped speed up program creation. It also came with the
distraction of inheritance polymorphism. This birthed an entire education and consulting industry explaining how to cram every design into an inheritance hierarchy
Inheritance polymorphism does sometimes prove useful, but not everywhere, all the
time.
C++ added object-oriented features from the Simula language while maintaining
backward compatibility with the C language. C++ had a strong emphasis on static
type checking. Java was created as a counterpoint to C++ and was heavily inspired
by the Smalltalk language.
Smalltalk’s success came from its ability to rapidly create systems by adding
functionality to existing objects. This introduced a conundrum, because Smalltalk
is a dynamic language, and Java, like C++, is statically typed. Smalltalk can be
thought of as supporting an experimental style of programming: you send a message
to an object and discover at runtime whether the object knows what to do with
that message. But C++ and Java ensure everything is valid, at compile time (along
with escape mechanisms that effectively disable that type checking). This conundrum
is exemplified by the Liskov Substitution Principle, which says that you shouldn’t
add new methods to an inherited type — and yet that activity is the foundation of
Smalltalk.
The Agile methodologies that began in the early 2000’s were another attempt to
produce software faster, but through a more bottom-up lens. Agile was primarily
focused on improving communication between stakeholders and developers, and
producing more rapid round trips between needs and experiments. This improves the
chance that the stakeholders will get what they need, faster. Agile helped the process
of software development, but again, the focus is on developing software quickly, not
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional
28
on developing reliable software.
The most important thing to take away from this language history is that the
fundamental goal of the various techniques was speed of creation. There seems to be
an underlying assumption that these approaches will somehow automatically create
more reliable software. As a result, we have languages that quickly create unreliable
software. And in many cases we’ve been able to get by with that. For one thing,
this approach has greatly advanced testing technology, because it was necessary.
Customers have learned to put up with buggy software. They’ve often been willing
to accept buggy software when the alternative is no software at all.
The world has changed. Back then, the drive was to speed up activities that humans
were doing. Those humans could compensate for bugs. Now, however, more and
more software is doing things that humans can’t do, so failures in software cannot
be propped up by humans. Unreliable software is no longer an inconvenience, but a
serious problem.
Quickly creating unreliable software is no longer acceptable. We must delve into
the reasons that software fails —either it doesn’t do what it’s supposed to, or it just
breaks.
Reuse
How do we create software? When you first learned to program, you probably solved
problems by writing code using the basic constructs of your language. But at some
point you began realizing that you could only produce and debug so much code
by yourself. If you could use code that was already written and debugged by other
people, you could produce solutions faster.
You might have gone through a cut-and-paste phase before discovering that formalized libraries were easier and more reliable. Even then, library ease of use depended
on the sophistication of your language. For the reasons mentioned, using a C library
could be tricky and difficult. C++ made this much easier and paved the way for
the acceptance of languages like Java, Python, Scala, and Kotlin. Indeed, any new
language that doesn’t support easy code reuse is not taken seriously.
But code reuse in object-oriented languages was still limited. You could either use
objects in a library directly, or you could add those library classes into new classes
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional
29
using composition. This was a big step, and it helped a lot. In contrast, composing C
libraries wasn’t particularly realistic —it was just too messy and complicated.
The problem is reliability. If you create a new class using composition, you combine
problems with the existing class(es) with any bugs you introduced in your new class.
As you build up bigger systems, the problem of bugs multiplies.
To compose systems rapidly and reliably, we return to first principles and figure out
how to:
1. Create basic components that are completely reliable.
2. Combine those components in a way that does not introduce new bugs.
To achieve these goals we must examine the fundamentals of how we think about
software.
Pure Functions
Composition in an object-oriented language doesn’t attempt to manage bugs, so it
ends up amplifying them. If we want to compose pieces of software, we must discover
what creates a fundamentally unbreakable piece, then how to assemble those pieces
without producing a broken result.
First, what constitutes a reliable,unbreakable piece of software? We’ve already seen
that objects are not inherently unbreakable, so we’ll move back to a more basic
software component: the function. What are the characteristics of an unbreakable
function?
What we want is the same kind of function we have in math. This means that the
function does nothing except produce a result from its arguments. And given the
same arguments, it always produces the same result.
This behavior imposes additional constraints: The function cannot affect its environment, and the environment cannot affect the function —otherwise, the function
has a history and behaves differently at one point in time vs. another. Running that
function doesn’t necessarily produce the same results from one call to the next.
If a function affects its environment, we call that a side effect. It’s “on the side”
because it’s something other than just producing a result from the function. Many
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional
30
programming languages have side effects built in, in the form of statements. A
statement doesn’t return a result, so the only reason to execute a statement is for
its side effect. For example, “print” is typically a statement that returns nothing
but causes the side effect of displaying text on a console. On the other hand, an
expression produces (“expresses”) a result. A functional language avoids statements
and attempts to make everything an expression that produces a result.
What about the environment affecting the function? This is a bit more subtle, and
requires that we think in more general terms than just basic imperative programming.
In particular, we must consider concurrency. If multiple tasks are running in our
program, then at any point another task might see variables in our function. A
variable can change, so that means this other task might see different values at
different points in the function’s execution. And if that variable is modified by some
other task, we have no way of predicting the result, and we don’t get the reliable
mathematical function that we want.
We solve this problem through immutability. That is, instead of using variables, we
create values that cannot change. This way, it doesn’t matter if an external task sees
our values, because it will only see that one value and not something that is different
from one moment to the next. And the external task cannot change the value and
cause the function to produce a different result.
Functions that behave mathematically, that always produce the same results from
the same inputs and have no side effects, are called pure functions. When we add the
additional constraint of immutability, we produce functions that compose without
introducing points of breakage. We can reliably reason about such functions.
Composability
TODO Reconsider this formal math style. It doesn’t match our other examples, where
we really stick to Scala code
If functions g and h are pure, we should be able to combine them directly (assuming
the types agree) to produce a new function f:
f(a) = g(h(a))
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional
31
This assumes that all functions involved are complete, meaning that they produce a
legitimate result for every possible value of a. This is not always true. For example,
dividing a number by zero is undefined, and so cannot produce a reasonable number
as a result. Using a key to look up a value in a map is undefined if that key doesn’t
exist in the map. <– TODO Weird wording
An incomplete function requires more operations when using it, to handle the
problematic inputs. You can think of the solution as stepwise composability. Instead
of calling g(h(a)), we break the process into steps: x = h(a), then check the
success of the operation. If successful, pass the result to g. These extra steps make
composability sound like it could get tedious, and languages like Scala that provide
more thorough support for functional programming provide syntax to make this kind
of programming feasible.
Effects
Now we have created this perfect world of pure functions that behave just like
the functions in theoretical mathematics. They have no side effects and cannot be
affected by other functions, and can be neatly and safely composed.
“But,” you wonder, “if all I can do with the result of one pure function is pass it as an
argument to another pure function, what’s the point of all these pure function calls?
If these functions have no effect on the world, they seem like an intellectual exercise
that merely heats up the CPU.”
This is absolutely true. A program that never affects the world is pointless. For a
program to be useful, it must be affected by the world, and it must have effects upon
the world.
The phrase “side effect” implies an incidental or accidental impact on the world. What
we need to do is formalize this idea and bring it under our control. We can then call
it simply an “effect,” without the “side.” The solution is to manage these effects, so
they are under our control.
This bridge between pure functions and practical programs with controlled and
managed effects is the reason for the title of this book.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional
32
Core Differences Between OO and Functional
An OO language worries about managing state. It “encapsulates” a data structure in
privacy and surrounds it with custom methods (aka member functions) which are
ideally the only way to access and modify the state of that data structure. This is
important because an OO data structure is typically mutable. This OO ceremony
attempts to create predictability by knowing how the data structure can be mutated.
Functional programming abstracts common behavior into reusable functional components. These components are adapted to specific needs using other functions. This
is why lambdas are so important, because you constantly need to adapt general code
to specific purposes, They enable concise code that would otherwise be awkward and
intrusive to right as a standalone function.
Functions in a functional language don’t need to be tied to a particular data structure.
Thus, they can often be written for more general use and to reduce duplication.
Functional languages come with a general set of well-tested, reusable operations that
can be applied in many situations.
A functional language relies on immutability. An immutable data structure doesn’t
need privacy because it is safe for any task to read, and it cannot be written (only
initialized). Ojects in functional languages are simply naked data structures along
with constructors. When everything is immutable, there is no need for private
properties or methods to maintain the state of an object.
Summary: Style vs Substance
Functional programming abstracts common behavior into reusable functional components. These components are adapted to specific needs using other functions. This
is why lambdas are so important, because you constantly need to adapt general code
to specific purposes, often with a brief amount of code that would otherwise be
awkward and intrusive to write as a standalone function.
The two things we do with functions is compose them to make more complex
functions, and adapt to them to our specific problem.
However, there could be significantly more than:
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional
33
• a function’s ability to create other functions
• transforming elements in a collection using map
Those are indeed important benefits, but they just dip into the possibilities. Adopting
some of the styles found in functional programming does not make a language
functional.
In this book we want to get to the heart of what it means to be functional. In particular,
we want to show what it takes to make reliable functional code that can be composed
without propagating or amplifying flaws in its components.
Edit This Chapter
Edit This Chapter⁹
⁹https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/04_Why_Functional.md
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Configuration
1. Application startup uses the same tools that you utilize for the rest of your
application
General/Historic discussion
One reason to modularize an application into “parts” is that the relationship between
the parts can be expressed and also changed depending on the needs for a given
execution path.
Typically, this approach to breaking things into parts and expressing what they need,
is called “Dependency Injection.”
… Why is it called “Dependency Injection” ? Avoid having to explicitly pass things
down the call chain.
There is one way to express dependencies.
Let’s consider an example: We want to write a function that fetches Accounts from a
database The necessary parts might be a DatabaseService which provides database
connections and a UserService which provides the access controls. By separating
these dependencies our from the functionality of fetching accounts, tests can “fake”
or “mock” the dependencies to simulate the actual dependency.
In the world of Java these dependent parts are usually expressed through annotations
(e.g. @Autowired in Spring). But these approaches are “impure” (require mutability),
often rely on runtime magic (e.g. reflection), and require everything that uses the
annotations to be created through a Dependency Injection manager, complicating
construction flow.
An alternative to this approach is to use “Constructor Injection” which avoids some
of the pitfalls associated with “Field Injection” but doesn’t resolve some of the
underlying issues, including the ability for dependencies to be expressed at compile
time.
Configuration
35
If instead functionality expressed its dependencies through the type system, the
compiler could verify that the needed parts are in-fact available given a particular
path of execution (e.g. main app, test suite one, test suite two).
What ZIO Provides Us.
With ZIO’s approach to dependencies, you get many desirable characteristics at
compile-time, using standard language features. Your services are defined as classes
with constructor arguments, just as in any vanilla Scala application. No annotations
that kick off impenetrable wiring logic outside your normal code.
For any given service in your application, you define what it needs in order to execute.
Finally, when it is time to build your application, all of these pieces can be provided in
one, flat space. Each component will automatically find its dependencies, and make
itself available to other components that need it.
To aid further in understanding your application architecture, you can visualize the
dependency graph with a single line.
You can also do things that simply are not possible in other approaches, such as
sharing a single instance of a dependency across multiple test classes, or even
multiple applications.
DI-Wow!
TODO Values to convey: - Layer Graph - Cycles are a compile error - Visualization
with Mermaid - test implementations - Layer Resourcefulness - Layers can have setup
& teardown (open & close)
TODO Rewrite this without
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Configuration
36
// Explain private constructor approach
case class Dough():
val letRise =
ZIO.debug:
"Dough is rising"
object Dough:
val fresh =
ZLayer
.derive[Dough]
.tapWithMessage("Making Fresh Dough")
Step 1: Effects can express dependencies
Effects can’t be run until their dependencies have been fulfilled
TODO: Decide what to do about the compiler error differences between these
approaches
TODO: Can we avoid the .provide() and still get a good compile
error
in
mdoc
scala runDemo: ZIO.serviceWithZIO[Dough](_-
.letRise) .provide() // error: // // // ──── ZLAYER ERROR
──────────────────────────────────────────────────── //
// Please provide a layer for the following type: // // 1.
repl.MdocSession.MdocApp.Dough // // ───────────────────────────────────────
// //
Step 2: Provide Dependency Layers to Effects
Then the effect can be run.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Configuration
37
runDemo:
ZIO.serviceWithZIO[Dough]:
_.letRise
.provide:
Dough.fresh
// Making Fresh Dough
// Dough is rising
// ()
For code organization, and legibility at call sites, we are defining several layers within
the Heat companion object. They will all be used soon.
case class Heat()
object Heat:
val oven =
ZLayer
.derive[Heat]
.tapWithMessage:
"Heating Oven"
val toaster =
ZLayer
.derive[Heat]
.tapWithMessage:
"Heating Toaster"
val broken =
ZLayer.fail:
"**Power Out**"
Step 3: Effects can require multiple
dependencies
Note: The following is copy&pasted and might just need a slight diversion
to &’d typed parameters
Intersections AKA Products AKA Case Classes AKA Ands
The requirements for each ZIO operation are combined as an anonymous product
type denoted by the & symbol.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Configuration
38
// TODO Restore private constructor after failure scenario is dialed in
// TODO Can we make Bread a trait,
//
Then we would have BreadHomemade & BreadStoreBought
trait Bread:
val eat =
ZIO.debug:
"Eating bread!"
case class BreadHomeMade(heat: Heat, dough: Dough) extends Bread
// TODO Move StoreBought further down?
case class BreadStoreBought() extends Bread
object Bread:
// TODO Explain ZLayer.fromZIO in prose
// immediately before/after this
val homemade =
ZLayer
.derive[BreadHomeMade]
.tapWithMessage:
"Making Homemade Bread"
val storeBought =
ZLayer
.derive[BreadStoreBought]
.tapWithMessage:
"Buying Bread"
end Bread
runDemo:
Bread
.homemade
.build
.provide(
Dough.fresh,
Heat.oven,
Scope.default
)
// ZEnvironment(MdocSession::MdocApp::BreadHomeMa
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Configuration
39
Step 4: Dependencies can “automatically”
assemble to fulfill the needs of an effect
Something around how like typical DI, the “graph” of dependencies gets resolved
“for you” This typically happens in some completely new/custom phase, that does
follow standard code paths. Dependencies on effects propagate to effects which use
effects.
// TODO Figure out why Bread.eat debug isn't showing up
runDemo:
ZIO.serviceWithZIO[Bread]:
_.eat
.provide(
// Highlight that homemade needs the other
// dependencies.
Bread.homemade,
Dough.fresh,
Heat.oven
)
// ()
Step 5: Different effects can require the same
dependency
Eventually, we grow tired of eating plain Bread and decide to start making Toast.
Both of these processes require Heat.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Configuration
40
// Is it worth the complexity of making this private?
// It would keep people from creating Toasts without using the make met\
hod
case class Toast (heat: Heat, bread: Bread)
object Toast:
val make =
ZLayer.derive[Toast]
.tapWithMessage("Making Toast")
It is possible to also use the oven to provide Heat to make the Toast.
TODO Update refs here The dependencies are based on the type, so in this case both
Toast.make and Bread.make require heat, but
Notice - Even though we provide the same dependencies in this example, Heat.oven
is also required by Toast.make
runDemo:
ZLayer.make[Toast](
Toast.make,
Bread.homemade,
Dough.fresh,
Heat.oven
).build
// ZEnvironment(MdocSession::MdocApp::Toast -> To
However, the oven uses a lot of energy to make Toast. It would be great if we can
instead use our dedicated toaster!
Step 6: Dependencies must be fulfilled by
unique types
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Configuration
41
runDemo:
ZLayer.make[Toast](
Toast
.make,
Dough.fresh,
Bread.homemade,
Heat.oven,
Heat.toaster
).build
// error:
//
//
// ──── ZLAYER ERROR ──────────────────────────────────────────────────\
──
//
// Ambiguous layers! I cannot decide which to use.
// You have provided more than one layer for the following type:
//
//
repl.MdocSession.MdocApp.Heat is provided by:
//
1. Heat.oven
//
2. Heat.toaster
//
// ────────────────────────────────────────────────────────────────────\
──
//
//
Unfortunately our program is now ambiguous. It cannot decide if we should be
making Toast in the oven, Bread in the toaster, or any other combination.
Step 7: Providing Dependencies at Different
Levels
This enables other effects that use them to provide their own dependencies of the
same type
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Configuration
runDemo:
val bread =
ZLayer.make[Bread](
Bread.homemade,
Dough.fresh,
Heat.oven
)
ZLayer.make[Toast](
Toast.make,
bread,
Heat.toaster
).build
// ZEnvironment(MdocSession::MdocApp::Toast -> To
Step 8: Dependencies can fail
TODO Explain .build before using it to demo layer construction
Bread2.fromFriend: ZLayer[Any, String, Bread]
runDemo:
ZIO.serviceWithZIO[Bread]:
_.eat
.provide:
Bread2.fromFriend
// **Power out**
// **Power out Rez**
Step 9: Dependency Retries
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
42
Configuration
runDemo:
val bread =
Bread2
.fromFriend
.retry:
Schedule.recurs:
3
ZIO.serviceWithZIO[Bread]:
_.eat
.provide:
bread
// **Power out**
// **Power out**
// Power is on
// ()
Step 10: Fallback Dependencies
runDemo:
val bread =
Bread2
.fromFriend
.orElse:
Bread.storeBought
ZLayer.make[Toast](
Toast.make,
bread,
Heat.toaster
).build
// **Power out**
// ZEnvironment(MdocSession::MdocApp::Toast -> To
Step 11: Layer Retry + Fallback?
Maybe retry on the ZLayer eg. (BreadDough.rancid, Heat.brokenFor10Seconds)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
43
Configuration
44
runDemo:
Bread2
.fromFriend
.retry:
Schedule.recurs:
1
.orElse:
Bread.storeBought
.build // TODO Stop using build, if possible
.debug
// **Power out**
// **Power out**
// ZEnvironment(MdocSession::MdocApp::BreadStoreB
Changing things based on the running environment.
• CLI Params
• Config Files
• Environment Variables
Testing Effects
Effects need access to external systems thus are unpredictable.
Tests are ideally predictable so how do we write tests for effects that are predictable?
With ZIO we can replace the external systems with predictable ones when running
our tests.
With ZIO Test we can use predictable replacements for the standard systems effects
(Clock, Random, Console, etc).
Random
An example of this is Random numbers. Randomness is inherently unpredictable. But
in ZIO Test, without changing our Effects we can change the underlying systems with
something predictable:
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Configuration
import zio.test.{TestRandom, TestAspect, assertCompletes}
val coinToss =
defer:
if (Random.nextBoolean.run)
ZIO
.debug:
"R: Heads."
.run
else
ZIO
.fail:
"Tails encountered. Ending performance."
.run
val rosencrantzAndGuildensternAreDead =
defer:
TestRandom
.feedBooleans(Seq.fill(8)(true) *)
.run
ZIO
.debug:
"*Performance Begins*"
.run
coinToss.repeatN(4).run
ZIO
.debug:
"G: There is an art to building suspense."
.run
coinToss.run
ZIO
.debug:
"G: Though it can be done by luck alone."
.run
coinToss.run
ZIO
.debug:
"G: ...probability"
.run
coinToss.run
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
45
Configuration
46
runSpec:
defer:
rosencrantzAndGuildensternAreDead.run
assertCompletes
// Test: PASSED*
val spec =
defer:
rosencrantzAndGuildensternAreDead.run
assertCompletes
// todo: maybe we can change runSpec so that this spec structure matche\
s the previous
runSpec(spec, TestAspect.withLiveRandom, TestAspect.eventually)
// Test: PASSED*
The Random Effect uses an injected something which when running the ZIO uses
the system’s unpredictable random number generator. In ZIO Test the Random
Effect uses a different something which can predictably generate “random” numbers.
TestRandom provides a way to define what those numbers are. This example feeds
in the Ints 1 and 2 so the first time we ask for a random number we get 1 and the
second time we get 2.
Anything an effect needs (from the system or the environment) can be substituted
in tests for something predictable. For example, an effect that fetches users from a
database can be simulated with a predictable set of users instead of having to setup
a test database with predictable users.
When your program treats randomness as an effect, testing unusual scenarios
becomes straightforward. You can preload “Random” data that will result in deterministic behavior. ZIO gives you built-in methods to support this.
Time
Even time can be simulated as using the clock is an effect.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Configuration
47
import zio.test.*
runSpec:
val slowOperation =
ZIO.sleep:
2.seconds
defer:
val fork =
slowOperation
.timeout:
1.second
.fork
.run
TestClock
.adjust:
2.seconds
.run
val result = fork.join.run
assertTrue:
result.isEmpty
// Test: PASSED*
By default in ZIO Test, the clock does not change unless instructed to. Calling a time
based effect like timeout would hang indefinitely with a warning like: Warning:
A test is using time, but is not advancing the test clock, which
may result in the test hanging. Use TestClock.adjust to manually
advance the time.
To test time based effects we need to fork those effects so that then we can adjust
the clock. After adjusting the clock, we can then join the effect where in this case
the timeout has then been reached causing the effect to return a None.
Using a simulated Clock means that we no longer rely on real-world time for time.
So this example runs in milliseconds of real-world time instead of taking an actual
1 second to hit the timeout. This way our time-based tests run much more quickly
since they are not based on actual system time. They are also more predictable as the
time adjustments are fully controlled by the tests.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Configuration
48
Targeting Error-Prone Time Bands
Using real-world time also can be error prone because effects may have unexpected
results in certain time bands. For instance, if you have code that gets the time and it
happens to be 23:59:59, then after some operations that take a few seconds, you get
some database records for the current day, those records may no longer be the day
associated with previously received records. This scenario can be very hard to test
for when using real-world time. When using a simulated clock in tests, you can write
tests that adjust the clock to reliably reproduce the condition.
Edit This Chapter
Edit This Chapter¹⁰
¹⁰https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/05_Configuration.md
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Errors
1. Creating & Handling
2. Error composability
3. Retry
In a language that cannot throw, following the execution path is simple, following 2
basic rules:
- At a branch, execute only the first match
- Otherwise, Read everything from left-to-right, top-to-bottom,
Once you add throw, the rules are more complicated
t
At a branch, execute only the first match
Otherwise, Read everything from left-to-right, top-to-bottom,
Unless we `throw`, which means immediately jumping through a differen\
dimension away from the code you're viewing
Historic approaches to Error-handling
If you are not interested in the discouraged ways to handle errors, and just want to
see the ZIO approach, jump down to ZIO Error Handling
In the past, some programs have thrown exceptions to indicate failures. Imagine a
program that displays the local temperature the user based on GPS position and a
network call. There are distinct levels of problems in any given program. They require
different types of handling by the programmer.
Temperature: 30 degrees
50
Errors
class GpsException()
extends RuntimeException
class NetworkException() extends RuntimeException
enum Scenario:
case Success,
NetworkError,
GPSError
def render(value: String) =
s"Temperature: $value"
def calculateTemp(behavior: Scenario): String =
behavior match
case Scenario.GPSError =>
throw GpsException()
case Scenario.NetworkError =>
throw NetworkException()
case Scenario.Success =>
"35 degrees"
def currentTemperatureUnsafe(
behavior: Scenario
): String =
render:
calculateTemp:
behavior
currentTemperatureUnsafe:
Scenario.Success
// res0: String = "Temperature: 35 degrees"
On the happy path, everything looks as desired. If the network is unavailable, what is
the behavior for the caller? This can take many forms. If we don’t make any attempt
to handle our problem, the whole program blows up and shows the gory details to
the user.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
51
Errors
// Note - Can't make this output prettier/simpler because it's *not* us\
ing ZIO
currentTemperatureUnsafe:
Scenario.NetworkError
// repl.MdocSession$MdocApp$NetworkException
//
at repl.MdocSession$MdocApp.calculateTemp(06_Errors.md:28)
//
at repl.MdocSession$MdocApp.currentTemperatureUnsafe(06_Errors.md:4\
0)
//
at repl.MdocSession$MdocApp.$init$$$anonfun$1(06_Errors.md:53)
We could take the bare-minimum approach of catching the Exception and returning
null:
def currentTemperatureNull(
behavior: Scenario
): String =
render:
try
calculateTemp:
behavior
catch
case ex: RuntimeException =>
null
currentTemperatureNull:
Scenario.NetworkError
// res1: String = "Temperature: null"
This is slightly better, as the user can at least see the outer structure of our UI element,
but it still leaks out code-specific details world.
Maybe we could fallback to a sentinel value, such as 0 or -1 to indicate a failure?
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
52
Errors
def currentTemperature(
behavior: Scenario
): String =
render:
try
calculateTemp:
behavior
catch
case ex: RuntimeException =>
"-1 degrees"
currentTemperature:
Scenario.NetworkError
// res2: String = "Temperature: -1 degrees"
Clearly, this isn’t acceptable, as both of these common sentinel values are valid
temperatures. We can take a more honest and accurate approach in this situation.
def currentTemperature(
behavior: Scenario
): String =
render:
try
calculateTemp:
behavior
catch
case ex: RuntimeException =>
"Unavailable"
currentTemperature:
Scenario.NetworkError
// res3: String = "Temperature: Unavailable"
We have improved the failure behavior significantly; is it sufficient for all cases?
Imagine our network connection is stable, but we have a problem in our GPS
hardware. In this situation, do we show the same message to the user? Ideally, we
would show the user a distinct message for each scenario. The Network issue is
transient, but the GPS problem is likely permanent.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
53
Errors
def currentTemperature(
behavior: Scenario
): String =
try
render:
calculateTemp:
behavior
catch
case ex: NetworkException =>
"Network Unavailable"
case ex: GpsException =>
"GPS problem"
currentTemperature:
Scenario.NetworkError
// res4: String = "Network Unavailable"
currentTemperature:
Scenario.GPSError
// res5: String = "GPS problem"
Wonderful! We have specific messages for all relevant error cases. However, this still
suffers from downsides that become more painful as the codebase grows.
• The signature of currentTemperature does not alert us that it might fail
• If we realize it can fail, we must dig through the implementation to discover
the multiple failure values
• We never have certainty about the failure paths of our full application, or any
subset of it.
{{ TODO Tear apart exceptions more }}
Encountering an error during a function call generally means two things:
1. You can’t continue executing the function in the normal fashion.
2. You can’t return a normal result.
Many languages use exceptions for handling errors. An exception throws out of the
current execution path to locate a user-written handler to deal with the error. There
are two goals for exceptions:
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
54
Errors
1. Separate error-handling code from “success-path” code, so the success-path
code is easier to understand and reason about.
2. Reduce redundant error-handling code by handling associated errors in a single
place.
Exceptions have problems:
1. They can be “swallowed.” Just because code throws an exception, there’s no
guarantee that issue will be dealt with.
2. They can lose important information. Once an exception is caught, it is
considered to be “handled,” and the program doesn’t need to retain the failure
information.
3. They aren’t typed. Java’s checked exceptions provide a small amount of type
information, but it’s not that helpful compared to a full type system. Unchecked
exceptions provide no information at all.
4. Because they are handled dynamically, the only way to ensure your program
won’t crash is by testing it through all possible execution paths. A staticallytyped error management solution can ensure—at compile time—that all errors
are handled.
5. They don’t scale. {{Need to think about this more to make the case.}}
6. Hard to reason about. {{Also need to make this case}}
7. Difficult or impossible to retry an operation if it fails. Java {{and Scala?}} use
the “termination” model of exception handling. This assumes the error is so
critical there’s no way to get back to where the exception occurred. If you’re
performing an operation that you’d like to retry if it fails, exceptions don’t help
much.
Exceptions were a valiant attempt to produce a consistent error-reporting interface,
and they are definitely better than what’s in C. But they don’t end up solving the
problem very well, and you just don’t know what you’re going to get when you use
exceptions.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
55
Errors
What’s wrong with Try?
ADTS as another step forward
ZIO Error Handling
Now we will explore how ZIO enables more powerful, uniform error-handling.
TODO {{Update verbiage now that ZIO section is first}}
• ZIO Error Handling
• Wrapping Legacy Code
ZIO-First Error Handling
def getTemperatureZ(behavior: Scenario) =
behavior match
case Scenario.GPSError =>
ZIO.fail:
GpsException()
case Scenario.NetworkError =>
// TODO Use a non-exceptional error
ZIO.fail:
NetworkException()
case Scenario.Success =>
ZIO.succeed:
"35 degrees"
runDemo:
getTemperatureZ:
Scenario.Success
// 35 degrees
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
56
Errors
// TODO make MDoc:fail adhere to line limits?
runDemo:
getTemperatureZ:
Scenario.Success
.catchAll:
case ex: NetworkException =>
ZIO.succeed:
"Network Unavailable"
// error:
// match may not be exhaustive.
//
// It would fail on pattern case: _: GpsException
//
TODO Demonstrate ZIO calculating the error types without an explicit annotation
being provided
runDemo:
getTemperatureZ:
Scenario.GPSError
// repl.MdocSession$MdocApp$GpsException
Wrapping Legacy Code
If we are unable to re-write the fallible function, we can still wrap the call We are
re-using the calculateTemp
{{TODO }}
def calculateTempWrapped(
behavior: Scenario
) =
ZIO.attempt:
calculateTemp:
behavior
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
57
Errors
def displayTemperatureZWrapped(
behavior: Scenario
) =
calculateTempWrapped:
behavior
.catchAll:
case ex: NetworkException =>
ZIO.succeed:
"Network Unavailable"
case ex: GpsException =>
ZIO.succeed:
"GPS problem"
runDemo:
displayTemperatureZWrapped:
Scenario.Success
// 35 degrees
runDemo:
displayTemperatureZWrapped:
Scenario.NetworkError
// Network Unavailable
This is decent, but does not provide the maximum possible guarantees. Look at what
happens if we forget to handle one of our errors.
def getTemperatureZGpsGap(
behavior: Scenario
) =
calculateTempWrapped:
behavior
.catchAll:
case ex: NetworkException =>
ZIO.succeed:
"Network Unavailable"
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
58
Errors
runDemo:
getTemperatureZGpsGap:
Scenario.GPSError
// Defect: GpsException
The compiler does not catch this bug, and instead fails at runtime. Take extra care
when interacting with legacy code, since we cannot automatically recognize these
situations at compile time. We have 2 options in these situations.
First, we can provide a fallback case that will report anything we missed:
def getTemperatureZWithFallback(
behavior: Scenario
) =
calculateTempWrapped:
behavior
.catchAll:
case ex: NetworkException =>
ZIO.succeed:
"Network Unavailable"
case other =>
ZIO.succeed:
"Error: " + other
runDemo:
getTemperatureZWithFallback:
Scenario.GPSError
// Error: repl.MdocSession$MdocApp$GpsException
This lets us avoid the most egregious gaps in functionality, but it does not take full
advantage of ZIO’s type-safety.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
59
Errors
def getTemperatureZAndFlagUnhandled(
behavior: Scenario
) =
calculateTempWrapped:
behavior
.catchSome:
case ex: NetworkException =>
ZIO.succeed:
"Network Unavailable"
// TODO Eh, find a better version of this.
.mapError(_.asInstanceOf[GpsException])
runDemo:
getTemperatureZAndFlagUnhandled:
Scenario.GPSError
// repl.MdocSession$MdocApp$GpsException
{{TODO show catchSome}}
Note: The following is copy&pasted and needs work
Unions AKA Sum Types AKA Enums AKA Ors
Note - Avoid official terminology in most prose. Just say “And”/”Or” where appropriate.
Scala 3 automatically aggregates the error types by synthesizing an anonymous sum
type from the combined errors.
Functions usually transform the Answer from one type to another type. Errors often
aggregate.
Consider 2 error types
case class UserNotFound()
case class PermissionError()
In the type system, the most recent ancestor between them is Any.
Unfortunately, you cannot make any meaningful decisions based on this type.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
60
Errors
graph TD;
UserNotFound-->Nothing;
PermissionError-->Nothing;
We need a more specific way to indicate that our code can fail with either of these
types. The | (or) tool provides maximum specificity without the need for inheritance.
TODO Figure out how to use pipe symbol in Mermaid
graph TD;
UserNotFound-->UserNotFound_OR_PermissionError;
PermissionError-->UserNotFound_OR_PermissionError;
UserNotFound-->Nothing;
PermissionError-->Nothing;
Often, you do not care that Nothing is involved at all. The mental model can be
simply:
graph TD;
UserNotFound-->UserNotFound_OR_PermissionError;
PermissionError-->UserNotFound_OR_PermissionError;
case class UserService()
case class User(name: String)
case class SuperUser(name: String)
def getUser(
userId: String
) =
if (userId == "morty" || userId = "rick")
ZIO.succeed:
User(userId)
else
ZIO.fail:
UserNotFound()
def getSuperUser(
user: User
) =
if (user.name = "rick")
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
61
Errors
ZIO.succeed:
SuperUser(user.name)
else
ZIO.fail:
PermissionError()
def loginSuperUser(userId: String) =
defer:
val basicUser = getUser(userId).run
getSuperUser(basicUser).run
Edit This Chapter
Edit This Chapter¹¹
¹¹https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/06_Errors.md
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Contract-Based Composability
Good contracts make good composability.
contracts are what makes composability work at scale our effects put in place
contracts on how things can compose exceptions do not put in place a contract
maybe something about how exceptions do not convey a contract in either direction.
Anything can be wrapped with a try. Things that produce exceptions don’t need to
be wrapped with trys.
possible example of Scope for Environment contracts
possible contract on provide for things not needed
ZIO.succeed("asdf").someOrFail("error")
// error:
//
// This operator requires that the output type be a subtype of Option[A\
ny]
// But the actual type was String..
// I found:
//
//
IsSubtypeOfOutput.impl[A, B](/* missing */summon[A <:< B])
//
// But no implicit values were found that match type A <:< B.
// def logAndProvideDefault(e: Throwable) =
//
this works as the contract is that the
Contract-Based Composability
63
ZIO.succeed(maybeThing()).someOrFail("error")
// res1: ZIO[Any, String, Unit] = OnSuccess(
//
trace = "repl.MdocSession.MdocApp.res1(07_Composability.md:20)",
//
first = Sync(
//
trace = "repl.MdocSession.MdocApp.res1(07_Composability.md:20)",
//
eval = zio.ZIOCompanionVersionSpecific$$Lambda$14961/0x000000080\
3cd6840@5d634918
//
),
//
successK = zio.ZIO$$Lambda$17731/0x0000000801e5b040@2e13e6d2
// )
ZIO
.succeed(println("Always gonna work"))
.retryN(100)
// error:
// This error handling operation assumes your effect can fail. However,\
your effect has Nothing for the error type, which means it cannot fail\
, so there is no need to handle the failure. To find out which method y\
ou can use instead of this operation, please see the reference chart at\
: https://zio.dev/can_fail.
// I found:
//
//
CanFail.canFail[E](/* missing */summon[util.NotGiven[E =:= Nothi\
ng]])
//
// But no implicit values were found that match type util.NotGiven[E =:\
= Nothing].
// def logAndProvideDefault(e: Throwable) =
//
ZIO
.attempt(println("This might work"))
.retryN(100)
// res3: ZIO[Any, Throwable, Unit] = OnSuccess(
//
trace = "repl.MdocSession.MdocApp.res3(07_Composability.md:35)",
//
first = Sync(
//
trace = "repl.MdocSession.MdocApp.res3(07_Composability.md:35)",
//
eval = zio.ZIOCompanionVersionSpecific$$Lambda$14961/0x000000080\
3cd6840@43f99fc3
//
),
//
successK = zio.ZIO$$$Lambda$14963/0x0000000803cd5040@2da21ecd
// )
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Contract-Based Composability
64
is this about surfacing the hidden information through a “bookkeeper” that conveys
the constraints to the caller
An essential part of creating programs is the ability to combine small pieces into
larger pieces.
Different languages / paradigms provide different ways to accomplish these combinations.
Objects can be combined by creating objects that contain other objects.
Functions can be combined by creating new functions that call other functions.
These are types of “composition” but these traditional approaches do not address all
of the aspects of a program.
For example, functions that use resources which need to be opened and closed, do
not compose.
ZIOs compose including errors, async, blocking, resource managed, cancellation,
eitherness, environmental requirements.
Composability Explanation
1. But Functions & Specialized Data Types Don’t Compose for Effects
2. Composability 1. Limitations of Functions & SDTs 1. Some intro to Universal
Effect Data Types ie ZIO 1. The ways in which ZIOs compose (contrasted
to limitations) 1. Note: Merge chapters: composability, Unit, The_ZIO_Type 1.
Note: Avoid explicit anonymous sum & product types at this point
Alternatives and their downsides
Other framings/techniques and their pros/cons:
Plain functions that return Unit
Unit can be viewed as the bare minimum of effect tracking.
Consider a function
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Contract-Based Composability
65
def saveInformation(info: String): Unit = ???
If we look only at the types, this function is an Any=>Unit. Unit is the single, blunt
tool to indicate effectful functions in plain Scala. When we see it, we know that some
type of side-effect is being performed.
When a function returns Unit, we know that the only reason we are calling the
function is to perform an effect. Alternatively, if there are no arguments to the
function, then the input is Unit, indicating that an effect is used to produce the result.
Unfortunately, we can’t do things like timeout/race/etc these functions. We can either
execute them, or not, and that’s about it, without resorting to additional tools for
manipulating their execution.
Plain functions that throw Exceptions
• We cannot union these error possibilities and track them in the type system
• Cannot attach behavior to deferred functions
Plain functions that block
• We can’t indicate if they block or not
• Too many concurrent blocking operations can prevent progress of other operations
• Very difficult to manage
• Blocking performance varies wildly between environments
Functions that return Either/Option/Try/etc
• We can manage the errors in the type system, but we can’t interrupt the code
that is producing these values
• All of these types must be manually transformed into the other types
• Execution is not deferred
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Contract-Based Composability
66
Functions that return a Future
•
•
•
•
•
•
•
Can be interrupted example1[future_interrupted_1] two[future_interrupted_2]
Cleanup is not guaranteed¹²
Manual management of cancellation
Start executing immediately
Must all fail with Exception
Implicits
Are not automatically managed by the compiler, you must explicitly add each
one to your parent function
• Resolving the origin of a provided implicit can be challenging
Try-with-resources
• These are statically scoped
• Unclear who is responsible for acquisition & cleanup
Each of these approaches gives you benefits, but you can’t assemble them all
together. Instead of the best of all worlds, you get the pain of all worlds. eg
Closeable[Future[Either[Throwable, A]]] The ordering of the nesting is significant, and not easily changed.
The number of combinations is something like: PairsIn(numberOfConcepts)
Universal Composability with ZIO
ZIOs compose including errors, async, blocking, resource managed, cancellation,
eitherness, environmental requirements.
The types expand through generic parameters. ie composing a ZIO with an error of
String with a ZIO with an error of Int results in a ZIO with an error of String |
Int.
¹²./15_Concurrency_Interruption.md#Future-Cancellation
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Contract-Based Composability
67
With functions there is one way to compose. f(g(h)) will sequentially apply the
functions from the inside out.
Another term for this form of composition is called andThen in Scala.
With ZIO you can use zio-direct to compose ZIOs sequentially with:
runDemo:
defer:
val topStory = findTopNewsStory.run
textAlert:
topStory
.run
// Texting story: Battery Breakthrough
// ()
There are many other ways you can compose ZIOs. The methods for composability
depend on the desired behavior. For example, to compose a ZIO that can produce an
error with a ZIO that logs the error and then produces a default value, you can use
the catchAll like:
def logAndProvideDefault(e: Throwable) =
Console
.printLine:
e.getMessage
.as:
"default value"
runDemo:
ZIO
.attempt:
???
.catchAll:
logAndProvideDefault
// an implementation is missing
// default value
All The Thing Example
….
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Contract-Based Composability
68
Hedging
TODO Determine final location for Hedging ### Why? This technique snips off the
most extreme end of the latency tail.
Determine the average response time for the top 50% of your requests. If you make
a call that does not get a response within this average delay, make an additional,
identical request. However, you do not give up on the 1st request, since it might
respond immediately after sending the 2nd. Instead, you want to race them and use
the first response you get
To be clear - this technique will not reduce the latency of the fastest requests at all.
It only alleviates the pain of the slowest responses.
You have 1/n chance of getting the worst case response time. This approach turns
that into a 1/n^2 chance. The cost of this is only ∼3% more total requests made.
Citations needed
Further, if this is not enough to completely eliminate your extreme tail, you can
employ the exact same technique once more. Then, you end up with 1/n^3 chance
of getting that worst performance.
Repeats
Repeating is a form of composability, because you are composing a program with
itself
Injecting Behavior before/after/around
Edit This Chapter
Edit This Chapter¹³
¹³https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/07_Composability.md
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Contract-Based Composability
69
Automatically attached experiments.
These are included at the end of this chapter because their package in the experiments
directory matched the name of this chapter. Enjoy working on the code with full
editor capabilities :D
experiments/src/main/scala/composability/AllTheThings.scala
package composability
import zio.*
import scala.concurrent.Future
import zio.direct.*
import scala.Option
import scala.util.Try
trait NewsService:
def getHeadline(): Future[String]
trait ContentAnalyzer:
def findTopicOfInterest(
content: String
): Option[String]
trait HistoricalRecord:
case class DetailedHistory(content: String)
case class NoRecordsAvailable(reason: String)
def summaryFor(
topic: String
): Either[NoRecordsAvailable, DetailedHistory]
trait CloseableFile extends AutoCloseable:
def existsInFile(searchTerm: String): Boolean
def write(entry: String): Try[Unit]
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Contract-Based Composability
case class Scenario(
newsService: NewsService,
contentAnalyzer: ContentAnalyzer,
historicalRecord: HistoricalRecord,
closeableFile: CloseableFile
):
def asyncThing(i: Int) = ZIO.sleep(i.seconds)
val resourcefulThing =
val open =
defer:
ZIO.debug("open").run
"asdf"
val close = (_: Any) => ZIO.debug("close")
ZIO.acquireRelease(open)(close)
val logic =
defer:
// todo: useful order, maybe async first or
// near first?
// maybe something parallel in here too?
// Convert from AutoCloseable
// maybe add Future or make asyncThing a
// Future `
val headline: String =
ZIO
.from:
newsService.getHeadline()
.run
val topic =
ZIO
.from:
contentAnalyzer.findTopicOfInterest:
headline
.run
val summaryFileZ =
ZIO
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
70
Contract-Based Composability
.fromAutoCloseable:
ZIO.succeed:
closeableFile
.run
val topicIsFresh =
summaryFileZ.existsInFile:
topic
if (topicIsFresh)
val newInfo =
ZIO
.from:
historicalRecord.summaryFor:
topic
.run
ZIO
.from:
summaryFileZ.write:
newInfo.content
.run
ZIO
.debug:
"topicIsFresh: " + topicIsFresh
.run
asyncThing(1).run
// todo: some error handling to show that
// the errors weren't lost along the way
.catchAll:
case t: Throwable =>
???
case _: Any =>
???
end Scenario
object AllTheThings extends ZIOAppDefault:
type Nail = ZIO.type
/* If ZIO is your hammer, it's not that you
* _see_ everything as nails.
* You can actually _convert_ everything into
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
71
Contract-Based Composability
72
* nails. */
/*
*
*
*
*
*
*
*
*
*
*
*
*
Possible scenario:
Get headline - Future Analyze for
topic/persons of interest - Option Check if
we have made an entry for them in today's
summary file - Resource If not:
Dig up supporting information on the topic
from a DB - Try Make new entry in today's
summary file - Resource
Is Either different enough to demo here?
It basically splits the difference between
Option/Try I think if we show both of them,
we can skip Either. */
override def run =
Scenario(
Implementations.newsService,
Implementations.contentAnalyzer,
Implementations.historicalRecord,
Implementations.closeableFile
).logic
end AllTheThings
experiments/src/main/scala/composability/Invisible.scala
package composability
import scala.concurrent.Future
import scala.util.Try
object Implementations:
val newsService =
new NewsService:
def getHeadline(): Future[String] =
Future.successful:
"The stock market is crashing!"
val contentAnalyzer =
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Contract-Based Composability
new ContentAnalyzer:
override def findTopicOfInterest(
content: String
): Option[String] =
Option.when(
content.contains("stock market")
):
"stock market"
val historicalRecord =
new HistoricalRecord:
override def summaryFor(
topic: String
): Either[
NoRecordsAvailable,
DetailedHistory
] =
topic match
case "stock market" =>
Right(
DetailedHistory("detailed history")
)
case "TODO_OTHER_MORE_OBSCURE_TOPIC" =>
Left(NoRecordsAvailable("blah blah"))
val closeableFile =
new CloseableFile:
override def close =
println("Closing file now!")
override def existsInFile(
searchTerm: String
): Boolean = searchTerm == "stock market"
override def write(
entry: String
): Try[Unit] =
println("Writing to file: " + entry)
if (entry == "stock market")
Try(
throw new Exception(
"Stock market already exists!"
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
73
Contract-Based Composability
)
else
Try(())
end Implementations
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
74
Concurrency High Level
1. forEachPar, collectAllPar
TODO Prose
def sleepThenPrint(
d: Duration
) =
defer:
ZIO.sleep(d).run
ZIO.debug(s"${d.render} elapsed").run
d
runDemo:
ZIO.foreach(Seq(2, 1)): i =>
sleepThenPrint(i.seconds)
// List(PT2S, PT1S)
runDemo:
ZIO.foreachPar(Seq(2, 1)): i =>
sleepThenPrint(i.seconds)
// List(PT2S, PT1S)
runDemo:
defer:
val durations =
ZIO
.collectAllPar:
Seq(
sleepThenPrint(2.seconds),
sleepThenPrint(1.seconds)
)
.run
val total =
durations.fold(Duration.Zero)(_ + _).render
Console
.printLine:
total
.run
// ()
Concurrency High Level
def slowFailableRandom(duration: Duration) =
defer:
val randInt =
Random.nextIntBetween(0, 100).run
ZIO.sleep(duration).run
ZIO
.when(randInt < 10)(
ZIO.fail("Number is too low")
)
.run
duration
// Massive example
runDemo:
defer:
val durations =
ZIO
.collectAllSuccessesPar:
Seq
.fill(1_000)(1.seconds)
.map(duration =>
slowFailableRandom(duration)
)
.run
durations.fold(Duration.Zero)(_ + _).render
// 15 m 9 s
zipPar, zipWithPar
validateWithPar?
withParallelism
Edit This Chapter
Edit This Chapter¹⁴
¹⁴https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/08_Concurrency.md
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
76
Concurrency Interruption
Why Interruption Is Necessary Throughout
the Stack
In order for the Runtime to operate and provide the super powers of ZIO, it needs to
be able to interrupt running workflows without resource leaks.
Timeout
Since we can reliably interrupt arbitrary ZIOs, we can attach an upper bound for the
runtime of the ZIO. This does not change the type of the workflow, it only changes
the runtime behavior.
Race
If we have 2 ZIO’s that each produce the same types, we can race them, acquiring the
first result and cancel the ongoing calculation. We have taken 2 completely separate
workflows and fused them into one.
.withFinalizer
## .onInterrupt
Fork Interruption
Interruption is explicit in the previous situations, but there is an implicit interruption
point that you should be aware of. If an operation is forked, and we exit the scope
that created it without joining, then it will be interrupted.
Concurrency Interruption
78
runDemo:
defer:
ZIO
.debug:
"About to sleep forever"
.run
ZIO
.sleep:
Duration.Infinity
.onInterrupt:
ZIO.succeed:
// More mdoc console weirdness :(
println:
"Interrupted the eternal sleep"
.fork
.run
// About to sleep forever
// Interrupted the eternal sleep
// zio.internal.FiberRuntime@3ae9036c
If we encounter an error between forking and joining, the fibers will also be
interrupted.
runDemo:
defer:
val fiber1 =
createProcess(
"Fiber 1",
ZIO.sleep(5.seconds)
).fork.run
val fiber2 =
createProcess(
"Fiber 2",
ZIO.sleep(5.seconds)
).fork.run
// Once we fail here, the fibers will be
// interrupted.
ZIO.fail("Youch!").run
fiber1.join.run
fiber2.join.run
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Concurrency Interruption
79
// Interrupt Fiber 2
// Interrupt Fiber 1
// Youch!
Uninterruptable
### .acquireRelease effects are uninterruptible There are certain cases where you
want to ensure code is not interrupted. For example, when you have a finalizer that
needs to free up resources, you need to ensure it completes.
Tight loops
Tight loops that aren’t performing ZIO operations cannot be interrupted by the ZIO
runtime.
def longOperation() = "**TODO**"
runDemo:
defer:
ZIO
.succeed:
longOperation()
.timeout(1.seconds)
.timed
.debug("Time:")
.run
// (PT0.000481772S,Some(**TODO**))
We can see 2 significant behaviors here:
• timeout did cause the long operation to return None
• It did not interrupt the operation early.
Future Cancellation (Contra-example. Not
necessary for happy path explanation)
We show that Future’s are killed with finalizers that never run
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Concurrency Interruption
import scala.concurrent.Future
runDemo:
ZIO
.fromFuture:
Future:
try
println:
"Starting operation"
Thread.sleep:
500
println:
"Ending operation"
finally
println:
"Cleanup"
.timeout:
25.millis
// Starting operation
// None
.fromFutureInterrupt
Edit This Chapter
Edit This Chapter¹⁵
¹⁵https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/09_Interruption.md
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
80
State
1. Ref
2. Thundering Herds
Functional programmers often sing the praises of immutability. The advantages
are real and numerous. However, it is easy to find situations that are intrinsically
mutable.
• How many people are currently inside a building?
• How much fuel is in your car?
• How much money is in your bank account?
Rather than avoiding mutability entirely, we want to avoid unprincipled, unsafe
mutability. If we codify and enumerate everything that we need from Mutability,
then we can wield it safely. Required Operations:
• Update the value
• Read the current value
These are both effectful operations. In order to confidently use them, we need certain
guarantees about the behavior:
• The underlying value cannot be changed during a read
• Multiple writes cannot happen concurrently, which would result in lost updates
Less obviously, we also need to create the Mutable reference itself. We are changing
the world, by creating a space that we can manipulate.
Unreliable Counting
82
State
val unreliableCounting =
var counter = 0
val increment =
ZIO.succeed:
counter = counter + 1
defer:
ZIO
.foreachParDiscard(Range(0, 100000)): _ =>
increment
.run
// It's not obvious to the reader why
// we need to wrap counter in .succeed
"Final count: " + ZIO.succeed(counter).run
runDemo:
unreliableCounting
// Final count: 99799
Due to the unpredictable nature of shared mutable state, we do not know exactly
what the final count above is. Each time we publish a copy of this book, the code is reexecuted and a different wrong result is generated. However, conflicts are extremely
likely, so some of our writes get clobbered by others, and we end up with less than
the expected 100,000. Ultimately, we lose information with this approach.
TODO Consider making a diagram parallel writes
Performing our side effects inside ZIO’s does not magically make them safe. We need
to fully embrace the ZIO components, utilizing Ref for correct mutation.
Reliable Counting
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
83
State
lazy val reliableCounting =
def incrementCounter(counter: Ref[Int]) =
counter.update:
_ + 1
defer:
val counter = Ref.make(0).run
ZIO
.foreachParDiscard(Range(0, 100000)): _ =>
incrementCounter:
counter
.run
"Final count: " + counter.get.run
runDemo:
reliableCounting
// Final count: 100000
Now we can say with full confidence that our final count is 100000. Additionally,
these updates happen without blocking. This is achieved through a strategy called
“Compare & Swap”, which we will not cover in detail. TODO Link/reference supplemental reading
Although there are significant advantages; a basic Ref is not the solution for
everything. We can only pass pure functions into update. The API of the plain
Atomic Ref steers you in the right direction by not accepting ZIOs as parameters to
any of its methods. To demonstrate why this restriction exists, we will deliberately
undermine the system by sneaking in a side effect. First, we will create a helper
function that imitates a long-running calculation.
def expensiveCalculation() =
Thread.sleep:
35
Our side effect will be a mock alert that is sent anytime our count is updated: scala
def sendNotification() = println: "Alert: updating count!"
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
84
State
def update(counter: Ref[Int]) =
counter.update: previousValue =>
expensiveCalculation()
sendNotification()
previousValue + 1
runDemo:
defer:
val counter = Ref.make(0).run
ZIO
.foreachParDiscard(Range(0, 4)): _ =>
update(counter)
.run
"Final count: " + counter.get.run
// Alert: updating count!
// Alert: updating count!
// Alert: updating count!
// Alert: updating count!
// Alert: updating count!
// Alert: updating count!
// Alert: updating count!
// Alert: updating count!
// Alert: updating count!
// Alert: updating count!
// Final count: 4
What is going on?! Previously, we were losing updates because of unsafe mutability.
Now, we have the opposite problem! We are sending far more alerts than intended,
even though we can see that our final count is 4.
TODO This section will need significant attention and polish
Now we must consider the limitations of the “Compare & Swap” system. It achieves
lock-free performance by letting each fiber freely make their updates, and then doing
a last-second check to see if the underlying value changed during its update. If the
value has not changed, the update is made. If it has changed, then the entire function
that was passed into update is re-executed until it completes with a stable value. The
higher the parallelism, or the longer the operation takes, the higher the likelihood of
a compare-and-swap retry.
This retry behavior is safe with pure functions, which can be executed an arbitrary
number of times. However, it is completely inappropriate for effects, which should
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
85
State
only be executed a single time. For these situations, we need a specialized variation
of Ref
Ref.Synchronized
Ref.Synchronized guarantees only a single execution of the update body and any
of the effects contained inside. The only change required is replacing Ref.make with
Ref.Synchronized.make
val sideEffectingUpdatesSync =
defer:
val counter = Ref.Synchronized.make(0).run
ZIO
.foreachParDiscard(Range(0, 4)): _ =>
counter.update: previousValue =>
expensiveCalculation()
sendNotification()
previousValue + 1
.run
"Final count: " + counter.get.run
runDemo:
sideEffectingUpdatesSync
// Alert: updating count!
// Alert: updating count!
// Alert: updating count!
// Alert: updating count!
// Final count: 4
Now we see exactly the number of alerts that we expected. This correctness comes
with a cost though, as the name of this type implies. Each of your updates will run
sequentially, despite initially launching them all in parallel. This is the only known
way to avoid retries. Try to structure your code to minimize the coupling between
effects and updates, and use this type only when necessary.
Edit This Chapter
Edit This Chapter¹⁶
¹⁶https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/10_State.md
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Reliability
Caching
One way to achieve better reliability is with caching.
val thunderingHerdsScenario =
defer:
val popularService =
ZIO.service[PopularService].run
ZIO
.foreachPar(List.fill(100)(())): _ =>
popularService.retrieve:
Path.of("awesomeMemes")
.run
val cloudStorage =
ZIO.service[CloudStorage].run
cloudStorage.invoice.debug.run
object PopularService:
private def lookup(key: Path) =
defer:
val cloudStorage =
ZIO.service[CloudStorage].run
cloudStorage.retrieve(key).run
val make =
defer:
val cloudStorage =
ZIO.service[CloudStorage].run
PopularService(cloudStorage.retrieve)
// james don't like
87
Reliability
val makeCached =
defer:
val cache =
Cache
.make(
capacity = 100,
timeToLive = Duration.Infinity,
lookup = Lookup(lookup)
)
.run
PopularService(cache.get)
end PopularService
runDemo:
thunderingHerdsScenario.provide(
CloudStorage.live,
ZLayer.fromZIO(PopularService.make)
)
// Amount owed: $100
runDemo:
thunderingHerdsScenario.provide(
CloudStorage.live,
ZLayer.fromZIO(PopularService.makeCached)
)
// Amount owed: $1
Staying under rate limits
Constraining concurrent requests
If we want to ensure we don’t accidentally DDOS a service, we can restrict the
number of concurrent requests to it.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
88
Reliability
runDemo:
defer:
ZIO
.foreachPar(1 to 10): _ =>
//
bulkhead:
ZIO.serviceWithZIO[DelicateResource](
_.request
)
.as("All Requests Succeeded")
.catchAll(err => ZIO.succeed(err))
.debug
.run
.provideSome[Scope]:
DelicateResource.live
// Delicate Resource constructed.
// Do not make more than 3 concurrent requests!
// Killed the server!!
import nl.vroste.rezilience.Bulkhead
runDemo:
defer:
val bulkhead: Bulkhead =
Bulkhead.make(maxInFlightCalls = 3).run
ZIO
.foreachPar(1 to 10): _ =>
bulkhead:
ZIO.serviceWithZIO[DelicateResource](
_.request
)
.as("All Requests Succeeded")
.catchAll(err => ZIO.succeed(err))
.debug
.run
.provideSome[Scope]:
DelicateResource.live
// All Requests Succeeded
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
89
Reliability
Circuit Breaking
Hedging
Edit This Chapter
Edit This Chapter¹⁷
¹⁷https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/11_Reliability.md
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
ZIO and ZLayer
Connect the DI & Errors chapter to how they are represented in the ZIO data type.
The way we get good compile errors is by having data types which “know” the …
ZIO
We need an Answer about this scenario. The scenario requires things and could
produce an error. trait ZIO[Requirements, Error, Answer]
The ZIO trait is at the center of our Effect-oriented world.
trait ZIO[R, E, A]
A trait with 3 type parameters can be intimidating, but each one serves a distinct,
important purpose.
R - Requirements
TODO
E - Errors
This parameter tells us how this operation might fail.
def parse(
contents: String
): ZIO[Any, IllegalArgumentException, Unit] = ???
A - Answer
This is what our code will return if it completes successfully.
ZIO and ZLayer
91
def defaultGreeting()
: ZIO[Any, Nothing, String] = ???
ZLayer
R - Requirements
### E - Errors ### A - Answer
Composing
Managing and wiring dependencies has been a perennial challenge in software
development.
ZIO provides the ZLayer class to solve many of the problems in this space. If you
pay the modest, consistent cost of constructing pieces of your application as ZLayers,
you will get benefits that scale with the complexity of your project. Consistent with
ZIO itself, ZLayer has 3 type parameters that represent:
• What it needs from the environment
• How it can fail
• What it produces when successful.
With the same type parameters, and many of the same methods, you might be
wondering why we even need a separate data type - why not just use ZIO itself
for our dependencies? The environment type parameter for ZLayer maps directly
to unique, singleton services in your application. The environment type parameter
for ZIO might have many possible instances. ZLayer provides additional behaviors
that are valuable specifically in this domain. Typically, you only want a single
instance of a dependency to exist across your application. This may be to reduce
memory/resource usage, or even to ensure basic correctness. ZLayer output values
are shared maximally by default. They also build in scope management that will
ensure resource cleanup in asynchronous, fallible situations.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
ZIO and ZLayer
ZIO <-> ZLayer
ZLayer.fromZIO
Edit This Chapter
Edit This Chapter¹⁸
¹⁸https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/12_ZIO_and_ZLayer.md
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
92
Appendix Running Effects
TODO: Make an appendix?
Building applications from scratch
One way to run ZIOs is to use a “main method” program (something you can start
in the JVM). However, setting up the pieces needed for this is a bit cumbersome if
done without helpers.
ZIOAppDefault
ZIO provides an easy way to do this with the ZIOAppDefault trait.
To use it create a new object that extends the ZIOAppDefault trait and implements
the run method. That method returns a ZIO so you can now give it the example
ZIO.debug data:
object HelloWorld extends zio.ZIOAppDefault:
def run =
ZIO.debug:
"hello, world"
This can be run on the JVM in the same way as any other class that has a static
void main method.
The ZIOAppDefault trait sets up the ZIO runtime which interprets ZIOs and
provides some out-of-the-box functionality, and then runs the provided data in that
runtime.
If you are learning ZIO, you should start your exploration with ZIOAppDefault. It
is the standard, simplest way to start executing your recipes.
Appendix Running Effects
94
// NOTE We cannot execute invoke main on this
// because it crashes mdoc in the CI process
object RunningZIOs extends ZIOAppDefault:
def run =
// TODO Console/debug don't work
ZIO.attempt:
println:
"Hello World!"
You can provide arbitrary ZIO instances to the run method, as long as you have
provided every piece of the environment. In other words, it can accept ZIO[Any, _,
_].
There is a more flexible ZIOApp that facilitates sharing layers between applications,
but this is advanced and not necessary for most applications.
runDemo
While the ZIOApp* types are great for building real applications, they are not ideal
for demonstrating code for a book. We created the runDemo function to streamline
this use-case. It is a function that takes a ZIO and executes it in a runtime, returning
the result. It uses most of the same techniques that are used in ZIOAppDefault, but
is more single purpose, always immediately executing the ZIO provided to it.
runDemo:
ZIO.debug:
"hello, world"
// hello, world
// ()
Testing code
ZIOSpecDefault
Similar to ZIOAppDefault, there is a ZIOSpecDefault that should be your starting
point for testing ZIO applications. ZIOSpecDefault provides test-specific implementations built-in services, to make testing easier. When you run the same ZIO in these
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Appendix Running Effects
95
2 contexts, the only thing that changes are the built-in services provided by the
runtime.
TODO - Decide which scenario to test
import zio.test._
object TestingZIOs extends ZIOSpecDefault:
def spec =
test("Hello Tests"):
defer:
ZIO.console.run
assertTrue:
Random.nextIntBounded(10).run > 10
runSpec
runSpec:
defer:
assertTrue:
Random.nextIntBounded(10).run < 10
// Test: PASSED*
TODO Justify defer syntax over for-comp for multi-statement assertions I think this
example completes the objective TODO Change this to a Console app, where the
logic & testing is more visceral
runSpec:
defer:
assertTrue:
Random.nextIntBetween(0, 10).run <= 10 &&
Random.nextIntBetween(10, 20).run <=
20 &&
Random.nextIntBetween(20, 30).run <= 30
// Test: PASSED*
Consider a Console application:
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Appendix Running Effects
96
val logic =
defer:
val username =
Console
.readLine:
"Enter your name\n"
.run
Console
.printLine:
s"Hello $username"
.run
.orDie
If we try to run this code in the same way as most of the examples in this book, we
encounter a problem. scala runDemo: logic.timeout(1.second) // Defect:
scala.NotImplementedError: an implemen We cannot execute this code and
render the results for the book because it requires interaction with a user. However,
even if you are not trying to write demo code for a book, it is very limiting to need
a user at the keyboard for your program to execute. Even for the smallest programs,
it is slow, error-prone, and boring.
runSpec:
defer:
TestConsole
.feedLines:
"Zeb"
.run
logic.run
val capturedOutput: String =
TestConsole.output.run.mkString
val expectedOutput =
s"""|Enter your name
|Hello Zeb
|""".stripMargin
assertTrue:
capturedOutput == expectedOutput
// Test: PASSED*
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Appendix Running Effects
97
Interop with existing/legacy code via Unsafe
In some cases your ZIOs may need to be run outside a main program, for example
when embedded into other programs. In this case you can use ZIO’s Unsafe utility
which is called Unsafe to indicate that the code may perform side effects.
To do the same ZIO.debug with Unsafe do:
Unsafe.unsafe { implicit u: Unsafe =>
Runtime
.default
.unsafe
.run:
ZIO.debug:
"hello, world"
.getOrThrowFiberFailure()
}
// hello, world
If needed you can even interop to Scala Futures through Unsafe, transforming the
output of a ZIO into a Future.
Edit This Chapter
Edit This Chapter¹⁹
¹⁹https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/13_Appendix_RunningEffects.md
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Experiments
These experiments are not currently attached to a chapter, but are included for
previewing. Before publication, we should not have any lingering experiments here.
rezilience
experiments/src/main/scala/rezilience/CircuitBreakerDemo.sca
package rezilience
import nl.vroste.rezilience.*
import nl.vroste.rezilience.CircuitBreaker.*
import zio.{Schedule, ZLayer}
case class Cost(value: Int)
case class Analysis(content: String)
trait ExpensiveSystem:
def call: ZIO[Any, String, Analysis]
val billToDate: ZIO[Any, String, Cost]
object Scenario:
enum Step:
case Success,
Failure
object CircuitBreakerDemo extends ZIOAppDefault:
val makeCircuitBreaker
: ZIO[Scope, Nothing, CircuitBreaker[
Any
]] =
CircuitBreaker.make(
99
Experiments
trippingStrategy =
TrippingStrategy
.failureCount(maxFailures = 2),
resetPolicy = Retry.Schedules.common(),
.exponentialBackoff(
min = 1.second,
max = 4.second,
),
//
//
//
//
onStateChange =
state =>
ZIO.debug(s"State change: $state")
)
def run =
defer:
ZIO
.serviceWithZIO[ExpensiveSystem](_.call)
.ignore
.repeat(
Schedule.recurs(8) &&
Schedule.spaced(200.millis)
)
.run
ZIO
.serviceWithZIO[ExpensiveSystem](
_.billToDate
)
.debug
.run
.provide:
//
ExternalSystem // TOGGLE
ExternalSystemProtected // TOGGLE
.live
end CircuitBreakerDemo
case class ExternalSystemProtected(
externalSystem: ExpensiveSystem,
circuitBreaker: CircuitBreaker[String]
) extends ExpensiveSystem:
val billToDate: ZIO[Any, String, Cost] =
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
100
Experiments
externalSystem.billToDate
def call: ZIO[Any, String, Analysis] =
circuitBreaker:
externalSystem.call
.tap(r => ZIO.debug(s"Result: $r"))
.mapError:
case CircuitBreakerOpen =>
"Circuit breaker blocked the call to our external system"
case WrappedError(e) =>
println("ignored boom?")
s"External system threw an exception: $e"
.tapError(e => ZIO.debug(e))
object ExternalSystemProtected:
val live
: ZLayer[Any, Nothing, ExpensiveSystem] =
ZLayer.fromZIO:
defer:
ExternalSystemProtected(
ZIO.service[ExpensiveSystem].run,
CircuitBreakerDemo
.makeCircuitBreaker
.run
)
.provide(
ExternalSystem.live,
Scope.default
)
experiments/src/main/scala/rezilience/CircuitBreakerInvisible.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
101
Experiments
package rezilience
import java.time.Instant
import scala.concurrent.TimeoutException
// Invisible mdoc fencess
import Scenario.Step.*
object ExternalSystem:
val live
: ZLayer[Any, Nothing, ExpensiveSystem] =
ZLayer.fromZIO:
defer:
val valueProducer =
scheduledValues(
(300.millis, Success),
(200.millis, Failure),
// TODO Restore when I can get CB to
// reconnect :(
(400.millis, Failure),
(5.seconds, Success)
).run
ExternalSystem(
Ref.make(0).run,
valueProducer
)
case class ExternalSystem(
requests: Ref[Int],
responseAction: ZIO[
Any, // access time
TimeoutException,
Scenario.Step
]
) extends ExpensiveSystem:
// TODO: Better error type than Throwable
val billToDate: ZIO[Any, String, Cost] =
requests
.get
.map:
Cost(_)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
102
Experiments
def call: ZIO[Any, String, Analysis] =
defer:
ZIO
.debug(
"Called underlying ExternalSystem"
)
.run
val requestCount =
requests.updateAndGet(_ + 1).run
responseAction.orDie.run match
case Success =>
ZIO
.succeed:
Analysis:
s"Expensive report #$requestCount"
.run
case Failure =>
ZIO
.debug:
"boom"
.run
ZIO
.fail:
"Something went wrong"
.run
end ExternalSystem
// TODO Consider deleting
object InstantOps:
extension (i: Instant)
def plusZ(duration: zio.Duration): Instant =
i.plus(duration.asJava)
import InstantOps._
/*
*
*
*
*
Goal: If I accessed this from:
0-1 seconds, I would get "First Value" 1-4
seconds, I would get "Second Value" 4-14
seconds, I would get "Third Value" 14+
seconds, it would fail */
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
103
Experiments
// TODO Consider TimeSequence as a name
def scheduledValues[A](
value: (Duration, A),
values: (Duration, A)*
): ZIO[
Any, // construction time
Nothing,
ZIO[
Any, // access time
TimeoutException,
A
]
] =
defer {
val startTime = Clock.instant.run
val timeTable =
createTimeTableX(
startTime,
value,
values* // Yay Scala3 :)
)
accessX(timeTable)
}
// TODO Some comments, tests, examples, etc to
// make this function more obvious
private def createTimeTableX[A](
startTime: Instant,
value: (Duration, A),
values: (Duration, A)*
): Seq[ExpiringValue[A]] =
values.scanLeft(
ExpiringValue(
startTime.plusZ(value._1),
value._2
)
) {
case (
ExpiringValue(elapsed, _),
(duration, value)
) =>
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
104
Experiments
ExpiringValue(
elapsed.plusZ(duration),
value
)
}
/** Input: (1 minute, "value1") (2 minute,
* "value2")
*
* Runtime: Zero value: (8:00 + 1 minute,
* "value1")
*
* case ((8:01, _) , (2.minutes, "value2")) =>
* (8:01 + 2.minutes, "value2")
*
* Output: ( ("8:01", "value1"), ("8:03",
* "value2") )
*/
private def accessX[A](
timeTable: Seq[ExpiringValue[A]]
): ZIO[Any, TimeoutException, A] =
defer {
val now = Clock.instant.run
ZIO
.getOrFailWith(
new TimeoutException("TOO LATE")
) {
timeTable
.find(_.expirationTime.isAfter(now))
.map(_.value)
}
.run
}
private case class ExpiringValue[A](
expirationTime: Instant,
value: A
)
experiments/src/main/scala/rezilience/RateLimiter.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
105
Experiments
package rezilience
import nl.vroste.rezilience.*
/** This is useful for scenarios such as:
*
- Making sure you don't suddenly spike your
*
AWS bill
*
- Not accidentally DDOSing a service
*/
val makeRateLimiter
: ZIO[Scope, Nothing, RateLimiter] =
RateLimiter.make(max = 1, interval = 1.second)
// We use Throwable as error type in this example
def rsaKeyGenerator: ZIO[Any, Throwable, Int] =
Random.nextIntBounded(1000)
import zio_helpers.timedSecondsDebug
object RateLimiterDemoWithLogging
extends ZIOAppDefault:
def run =
defer:
val rateLimiter = makeRateLimiter.run
rateLimiter(rsaKeyGenerator)
// Print the time to generate each key:
.timedSecondsDebug("Generated key")
// Repeat as fast as the limiter allows:
.repeatN(3)
// Print the last result
.timedSecondsDebug("Result").run
object RateLimiterDemoGlobal
extends ZIOAppDefault:
import zio_helpers.repeatNPar
def run =
defer:
val rateLimiter = makeRateLimiter.run
ZIO
.repeatNPar(3): i =>
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
106
Experiments
rateLimiter:
rsaKeyGenerator
.timedSecondsDebug(
s"${i.toString} generated a key"
)
// Repeats as fast as allowed
.repeatN(2).debug(s"Result $i")
.unit
.timedSecondsDebug("Total time")
.run
end RateLimiterDemoGlobal
performance
experiments/src/main/scala/performance/Hedging.scala
package performance
object Hedging extends ZIOAppDefault:
def run =
defer:
val fastResponses
= Ref.make(0).run
val contractBreaches = Ref.make(0).run
ZIO
.foreachPar(List.fill(50_000)(())):
_ => // james still hates this
defer:
val randomResponse =
Response.random
val hedged =
randomResponse.race:
randomResponse.delay:
25.millis
// todo: extract to invisible
// function
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
107
Experiments
hedged.run match
case Response.Fast =>
fastResponses.update(_ + 1).run
case Response.BreachOfContract =>
contractBreaches
.update(_ + 1)
.run
.run
fastResponses
.get
.debug("Fast responses")
.run
contractBreaches
.get
.debug("Slow responses")
.run
end Hedging
// invisible below
enum Response:
case Fast,
BreachOfContract
object Response:
def random: ZIO[Any, Nothing, Response] =
defer:
val random =
Random.nextIntBounded(1_000).run
random match
case 0 =>
ZIO
.sleep:
3.seconds
.run
ZIO
.succeed:
Response.BreachOfContract
.run
case _ =>
Response.Fast
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
108
Experiments
zio_helpers
experiments/src/main/scala/zio_helpers/Helpers.scala
package zio_helpers
extension (z: ZIO.type)
def repeatNPar[R, E, A](
numTimes: Int
)(op: Int => ZIO[R, E, A]): ZIO[R, E, Seq[A]] =
z.foreachPar(0 until numTimes)(op)
def repeatNPar[R, E, A](
numTimes: Int
)(op: ZIO[R, E, A]): ZIO[R, E, Seq[A]] =
z.foreachPar(0 until numTimes)(_ => op)
extension [R, E, A](z: ZIO[R, E, A])
def timedSecondsDebug(
message: String
): ZIO[R, E, A] =
z.timed
.tap: (duration, res) =>
res match
// Don't bother printing Unit results
case () =>
ZIO.debug:
message + " [took " +
duration.getSeconds + "s]"
case _ =>
ZIO.debug:
message + ": " + res + " [took " +
duration.getSeconds + "s]"
.map(_._2)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Download