Uploaded by Zoran Jeremic

effect oriented programming

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 2022-12-23
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 - 2022 Bill Frasure, Bruce Eckel and James Ward
CONTENTS
Contents
Copyright . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Effect Oriented Programming . . . . . . . . . . . . . . . . . . . . . . . . . . .
Source Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1
2
Superpowers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
Underlying . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
Introduction . . . . . . . .
Untangling the Chaos
The State of Software
The Software Crisis .
Reliability . . . . . . .
What is an Effect? . .
Managing Effects . . .
Who This Book Is For
How to Use This Book
Acknowledgements .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
7
8
9
10
11
12
12
13
13
Why Functional? . . . . . . . . . . . . . . . . . . . .
A Different Goal . . . . . . . . . . . . . . . . . .
Reuse . . . . . . . . . . . . . . . . . . . . . . . . .
Pure Functions . . . . . . . . . . . . . . . . . . .
Composability . . . . . . . . . . . . . . . . . . . .
Effects . . . . . . . . . . . . . . . . . . . . . . . .
Immutability During Repetition . . . . . . . . .
Core Differences Between OO and Functional
Summary: Style vs Substance . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
14
14
17
18
19
20
20
22
23
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
CONTENTS
Effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Effects VS Side-Effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
24
25
26
Unit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
The ZIO Type . . . . . . . . . . . . . . . . . .
R - The Environment . . . . . . . . . . .
E - The Error . . . . . . . . . . . . . . . .
A - The Result . . . . . . . . . . . . . . . .
Conversions from standard Scala types
.
.
.
.
.
29
29
29
30
30
And / Or . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Unions AKA Sum Types AKA Enums AKA Ors . . . . . . . . . . . . . . . .
Intersections AKA Products AKA Case Classes AKA Ands . . . . . . . . .
32
32
34
Built-in Services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.x . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.x . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36
36
36
Console . . . . . . . . . . . . . . . . . . . .
The Unprincipled Way . . . . . . . .
Building a Better Way . . . . . . . . .
Official ZIO Approach . . . . . . . . .
ZIO Super-Powers . . . . . . . . . . .
Automatically attached experiments.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
37
37
37
41
41
42
Mutability . . . . . . . . . . . . . . . . . . .
Unreliable Counting . . . . . . . . . .
Reliable Counting . . . . . . . . . . .
Ref.Synchronized . . . . . . . . . . . .
Automatically attached experiments.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
45
45
46
49
50
Time . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . .
52
52
Environment Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Historic Approach . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
59
59
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
CONTENTS
Building a Better Way . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Official ZIO Approach . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
61
66
67
Random . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . .
70
70
Hello Failures . . . . . . . . . . . . . . . . .
Historic approaches to Error-handling
ZIO Error Handling . . . . . . . . . . .
Automatically attached experiments. .
.
.
.
.
79
79
83
87
Cause . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Avoided Technique - Throwing Exceptions . . . . . . . . . . . . . . . . . . .
Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . .
96
96
98
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
New Effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
Location . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
Environment Exploration . . . . . . . . . . .
Dependency Injection . . . . . . . . . . .
ZEnvironment: Powered by a TypeMap
ZLayer . . . . . . . . . . . . . . . . . . . .
Automatically attached experiments. . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
108
108
108
109
109
Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
STM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . . 115
Executing External Programs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
Basic shell tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . . 121
Experiments . . . . . . . .
resourcemanagement
energygrid . . . . . . .
microcontrollers . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
124
124
127
128
CONTENTS
diningphilosophers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
interpreter-chaining_previous_result_and_environmental_dependency .
experiments-src-test-scala-energrygrid . . . . . . . . . . . . . . . . . . . . .
layers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
javawrappers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
interpreter-level1_nochaining . . . . . . . . . . . . . . . . . . . . . . . . . .
bigdec . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
typeclasses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ZIOFromNothing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
experiments-src-test-scala-test_aspects . . . . . . . . . . . . . . . . . . . .
testcontainers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
interpreter-level2_chaining . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Hubs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
TicTacToe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
runtime . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
experiments-src-test-scala-testcontainers . . . . . . . . . . . . . . . . . . .
crypto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
environment_exploration-opaque . . . . . . . . . . . . . . . . . . . . . . . .
simulations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
HelloZio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Parallelism . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
game_theory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
scenarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
logging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
fibers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
prelude . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
std_type_conversions_to_zio . . . . . . . . . . . . . . . . . . . . . . . . . . .
interpreter-chaining_with_previous_result . . . . . . . . . . . . . . . . . .
experiments-src-test-scala-zio_test . . . . . . . . . . . . . . . . . . . . . . .
helloworld . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
experiments-src-test-scala-layers . . . . . . . . . . . . . . . . . . . . . . . .
experiments-src-test-scala-bigdec . . . . . . . . . . . . . . . . . . . . . . . .
virtual_meeting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
zio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
experiments-src-test-scala-time . . . . . . . . . . . . . . . . . . . . . . . . .
zio_intro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
130
132
135
138
142
143
144
146
147
149
151
159
162
172
174
174
180
182
183
185
190
198
203
218
220
221
224
227
229
232
232
235
237
239
240
241
CONTENTS
experiments-src-test-scala-concurrency . . . . . . . . . . . . . . . . . . . . . 247
interpreter-chaining_with_monads . . . . . . . . . . . . . . . . . . . . . . . . 249
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
Superpowers
• Racing
• Timeout
• Error-handling
– Fallback
– Retry
• Repeat
• Parallelism
• Resource Safety
• Mutability that you can trust
• Human-readable
• Cross-cutting Concerns / Observability / Regular Aspects
– timed
– metrics
– debug
– logging
Underlying
•
•
•
•
•
Composability
Success VS failure
Interruption/Cancellation
Fibers
Processor Utilization
– Fairness
– Work-stealing
• Resource Control/Management
• Programs as values
Introduction
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.
Hundreds of students sprint from their 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/
8
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
9
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
10
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 combine 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
11
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.
It can seem like a painfully long process before you begin writing working programs.
// TODO Discuss 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.
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. }}
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:
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
12
Introduction
• 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
Managing Effects
{{ A very high-level overview of what an effects-management system does }}
Who This Book Is For
• Your background
• What to expect
This is not a comprehensive Scala 3 book. For that we recommend Programming in
Scala 3⁴. We expect the kind of basic programming knowledge that allows you to
effectively guess at the meaning of basic Scala code. We explain more complex Scala
syntax as it appears in the book. However, we avoid the use of complex Scala syntax,
as our goal is to teach ZIO in as simple a fashion as possible.
⁴https://www.TODO.com
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
13
Introduction
How to Use This Book
This book is designed to be a self-study guide together with exercises and solutions.
All examples and exercise solutions are available as copyrighted freeware, distributed
via Github⁵. To ensure you have the most current version, this is the official code
distribution site. The README for this repository contains thorough, step-by-step
instructions for setting up your computer to compile and run the examples and
exercise solutions.
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.
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.
⁵https://github.com/EffectOrientedProgramming/EOPCode
⁶\protect\char”007B\relax\protect\char”007B\relax???\protect\char”007D\relax\protect\char”007D\relax
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional?
In your journey to this book, you have undoubtedly learned at least one
way to think about programming, and possibly several.
You might have had reasonable success using that approach.
You might also have encountered programming constructs inspired by functional
programming. For example, Java 8 introduced lambdas along with library support
for streams and functional primitives like map. Python has always allowed functions
to be passed into, be created by, and be returned from other functions, and includes
other functional support. 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 to the trouble to
use something like fold or reduce when a simple for loop will do the job, and be
much easier to understand? 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, it helps to 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. Assembly language had primitive function-like constructs
called subroutines, but you had to do the work of setting up a call (storing arguments
in registers or on the stack), then inside the subroutine you had to access those
arguments, and then before returning from the subroutine you had to somehow pass
the return value back to the caller (again, typically using either a register or the stack).
You had to do all this by hand. It was worth it if you knew you were going to reuse
that subroutine, but if you thought you were only ever going to use that code once
Why Functional?
15
then it was easier and more efficient to just insert 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 hurdle; 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?”
Because languages like C and Pascal made it easy to not only write functions
but to call them, they were a big improvement over assembly language, although
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’d often have to call malloc to allocate memory before
calling a library function, and later free to release the memory when that function
was done with it. You also had to learn how to pass information from one library
function to another. You had to learn how a 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 seemed like a good idea, because it
combined a common data structure, automatic initialization and cleanup of that
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional?
16
data structure, together with all the functions that act upon that data. If you wanted
to reuse some code, you just created an object with some initialization values and
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 and 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
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.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional?
17
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
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:
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional?
18
1. Create basic components that are completely reliable.
2. Combine those components in a way that doesn’t 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
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.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional?
19
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
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))
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.
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. We will look at this support in the [Monads]{{???}} chapter.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional?
20
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.
Immutability During Repetition
{{ This might be moved somewhere else… }}
Many functional programming tutorials begin by introducing recursion. Such tutorials assume you will just accept that recursion is important. This can make the reader
wonder whether the entire language will be filled with what seems like theoretical
exercises.
Any time you perform a repetitive task, you could use recursion, but why would
you? It’s much easier to think about an ordinary looping construct. You just count
through the elements in a sequence and perform operations upon them. Recursion
seems to add needless complexity to an otherwise simple piece of code.
The problem is that recursion is not properly motivated in such tutorials. You must
first understand the need for immutability, then encounter the problem of repetition
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional?
21
and see that your loop variable(s) mutate. How do you get rid of this mutation?
By initializing values but never changing them. To achieve this when you iterate
through a sequence, you can create a new frame for each iteration, and what was
originally a loop variable becomes a value that is initialized to the next step for each
frame.
The stack frame of a function call is already set up to hold arguments and return the
result. Thus, by creating a stack frame for each iteration, we can initialize the next
count of the loop value and never change it within that frame. A recursive function
is an excellent solution for the problem of iterating without a mutating loop variable.
This solution comes with its own problem. By calling itself and creating a new stack
frame for each recursion, a recursive function always has the possibility that it will
exhaust (overflow) the stack. The capacity of the stack depends on the size required
for that particular function along with the hardware, operating system and often the
load on the computer—all factors that make it effectively unpredictable. Having an
unpredictable error occur during recursion does not meet our goal of reliability.
The fix to this issue is a hack called tail recursion, which typically requires the
programmer to organize their code such that the return expression for the recursive
function does not perform any additional calculations, but simply returns a finished
result. When this criterion is met, the compiler is able to rewrite the recursive code
so that it becomes simple imperative code, without the function calls that can lead
to a stack overflow. This produces code that is reliable from the safety standpoint
(it doesn’t overflow the stack) and from an immutability standpoint (there’s no
mutating loop variable).
At this point you might be wondering, “Wait, are you telling me that every time
I want to perform some kind of operation on a sequence, I’m supposed to write
recursive code rather than just an imperative loop?” Although you would certainly
get better at recursion with practice, it does sound exhausting. Fortunately, functional
programming goes one step further by implementing basic repetitive operations
for you, using recursion. This is why you see operations like map, reduce, fold,
etc., instead of loops, in functional languages, or even languages that support a
functional style of programming. These operations allow you to benefit from the
purity of recursion without implementing your own recursive functions except on
rare occasions.
There’s another fascinating factor that recursion exposes. Under the covers, tail
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional?
22
recursion uses mutation—which seems like a violation of functional programming’s
immutability goal. However, because tail recursion is implemented by the compiler,
it can be completely (and provably) invisible. No other code can even know about
any mutable state used to implement tail recursion, much less read or change it.
The concept of immutability only requires that storage be effectively immutable—if
something is mutated (often for efficiency), it’s OK as long as no other part of the
program can be affected by that mutation.
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, often with a brief amount of 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 almost mathematically 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). Because immutability dramatically simplifies everything, objects
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.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Why Functional?
23
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.
We assume that many readers are attracted to this book because they have some
experience with functional programming constructs in other languages such as Java
(version 8 or newer), Kotlin, Python or some other language that provides a modicum
of support. However, we also assume you have heard—or you have a sense—that there
could be significantly more than, for example, a function’s ability to create other
functions, or putting elements into a stream and acting upon that stream with map, or
parallelizing stream operations. 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. A core way
this is accomplished in ZIO is through the use of monads, which we gently introduce
in the next chapter.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Effects
A pure function accepts arguments and produces a result. Nothing more. Because of
this we can combine pure functions and the result will also be a pure function. Pure
functions are basically mathematical functions, and so obey the laws of math. This
means we can reason about pure functions in a mathematical manner.
For example, identical arguments produce the same result every time. It’s as if a pure
function is only a lookup table.
Because of all the benefits of pure functions, we clearly want to use them everywhere
we can. A program composed of pure functions would be very reliable indeed. It
would also have no interactions with the outside world, neither reading from nor
writing to. A program comprised solely of pure functions has no effect upon the
world. Without that, the only evidence that you ran the program is the additional
warmth generated by your computer.
A program that does interact with the world introduces a lot of uncertainty. In
particular, the world is a completely uncertain state, as far as the program is
concerned. The output effects of the program depend on the input effects when the
program runs. No longer are identical inputs producing the same result, every time.
{{???}}
With the addition of all this uncertainty, errors become a serious possibility. Errors
also have an affect on the world (if they do not there is no point to their existence).
The problem is this: functions that affect or are affected by the world do so by directly
contacting the world. In the previous chapter we solved the problem of needing to
return extra information by boxing all necessary information into the function’s
result value, as a monad. In this chapter we solve the problem of effects by boxing
further information into the result monad. Doing so captures the effects so we can
keep an eye on them. We write as many pure functions as possible, and when we
need to do something effect-full, we isolate that in its own function, and limit the
spread of uncertainty. The result is not deterministic, but we have isolated the effects.
This allows us to much more effectively reason about the behavior of our program.
25
Effects
Basics
Consider a function that affects its surroundings:
trait X
object X:
var x: Int = 0
def combine(a: Int, b: Int): Int =
X.x += 1
a + b + X.x
combine(1, 2)
// res0: Int = 4
combine(1, 2)
// res1: Int = 5
Because combine both writes to and reads from the global variable X.x, identical
arguments will not produce the same result every time. combine modifies X.x and
also depends on it to produce its result. combine is not pure.
We want to manage this effect X. We repeat the trick we used in [Monads] but instead
of packaging the return value with failure information, we package it with the type
X:
trait XIO[IO, R]
case class IntXIO(i: Int) extends XIO[X, Int]
def combine2(a: Int, b: Int): XIO[X, Int] =
X.x += 1
IntXIO(a + b + X.x)
combine2(1, 2)
// res2: XIO[X, Int] = IntXIO(i = 6)
At first glance this doesn’t seem to fix anything. combine2 returns an XIO instead of
the simple Int produced by combine. The call to combine2 shows that we still see
the side effect. What have we achieved?
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
26
Effects
We haven’t prevented the side effect produced by the accesses to X.x, but that is
presumably an essential part of the function. What we have done is tracked that
effect by tagging it inside the XIO result. The fact that a side effect occurs is now
tagged inside the type of XIO, and this information persists at runtime.
What can we do with this information?
We need to interpret this effect information at runtime. To achieve this we delay the
evaluation of the program and hand it to an interpreter, which knows what to do
with the effects.
{{ This seems challenging (albeit illuminating). If the example were extremely specific
(say, an interpreter that only knows about IntXIO) perhaps it could work.}}
Effects VS Side-Effects
The distinction between the terms effects and side-effects are important. Each
represents a fundamentally different way of modeling a program.
Side-effecting code observes or changes the world in some way that is not apparent
in the type signature. Effectful code signals this in the type signature.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Unit
The bare minimum of effect tracking
Consider a simple function
def saveInformation(info: Any): Unit = ???
If we consider only 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, but without any specificity.
When a function returns Unit, we know that the result is 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.
Consider a simple WeatherService API:
trait WeatherService:
def forecast(): String
If we do not have access to the implementation source code, there is no way to discern
what effects are needed at compile time. The only way to figure it out is to run the
code and see what happens.
ClosedSourceWeatherService().forecast()
// READ GPS SIGNAL
// NETWORK CALL
// res0: String = "Sunny"
It is possible that we are using entirely open-source or in-house code throughout our
entire application. That means that we could theoretically dig into every function
involved in a complex path and note every effect.
In practice this quickly becomes impossible.
28
Unit
object OpenSourceLibrary:
def sendToService(payload: String): Unit =
println(s"NETWORK: Sending payload")
save(payload)
private def save(userData: String): Unit =
Analytics.demographicsFrom(userData)
println(s"DATABASE: Saving data")
object Analytics:
def demographicsFrom(userData: String): Unit =
println(s"LOGGER: Key demographic found")
def logic(): Unit =
// ...Other calls...
OpenSourceLibrary
.sendToService("Network Payload")
// ...Other calls...
logic()
// NETWORK: Sending payload
// LOGGER: Key demographic found
// DATABASE: Saving data
Here our simple program performs 3 very different side-effects, but everything
is boiled down to the same Unit type. If we extrapolate this is to a production
application with hundreds and thousands of functions, it is overwhelming.
Ideally, we could leverage the type system and the compiler to track the requirements
for arbitrarily complex pieces of code.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
The ZIO Type
We need an Answer about this scenario. The scenario requires things and could
produce an error. trait ZIO[Requirements, Error, Answer]
One downside of these type parameters
The ZIO trait is at the center of our Effect-oriented world.
trait ZIO[R, E, A]
import zio.ZIO
A trait with 3 type parameters can be intimidating, but each one serves a distinct,
important purpose.
R - The Environment
This is the piece that distinguishes the ZIO monad. It indicates which pieces of the
world we will be observing or changing.
import zio.Console
def print(
msg: String
): ZIO[Console, Nothing, Unit] = ???
This type signature tells us that print needs a Console in its environment to execute.
E - The Error
This parameter tells us how this operation might fail.
The ZIO Type
def parse(
contents: String
): ZIO[Any, IllegalArgumentException, Unit] = ???
A - The Result
This is what our code will return if it completes successfully.
def defaultGreeting()
: ZIO[Any, Nothing, String] = ???
Conversions from standard Scala types
ZIO provides simple interop with may of the built-in Scala data types, namely
•
•
•
•
•
Option
Either
Try
scala.concurrent.Future
Promise
And even some Java types • java.util.concurrent.Future
• AutoCloseable
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
30
The ZIO Type
31
import zio.{ZIO, ZIOAppDefault}
import scala.concurrent.Future
import mdoc.unsafeRunPrettyPrint
val zFuture =
ZIO.fromFuture(implicit ec =>
Future.successful("Success!")
)
// zFuture: ZIO[Any, Throwable, String] = Stateful(
//
trace = "repl.MdocSession.MdocApp.zFuture(06_The_ZIO_Type.md:47)",
//
onState = zio.ZIO$$$Lambda$14201/99502783@74ce015f
// )
val zFutureFailed =
ZIO.fromFuture(implicit ec =>
Future.failed(new Exception("Failure :("))
)
// zFutureFailed: ZIO[Any, Throwable, Nothing] = Stateful(
//
trace = "repl.MdocSession.MdocApp.zFutureFailed(06_The_ZIO_Type.md\
:54)",
//
onState = zio.ZIO$$$Lambda$14201/99502783@599410f9
// )
unsafeRunPrettyPrint(zFuture)
// Success!
unsafeRunPrettyPrint(zFutureFailed)
// java.lang.Exception: Failure :(
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
And / Or
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.
import zio.ZIO
trait Error1
trait Error2
def failableFunction()
: ZIO[Any, Error1 | Error2, Unit] = ???
Consider 2 error types
trait UserNotFound
trait PermissionError
In the type system, the most recent ancestor between them is Any.
Unfortunately, you cannot make any meaningful decisions based on this type.
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
33
And / Or
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;
trait User
trait SuperUser
def getUser(
userId: String
): ZIO[UserService, UserNotFound, User] = ???
def getSuperUser(
user: User
): ZIO[UserService, PermissionError, SuperUser] =
???
def loginSuperUser(userId: String): ZIO[
UserService,
UserNotFound | PermissionError,
SuperUser
] =
for
basicUser <- getUser(userId)
superUser <- getSuperUser(basicUser)
yield superUser
trait Status
trait NetworkService
def statusOf(
user: User
): ZIO[NetworkService, UserNotFound, Status] =
???
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
34
And / Or
def check(userId: String): ZIO[
UserService & NetworkService,
UserNotFound,
Status
] =
for
user
<- getUser(userId)
status <- statusOf(user)
yield status
Intersections AKA Products AKA Case Classes
AKA Ands
graph TD;
Any-->User;
Any-->Account;
trait Piece1
trait Piece2
def needyFunction()
: ZIO[Piece1 & Piece1, Nothing, Unit] = ???
For your Answer, it can be desirable to give a clear name that is relevant to your
domain.
The requirements for each ZIO are combined as an anonymous product type denoted
by the & symbol.
trait AccountService
trait UserService
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
35
And / Or
import zio.ZIO
trait Account
trait AccountError
def userToAccount(
user: User
): ZIO[AccountService, AccountError, Account] =
???
def getAccount(userId: String): ZIO[
UserService & AccountService,
AccountError | UserNotFound,
Account
] =
for
user
<- getUser(userId)
account <- userToAccount(user)
yield account
case class SomeServices(userService: UserService, accountService: Accou\
ntService)
//trait SomeServices extends UserService with AccountService
You have the ability to handle all the possible errors from your logic without needing
to create a new name that encompasses all of them.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Built-in Services
Some Services are considered fundamental/primitive by ZIO. They are built-in to the
runtime and available to any program.
1.x
Originally, ZIO merely provided default implementations of these services. There
was nothing else special about them. If you wanted to write to the console in your
code, you needed a ZIO[Console, _, _]. If you wanted to write random,
timestamped numbers, accompanied by some system information to the
console, you needed a ZIO[Random with Clock with System with Console, _,
_]. This is maximally informative, but it is also a lot of boilerplate code.
2.x
Late in the development of ZIO 2.0, the team decided to bake these deeper into the
runtime. Now you can use any of these services without an impact on your method
signatures. This reduces boilerplate, with a trade-off. You can no longer discern which
piece of the Environment/Runtime is being accessed by reading the signature.
Console
NON-MDOC examples throughout this file after 2.0.0 upgrade. TODO Fix before
release
The Unprincipled Way
This is generally the first effect that we will want as we learn to construct functional
programs. It is so basic that most languages do not consider it as anything special.
The typical first scala program is something like:
println("Hi there.")
// Hi there.
Simple enough, and familiar to anyone that has programmed before. Take a look at
the signature of this function in the Scala Predef object:
def println(x: Any): Unit = ???
Based on the name, it is likely that the Console is involved. Unfortunately the type
signature does not indicate that. If we do not have access to the implementation
source code, this is a surprise to us at runtime.
Building a Better Way
Before looking at the official ZIO implementation, we will create a simpler version.
TODO: Decide whether explaining this pattern belongs in a standalone section. It
is important in isolation, but probably hard to appreciate without a use-case, and
Console is likely the simplest example.
The pattern used here is fundamental to designing composable, ergonomic ZIO
Services.
38
Console
1.
2.
3.
4.
Create a trait with the needed functions.
Create an implementation of the trait.
(Optional) Put “accessor” methods in trait companion object.
(Optional) Provide implementation instance in a Layer as a object field - live.
We will go through each of these steps in detail in this chapter, and more concisely in
the rest. Steps 1 and 2 steps will be familiar to many programmers. Steps 3 and 4 are
less familiar, and a bit harder to appreciate. We endeavor in the following chapters
to make a compelling case for them. If we succeed, the reader will add them when
creating their own Effects.
One: Create the trait
This trait represents a piece of the Environment that our codes need to interact
with. It contains the methods for effectful interactions.
import zio.ZIO
trait Console:
def printLine(
output: String
): ZIO[Any, Nothing, Unit]
Two: Create the implementation
object ConsoleLive extends Console:
def printLine(
output: String
): ZIO[Any, Nothing, Unit] =
// TODO Get this working without Predef
ZIO.succeed(Predef.println(output))
TODO{Determine how to best split the 2 pieces we need to add to the same object
for these steps}
Three: Create Accessor Methods in Companion
The first two steps are enough for us to track Effects in our system, but the ergonomics
are not great.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
39
Console
val logicClunky: ZIO[Console, Nothing, Unit] =
for
_ <ZIO.serviceWithZIO[Console](
_.printLine("Hello")
)
_ <ZIO.serviceWithZIO[Console](
_.printLine("World")
)
yield ()
import mdoc.unsafeRunPrettyPrint
import zio.ZLayer
unsafeRunPrettyPrint(
logicClunky.provide(
ZLayer.succeed[Console](ConsoleLive)
)
)
// Hello
// World
// ()
The caller has to handle the ZIO environment access, which is a distraction from the
logic they want to implement.
// TODO Consider deleting this entirely
// TODO remove alt companions and make top-level
// functions
object ConsoleWithAccessor:
def printLine(
variable: => String
): ZIO[Console, Nothing, Unit] =
ZIO.serviceWith(_.printLine(variable))
With this function, our callers have a much nicer experience.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
40
Console
val logic: ZIO[Console, Nothing, Unit] =
for
_ <- ConsoleWithAccessor.printLine("Hello")
_ <- ConsoleWithAccessor.printLine("World")
yield ()
However, providing dependencies to the logic is still tedious.
import zio.ZLayer
import zio.Runtime.default.unsafe
unsafeRunPrettyPrint(
logic.provide(
ZLayer.succeed[Console](ConsoleLive)
)
)
// ()
Four: Create object Effect.live field
Rather than making each caller wrap our instance in a Layer, we can do that a single
time in our companion.
import zio.ZLayer
object ConsoleWithLayer:
val live: ZLayer[Any, Nothing, Console] =
ZLayer.succeed[Console](ConsoleLive)
Now executing our code is as simple as describing it.
unsafeRunPrettyPrint(
logic.provide(ConsoleWithLayer.live)
)
// ()
In real application, both of these will go in the companion object directly.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
41
Console
import zio.ZLayer
object Console:
def printLine(
variable: => String
): ZIO[Console, Nothing, Unit] =
ZIO.serviceWith(_.printLine(variable))
val live: ZLayer[Any, Nothing, Console] =
ZLayer.succeed[Console](ConsoleLive)
Official ZIO Approach
TODO
ZIO Super-Powers
Single expression debugging
When debugging code, we often want to stick a println among our logic.
def crunch(a: Int, b: Int) = (a * 2) / (a * 10)
Historically, this has caused friction for chained expressions. We must surround our
expression in braces, in order to add this statement before it. TODO Disclaimer that
this is less compelling in a “fewer braces” world
def crunchDebugged(a: Int, b: Int) =
println("")
a * a
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
42
Console
unsafeRunPrettyPrint(
ZIO.debug("ping") *>
ConsoleLive.printLine("Normal logic")
)
// ping
// Normal logic
// ()
object ConsoleSanitized extends Console:
private val socialSecurity =
"\\d{3}-\\d{2}-\\d{4}"
def printLine(
output: String
): ZIO[Any, Nothing, Unit] =
val sanitized =
output.replaceAll(
socialSecurity,
"***-**-****"
)
ConsoleLive.printLine(sanitized)
val leakSensitiveInfo
: ZIO[Console, java.io.IOException, Unit] =
zio
.Console
.printLine("Customer SSN is 000-00-0000")
unsafeRunPrettyPrint(
leakSensitiveInfo.provide(
ZLayer.succeed[Console](ConsoleSanitized)
)
)
// Customer SSN is 000-00-0000
// ()
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
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
43
Console
experiments/src/main/scala/console/FakeConsole.scala
package console
import zio._
import zio.Console
import zio.Console._
import java.io.IOException
object FakeConsole:
val name: Console = single("(default name)")
val word: Console
= single("Banana")
val number: Console = single("1")
def single(hardcodedInput: String) =
new Console:
def print(line: => Any)(implicit
trace: zio.Trace
): zio.IO[java.io.IOException, Unit] =
ZIO.succeed(print("Hard-coded: " + line))
def printError(line: => Any)(implicit
trace: zio.Trace
): zio.IO[java.io.IOException, Unit] = ???
def printLine(line: => Any)(implicit
trace: zio.Trace
): zio.IO[java.io.IOException, Unit] =
ZIO.succeed(
println("Hard-coded: " + line)
)
def printLineError(line: => Any)(implicit
trace: zio.Trace
): zio.IO[java.io.IOException, Unit] = ???
def readLine(implicit
trace: zio.Trace
): zio.IO[java.io.IOException, String] =
ZIO.succeed(hardcodedInput)
def withInput(
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
44
Console
hardcodedInput: String*
): ZIO[Any, Nothing, Console] =
for inputVariable <Ref.make(hardcodedInput.toSeq)
yield inputConsole(inputVariable)
private def inputConsole(
hardcodedInput: Ref[Seq[String]]
) =
new Console:
def print(line: => Any)(implicit
trace: zio.Trace
): zio.IO[java.io.IOException, Unit] =
ZIO.succeed(print(line))
def printError(line: => Any)(implicit
trace: zio.Trace
): zio.IO[java.io.IOException, Unit] = ???
def printLine(line: => Any)(implicit
trace: zio.Trace
): zio.IO[java.io.IOException, Unit] =
ZIO
.succeed(println("Automated: " + line))
def printLineError(line: => Any)(implicit
trace: zio.Trace
): zio.IO[java.io.IOException, Unit] = ???
def readLine(implicit
trace: zio.Trace
): zio.IO[java.io.IOException, String] =
for
curInput <- hardcodedInput.get
_ <- hardcodedInput.set(curInput.tail)
yield curInput.head
end FakeConsole
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Mutability
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.
import zio.UIO
trait RefZ[A]:
def get: UIO[A]
def update(a: A => A): UIO[Unit]
Less obviously, we also need to create the Mutable reference itself. We are changing
the world, by creating a space that we can manipulate. This operation can live in the
companion object:
object RefZ:
def make[A](a: A): UIO[RefZ[A]] = ???
In order to confidently use this, 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
Unreliable Counting
46
Mutability
import zio.{Ref, ZIO}
import mdoc.unsafeRunPrettyPrint
object UnreliableCounting:
var counter = 0
val increment =
ZIO.succeed {
counter = counter + 1
}
val logic =
for _ <ZIO.foreachParDiscard(Range(0, 100000))(
_ => increment
)
yield "Final count: " + counter
unsafeRunPrettyPrint(UnreliableCounting.logic)
// Final count: 99971
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 Demo/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
47
Mutability
object ReliableCounting:
def incrementCounter(counter: Ref[Int]) =
counter.update(_ + 1)
val logic =
for
counter <- Ref.make(0)
_ <ZIO.foreachParDiscard(Range(0, 100000))(
_ => incrementCounter(counter)
)
finalResult <- counter.get
yield "Final count: " + finalResult
unsafeRunPrettyPrint(ReliableCounting.logic)
// 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: We have updated our
count!")
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
48
Mutability
object SideEffectingUpdates:
val logic =
for
counter <- Ref.make(0)
_ <ZIO.foreachParDiscard(Range(0, 4))(_ =>
counter.update { previousValue =>
expensiveCalculation()
sendNotification()
previousValue + 1
}
)
finalResult <- counter.get
yield "Final count: " + finalResult
unsafeRunPrettyPrint(SideEffectingUpdates.logic)
// Alert: We have updated our count!
// Alert: We have updated our count!
// Alert: We have updated our count!
// Alert: We have updated our count!
// Alert: We have updated our count!
// Alert: We have updated our count!
// Alert: We have updated our count!
// Alert: We have updated our count!
// Alert: We have updated our count!
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
49
Mutability
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
object SideEffectingUpdatesSync:
val logic =
for
counter <- Ref.Synchronized.make(0)
_ <ZIO.foreachParDiscard(Range(0, 4))(_ =>
counter.update { previousValue =>
expensiveCalculation()
sendNotification()
previousValue + 1
}
)
finalResult <- counter.get
yield "Final count: " + finalResult
unsafeRunPrettyPrint(
SideEffectingUpdatesSync.logic
)
// Alert: We have updated our count!
// Alert: We have updated our count!
// Alert: We have updated our count!
// Alert: We have updated our count!
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.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
50
Mutability
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/mutability/ComplexRefs.scala
package mutability
import zio.{Ref, ZIO, ZIOAppDefault}
object ComplexRefs extends ZIOAppDefault:
class Sensor(lastReading: Ref[SensorData]):
def read: ZIO[Any, Nothing, SensorData] =
zio
.Random
.nextIntBounded(10)
.map(SensorData(_))
object Sensor:
val make: ZIO[Any, Nothing, Sensor] =
for lastReading <- Ref.make(SensorData(0))
yield Sensor(lastReading)
case class SensorData(value: Int)
case class World(sensors: List[Sensor])
val readFromSensors =
for
sensors <ZIO.foreach(List.fill(100)(0))(_ =>
Sensor.make
)
world = World(sensors)
_ <ZIO
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
51
Mutability
.foreach(world.sensors)(_.read)
.debug("Current data: ")
yield ()
def run = readFromSensors
end ComplexRefs
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Time
Time based functions are effectful because they rely on a variable that is constantly
changing.
Your program displays 2 sections: Summary -Time range -totalNumberOfTransactions
-All Participants
Details
- List[Transaction]
Show how these can be out of sync with unprincipled Clock access
.now()
How often it is overlooked/minimized “Race Condition” vs “race operation” Example
possibilities - Progress bar - query(largeRange) followed by query(smallRange), and
getting new results in the 2nd call
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/time/OutOfSync.scala
53
Time
package time
import java.time.{Instant, Period}
import zio.{IO, UIO, ZIO, ZIOAppDefault}
object OutOfSync
// TODO Consider deduping User throughout the book
case class Post(content: String)
case class Summary(numberOfPosts: Int)
case class TransactionDetails(
transactions: Seq[Post]
)
object User:
case class User(name: String)
val frop = User("Frop")
val zeb
= User("Zeb")
val shtep = User("Shtep")
val cheep = User("Cheep")
import time.User.*
case class UserUI(
user: User,
summary: Summary,
transactionDetails: Seq[Post]
)
object TimeIgnorant:
private var summaryCalledTime
: Option[Instant] = None
def summaryFor(
participant: User
): UIO[Summary] =
summaryCalledTime match
case Some(value) =>
()
case None =>
summaryCalledTime = Some(Instant.now())
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
54
Time
ZIO.succeed(Summary(1))
def postsBy(
participant: User
): IO[String, Seq[Post]] =
val executionTimeStamp = Instant.now()
for
_ <ZIO
.getOrFailWith(
"Must call summary before posts"
)(summaryCalledTime)
.flatMap(timeStamp =>
ZIO.debug(
"Summary called: " + timeStamp
)
)
_ <ZIO.debug(
"Getting posts: " + executionTimeStamp
)
yield Seq(Post("Hello!"), Post("Goodbye!"))
end postsBy
end TimeIgnorant
object DemoSyncIssues extends ZIOAppDefault:
def run =
for
summary <- TimeIgnorant.summaryFor(shtep)
transactions <- TimeIgnorant.postsBy(shtep)
uiContents =
UserUI(shtep, summary, transactions)
_ <- zio.Console.printLine(uiContents)
yield ()
experiments/src/main/scala/time/ScheduledValues.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
55
Time
package time
import
import
import
import
import
import
import
zio.Duration
zio.Clock
zio.ZIO
zio.URIO
zio.Schedule
zio.ExitCode
zio.durationInt
import java.util.concurrent.TimeUnit
import java.time.Instant
import scala.concurrent.TimeoutException
import javawrappers.InstantOps.plusZ
/*
*
*
*
*
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 */
// 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
]
] =
for
startTime <- Clock.instant
timeTable =
createTimeTableX(
startTime,
value,
values* // Yay Scala3 :)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
56
Time
)
yield accessX(timeTable)
// TODO Some comments, tests, examples, etc to
// make this function more obvious
private[time] 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)
) =>
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[time] def accessX[A](
timeTable: Seq[ExpiringValue[A]]
): ZIO[Any, TimeoutException, A] =
for
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
57
Time
now <- Clock.instant
result <ZIO.getOrFailWith(
new TimeoutException("TOO LATE")
) {
timeTable
.find(_.expirationTime.isAfter(now))
.map(_.value)
}
yield result
private case class ExpiringValue[A](
expirationTime: Instant,
value: A
)
experiments/src/main/scala/time/TimedTapTap.scala
package time
import zio.*
import zio.Console.*
val longRunning =
ZIO.sleep(5.seconds) *> printLine("done")
val runningNotifier =
(
ZIO.sleep(1.seconds) *>
printLine("Still running")
).onInterrupt {
printLine("finalized").orDie
}
object TimedTapTapJames extends ZIOAppDefault:
def run =
for
lr <- longRunning.fork
_ <- runningNotifier.fork
_ <- lr.join
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
58
Time
yield ()
object TimedTapTapBill extends ZIOAppDefault:
def run =
longRunning
.race(runningNotifier *> ZIO.never)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Environment Variables
Historic Approach
Environment Variables are a common way of providing dynamic and/or sensitive
data to your running application. A basic use-case looks like this:
val apiKey = sys.env.get("API_KEY")
// apiKey: Option[String] = Some(value = "SECRET_API_KEY")
This seems rather innocuous; however, it can be an annoying source of problems as
your project is built and deployed across different environments. Given this API:
trait HotelApi:
def cheapest(
zipCode: String,
apiKey: String
): Either[Error, Hotel]
case class Hotel(name: String)
case class Error(msg: String)
To augment the built-in environment function, we will create a wrapper.
def envRequiredUnsafe(
variable: String
): Either[Error, String] =
sys
.env
.get(variable)
.toRight(Error("Unconfigured Environment"))
toRight is an Option method that turns the Option into an Either.
Our business logic now looks like this:
Environment Variables
60
def fancyLodgingUnsafe(
hotelApi: HotelApi
): Either[Error, Hotel] =
for
apiKey <- envRequiredUnsafe("API_KEY")
hotel <- hotelApi.cheapest("90210", apiKey)
yield hotel
When you look up an Environment Variable, you are accessing information that was
not passed into your function as an explicit argument. Now we will simulate running
the function with the same arguments in 3 different environments.
Your Machine:
fancyLodgingUnsafe(HotelApiImpl)
// res0: Either[Error, Hotel] = Right(
//
value = Hotel(name = "Eddy's Roach Motel")
// )
Collaborator’s Machine:
fancyLodgingUnsafe(HotelApiImpl)
// res2: Either[Error, Hotel] = Left(
//
value = Error(msg = "Invalid API Key")
// )
Continuous Integration Server:
fancyLodgingUnsafe(HotelApiImpl)
// res4: Either[Error, Hotel] = Left(
//
value = Error(
//
msg = "Unconfigured Environment"
//
)
// )
On your own machine, everything works as expected. However, your collaborator
has a different value stored in this variable, and gets a failure when they execute this
code. Finally, the CI server has not set any value, and fails at runtime.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Environment Variables
61
Building a Better Way
Before looking at the official ZIO implementation of System, we will create a
less-capable version. We need a trait that will indicate what is needed from the
environment. The real implementation is a bit more complex, to handle corner cases.
import zio.ZIO
trait System:
def env(
variable: String
): ZIO[Any, Nothing, Option[String]]
Now, our live implementation will wrap our original, unsafe function call. For easier
usage by the caller, we also create an accessor.
import zio.ZLayer
object System:
object Live extends System:
def env(
variable: String
): ZIO[Any, Nothing, Option[String]] =
ZIO.succeed(sys.env.get("API_KEY"))
val live: ZLayer[Any, Nothing, System] =
ZLayer.succeed(Live)
def env(
variable: => String
): ZIO[System, Nothing, Option[String]] =
ZIO.serviceWithZIO[System](_.env(variable))
Now if we use this code, our caller’s type tells us that it requires a System to execute.
This is safe, but it is not the easiest code to use or read. We then build on first accessor
to flatten out the function signature.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Environment Variables
trait SystemStrict:
def envRequired(
variable: String
): ZIO[Any, Error, String]
object SystemStrict:
val live
: ZLayer[System, Nothing, SystemStrict] =
ZLayer
.fromZIO(ZIO.service[System].map(Live(_)))
def envRequired(
variable: String
): ZIO[SystemStrict, Error, String] =
ZIO.serviceWithZIO[SystemStrict](
_.envRequired(variable)
)
case class Live(system: System)
extends SystemStrict:
def envRequired(
variable: String
): ZIO[Any, Error, String] =
for
variableAttempt <- system.env(variable)
res <ZIO
.fromOption(variableAttempt)
.mapError(_ =>
Error("Unconfigured Environment")
)
yield res
end SystemStrict
Similarly, we wrap our API in one that leverages ZIO.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
62
Environment Variables
63
trait HotelApiZ:
def cheapest(
zipCode: String
): ZIO[Any, Error, Hotel]
object HotelApiZ:
def cheapest(zipCode: String): ZIO[
SystemStrict with HotelApiZ,
Error,
Hotel
] =
ZIO.serviceWithZIO[HotelApiZ](
_.cheapest(zipCode)
)
case class Live(system: SystemStrict)
extends HotelApiZ:
def cheapest(
zipCode: String
): ZIO[Any, Error, Hotel] =
for
apiKey <- system.envRequired("API_KEY")
res <ZIO.fromEither(
HotelApiImpl
.cheapest(zipCode, apiKey)
)
yield res
val live: ZLayer[
SystemStrict,
Nothing,
HotelApiZ
] =
ZLayer.fromZIO(
ZIO.service[SystemStrict].map(Live(_))
)
end HotelApiZ
This helps us keep a flat Error channel when we write our domain logic.
This was quite a process; where did it get us? Our fully ZIO-centric, side-effect-free
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Environment Variables
64
logic looks like this:
val fancyLodging: ZIO[
SystemStrict with HotelApiZ,
Error,
Hotel
] =
for hotel <- HotelApiZ.cheapest("90210")
yield hotel
// fancyLodging: ZIO[SystemStrict & HotelApiZ, Error, Hotel] = OnSucces\
s(
//
trace = "repl.MdocSession.MdocApp.fancyLodging(12_Environment_Vari\
ables.md:262)",
//
first = OnSuccess(
//
trace = "repl.MdocSession.MdocApp.HotelApiZ.cheapest(12_Environm\
ent_Variables.md:226)",
//
first = Sync(
//
trace = "repl.MdocSession.MdocApp.HotelApiZ.cheapest(12_Enviro\
nment_Variables.md:226)",
//
eval = zio.ZIOCompanionVersionSpecific$$Lambda$14266/136541955\
2@2c0f4f23
//
),
//
successK = zio.ZIO$$$Lambda$14242/2025822708@2b1a7c5c
//
),
//
successK = zio.ZIO$$Lambda$14229/567850394@7c462daa
// )
Original, unsafe:
def fancyLodgingUnsafe(
hotelApi: HotelApi
): Either[Error, Hotel] =
for
apiKey <- envRequiredUnsafe("API_KEY")
hotel <- hotelApi.cheapest("90210", apiKey)
yield hotel
The logic is identical to our original implementation! The only difference is the result
type. It now reports the System and HotelApiZ dependencies of our function.
This is what it looks like in action:
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Environment Variables
import zio.ZLayer
import mdoc.unsafeRunPrettyPrint
import mdoc.unsafeRunPrettyPrintValue
Your Machine:
// TODO Do this for CI environment too
val originalAuthor = HotelApiZ.live
unsafeRunPrettyPrint(
fancyLodging.provideLayer(
System.live >>> SystemStrict.live >+>
originalAuthor
)
)
// Hotel(Eddy's Roach Motel)
Collaborator’s Machine:
// TODO Do this for CI environment too
val collaborater = HotelApiZ.live
val colaboraterLayer =
collaborater ++ System.live
unsafeRunPrettyPrint(
fancyLodging.provideLayer(
System.live >>> SystemStrict.live >+>
collaborater
)
)
// Error(Invalid API Key)
Continuous Integration Server:
val ci = HotelApiZ.live
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
65
Environment Variables
66
unsafeRunPrettyPrint(
fancyLodging.provideLayer(
System.live >>> SystemStrict.live >+> ci
)
)
// Error(Unconfigured Environment)
TODO{{The actual line looks the same, which I highlighted as a problem before. How
should we indicate that the Environment is different?}}
When constructed this way, it becomes very easy to test. We create a second
implementation that accepts test values and serves them to the caller.
case class SystemHardcoded(
environmentVars: Map[String, String]
) extends System:
def env(
variable: String
): ZIO[Any, Nothing, Option[String]] =
ZIO.succeed(environmentVars.get(variable))
We can now provide this to our logic, for testing both the success and failure cases.
val testApiLayer =
ZLayer.succeed[System](
SystemHardcoded(
Map("API_KEY" -> "Invalid Key")
)
) >>> SystemStrict.live >+> HotelApiZ.live
import mdoc.unsafeRunPrettyPrint
unsafeRunPrettyPrint(
fancyLodging.provide(testApiLayer)
)
// Error(Invalid API Key)
Official ZIO Approach
ZIO provides a more complete System API in the zio.System
TODO
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Environment Variables
import zio.System
def fancyLodgingZ(): ZIO[
zio.System,
SecurityException,
Either[Error, Hotel]
] =
for apiKey <- zio.System.env("API_KEY")
yield HotelApiImpl.cheapest(
"90210",
apiKey.get // unsafe! TODO Use either
)
Exercises
import zio.test.TestSystem
import zio.test.TestSystem.Data
Exercise 1: Create a function will report missing Environment Variables as
NoSuchElementException failures, instead of an Option success case.
trait Exercise1:
def envOrFail(variable: String): ZIO[
zio.System,
SecurityException | NoSuchElementException,
String
]
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
67
Environment Variables
val exercise1case1 =
unsafeRunPrettyPrintValue(
Exercise1Solution
.envOrFail("key")
.provide(
TestSystem.live(
Data(envs = Map("key" -> "value"))
)
)
)
// value
// exercise1case1: String = "value"
assert(exercise1case1 == "value")
val exercise1case2 =
unsafeRunPrettyPrintValue(
Exercise1Solution
.envOrFail("key")
.catchSome {
case _: NoSuchElementException =>
ZIO.succeed("Expected Error")
}
.provide(
TestSystem.live(Data(envs = Map()))
)
)
// Expected Error
// exercise1case2: String = "Expected Error"
assert(exercise1case2 == "Expected Error")
Exercise 2: Create a function will attempt to parse a value as an Integer
and report errors as a NumberFormatException.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
68
Environment Variables
trait Exercise2:
def envInt(variable: String): ZIO[
Any,
NoSuchElementException |
NumberFormatException,
Int
]
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
69
Random
There
{{Subject Dependencies: Console, ZIO.serviceWith}}
TODO All the prose to justify these hoops
NOTE Moved code to experiments/src/main/scala/random due to dependency
on code not in Chapters
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/random/Examples.scala
package random
import scala.util.Random
def rollDice(): Int = Random.nextInt(6) + 1
@main
def randNumEx =
println(rollDice())
println(rollDice())
enum GameState:
case InProgress(roundResult: String)
case Win
case Lose
71
Random
def scoreRound(input: Int): GameState =
input match
case 6 =>
GameState.Win
case 1 =>
GameState.Lose
case _ =>
GameState.InProgress("Attempt: " + input)
def fullRound(): GameState =
val roll = rollDice()
scoreRound(roll)
@main
def playASingleRound() = println(fullRound())
import zio.ZIO
val rollDiceZ
: ZIO[RandomBoundedInt, Nothing, Int] =
RandomBoundedInt.nextIntBetween(1, 7)
import zio.{ZIO, ZIOAppDefault}
object RollTheDice extends ZIOAppDefault:
val logic =
for
roll <- rollDiceZ
_
<- ZIO.debug(roll)
yield ()
def run =
logic.provideLayer(RandomBoundedInt.live)
val fullRoundZ
: ZIO[RandomBoundedInt, Nothing, GameState] =
for roll <- rollDiceZ
yield scoreRound(roll)
// The problem above is that you can test the winner logic completely s\
eparate from the random number generator.
// The next example cannot be split so easily.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
72
Random
import zio.Ref
val threeChances =
for
remainingChancesR <- Ref.make(3)
finalGameResult <(
for
roll <- rollDiceZ
remainingChances <remainingChancesR.getAndUpdate(_ - 1)
yield
if (remainingChances == 0)
GameState.Lose
else
scoreRound(roll)
).repeatWhile {
case GameState.InProgress(_) =>
true
case _ =>
false
}
_ <ZIO.debug(
"Final game result: " + finalGameResult
)
yield ()
object ThreeChances extends ZIOAppDefault:
def run =
threeChances.provide(
RandomBoundedIntFake.apply(Seq(2, 5, 6))
)
object LoseInTwoChances extends ZIOAppDefault:
def run =
threeChances.provide(
RandomBoundedIntFake.apply(Seq(2, 1))
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
73
Random
experiments/src/main/scala/random/RandomBoundedInt.scala
package random
import zio.{Tag, UIO, ZIO, ZIOAppArgs}
import scala.util.Random
trait RandomBoundedInt:
def nextIntBetween(
minInclusive: Int,
maxExclusive: Int
): UIO[Int]
import zio.{UIO, ZIO, ZLayer}
import scala.util.Random
object RandomBoundedInt:
def nextIntBetween(
minInclusive: Int,
maxExclusive: Int
): ZIO[RandomBoundedInt, Nothing, Int] =
ZIO.serviceWithZIO[RandomBoundedInt](
_.nextIntBetween(
minInclusive,
maxExclusive
)
)
object RandomBoundedIntLive
extends RandomBoundedInt:
override def nextIntBetween(
minInclusive: Int,
maxExclusive: Int
): UIO[Int] =
ZIO.succeed(
Random
.between(minInclusive, maxExclusive)
)
val live
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
74
Random
: ZLayer[Any, Nothing, RandomBoundedInt] =
ZLayer.succeed(RandomBoundedIntLive)
end RandomBoundedInt
experiments/src/main/scala/random/RandomBoundedIntFake.
package random
import zio.{Ref, UIO, ZIO, ZLayer}
class RandomBoundedIntFake private (
values: Ref[Seq[Int]]
) extends RandomBoundedInt:
def nextIntBetween(
minInclusive: Int,
maxExclusive: Int
): UIO[Int] =
for
remainingValues <- values.get
nextValue <if (remainingValues.isEmpty)
ZIO.die(
new Exception(
"Did not provide enough values!"
)
)
else
ZIO.succeed(remainingValues.head)
_ <- values.set(remainingValues.tail)
yield remainingValues.head
end RandomBoundedIntFake
object RandomBoundedIntFake:
def apply(
values: Seq[Int]
): ZLayer[Any, Nothing, RandomBoundedInt] =
ZLayer.fromZIO(
for valuesR <- Ref.make(values)
yield new RandomBoundedIntFake(valuesR)
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
75
Random
experiments/src/main/scala/random/RandomGuessingGame.sc
package random
import zio.{Console, UIO, Unsafe, ZIO, ZLayer}
//import console.FakeConsole
import zio.Runtime.default.unsafe
val low = 1
/* val high = 10
*
* val prompt =
* s"Pick a number between $low and $high: "
*
* // TODO Determine how to handle .toInt failure
* // possibility def checkAnswer( answer: Int,
* guess: String ): String =
* if answer == guess.toInt then "You got it!"
* else s"BZZ Wrong!! Answer was $answer"
*
* val sideEffectingGuessingGame =
* for _ <- Console.print(prompt) answer =
* scala.util.Random.between(low, high) guess <* Console.readLine response =
* checkAnswer(answer, guess) yield prompt +
* guess + "\n" + response
*
* @main def runSideEffectingGuessingGame =
* Unsafe.unsafe { (u: Unsafe) => given Unsafe =
* u unsafe .run(
* sideEffectingGuessingGame.provideLayer(
* ZLayer.succeed(FakeConsole.single("3")) ) )
* .getOrThrowFiberFailure() }
*
* import zio.Console.printLine
*
* val effectfulGuessingGame =
* for _ <- Console.print(prompt) answer <* RandomBoundedInt.nextIntBetween(low, high)
* guess <- Console.readLine response =
* checkAnswer(answer, guess) yield prompt +
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
76
Random
*
*
*
*
*
*
*
*
*
guess + "\n" + response
@main def runEffectfulGuessingGame =
Unsafe.unsafe { (u: Unsafe) => given Unsafe =
u unsafe .run(
effectfulGuessingGame.provideLayer( ZLayer
.succeed(FakeConsole.single("3")) ++
RandomBoundedInt.live ) )
.getOrThrowFiberFailure() } */
experiments/src/main/scala/random/RandomZIOFake.scala
package random
import zio.{
BuildFrom,
Chunk,
Console,
Random,
UIO,
ZIO,
ZLayer,
Trace
}
import zio.Console.printLine
import java.util.UUID
class RandomZIOFake(i: Int) extends Random:
def nextUUID(implicit
trace: Trace
): UIO[UUID] = ???
def nextBoolean(implicit
trace: zio.Trace
): zio.UIO[Boolean] = ???
def nextBytes(length: => Int)(implicit
trace: zio.Trace
): zio.UIO[zio.Chunk[Byte]] = ???
def nextDouble(implicit
trace: zio.Trace
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
77
Random
): zio.UIO[Double] = ???
def nextDoubleBetween(
minInclusive: => Double,
maxExclusive: => Double
)(implicit trace: zio.Trace): zio.UIO[Double] =
???
def nextFloat(implicit
trace: zio.Trace
): zio.UIO[Float] = ???
def nextFloatBetween(
minInclusive: => Float,
maxExclusive: => Float
)(implicit trace: zio.Trace): zio.UIO[Float] =
???
def nextGaussian(implicit
trace: zio.Trace
): zio.UIO[Double] = ???
def nextInt(implicit
trace: zio.Trace
): zio.UIO[Int] = ???
def nextIntBetween(
minInclusive: => Int,
maxExclusive: => Int
)(implicit trace: zio.Trace): zio.UIO[Int] =
???
def nextIntBounded(n: => Int)(implicit
trace: zio.Trace
): zio.UIO[Int] = ???
def nextLong(implicit
trace: zio.Trace
): zio.UIO[Long] = ???
def nextLongBetween(
minInclusive: => Long,
maxExclusive: => Long
)(implicit trace: zio.Trace): zio.UIO[Long] =
???
def nextLongBounded(n: => Long)(implicit
trace: zio.Trace
): zio.UIO[Long] = ???
def nextPrintableChar(implicit
trace: zio.Trace
): zio.UIO[Char] = ???
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
78
Random
def nextString(length: => Int)(implicit
trace: zio.Trace
): zio.UIO[String] = ???
def setSeed(seed: => Long)(implicit
trace: zio.Trace
): zio.UIO[Unit] = ???
def shuffle[A, Collection[+Element]
<: Iterable[Element]](
collection: => Collection[A]
)(implicit
bf: BuildFrom[Collection[A], A, Collection[
A
]],
trace: Trace
): UIO[Collection[A]] = ???
end RandomZIOFake
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Hello Failures
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
Historic approaches to 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
class GpsException()
extends RuntimeException
class NetworkException() extends RuntimeException
enum Scenario:
case Success,
NetworkError,
GPSError
def displayTemperature(
behavior: Scenario
): String =
if (behavior == Scenario.GPSError)
throw new GpsException()
else if (behavior == Scenario.NetworkError)
throw new NetworkException()
else
"35 degrees"
Hello Failures
80
def currentTemperatureUnsafe(
behavior: Scenario
): String =
"Temperature: " + displayTemperature(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.
// 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.displayTemperature(14_Hello_Failures.md\
:25)
//
at repl.MdocSession$MdocApp.currentTemperatureUnsafe(14_Hello_Failu\
res.md:35)
//
at repl.MdocSession$MdocApp.$init$$$anonfun$1(14_Hello_Failures.md:\
46)
We could take the bare-minimum approach of catching the Exception and returning
null:
def currentTemperatureNull(
behavior: Scenario
): String =
try
"Temperature: " +
displayTemperature(behavior)
catch
case (ex: RuntimeException) =>
"Temperature: " + 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
Hello Failures
81
def currentTemperature(
behavior: Scenario
): String =
try
"Temperature: " +
displayTemperature(behavior)
catch
case (ex: RuntimeException) =>
"Temperature: -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 =
try
"Temperature: " +
displayTemperature(behavior)
catch
case (ex: RuntimeException) =>
"Temperature 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
Hello Failures
82
def currentTemperature(
behavior: Scenario
): String =
try
"Temperature: " +
displayTemperature(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:
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.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Hello Failures
83
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.
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}}
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Hello Failures
84
• ZIO Error Handling
• Wrapping Legacy Code
ZIO-First Error Handling
import zio.ZIO
import mdoc.unsafeRunPrettyPrint
def getTemperatureZ(behavior: Scenario): ZIO[
Any,
GpsException | NetworkException,
String
] =
if (behavior == Scenario.GPSError)
ZIO.fail(new GpsException())
else if (behavior == Scenario.NetworkError)
// TODO Use a non-exceptional error
ZIO.fail(new NetworkException())
else
ZIO.succeed("30 degrees")
unsafeRunPrettyPrint(
getTemperatureZ(Scenario.Success)
)
// 30 degrees
// TODO make MDoc:fail adhere to line limits?
unsafeRunPrettyPrint(
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
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Hello Failures
85
unsafeRunPrettyPrint(
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 displayTemperature
{{TODO }}
import zio.{Task, ZIO}
def displayTemperatureZWrapped(
behavior: Scenario
): ZIO[Any, Nothing, String] =
ZIO
.attempt(displayTemperature(behavior))
.catchAll {
case ex: NetworkException =>
ZIO.succeed("Network Unavailable")
case ex: GpsException =>
ZIO.succeed("GPS problem")
}
unsafeRunPrettyPrint(
displayTemperatureZWrapped(Scenario.Success)
)
// 35 degrees
unsafeRunPrettyPrint(
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.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Hello Failures
86
def getTemperatureZGpsGap(
behavior: Scenario
): ZIO[Any, Nothing, String] =
ZIO
.attempt(displayTemperature(behavior))
.catchAll { case ex: NetworkException =>
ZIO.succeed("Network Unavailable")
}
unsafeRunPrettyPrint(
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: scala
def getTemperatureZWithFallback( behavior: Scenario ): ZIO[Any,
Nothing, String] = ZIO .attempt(displayTemperature(behavior))
.catchAll { case ex: NetworkException => ZIO.succeed("Network
Unavailable") case other => ZIO.succeed("Error: " + other) }
unsafeRunPrettyPrint(
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. scala def
getTemperatureZAndFlagUnhandled( behavior: Scenario ): ZIO[Any,
GpsException, String] = ZIO .attempt(displayTemperature(behavior))
.catchSome { case ex: NetworkException => ZIO.succeed("Network
Unavailable") } // TODO Eh, find a better version of this.
.mapError(_.asInstanceOf[GpsException])
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Hello Failures
87
unsafeRunPrettyPrint(
getTemperatureZAndFlagUnhandled(
Scenario.GPSError
)
)
// repl.MdocSession$MdocApp$GpsException
{{TODO show catchSome}}
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/hello_failures/BadTypeManagement.scala
package hello_failures
import zio.ZIO
object BadTypeManagement
extends zio.ZIOAppDefault:
val logic: ZIO[Any, Exception, String] =
for
_ <- ZIO.debug("ah")
result <failable(1).catchAll {
case ex: Exception =>
ZIO.fail(ex)
case ex: String =>
ZIO.succeed(
"recovered string error: " + ex
)
}
_ <- ZIO.debug(result)
yield result
def run = logic
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Hello Failures
def failable(
path: Int
): ZIO[Any, Exception | String, String] =
if (path < 0)
ZIO.fail(new Exception("Negative path"))
else if (path > 0)
ZIO.fail("Too big")
else
ZIO.succeed("just right")
end BadTypeManagement
experiments/src/main/scala/hello_failures/KeepSuccesses.scala
package hello_failures
import zio.Console.printLine
import zio.ZIO
object KeepSuccesses extends zio.ZIOAppDefault:
val allCalls =
List("a", "b", "large payload", "doomed")
case class GoodResponse(payload: String)
case class BadResponse(payload: String)
val initialRequests =
allCalls.map(fastUnreliableNetworkCall)
val logic =
for
results <ZIO.collectAllSuccesses(
initialRequests.map(
_.tapError(e =>
printLine("Error: " + e)
)
)
)
_ <- printLine(results)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
88
Hello Failures
yield ()
val moreStructuredLogic =
for
results <ZIO.partition(allCalls)(
fastUnreliableNetworkCall
)
_ <results match
case (failures, successes) =>
for
_ <ZIO.foreach(failures)(e =>
printLine(
"Error: " + e +
". Should retry on other server."
)
)
recoveries <ZIO.collectAllSuccesses(
failures.map(failure =>
slowMoreReliableNetworkCall(
failure.payload
).tapError(e =>
printLine(
"Giving up on: " + e
)
)
)
)
_ <printLine(
"All successes: " +
(successes ++ recoveries)
)
yield ()
yield ()
val logicSpecific =
ZIO.collectAllWith(initialRequests)(
_.payload.contains("a")
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
89
Hello Failures
90
)
def run =
//
logic
moreStructuredLogic
def fastUnreliableNetworkCall(input: String) =
if (input.length < 5)
ZIO.succeed(GoodResponse(input))
else
ZIO.fail(BadResponse(input))
def slowMoreReliableNetworkCall(
input: String
) =
if (input.contains("a"))
ZIO.succeed(GoodResponse(input))
else
ZIO.fail(BadResponse(input))
end KeepSuccesses
experiments/src/main/scala/hello_failures/OrDie.scala
package hello_failures
import zio.ZIO
object OrDie extends zio.ZIOAppDefault:
val logic =
for _ <- failable(-1).orDie
yield ()
def run = logic
def failable(
path: Int
): ZIO[Any, Exception, String] =
if (path < 0)
ZIO.fail(new Exception("Negative path"))
//
else if (path > 0)
//
ZIO.fail("Too big")
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Hello Failures
else
ZIO.succeed("just right")
experiments/src/main/scala/hello_failures/catching.scala
package hello_failures
import
import
import
import
zio.*
zio.Console.*
hello_failures.file
java.io.IOException
def standIn: ZIO[Any, IOException, Unit] =
printLine("Im a stand-in")
object catching extends zio.ZIOAppDefault:
val logic = loadFile("TargetFile")
def run =
logic
.catchAll(_ =>
println("Error Caught")
loadBackupFile()
)
.exitCode
// standIn.exitCode
experiments/src/main/scala/hello_failures/fallback.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
91
Hello Failures
package hello_failures
import zio.*
import zio.Console
import scala.util.Random
// A useful way of dealing with errors is by
// using the
// `orElse()` method.
case class file(name: String)
def loadFile(fileName: String) =
if (Random.nextBoolean())
println("First Attempt Successful")
ZIO.succeed(file(fileName))
else
println("First Attempt Not Successful")
ZIO.fail("File not found")
def loadBackupFile() =
println("Backup file used")
ZIO.succeed(file("BackupFile"))
object fallback extends zio.ZIOAppDefault:
// orElse is a combinator that can be used to
// handle
// effects that can fail.
def run =
val loadedFile: UIO[file] =
loadFile("TargetFile")
.orElse(loadBackupFile())
loadedFile.exitCode
experiments/src/main/scala/hello_failures/folding.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
92
Hello Failures
93
package hello_failures
import
import
import
import
zio.*
zio.Console.*
hello_failures.file
hello_failures.standIn
object folding extends ZIOAppDefault:
// When applied to ZIO, fold() allows the
// programmer to handle both failure
// and success at the same time.
// ZIO's fold method can be broken into two
// pieces: fold(), and foldM()
// fold() supplied a non-effectful handler, why
// foldM() applies an effectful handler.
val logic = loadFile("targetFile")
def run =
logic
.foldZIO(
_ => loadBackupFile(),
_ =>
printLine(
"The file opened on first attempt!"
)
) // Effectful handling
.exitCode
end folding
experiments/src/main/scala/hello_failures/value.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Hello Failures
package hello_failures
import zio.*
object value:
// Either and Absolve take ZIO types and
// 'surface' or 'submerge'
// the error.
//
//
//
//
Either takes an ZIO[R, E, A] and produces an
ZIO[R, Nothing, Either[E,A]]
The error is 'surfaced' by making a
non-failing ZIO that returns an Either.
//
//
//
//
Absolve takes an ZIO[R, Nothing,
Either[E,A]], and returns a ZIO[R,E,A]
The error is 'submerged', as it is pushed
from an either into a ZIO.
val zEither: UIO[Either[String, Int]] =
ZIO.fail("Boom").either
// IO.fail("Boom") is naturally type
// ZIO[R,String,Int], but is
// converted into type UIO[Either[String, Int]
def sqrt(
input: UIO[Double]
): IO[String, Double] =
ZIO.absolve(
input.map(value =>
if (value < 0.0)
Left("Value must be >= 0.0")
else
Right(Math.sqrt(value))
)
)
end value
// The Left-Right statements naturally from an
// 'either' of type either[String, Double].
// the ZIO.absolve changes the either into an
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
94
Hello Failures
// ZIO of type IO[String, Double]
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
95
Cause
Cause will track all errors originating from a single call in an application, regardless
of concurrency and parallelism.
import zio._
import mdoc.unsafeRunPrettyPrint
val logic =
ZIO
.die(new Exception("Connection lost"))
.ensuring(
ZIO.die(
throw new Exception("Release Failed")
)
)
unsafeRunPrettyPrint(logic)
// Defect: java.lang.Exception: Connection lost
Cause allows you to aggregate multiple errors of the same type
&&/Both represents parallel failures ++/Then represents sequential failures
Cause.die will show you the line that failed, because it requires a throwable Cause.fail
will not necessarily, because it can be any arbitrary type
Avoided Technique - Throwing Exceptions
Now we will highlight the deficiencies of throwing Exceptions. The previous code
might be written in this style:
97
Cause
import zio._
import mdoc.unsafeRunPrettyPrint
val thrownLogic =
ZIO.attempt(
try
throw new Exception(
"Client connection lost"
)
finally
try () // Cleanup
finally
throw new Exception("Release Failed")
)
// thrownLogic: ZIO[Any, Throwable, Nothing] = Stateful(
//
trace = "repl.MdocSession.MdocApp.thrownLogic(15_Cause.md:49)",
//
onState = zio.ZIOCompanionVersionSpecific$$Lambda$14256/1397083455\
@4903320a
// )
unsafeRunPrettyPrint(thrownLogic)
// java.lang.Exception: Release Failed
We will only see the later pool problem. If we throw an Exception in our logic, and
then throw another while cleaning up, we simply lose the original. This is because
thrown Exceptions cannot be composed.
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
Linear reporting
Everything must be reported linearly, even in systems that are executing on different
fibers, across several threads, amongst multiple cores.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
98
Cause
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/cause/CauseBasics.scala
package cause
import zio._
object CauseBasics extends App:
//
ZIO.fail(Cause.fail("Blah"))
println(
(
Cause.die(Exception("1")) ++
(Cause.fail(Exception("2a")) &&
Cause.fail(Exception("2b"))) ++
Cause
.stackless(Cause.fail(Exception("3")))
).prettyPrint
)
object CauseZIO extends ZIOAppDefault:
val x: ZIO[Any, Nothing, Nothing] =
ZIO.die(Exception("Blah"))
def run = ZIO.die(Exception("Blah"))
object LostInfo extends ZIOAppDefault:
def run =
ZIO.attempt(
try
throw new Exception(
"Client connection lost"
)
finally
try () // Cleanup
finally
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
99
Cause
throw new Exception(
"Problem relinquishing to pool"
)
)
experiments/src/main/scala/cause/MalcomInTheMiddle.scala
package cause
import zio.{ZIO, ZIOAppDefault}
object MalcomInTheMiddle extends ZIOAppDefault:
def run =
def turnOnLights() = throw new BurntBulb()
class BurntBulb() extends Exception
def getNewBulb() = throw new WobblyShelf()
class WobblyShelf() extends Exception
def grabScrewDriver() =
throw new SqueakyDrawer()
class SqueakyDrawer() extends Exception
def sprayWD40() = throw new EmptyCan()
class EmptyCan() extends Exception
def driveToStore() = throw new DeadCar()
class DeadCar() extends Exception
def repairCar() = throw new Nagging()
class Nagging() extends Exception
try
turnOnLights()
catch
case burntBulb: BurntBulb =>
try
getNewBulb()
catch
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
100
Cause
//
//
//
//
//
//
case wobblyShelf: WobblyShelf =>
try
grabScrewDriver()
catch
case squeakyDrawer: SqueakyDrawer =>
try
sprayWD40()
catch
case emptyCan: EmptyCan =>
try
driveToStore()
catch
case deadCar: DeadCar =>
try repairCar()
finally
ZIO
.debug(
"What does it look like I'm doing?!"
)
.exitCode
finally
println
end try
finally
ZIO
.debug(
"What does it look like I'm doing?!"
)
.exitCode
end run
/** try { turnOnLights } catch { case
* burntLightBulb => try {
*/
end MalcomInTheMiddle
experiments/src/main/scala/cause/MalcomInTheMiddleZ.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
101
Cause
package cause
import zio.*
object MalcomInTheMiddleZ extends ZIOAppDefault:
def run =
def turnOnLights() = ZIO.fail(BurntBulb())
class BurntBulb()
extends Exception("Burnt Bulb!")
def getNewBulb() =
ZIO.attempt(
throw new Exception("Wobbly Shelf!")
)
def grabScrewDriver() =
ZIO.fail(Exception("SqueakyDrawer"))
(
for
_ <turnOnLights()
.catchAllCause(originalError =>
getNewBulb()
.catchAllCause(bulbError =>
grabScrewDriver()
.mapErrorCause(
screwDriverError =>
(originalError ++
bulbError) ++
screwDriverError
)
)
)
_ <- ZIO.debug("Preserve failures!")
yield ()
).catchAllCause(bigError =>
ZIO.debug(
"Final error: " +
simpleStructureAlternative(bigError)
)
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
102
Cause
end run
end MalcomInTheMiddleZ
def simpleStructure(
cause: Cause[Throwable]
): String =
cause match
case Cause.Empty =>
???
case Cause.Fail(value, trace) =>
value.getMessage
case Cause.Die(value, trace) =>
???
case Cause.Interrupt(fiberId, trace) =>
???
case Cause.Stackless(cause, stackless) =>
???
case Cause.Then(left, right) =>
"Then(" + simpleStructure(left) + ", " +
simpleStructure(right) + ")"
case Cause.Both(left, right) =>
???
def simpleStructureAlternative(
cause: Cause[Throwable]
): String =
cause match
case Cause.Fail(value, trace) =>
value.getMessage
case Cause.Then(left, right) =>
simpleStructureAlternative(left) + " => " +
simpleStructureAlternative(right)
case Cause.Both(left, right) =>
???
case _ =>
???
experiments/src/main/scala/cause/MutationTracking.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
103
Cause
package cause
import zio.{Cause, IO, UIO, ZIO}
import zio.Console.*
class MutationTracking:
enum Stage:
case Hominini,
Chimpanzee,
Human
object TimelineFinally extends App:
try throw new Exception("Straightened Spine")
finally
try throw new Exception("Less Hair")
finally
throw new Exception("Fine Voice Control")
object Timeline extends zio.ZIOAppDefault:
val mutation1: UIO[Nothing] =
ZIO.die(Exception("Straightened Spine"))
val mutation2 = ZIO.die(Exception("Less Hair"))
val mutation3 =
ZIO.die(Exception("Fine voice control"))
val timeline =
mutation1
.ensuring(mutation2)
.ensuring(mutation3)
def run = timeline.sandbox
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
New Effects
So far, we have justified, rebuilt, and examined the built-in ZIO Services. While this
should aid you in creating basic ZIO applications, we now want to explore some
custom Services that could facilitate more complex programs.
Location
Consider the term Environment. In common speech, this often indicates where something happens. Previously, we have examined this in terms of “Which Machine?”
However, it is equally valid to treat this as a spatial location at which our code is
executed.
import zio.{ZIO}
trait HardwareFailure
case class GpsCoordinates(
latitude: Double,
longitude: Double
)
trait TimeZone
trait Location:
def gpsCoords
: ZIO[Any, HardwareFailure, GpsCoordinates]
def timezone: ZIO[Any, Nothing, TimeZone]
object Location:
def gpsCoords: ZIO[
Location,
HardwareFailure,
GpsCoordinates
] = ZIO.service[Location].flatMap(_.gpsCoords)
Now that we have basic Location-awareness, we can build more domain-specific
logic on top of it.
106
Location
trait FloodStatus
object Safe
extends FloodStatus
object Threatened extends FloodStatus
trait FloodWarning:
def seaLevelStatus
: ZIO[Any, Nothing, FloodStatus]
case class Slope(degrees: Float)
trait Topography:
def slope: ZIO[Location, Nothing, Slope]
case class Rainfall(inches: Int)
trait Almanac:
def averageAnnualRainfail
: ZIO[Location, Nothing, Rainfall]
case class Country(name: String)
trait CountryService:
def currentCountry
: ZIO[Location, HardwareFailure, Country]
object CountryService:
def currentCountry
: ZIO[Location, HardwareFailure, Country] =
for gpsCords <- Location.gpsCoords
yield Country("USA")
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
107
Location
trait LegalStatus
object Legal
extends LegalStatus
object Illegal extends LegalStatus
trait GeoPolitcalState
trait CurrentWar
enum Issue:
case OnlineGambling,
Alcohol
trait LawLibrary:
def status(
country: Country,
issue: Issue
): ZIO[
GeoPolitcalState,
CurrentWar,
LegalStatus
]
class LegalService(
countryService: CountryService,
lawLibrary: LawLibrary
):
def status(issue: Issue): ZIO[
Location & GeoPolitcalState,
CurrentWar | HardwareFailure,
LegalStatus
] =
for
country <- countryService.currentCountry
status <- lawLibrary.status(country, issue)
yield status
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Environment Exploration
The Environment type parameter distinguishes ZIO from most other IO monads. At
first, it might seem like a complicated way of passing values to your ZIO instances why can’t they just be normal function arguments?
• The developer does not need to manually plug an instance of type T into every
ZIO[T, _, _] in order to run them.
• ZIO instances can be freely composed, flatmapped, etc before ever providing
the required environment. It’s only needed at the very end when we want to
execute the code!
• Environments can be arbitrarily complex, without requiring a super-container
type to hold all of the fields.
• Compile time guarantees that you have
1. Provided everything required
2. Have not provided multiple, conflicting instances of the same type
Dependency Injection
TODO Decide if this requires a tangent, or just a single mention to let people know
we’re solving the same type of problem. TODO
ZEnvironment: Powered by a TypeMap
ZIO is able to accomplish all this through the ZEnvironment class.
TODO Figure out how/where to include the disclaimer that we’re stripping out many
of the implementation details TODO
Environment Exploration
109
ZEnvironment[+R](map: Map[LightTypeTag, (Any, Int)])
The crucial data structure inside is a Map[LightTypeTag, (Any)]. TODO Decide
how much to dig into LightTypeTag vs Tag[A] TODO Seeing Any here might be
confusing - ZEnvironment is supposed to give us type-safety when executing ZIOs!
Looking at the get method, we see specic, typed results.
def get[A >: R](implicit tagged: Tag[A]): A
How is this possible when all of our Map values are Anys? add holds the answer.
def add[A](a: A): ZEnvironment[R with A]
Even though each new entry is stored as an Any, we store knowledge of our new entry
in the R type parameter. We append the type of our new A to the R type parameter,
and get back a brand-new environment that we know contains all types from the
original and the type of the instance we just added.
Now look at the get implementation to see how this is used.
def get[A](tag: Tag[A]): A =
val lightTypeTag = taggedTagType(tag)
self.map.get(lightTypeTag) match:
case Some(a) => a.asInstanceOf[A]
case None => throw new Error(s"Defect: ${tag} not inside ${self}")
private def taggedTagType[A](tagged: Tag[A]): LightTypeTag
ZLayer
• Better Ergonomics than ZEnvironment
• Shared by default
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
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Environment Exploration
experiments/src/main/scala/environment_exploration/ToyEnvironment.scala
package environment_exploration
import scala.reflect.{ClassTag, classTag}
case class DBService(url: String)
// Yada yada yada lets talk about the environment
trait ToyEnvironmentT[+R]:
def add[A: ClassTag](
a: A
): ToyEnvironmentT[R & A]
def get[A >: R: ClassTag]: A
class ToyEnvironment[+R](
typeMap: Map[ClassTag[_], Any]
) extends ToyEnvironmentT[R]:
def add[A: ClassTag](
a: A
): ToyEnvironment[R & A] =
ToyEnvironment(typeMap + (classTag[A] -> a))
def get[A >: R: ClassTag]: A =
typeMap(classTag[A]).asInstanceOf[A]
@main
def demoToyEnvironment =
val env: ToyEnvironment[_] =
ToyEnvironment(Map.empty)
val env1: ToyEnvironment[String] =
env.add("hi")
val env2: ToyEnvironment[String & DBService] =
env1.add(DBService("blah"))
val env3: ToyEnvironment[
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
110
Environment Exploration
111
String & DBService & List[String]
] = env2.add(List("a", "b"))
println(env3.get[String])
println(env3.get[DBService])
println(env3.get[List[String]])
// We get some amount of compile time safety
// here, but not much
// println(env.get(classOf[List[DBService]]))
// Downside of the current approach is that it
// doesn't prevent duplicate types
env3.add("hi") // is accepted
end demoToyEnvironment
// Consider this runtime de-duping
class ToyEnvironmentRuntimeDeduplication[+R](
typeMap: Map[ClassTag[_], Any]
):
def add[A: ClassTag](
a: A
): ToyEnvironment[R & A] =
if (typeMap.contains(classTag[A]))
throw new IllegalArgumentException(
s"Cannot add ${classTag[A]} to environment, it already exists"
)
else
ToyEnvironment(
typeMap + (classTag[A] -> a)
)
experiments/src/main/scala/environment_exploration/TupledEnvironmentZio.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Environment Exploration
package environment_exploration
// trait TypeTag // TODO Or ClassTag?
// trait TypeInstance
// case class TypeMap(
//
typeMap: Map[TypeTag, TypeInstance]
// )
case class TupledEnvironmentZio[ENV, RESULT](
run: ENV => RESULT
):
def unsafeRun(env: ENV): RESULT = run(env)
// The tuple here is a step towards the
// full-featured TypeMap that ZIO uses
def flatMap[ENV2, RESULT2](
f: RESULT => TupledEnvironmentZio[
ENV2,
RESULT2
]
): TupledEnvironmentZio[(ENV, ENV2), RESULT2] =
TupledEnvironmentZio((env, env2) =>
f(run(env)).run(env2)
)
@main
def demoSingleEnvironmentInstance =
val customTypeMapZio
: TupledEnvironmentZio[Int, String] =
TupledEnvironmentZio(env =>
val result = env * 10
s"result: $result"
)
println(customTypeMapZio.unsafeRun(5))
val repeatMessage
: TupledEnvironmentZio[Int, String] =
TupledEnvironmentZio(env =>
s"Message \n" * env
)
println(repeatMessage.unsafeRun(5))
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
112
Environment Exploration
case class BigResult(message: String)
@main
def demoTupledEnvironment =
val squared: TupledEnvironmentZio[Int, Unit] =
TupledEnvironmentZio(env =>
println(
"Environment integer squared: " +
env * env
)
)
val repeatMessage
: TupledEnvironmentZio[String, BigResult] =
TupledEnvironmentZio(message =>
BigResult(s"Environment message: $message")
)
val composedRes: TupledEnvironmentZio[
(Int, String),
BigResult
] = squared.flatMap(_ => repeatMessage)
val finalResult =
composedRes.unsafeRun((5, "Hello"))
println(finalResult)
end demoTupledEnvironment
import zio.ZIO
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
113
Resources
Resources are finite / large overhead allocatable pools of things: - Connections File handles - Player Slots (Rule based or Fractional Computational Power - The
usefulness of the game degrades at some scale) - STM? Is it based on ZManaged?
Connection with dining philosophers
Externalizes the resource management so that the logic that acts on the resource can
be reused, refactored, composed.
Assembly of resources works the same as a single resource. If a resource is more than
1 resource, the logic acting on any / all resources doesn’t have to know what cleanup.
Similarly, the logic is unconcerned with the ability for all needed resources to be
available. Logic is only ever to be applied when all resources are available.
STM
This is going to be a tough one.
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/stm/SimpleTransfers.scala
package stm
import
import
import
import
zio.Console.printLine
zio.stm.{STM, TRef}
zio.Runtime.default.unsafe
zio.Unsafe
def transfer(
from: TRef[Int],
to: TRef[Int],
amount: Int
): STM[Throwable, Unit] =
for
senderBalance <- from.get
_ <if (amount > senderBalance)
STM.fail(
new Throwable("insufficient funds")
)
else
from.update(_ - amount) *>
116
STM
to.update(_ + amount)
yield ()
@main
def stmDemo() =
val logic =
for
fromAccount <- TRef.make(100).commit
toAccount
<- TRef.make(0).commit
_ <transfer(fromAccount, toAccount, 20)
.commit
//
_ <- transferTransaction.commit
toAccountFinal <- toAccount.get.commit
_ <printLine(
"toAccountFinal: " + toAccountFinal
)
yield ()
Unsafe.unsafe { (u: Unsafe) =>
given Unsafe = u
unsafe.run(logic).getOrThrowFiberFailure()
}
end stmDemo
experiments/src/main/scala/stm/TownResources.scala
package stm
import
import
import
import
import
zio.stm.STM
zio.stm.TRef
zio.Runtime.default.unsafe
zio.Console.printLine
zio.Unsafe
case class Cash(value: Int)
extends Resource[Cash]
case class Lumber(value: Int)
extends Resource[Lumber]
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
117
STM
case class Grain(value: Int)
extends Resource[Grain]
sealed trait Resource[A]:
val value: Int
def <=(other: Resource[A]): Boolean =
value <= other.value
// TODO Consider other names: Commodity
case class TownResources(
cash: Cash,
lumber: Lumber,
grain: Grain
):
def +[A](resource: Resource[A]) =
resource match
case c: Cash =>
copy(cash = Cash(cash.value + c.value))
case g: Grain =>
copy(grain =
Grain(grain.value + g.value)
)
case l: Lumber =>
copy(lumber =
Lumber(lumber.value + l.value)
)
def -[A](resource: Resource[A]) =
resource match
case c: Cash =>
copy(cash = Cash(cash.value - c.value))
case g: Grain =>
copy(grain =
Grain(grain.value - g.value)
)
case l: Lumber =>
copy(lumber =
Lumber(lumber.value - l.value)
)
def canSend[A](resource: Resource[A]) =
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
118
STM
resource match
case c: Cash =>
c <= cash
case l: Lumber =>
l <= lumber
case g: Grain =>
g <= grain
end TownResources
/** Goal: Demonstrate a useful 3 party trade.
*/
@main
def resourcesDemo() =
val logic =
for
treeTown <TRef
.make(
TownResources(
Cash(10),
Lumber(100),
Grain(0)
)
)
.commit
grainVille <TRef
.make(
TownResources(
Cash(0),
Lumber(0),
Grain(300)
)
)
.commit
_ <tradeResources(
treeTown,
Cash(3),
grainVille,
Grain(30)
).commit
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
119
STM
finalTreeTownResources <treeTown.get.commit
finalGrainVilleResources <grainVille.get.commit
_ <- printLine(finalTreeTownResources)
_ <- printLine(finalGrainVilleResources)
yield ()
Unsafe.unsafe { (u: Unsafe) =>
given Unsafe = u
unsafe.run(logic).getOrThrowFiberFailure()
}
end resourcesDemo
def tradeResources[
A <: Resource[A],
B <: Resource[B]
](
town1: TRef[TownResources],
town1Offering: A,
town2: TRef[TownResources],
town2Offering: B
): STM[Throwable, Unit] =
for
_ <- send(town1, town2, town1Offering)
_ <- send(town2, town1, town2Offering)
yield ()
def send[A <: Resource[A], B <: Resource[B]](
from: TRef[TownResources],
to: TRef[TownResources],
resource: A
): STM[Throwable, Unit] =
for
senderBalance <- from.get
canSend = senderBalance.canSend(resource)
_ <if (canSend)
from.update(_ - resource) *>
to.update(_ + resource)
else
STM.fail(
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
120
STM
new Throwable(
"Not enough resources to send: " +
resource
)
)
extraTransaction =
from.update(fResources =>
fResources.copy(cash =
Cash(fResources.cash.value + 1)
)
)
party2Balance <- to.get
yield ()
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Executing External Programs
Most of this book focuses on executing Scala code all confined within a single JVM.
However, there are times when you need to execute external programs. As a rule,
we must treat these programs as side-effecting, because there is no practical way of
ensuring they are pure. We will explore this using zio-process This chapter will
cover how to do that.
Basic shell tools
### Say We could start with things like echo or ls, but those are easily done within
Scala itself, so they are not very interesting.
Top
### Git ## Advanced tools ### Gource ### Shpotify ## Running other programming
languages ### Python ### Scala
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/executing_external_programs/Gource.scala
Executing External Programs
122
package executing_external_programs
import zio._
import zio.Console.printLine
import zio.process.{
Command,
ProcessInput,
ProcessOutput
}
/* Possibilities:
* - Show a certain time period
* - More recent activity
* - Cycle between different repositories */
object GourceDemo extends ZIOAppDefault:
def gource(repoDir: String) =
Command(
"gource",
//
"--follow-user", "bfrasure", // Highlights user, but still show\
s others
"--user-show-filter",
"bfrasure|Bill Frasure", // Only shows user
repoDir
)
val projects =
List(
"/Users/bfrasure/Repositories/book",
"/Users/bfrasure/Repositories/TestFrameworkComparison"
)
def showActivityForAWhile(repoDir: String) =
for
run1 <- gource(repoDir).run
_
<- ZIO.sleep(5.seconds)
_
<- run1.killForcibly
yield ()
def randomProjectActivity =
for
idx <Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Executing External Programs
Random.nextIntBounded(projects.length)
_ <- showActivityForAWhile(projects(idx))
yield ()
def run =
for _ <- randomProjectActivity.repeatN(2)
yield ()
end GourceDemo
experiments/src/main/scala/executing_external_programs/Say.scala
package executing_external_programs
import zio.process.{
Command,
ProcessInput,
ProcessOutput
}
import zio._
def say(message: String) =
Command("say", message)
object SayDemo extends ZIOAppDefault:
def run = say("Hello, world!").run
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
123
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.
resourcemanagement
experiments/src/main/scala/resourcemanagement/ChatSlots.s
package resourcemanagement
import zio.Console.printLine
import zio.{Ref, ZIO}
case class Slot(id: String)
case class Player(name: String, slot: Slot)
case class Game(a: Player, b: Player)
object ChatSlots extends zio.ZIOAppDefault:
enum SlotState:
case Closed,
Open
def run =
def acquire(ref: Ref[SlotState]) =
for
_ <printLine {
"Took a speaker slot"
}
_ <- ref.set(SlotState.Open)
yield "Use Me"
125
Experiments
def release(ref: Ref[SlotState]) =
for
_ <printLine("Freed up a speaker slot")
.orDie
_ <- ref.set(SlotState.Closed)
yield ()
for
ref <Ref.make[SlotState](SlotState.Closed)
managed =
ZIO.acquireRelease(acquire(ref))(_ =>
release(ref)
)
reusable =
managed.map(
printLine(_)
) // note: Can't just do (Console.printLine) here
_ <- reusable
_ <- reusable
_ <ZIO.scoped {
managed.flatMap { s =>
for
_ <- printLine(s)
_ <- printLine("Blowing up")
_ <- ZIO.fail("Arggggg")
yield ()
}
}
yield ()
end for
end run
end ChatSlots
experiments/src/main/scala/resourcemanagement/Trivial.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
126
Experiments
package resourcemanagement
import zio.Console
import zio.{Ref, ZIO}
object Trivial extends zio.ZIOAppDefault:
enum ResourceState:
case Closed,
Open
def run =
def acquire(ref: Ref[ResourceState]) =
for
_ <Console.printLine("Opening Resource")
_ <- ref.set(ResourceState.Open)
yield "Use Me"
def release(ref: Ref[ResourceState]) =
for
_ <Console
.printLine("Closing Resource")
.orDie
_ <- ref.set(ResourceState.Closed)
yield ()
def releaseSymbolic(
ref: Ref[ResourceState]
) =
Console
.printLine("Closing Resource")
.orDie *> ref.set(ResourceState.Closed)
//
//
//
//
//
//
//
This combines creating a managed resource
with using it.
In normal life, users just get a managed
resource from
a library and so they don't have to think
about acquire
& release logic.
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
127
Experiments
for
ref <Ref.make[ResourceState](
ResourceState.Closed
)
managed =
ZIO.acquireRelease(acquire(ref))(_ =>
release(ref)
)
reusable =
ZIO.scoped {
managed.map(Console.printLine(_))
} // note: Can't just do (Console.printLine) here
_ <- reusable
_ <- reusable
_ <ZIO.scoped {
managed.flatMap { s =>
for
_ <- Console.printLine(s)
_ <Console.printLine("Blowing up")
_ <- ZIO.fail("Arggggg")
yield ()
}
}
yield ()
end for
end run
end Trivial
energygrid
experiments/src/main/scala/energygrid/Grid.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
128
Experiments
package energygrid
object Grid {}
microcontrollers
experiments/src/main/scala/microcontrollers/Arduino.scala
package microcontrollers
import zio.Console.{printLine, readLine}
import zio.{
Clock,
Console,
Fiber,
IO,
Ref,
Runtime,
Schedule,
UIO,
URIO,
ZIO,
ZLayer,
durationInt
}
import zio.Clock.{currentTime, instant}
import zio.Duration.*
import java.io.IOException
import java.util.concurrent.TimeUnit
case class DigitalPin private (active: Boolean)
object DigitalPin:
object ON extends DigitalPin(true)
object OFF extends DigitalPin(false)
case class Arduino(pin1: DigitalPin):
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
129
Experiments
def passSignalToLight() =
if (pin1.active)
ZIO
.succeed("Sending current to light bulb")
else
ZIO.succeed("Leaving the lights off")
object MicroControllerExample
extends zio.ZIOAppDefault:
def turnOnPinAtRightTime(
inSeconds: Long,
startTime: Long
) =
if ((inSeconds - startTime) % 4 > 2)
DigitalPin.ON
else
DigitalPin.OFF
def loopLogic(
startTime: Long,
arduino: Ref[Arduino]
): ZIO[Any, IOException, Unit] =
for
inSeconds <- currentTime(TimeUnit.SECONDS)
originalArduino <- arduino.get
originalLightStatus <originalArduino.passSignalToLight()
signalOnPin =
turnOnPinAtRightTime(
inSeconds,
startTime
)
_ <- arduino.set(Arduino(signalOnPin))
updatedArduino <- arduino.get
updatedLightStatus <updatedArduino.passSignalToLight()
_ <if (
originalLightStatus !=
updatedLightStatus
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
130
Experiments
printLine(updatedLightStatus)
else
ZIO.succeed(1)
yield ()
def run =
for
arduino <Ref.make(Arduino(pin1 = DigitalPin.OFF))
inSeconds <- currentTime(TimeUnit.SECONDS)
_ <loopLogic(inSeconds, arduino).repeat(
// Can we calculate how long this is
// using Schedule APIs?
Schedule.recurs(60) &&
Schedule.spaced(100.milliseconds)
)
yield ()
end MicroControllerExample
diningphilosophers
experiments/src/main/scala/diningphilosophers/Philosophers.s
package diningphilosophers
/**
*
*
*
*
*
*
*
*
*
*
*
*
Problem statement
Five silent philosophers sit at a round table
with bowls of spaghetti. Forks are placed
between each pair of adjacent philosophers.
Each philosopher must alternately think and
eat. However, a philosopher can only eat
spaghetti when they have both left and right
forks. Each fork can be held by only one
philosopher at a time and so a philosopher
can use the fork only if it is not being used
by another philosopher. After an individual
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
131
Experiments
* philosopher finishes eating, they need to put
* down both forks so that the forks become
* available to others. A philosopher can only
* take the fork on their right or the one on
* their left as they become available and they
* cannot start eating before getting both
* forks.
*
* Eating is not limited by the remaining
* amounts of spaghetti or stomach space; an
* infinite supply and an infinite demand are
* assumed.
*
* The problem is how to design a discipline of
* behavior (a concurrent algorithm) such that
* no philosopher will starve; i.e., each can
* forever continue to alternate between eating
* and thinking, assuming that no philosopher
* can know when others may want to eat or
* think.
*/
class Fork()
case class Philosopher(
left: Option[Fork] = None,
right: Option[Fork] = None
):
def pickupLeftFork(): Philosopher = ???
def pickupRightFork(): Philosopher = ???
def eat(): Philosopher
= ???
/** Table: F1 <-> P1 <-> F2 <-> P2 <-> F3 <-> P3
* <-> F4 <-> P4 <-> F5 <-> P5 <-> F1
*/
class Table(
forks: List[Option[Fork]],
philosophers: List[Philosopher]
):
val circularForks = forks :+ forks.head
val alternateRep: Iterator[
((Option[Fork], Option[Fork]), Philosopher)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
132
Experiments
] =
circularForks
.sliding(2)
.map(l => (l(0), l(1)))
.zip(philosophers)
object Dinner:
val table =
Table(
List(
Some(Fork()),
Some(Fork()),
Some(Fork()),
Some(Fork()),
Some(Fork())
),
List(
Philosopher(),
Philosopher(),
Philosopher(),
Philosopher(),
Philosopher()
)
)
interpreter-chaining_previous_result_and_environmental_dependency
experiments/src/main/scala/interpreter/chaining_previous_result_and_environmental_dependency/ChainedWithPreviousResultAndEnvironmentDependency.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
133
Experiments
package interpreter.chaining_previous_result_and_environmental_dependen\
cy
import environment_exploration.ToyEnvironment
import zio.{ZIO, ZIOAppDefault}
import scala.reflect.{ClassTag, classTag}
import scala.util.Random
trait Operation[
Dependencies <: Service: ClassTag
]:
val dep: ClassTag[Dependencies] =
classTag[Dependencies]
case class Value(value: String)
extends Operation[AnyService]
case class StringManipulation(
action: String => String
) extends Operation[AnyService]:
def actOn(input: String): String =
action(input)
case class Print() extends Operation[Printer]
case class RandomString()
extends Operation[RandomService]
trait Service
case class AnyService()
extends Service
case class RandomService() extends Service
val program =
Seq(
Value("Hello There"),
Print(),
StringManipulation(_.toUpperCase()),
Print(),
StringManipulation(_.take(5)),
Print(),
RandomString(),
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
134
Experiments
Print(),
StringManipulation(_.toUpperCase()),
Print()
)
trait Printer extends Service:
def print(input: String): Unit
def interpretWithEnvironment(
program: Seq[Operation[_]],
environment: ToyEnvironment[Service]
): String =
program.foldLeft("") { (acc, op) =>
//
environment.get(op.dep)
op match
case Print() =>
//
environment.get[Printer].print(acc)
acc
case RandomString() =>
scala
.util
.Random
.alphanumeric
.take(10)
.mkString
case Value(value) =>
value
case StringManipulation(action) =>
action(acc)
}
@main
def demoInterpreter() =
val env =
ToyEnvironment(Map.empty)
.add(RandomService())
.add(AnyService())
interpretWithEnvironment(program, env)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
135
Experiments
experiments-src-test-scala-energrygrid
experiments/src/test/scala/energrygrid/GridSpec.scala
package energrygrid
import
import
import
import
zio.*
zio.test.*
energrygrid.GridErrors.*
zio.test.TestAspect.ignore
/* TODO Simulate a home-level grid managing power
* needs and production Why?
* - This is code that interacts with TheWorld in
* numerous ways
* - It's something I've wanted a more visceral
* understanding of */
trait EnergyParticipant
trait EnergyProvider:
val producingPriority: Int
sealed trait EnergyProducer
extends EnergyParticipant
with EnergyProvider:
object SolarPanels
extends EnergyProducer
with EnergyProvider:
override val producingPriority = 10
object Generator
extends EnergyProducer
with EnergyProvider:
override val producingPriority = 1
def sendPowerTo(
consumer: EnergyConsumer |
EnergyBidirectional
): ZIO[Any, Overheat, Unit] = ???
sealed trait EnergyConsumer
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
136
Experiments
extends EnergyParticipant:
val consumerPriority: Int
object EnergyConsumer:
object Dishwasher extends EnergyConsumer:
override val consumerPriority: Int = 3
object Wifi extends EnergyConsumer:
override val consumerPriority: Int = 4
object Refrigerator extends EnergyConsumer:
override val consumerPriority: Int = 5
def drawPowerFrom(
producer: EnergyProducer |
EnergyBidirectional
): ZIO[Any, InsufficientPower, Unit] = ???
sealed trait EnergyBidirectional
extends EnergyProvider
with EnergyConsumer:
def sendPowerTo(
consumer: EnergyConsumer |
EnergyBidirectional
): ZIO[Any, Overheat, Unit] = ???
def drawPowerFrom(
producer: EnergyProducer |
EnergyBidirectional
): ZIO[Any, InsufficientPower, Unit] = ???
object EnergyBidirectional:
object HomeBattery extends EnergyBidirectional:
override val producingPriority: Int = 3
override val consumerPriority: Int = 2
object ExternalGrid
extends EnergyBidirectional:
override val producingPriority: Int = 1
override val consumerPriority: Int = 1
case class Grid(
participants: Set[EnergyParticipant]
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
137
Experiments
trait MunicipalGrid
case class User():
val live: ZIO[Clock, UnsatisfiedNeeds, Unit] =
???
case class Home(family: User, grid: Grid):
val provide =
for _ <- family.live
yield ()
sealed trait GridErrors
object GridErrors:
trait Unpowered
trait UnsatisfiedNeeds
trait InsufficientPower
trait Overheat
object GridSpec extends ZIOSpecDefault:
def spec =
suite("GridSpec")(
test("recognizes grid input")(
for _ <- ZIO.unit
yield assertNever("Need a test!")
),
test("runs through an energy scenario")(
/* We start by running our dishwasher
* before the sun is hitting our panels,
* so we are drawing power fully from the
* grid. Once the panels are active, they
* provide most of the power, but we
* still need some from the grid. Once
* the dishes finished, we start feeding
* the solar power back into the grid.
*
* 8:00 8:30 9:00 Dishwasher -1.5kw
* -1.5kw 0kw Solar Panels 0kw +1.0kw
* +1.0kw CityGrid +1.5kw +0.5kw -1.0kw */
for _ <- ZIO.unit
yield assertNever("Need a test!")
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
138
Experiments
)
) @@ ignore
end GridSpec
layers
experiments/src/main/scala/layers/Festival.scala
package layers
import zio.{ZIO, ZLayer, Duration}
import zio.ZIO.debug
import zio.durationInt
case class Toilets()
val toilets =
ZLayer.scoped(
ZIO.acquireRelease(
debug("TOILETS: Setting up") *>
ZIO.succeed(Toilets())
)(_ => debug("TOILETS: Removing"))
)
case class Stage()
val stage =
ZLayer.scoped(
ZIO.acquireRelease(
activity(
"STAGE",
"Transporting",
2.seconds
) *>
activity(
"STAGE",
"Building",
4.seconds
) *> ZIO.succeed(Stage())
)(_ => debug("STAGE: Tearing down"))
)
case class Permit()
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
139
Experiments
val permit =
ZLayer.scoped(
ZIO.acquireRelease(
activity(
"PERMIT",
"Legal Request",
5.seconds
) *> ZIO.succeed(Permit())
)(_ => debug("PERMIT: Relinquished"))
)
case class Venue(stage: Stage, permit: Permit)
val venue = ZLayer.fromFunction(Venue.apply)
case class Speakers()
val speakers =
ZLayer.scoped(
ZIO.acquireRelease(
debug("SPEAKERS: Positioning") *>
ZIO.succeed(Speakers())
)(_ => debug("SPEAKERS: Packing up"))
)
case class Amplifiers()
val amplifiers =
ZLayer.scoped(
ZIO.acquireRelease(
debug("AMPLIFIERS: Positioning") *>
ZIO.succeed(Amplifiers())
)(_ => debug("AMPLIFIERS: Putting away"))
)
case class Wires()
val wires =
ZLayer.scoped(
ZIO.acquireRelease(
debug("WIRES: Unrolling") *>
ZIO.succeed(Wires())
)(_ => debug("WIRES: Spooling up"))
)
case class Fencing()
val fencing =
ZLayer.scoped(
ZIO.acquireRelease(
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
140
Experiments
debug("FENCING: Surrounding the area") *>
ZIO.succeed(Fencing())
)(_ => debug("FENCING: Tearing down"))
)
case class SoundSystem(
speakers: Speakers,
amplifiers: Amplifiers,
wires: Wires
)
val soundSystem =
for
layer <ZLayer.fromFunction(SoundSystem.apply)
scoped <ZLayer.scoped {
ZIO.acquireRelease(
debug(
"SOUNDSYSTEM: Hooking up speakers, amplifiers, and wires"
) *> ZIO.succeed(layer.get)
)(_ =>
debug(
"SOUNDSYSTEM: Disconnecting speakers, amplifiers, and wires"
)
)
}
yield scoped
val soundSystemShortedOut: ZLayer[
Speakers with Amplifiers with Wires,
String,
SoundSystem
] =
for
layer <ZLayer.fromFunction(SoundSystem.apply)
scoped <ZLayer.scoped {
ZIO.acquireRelease(
debug(
"SOUNDSYSTEM: Hooking up speakers, amplifiers, and wires"
) *> ZIO.fail("BZZZZ") *>
ZIO.succeed(layer.get)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
141
Experiments
)(_ =>
debug(
"SOUNDSYSTEM: Disconnecting speakers, amplifiers, and wires"
)
)
}
yield scoped
case class FoodTruck()
val foodtruck =
ZLayer.scoped(
ZIO.acquireRelease(
debug("FOODTRUCK: Driving in ") *>
activity(
"FOODTRUCK",
"Fueling",
2.seconds
) *> ZIO.succeed(FoodTruck())
)(_ => debug("FOODTRUCK: Driving out "))
)
case class Festival(
toilets: Toilets,
venue: Venue,
soundSystem: SoundSystem,
fencing: Fencing,
foodTruck: FoodTruck,
security: Security
)
val festival =
for
layer <- ZLayer.fromFunction(Festival.apply)
scoped <ZLayer.scoped {
ZIO.acquireRelease(
debug("FESTIVAL: We are all set!") *>
ZIO.succeed(layer.get)
)(_ =>
debug(
"FESTIVAL: Good job, everyone. Close it down!"
)
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
142
Experiments
}
yield scoped
case class Security(
toilets: Toilets,
foodTruck: FoodTruck
)
val security =
for
layer <- ZLayer.fromFunction(Security.apply)
_ <ZLayer.scoped(
ZIO.acquireRelease(
debug("SECURITY: Ready")
)(_ => debug("SECURITY: Going home"))
)
yield layer
def activity(
entity: String,
name: String,
duration: Duration
) =
debug(s"$entity: BEGIN $name") *>
debug(s"$entity: END $name").delay(duration)
javawrappers
experiments/src/main/scala/javawrappers/InstantEOP.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
143
Experiments
package javawrappers
import zio.duration2DurationOps
import java.time.Instant
// TODO Consider deleting
object InstantOps:
extension (i: Instant)
def plusZ(duration: zio.Duration): Instant =
i.plus(duration.asJava)
interpreter-level1_nochaining
experiments/src/main/scala/interpreter/level1_nochaining/1_singleOperation.scala
package interpreter.level1_nochaining
/* Programs with no chained operations.
* The interpreter only handles known types. */
trait Operation
case class Print(s: String) extends Operation
case class Random(f: Int => Unit)
extends Operation
object NoOp extends Operation
def interpret(operation: Operation): Unit =
operation match
case p: Print =>
println(p.s)
case r: Random =>
r.f(scala.util.Random.nextInt())
@main
def m1 =
val p1 = Print("hello")
val r1 = Random(println)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
144
Experiments
interpret(p1)
interpret(r1)
experiments/src/main/scala/interpreter/level1_nochaining/2_InterpretSequence.scala
package interpreter.level1_nochaining
def interpretSequence(
prints: Seq[Operation]
): Unit =
prints match
case Nil =>
()
case head :: tail =>
interpret(head)
interpretSequence(tail)
@main
def demoSequence =
val program =
Seq(
Print("asdf"),
Print("hello"),
Random(println)
)
interpretSequence(program)
bigdec
experiments/src/main/scala/bigdec/Main.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
145
Experiments
package bigdec
import zio.{
ZIO,
ZIOAppDefault,
Console,
Schedule
}
def inputBigDecimalValue(
prompt: String,
min: BigDecimal,
max: BigDecimal
): ZIO[Any, Exception, BigDecimal] =
for
_
<- Console.printLine(prompt)
input <- Console.readLine
result <ZIO
.attempt(BigDecimal(input))
.mapError(_ =>
Exception("Invalid input.")
)
_ <ZIO.unless(min <= result && result <= max)(
ZIO.fail(
Exception(
s"Input out of the range from $min to $max"
)
)
)
yield result
object Main extends ZIOAppDefault:
def run =
inputBigDecimalValue("Enter a number", 1, 10)
.tapError(e =>
Console.printLineError(e.getMessage)
)
.retry(Schedule.forever)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
146
Experiments
typeclasses
experiments/src/main/scala/typeclasses/PolymorphismUnboun
package typeclasses
import zio.*
class Dog():
def bark() = println("woof")
class Person():
def greet() = println("hello")
trait Communicate[T]:
extension (t: T)
def communicate(): Unit
trait Eater[T]:
extension (t: T)
def eat(): Unit
given Communicate[Person] with
extension (t: Person)
override def communicate(): Unit = t.greet()
given Communicate[Dog] with
extension (t: Dog)
override def communicate(): Unit = t.bark()
class Cat()
object PolymorphismUnbound extends App:
def demo[T](instance: T)(using
Communicate[T]
) = instance.communicate()
demo(
//
Person()
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
147
Experiments
Dog()
)
ZIOFromNothing
experiments/src/main/scala/ZIOFromNothing/ZIOFromScratch.
package ZIOFromNothing
class XEnvironment():
def increment(y: Int): Int =
XEnvironment.x += y
XEnvironment.x
object XEnvironment:
private var x: Int = 0
case class IO(behavior: () => Unit):
def compose(other: IO) =
IO(() =>
behavior()
println("New behavior from compose")
other.behavior()
)
object Interpreter:
def run(io: IO) = io.behavior()
@main
def runEffects =
val hi
= IO(() => println("hi "))
val there = IO(() => println("there!"))
val fullApp = hi.compose(there)
Interpreter.run(fullApp)
case class XIO[A](behavior: (x: String) => A):
def compose(other: XIO[A]) =
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
148
Experiments
XIO[A]((x: String) =>
println(s"Executing with Environment: $x")
behavior(x)
other.behavior(x)
)
object XInterpreter:
def run[A](io: XIO[A], x: String) =
io.behavior(x)
@main
def XrunEffects =
val hi
= XIO(x => println("hi "))
val there = XIO(x => println("there!"))
val x
= "Planet Z"
val fullApp = hi.compose(there)
XInterpreter.run(fullApp, x)
// 3
import zio._
import zio.Console.printLine
val zioLogic =
for
_ <- printLine("Accessing the environment")
state1 <ZIO.serviceWithZIO[XEnvironment](env =>
ZIO.succeed(env.increment(1))
)
_ <- printLine("state1: " + state1)
state2 <ZIO.serviceWithZIO[XEnvironment](env =>
ZIO.succeed(env.increment(1))
)
_ <- printLine("state2: " + state2)
yield ()
object RealZIOEnvironmentPassingExplicitlyProvided
extends ZIOAppDefault:
def run =
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
149
Experiments
zioLogic.provideLayer(
ZLayer.succeed(XEnvironment())
)
object RealZIOEnvironmentPassingProvidingSome
extends ZIOAppDefault:
def run =
zioLogic.provideSomeLayer(
ZLayer.succeed(XEnvironment())
)
case class XIO3[X, A](behavior: (x: X) => A):
def compose(other: XIO3[X, A]) =
XIO3[X, A]((x: X) =>
behavior(x)
println("New behavior from compose")
other.behavior(x)
)
object XIO3Interpreter:
def run[X, A](io: XIO3[X, A], x: X) =
io.behavior(x)
@main
def XIO3runEffects =
val hi
= XIO3(x => println("hi "))
val there
= XIO3(x => println("there!"))
val fullApp = hi.compose(there)
val z = "Planet Z"
XIO3Interpreter.run(fullApp, z)
val magicNumber = "42"
XIO3Interpreter.run(fullApp, magicNumber)
experiments-src-test-scala-test_aspects
experiments/src/test/scala/test_aspects/WithLiveSpec.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
150
Experiments
package test_aspects
import zio.*
import zio.test.*
import zio.test.TestAspect.*
object WithLiveSpec extends ZIOSpecDefault:
def halfFlaky[A](a: A): ZIO[Any, String, A] =
for
b <- zio.Random.nextBoolean.debug
o <ZIO
.cond(b, a, "failed")
.tapError(ZIO.logError(_))
yield o
val song =
for _ <- halfFlaky("works").debug
yield assertCompletes
val song1: Spec[Any, String] =
test("Song 1")(song)
val songFlaky
: Spec[Live & Annotations, String] =
test("Song Flaky")(song) @@ flaky(10) @@
withLiveRandom
val spec =
suite("Play some music")(
song1,
songFlaky,
test("Song 2")(assertCompletes)
)
end WithLiveSpec
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
151
Experiments
testcontainers
experiments/src/main/scala/testcontainers/InteractWithDatab
package testcontainers
import io.github.scottweaver.zio.testcontainers.postgres.ZPostgreSQLCon\
tainer
import zio.*
object InteractWithDatabase
extends ZIOAppDefault:
// val logic =
//
for {
//
_ <//
//
}
def run =
UserService
.get("blah")
.provide(
UserServiceLive.layer,
//
UserActionServiceLive.layer,
QuillContext.dataSourceLayer
)
experiments/src/main/scala/testcontainers/QuillContext.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
152
Experiments
package testcontainers
import com.typesafe.config.ConfigFactory
import io.getquill.context.ZioJdbc.DataSourceLayer
import io.getquill.jdbczio.Quill
import io.getquill.{
NamingStrategy,
PluralizedTableNames,
PostgresZioJdbcContext,
SnakeCase
}
import zio.*
import javax.sql.DataSource
import scala.jdk.CollectionConverters.MapHasAsJava
/** QuillContext houses the datasource layer
* which initializes a connection pool. This has
* been slightly complicated by the way Heroku
* exposes its connection details. Database URL
* will only be defined when run from Heroku in
* production.
*/
object QuillContext
extends PostgresZioJdbcContext(
NamingStrategy(
PluralizedTableNames,
SnakeCase
)
):
val dataSourceLayer
: ZLayer[Any, Nothing, DataSource] =
ZLayer {
for
_ <- ZIO.debug("Hi")
herokuURL <System.env("DATABASE_URL").orDie
_ <- ZIO.debug("Bye")
localDBConfig =
Map(
"dataSource.user"
-> "postgres",
"dataSource.password" -> "",
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
153
Experiments
"dataSource.url" ->
"jdbc:postgresql://localhost:5432/postgres"
)
configMap =
herokuURL
.map(parseHerokuDatabaseUrl(_).toMap)
.getOrElse(localDBConfig)
config =
ConfigFactory.parseMap(
configMap
.updated(
"dataSourceClassName",
"org.postgresql.ds.PGSimpleDataSource"
)
.asJava
)
yield Quill
.DataSource
.fromConfig(config)
.orDie
}.flatten
/** HerokuConnectionInfo is a wrapper for the
* datasource information to make it
* compatible with Heroku
*/
final case class HerokuConnectionInfo(
username: String,
password: String,
host: String,
port: String,
dbname: String
):
def toMap: Map[String, String] =
Map(
"dataSource.user"
-> username,
"dataSource.password" -> password,
"dataSource.url" ->
s"jdbc:postgresql://$host:$port/$dbname"
)
/** Parses the necessary information out of the
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
154
Experiments
* Heroku formatted URL
*/
def parseHerokuDatabaseUrl(
string: String
): HerokuConnectionInfo =
string match
case s"postgres://$username:$password@$host:$port/$dbname" =>
HerokuConnectionInfo(
username,
password,
host,
port,
dbname
)
end QuillContext
experiments/src/main/scala/testcontainers/UserActionService.
package testcontainers
import io.getquill.{Query, Quoted}
import zio.*
import java.sql.SQLException
import java.time.{Instant, LocalDateTime}
import javax.sql.DataSource
enum ActionType:
case LogIn,
LogOut,
UpdatePreferences
case class UserAction(
userId: String,
actionType: ActionType,
timestamp: LocalDateTime
)
trait UserActionService:
def get(
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
155
Experiments
userId: String
): ZIO[Any, UserNotFound, List[UserAction]]
def insert(
user: UserAction
): ZIO[Any, Nothing, Long]
object UserActionService:
def get(
userId: String
): ZIO[UserActionService, UserNotFound, List[
UserAction
]] =
ZIO.serviceWithZIO[UserActionService](x =>
x.get(userId)
) // use .option ?
def insert(
user: UserAction
): ZIO[UserActionService, Nothing, Long] =
ZIO.serviceWithZIO[UserActionService](x =>
x.insert(user)
)
final case class UserActionServiceLive(
dataSource: DataSource
) extends UserActionService:
import io.getquill._
// SnakeCase turns firstName -> first_name
val ctx =
new PostgresZioJdbcContext(
NamingStrategy(
PluralizedTableNames,
SnakeCase
)
)
import ctx._
inline def runWithSourceQuery[T](
inline quoted: Quoted[Query[T]]
): ZIO[Any, SQLException, List[T]] =
run(quoted).provideEnvironment(
ZEnvironment(dataSource)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
156
Experiments
)
inline def runWithSourceInsert[T](
inline quoted: Quoted[Insert[T]]
): ZIO[Any, SQLException, Long] =
run(quoted).provideEnvironment(
ZEnvironment(dataSource)
)
import java.util.UUID
implicit val encodeUserAction
: MappedEncoding[ActionType, String] =
MappedEncoding[ActionType, String](
_.toString
)
implicit val decodeUserAction
: MappedEncoding[String, ActionType] =
MappedEncoding[String, ActionType](
ActionType.valueOf(_)
)
implicit val encodeUUID
: MappedEncoding[Instant, String] =
MappedEncoding[Instant, String](_.toString)
implicit val decodeUUID
: MappedEncoding[String, Instant] =
MappedEncoding[String, Instant](
Instant.parse(_)
)
def get(
userId: String
): ZIO[Any, UserNotFound, List[UserAction]] =
inline def somePeople =
quote {
query[UserAction]
.filter(_.userId == lift(userId))
}
runWithSourceQuery(somePeople).orDie
def insert(
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
157
Experiments
user: UserAction
): ZIO[Any, Nothing, Long] =
inline def insert =
quote {
query[UserAction].insertValue(lift(user))
}
runWithSourceInsert(insert).orDie
end UserActionServiceLive
object UserActionServiceLive:
val layer
: URLayer[DataSource, UserActionService] =
ZLayer.fromFunction(
UserActionServiceLive.apply _
)
experiments/src/main/scala/testcontainers/UserService.scala
package testcontainers
import io.getquill.{Query, Quoted}
import zio.*
import io.getquill._
import java.sql.SQLException
import javax.sql.DataSource
trait UserNotFound
case class User(userId: String, name: String)
trait UserService:
def get(
userId: String
): ZIO[Any, UserNotFound, User]
def insert(user: User): ZIO[Any, Nothing, Long]
// TODO update(user)
object UserService:
def get(userId: String): ZIO[
UserService with DataSource,
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
158
Experiments
UserNotFound,
User
] =
ZIO.serviceWithZIO[UserService](
_.get(userId)
) // use .option ?
def insert(user: User): ZIO[
UserService with DataSource,
Nothing,
Long
] = // TODO Um? Why Nothing?????
ZIO.serviceWithZIO[UserService](
_.insert(user)
)
final case class UserServiceLive(
dataSource: DataSource
) extends UserService:
// SnakeCase turns firstName -> first_name
val ctx =
new PostgresZioJdbcContext(
NamingStrategy(
PluralizedTableNames,
SnakeCase
)
)
import ctx.{run, lift, _}
def get(
userId: String
): ZIO[Any, UserNotFound, User] =
inline def somePeople =
quote {
query[User]
.filter(_.userId == lift(userId))
}
run(somePeople)
.provideEnvironment(
ZEnvironment(dataSource)
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
159
Experiments
.orDie
.map(_.head)
def insert(
user: User
): ZIO[Any, Nothing, Long] =
inline def insert =
quote {
query[User].insertValue(lift(user))
}
run(insert)
.provideEnvironment(
ZEnvironment(dataSource)
)
.orDie
end UserServiceLive
object UserServiceLive:
val layer =
ZLayer.fromZIO(
for datasource <- ZIO.service[DataSource]
yield UserServiceLive(datasource)
)
interpreter-level2_chaining
experiments/src/main/scala/interpreter/level2_chaining/3_chainedOperations.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
160
Experiments
package interpreter.level2_chaining
trait Operation:
val nextAction: String => Operation
object DoNothing extends Operation:
override val nextAction = s => DoNothing
case class Print(
s: String,
nextAction: String => Operation =
_ => DoNothing
) extends Operation
//case class Pure(
//
value: String
//) extends Operation
def interpreter3(doSomething: Operation): Unit =
doSomething match
case DoNothing =>
()
case Print(s, nextAction) =>
println(s)
// It's weird that we're giving this value
// during interpretation
interpreter3(nextAction("hello"))
@main
def m1 =
val conditionallyExecuteAnotherOperation
: String => Operation =
s =>
if s == "hello" then
Print(s)
else
DoNothing
val program3: Operation =
Print(
"asdf",
conditionallyExecuteAnotherOperation
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
161
Experiments
)
interpreter3(Print("some thing"))
interpreter3(program3)
experiments/src/main/scala/interpreter/level2_chaining/4_chainedRandom.scala
package interpreter.level2_chaining
case class ToyRandom(
nextAction: String => Operation
) extends Operation
val program: Operation = ToyRandom(s => Print(s))
def interpreter(doSomething: Operation): Unit =
doSomething match
case DoNothing =>
()
case r: ToyRandom =>
val i = scala.util.Random.nextInt()
interpreter(r.nextAction(i.toString))
case p: Print =>
println(p.s)
interpreter(p.nextAction(""))
@main
def m4 = interpreter(program)
experiments/src/main/scala/interpreter/level2_chaining/5_useEnvironmentInstances.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
162
Experiments
package interpreter.level2_chaining
import environment_exploration.ToyEnvironment
def interpret(
env: ToyEnvironment[
scala.util.Random & scala.Console.type
],
doSomething: Operation
): Unit =
doSomething match
case DoNothing =>
()
case r: ToyRandom =>
val i =
env.get[scala.util.Random].nextInt()
interpret(env, r.nextAction(i.toString))
case p: Print =>
env.get[scala.Console.type].println(p.s)
interpret(env, p.nextAction(""))
@main
def demoInterpretWithEnvironment() =
val env =
ToyEnvironment(Map.empty)
.add[scala.util.Random](scala.util.Random)
.add(scala.Console)
interpret(env, program)
Hubs
experiments/src/main/scala/Hubs/BasicHub.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
163
Experiments
package Hubs
import
import
import
import
zio.*
zio.Duration
zio.Clock
zio.Console
// The purpose of this example to to create a
// very basic hub that displays small
// capabilities.
object BasicHub extends zio.ZIOAppDefault:
// This example makes a hub, and publishes a
// String. Then, two entities take the
// published string and print it.
val logic1 =
Hub
.bounded[String](2)
.flatMap { Hub =>
ZIO.scoped {
Hub
.subscribe
.zip(Hub.subscribe)
.flatMap { case (left, right) =>
for
_ <Hub.publish(
"This is from Hub left!"
)
_ <left
.take
.flatMap(
Console.printLine(_)
)
_ <right
.take
.flatMap(
Console.printLine(_)
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
164
Experiments
yield ()
}
}
}
/*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
case class entity(name:String) case class
question(ques:String) case class
response(rep:String, ent:entity) val
entities = List(entity("Bob"),
entity("Smith")) //This example sends out a
question in the form of a string. Then, two
//entities respond with different reponses.
val logic2 =
for questHub <- Hub.bounded[question](1)
repHub <Hub.bounded[response](entities.size) _ <questHub.subscribe.zip(repHub.subscribe).use
{ case ( Quest, Resp ) =
} */
def run = logic1.exitCode
end BasicHub
experiments/src/main/scala/Hubs/QuizGame.scala
package Hubs
import console.FakeConsole
import java.io.IOException
import zio.{
Clock,
Console,
Dequeue,
Duration,
Hub,
Ref,
Schedule,
ZIO,
durationInt
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
165
Experiments
}
import zio.Console.printLine
object QuizGame extends zio.ZIOAppDefault:
case class Player(name: String)
case class Question(
text: String,
correctResponse: String
)
case class Answer(
player: Player,
text: String,
delay: Duration
)
case class RoundDescription(
question: Question,
answers: Seq[Answer]
)
def run = // Use App's run function
/* Teacher --> Questions --> Student1 -->
* Answers --> Teacher Student2 Student3 */
val
val
val
val
frop
zeb
shtep
cheep
=
=
=
=
Player("Frop")
Player("Zeb")
Player("Shtep")
Player("Cheep")
val students: List[Player] =
List(frop, zeb, shtep, cheep)
def submitAnswersAfterDelay(
answerHub: Hub[Answer],
answers: Seq[Answer]
) =
ZIO
.collectAllPar(
answers.map { answer =>
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
166
Experiments
for
_ <- ZIO.sleep(answer.delay)
_ <- answerHub.publish(answer)
yield ()
}
)
.unit
def recordCorrectAnswers(
correctAnswer: String,
answers: Dequeue[Answer],
correctRespondents: Ref[List[Player]]
) =
for // gather answers until there's a winner
answer <- answers.take
output <if (answer.text == correctAnswer)
for
currentCorrectRespondents <correctRespondents.get
_ <correctRespondents.set(
currentCorrectRespondents :+
answer.player
)
yield "Correct response from: " +
answer.player
else
ZIO.succeed(
"Incorrect response from: " +
answer.player
)
_ <- printLine(output)
yield ()
def untilWinnersAreFound(
correctRespondents: Ref[List[Player]]
) =
Schedule.recurUntilZIO(_ =>
correctRespondents.get.map(_.size == 2)
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
167
Experiments
def printRoundResults(
winners: List[Player]
) =
val finalOutput =
if (winners.isEmpty)
"Nobody submitted a correct response"
else if (winners.size == 2)
"Winners: " + winners.mkString(",")
else
"Winners of incomplete round: " +
winners.mkString(",")
printLine(finalOutput)
val roundWithMultipleCorrectAnswers =
RoundDescription(
Question(
"What is the southern-most European country?",
"Spain"
),
Seq(
Answer(zeb, "Germany", 2.seconds),
Answer(frop, "Spain", 1.seconds),
Answer(cheep, "Spain", 3.seconds),
Answer(shtep, "Spain", 4.seconds)
)
)
val roundWithOnly1CorrectAnswer =
RoundDescription(
Question(
"What is the lightest element?",
"Hydrogen"
),
Seq(
Answer(frop, "Lead", 2.seconds),
Answer(zeb, "Hydrogen", 1.seconds),
Answer(cheep, "Gold", 3.seconds),
Answer(shtep, "Hydrogen", 10.seconds)
)
)
val roundWhereEverybodyIsWrong =
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
168
Experiments
RoundDescription(
Question(
"What is the average airspeed of an unladen swallow?",
"INSUFFICIENT DATA FOR MEANINGFUL ANSWER"
),
Seq(
Answer(frop, "3.0 m/s", 1.seconds),
Answer(zeb, "Too fast", 1.seconds),
Answer(
cheep,
"Not fast enough",
1.seconds
),
Answer(shtep, "Scary", 1.seconds)
)
)
val rounds =
Seq(
roundWithMultipleCorrectAnswers,
roundWithOnly1CorrectAnswer,
roundWhereEverybodyIsWrong
)
val cahootSingleRound =
for
questionHub <- Hub.bounded[Question](1)
answerHub: Hub[Answer] <Hub.bounded[Answer](students.size)
correctRespondents: Ref[List[Player]] <Ref.make[List[Player]](List.empty)
_ <questionHub
.subscribe
.zip(answerHub.subscribe)
.flatMap {
case (
questions,
answers: Dequeue[Answer]
) =>
def playARound(
roundDescription: RoundDescription
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
169
Experiments
) =
for
_ <printLine(
"==============================="
)
_ <printLine(
"Question for round: " +
roundDescription
.question
.text
)
_ <correctRespondents
.set(List.empty)
_ <questionHub.publish(
roundDescription.question
)
question <- questions.take
_ <ZIO
.collectAllPar(
Seq(
submitAnswersAfterDelay(
answerHub,
roundDescription
.answers
),
recordCorrectAnswers(
roundDescription
.question
.correctResponse,
answers,
correctRespondents
).repeat(
untilWinnersAreFound(
correctRespondents
)
)
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
170
Experiments
)
.timeout(4.second)
winners <correctRespondents.get
_ <printRoundResults(winners)
_ <printLine(
"==============================="
)
yield ()
ZIO.foreach(rounds)(playARound)
}
yield ()
cahootSingleRound.exitCode
end run
end QuizGame
experiments/src/main/scala/Hubs/ReadIntAndMultiply.scala
package Hubs
import
import
import
import
import
import
console.FakeConsole
zio.ZIO
zio.*
zio.Duration.*
zio.Clock.*
zio.Console.*
object ReadIntAndMultiply
extends zio.ZIOAppDefault:
def run = // Use App's run function
val logic =
for
hub <- Hub.bounded[Int](2)
_ <ZIO.scoped {
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
171
Experiments
hub
.subscribe
.flatMap { hubSubscription =>
val getAndStoreInput =
for
_ <Console.printLine(
"Please provide an int"
)
input <- Console.readLine
nextInt = input.toInt
_ <- hub.publish(nextInt)
yield ()
val processNextIntAndPrint =
for
nextInt <hubSubscription.take
_ <Console.printLine(
"Multiplied Int: " +
nextInt * 5
)
yield ()
val reps = 5
for _ <ZIO
.collectAllPar(
Set(
getAndStoreInput
.repeatN(reps),
processNextIntAndPrint
.forever
)
)
.timeout(5.seconds)
yield ()
}
}
yield ()
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
172
Experiments
(
for
fakeConsole <FakeConsole.withInput(
"3",
"5",
"7",
"9",
"11",
"13"
)
_ <- logic.withConsole(fakeConsole)
yield ()
).exitCode
end run
end ReadIntAndMultiply
TicTacToe
experiments/src/main/scala/TicTacToe/TicTacToe.scala
// From: https://scalac.io/blog/write-command-line-application-with-zio/
package TicTacToe
import zio.{Console, ZIOAppDefault, ZIO}
enum MenuCommand:
case NewGame,
Resume,
Quit,
Invalid
object TicTacToe extends ZIOAppDefault:
val program
: ZIO[Any, java.io.IOException, Unit] =
Console.printLine("TicTacToe game!")
def run =
program.foldZIO(
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
173
Experiments
error =>
Console.printLine(
s"Execution failed with: $error"
) *> ZIO.succeed(1),
_ => ZIO.succeed(0)
)
// ----------------------------------abstract case class Name private (name: String)
object Name:
def make(name: String) = new Name(name) {}
// Can't inherit because constructor is private:
// class FirstName(fn: String) extends Name(fn)
def bar =
val name: Name = Name.make("Bob")
// val name2 = new Name("Joe") {}
// val jan = name.copy("Janet")
// ----------------------------------trait X
object X:
var x: Int = 0
trait XIO[IO, R]
case class IntXIO(i: Int) extends XIO[X, Int]
def combine2(a: Int, b: Int): XIO[X, Int] =
X.x += 1
IntXIO(a + b + X.x)
def foo = combine2(1, 2)
//trait YIO[ENV, F, R] extends Either[F, R]
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
174
Experiments
runtime
experiments/src/main/scala/runtime/BuiltInServices.scala
package runtime
import zio.{Console, ZIO, ZIOAppDefault}
import java.time.Instant
object BuiltInServices extends ZIOAppDefault:
val logic
: ZIO[Any, java.io.IOException, Instant] =
for
now <ZIO.clockWith(clock => clock.instant)
_ <- Console.printLine("Now: " + now)
yield now
def run = logic
experiments-src-test-scala-testcontainers
experiments/src/test/scala/testcontainers/DbMigration.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
175
Experiments
package testcontainers
import
import
import
import
import
io.github.scottweaver.models.JdbcInfo
org.flywaydb.core.Flyway
org.flywaydb.core.api.configuration.FluentConfiguration
zio.*
zio.test.TestAspect.{before, beforeAll}
object DbMigration:
type ConfigurationCallback =
(FluentConfiguration) => FluentConfiguration
private def doMigrate(
jdbcInfo: JdbcInfo,
configureCallback: ConfigurationCallback,
locations: String*
) =
ZIO.attempt {
val flyway =
configureCallback {
val flyway =
Flyway
.configure()
.dataSource(
jdbcInfo.jdbcUrl,
jdbcInfo.username,
jdbcInfo.password
)
if (locations.nonEmpty)
flyway.locations(locations: _*)
else
flyway
}.load()
flyway.migrate
}
def migrate(mirgationLocations: String*)(
configureCallback: ConfigurationCallback =
identity
) =
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
176
Experiments
ZIO
.service[JdbcInfo]
.flatMap(jdbcInfo =>
doMigrate(
jdbcInfo,
configureCallback,
mirgationLocations: _*
)
)
.orDie
def migratedLayer(
jdbcInfo: ZEnvironment[JdbcInfo]
): ZLayer[Any, Throwable, Unit] =
ZLayer.fromZIO(
DbMigration
.migrate("db")()
.provideEnvironment(jdbcInfo)
.unit
)
end DbMigration
experiments/src/test/scala/testcontainers/SharedDbLayer.scal
package testcontainers
import io.github.scottweaver.models.JdbcInfo
import io.github.scottweaver.zio.testcontainers.postgres.ZPostgreSQLCon\
tainer
import zio.ZLayer
import javax.sql.DataSource
object SharedDbLayer:
val layer =
for
layer <ZLayer.make[DataSource & JdbcInfo](
ZPostgreSQLContainer.live,
ZPostgreSQLContainer.Settings.default
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
177
Experiments
)
_ <- DbMigration.migratedLayer(layer)
yield layer
experiments/src/test/scala/testcontainers/TestContainerLayer
package testcontainers
import com.zaxxer.hikari.{
HikariConfig,
HikariDataSource
}
import io.github.scottweaver.models.JdbcInfo
import zio.*
import java.util.Properties
import javax.sql.DataSource
import scala.jdk.CollectionConverters.MapHasAsJava
object TestContainerLayers:
val dataSourceLayer
: ZLayer[JdbcInfo, Nothing, DataSource] =
ZLayer {
for
jdbcInfo <- ZIO.service[JdbcInfo]
datasource <ZIO
.attemptBlocking(
unsafeDataSourceFromJdbcInfo(
jdbcInfo
)
)
.orDie
yield datasource
}
private def unsafeDataSourceFromJdbcInfo(
jdbcInfo: JdbcInfo
): DataSource =
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
178
Experiments
val props = new Properties()
props.putAll(
Map(
"driverClassName" ->
jdbcInfo.driverClassName,
"jdbcUrl" -> jdbcInfo.jdbcUrl,
"username" -> jdbcInfo.username,
"password" -> jdbcInfo.password
).asJava
)
println("JdbcInfo: " + jdbcInfo)
new HikariDataSource(new HikariConfig(props))
end TestContainerLayers
experiments/src/test/scala/testcontainers/UserActionSpec.scal
package testcontainers
import com.dimafeng.testcontainers.PostgreSQLContainer
import io.github.scottweaver.models.JdbcInfo
import io.github.scottweaver.zio.aspect.DbMigrationAspect
import io.github.scottweaver.zio.testcontainers.postgres.ZPostgreSQLCon\
tainer
import io.github.scottweaver.zio.testcontainers.postgres.ZPostgreSQLCon\
tainer.{
Settings,
live
}
import org.postgresql.ds.PGSimpleDataSource
import zio.*
import zio.test.*
import java.sql.Connection
import javax.sql.DataSource
object UserActionSpec
/* extends ZIOSpec[DataSource & JdbcInfo]:
* val bootstrap = SharedDbLayer.layer
*
* def spec =
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
179
Experiments
*
*
*
*
*
*
suite("UserActionService")( test("inserts a
user") { for _ <- UserActionService
.get("uuid_hard_coded") .debug("Actions")
yield assertCompletes }
).provideSomeShared[DataSource](
UserActionServiceLive.layer ) */
experiments/src/test/scala/testcontainers/UserServiceSpec.sca
package testcontainers
import com.dimafeng.testcontainers.PostgreSQLContainer
import io.github.scottweaver.models.JdbcInfo
import zio.test.*
import zio.*
import io.github.scottweaver.zio.testcontainers.postgres.ZPostgreSQLCon\
tainer.live
import io.github.scottweaver.zio.testcontainers.postgres.ZPostgreSQLCon\
tainer.Settings
import io.github.scottweaver.zio.testcontainers.postgres.ZPostgreSQLCon\
tainer
import io.github.scottweaver.zio.aspect.DbMigrationAspect
import org.postgresql.ds.PGSimpleDataSource
import zio.test.TestAspect.ignore
import java.sql.Connection
import javax.sql.DataSource
object UserServiceSpec
/* extends ZIOSpec[DataSource & JdbcInfo]:
* val bootstrap = SharedDbLayer.layer def spec =
* suite("UserService")( test("retrieves an
* existin user")( for user <- UserService
* .get("uuid_hard_coded") .debug yield
* assertCompletes ), test("inserts a user") {
* val newUser =
* User("user_id_from_app", "Appy") for _ <* UserService.insert(newUser) user <* UserService.get(newUser.userId) yield
* assertTrue(newUser == user) }
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
180
Experiments
* ).provideSomeShared[DataSource](
* UserServiceLive.layer ) end UserServiceSpec */
crypto
experiments/src/main/scala/crypto/Mining.scala
package crypto
import zio._
import ZIO.debug
import zio.Random.nextIntBetween
import java.io.IOException
import scala.annotation.tailrec
object Mining extends ZIOAppDefault:
def run =
for
chain <- Ref.make[BlockChain](BlockChain())
_
<- raceForNextBlock(chain).repeatN(5)
_
<- chain.get.debug("Final")
yield ()
private val miners =
Seq("Zeb", "Frop", "Shtep")
.flatMap(minerName =>
Range(1, 50)
.map(i => new Miner(minerName + i))
)
def raceForNextBlock(
chain: Ref[BlockChain]
): ZIO[Any, Nothing, Unit] =
for
raceResult <- findNextBlock(miners)
(winner, winningPrime) = raceResult
_ <chain.update(chainCurrent =>
chainCurrent.copy(blocks =
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
181
Experiments
chainCurrent.blocks :+ winningPrime
)
)
_ <debug(
s"$winner mined block: $winningPrime"
)
yield ()
case class BlockChain(
blocks: List[Int] = List.empty
)
class Miner(val name: String):
def mine(
num: Int
): ZIO[Any, Nothing, (String, Int)] =
for
duration <- nextIntBetween(1, 4)
_
<- ZIO.sleep(duration.second)
yield (name, nextPrimeAfter(num))
def findNextBlock(
miners: Seq[Miner]
): ZIO[Any, Nothing, (String, Int)] =
for
startNum <- nextIntBetween(2000, 4000)
result <ZIO.raceAll(
miners.head.mine(startNum),
miners.tail.map(_.mine(startNum))
)
yield result
end Mining
// TODO Consider putting math functions somewhere else to avoid clutter\
ing example
private def isPrime(num: Int): Boolean =
(2 until num)
.forall(divisor => num % divisor != 0)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
182
Experiments
@tailrec
private def nextPrimeAfter(num: Int): Int =
if (isPrime(num))
num
else
nextPrimeAfter(num + 1)
environment_exploration-opaque
experiments/src/main/scala/environment_exploration/opaque/ToyEnvironment.scala
package environment_exploration.opaque
import scala.reflect.{ClassTag, classTag}
case class DBService(url: String)
opaque type ToyEnvironment[R] =
Map[ClassTag[_], _]
object ToyEnvironment:
def apply[A: ClassTag](
a: A
): ToyEnvironment[A] = Map(classTag[A] -> a)
extension [R](env: ToyEnvironment[R])
def add[A: ClassTag](
a: A
): ToyEnvironment[R & A] =
env + (classTag[A] -> a)
def get[A >: R: ClassTag]: A =
env(classTag[A]).asInstanceOf[A]
@main
def demoToyEnvironment =
val env1: ToyEnvironment[String] =
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
183
Experiments
ToyEnvironment("hi")
val env2: ToyEnvironment[String & DBService] =
env1.add(DBService("blah"))
val env3: ToyEnvironment[
String & DBService & List[String] & List[Int]
] = env2.add(List("a", "b")).add(List(1, 2))
println(env3.get[String])
println(env3.get[DBService])
println(env3.get[List[String]])
println(env3.get[List[Int]])
// We get some amount of compile time safety here, but not much
// println(env.get(classOf[List[DBService]]))
simulations
experiments/src/main/scala/simulations/Evolution.scala
package simulations
import zio._
enum Action:
case Stay
case Move(x: Int, y: Int)
object Evolution extends ZIOAppDefault:
case class Percentage(value: Int):
assert(value >= 0 & value <= 100)
case class Creature(
energy: Int,
explore: Percentage
)
case class Coordinate(
food: Int,
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
184
Experiments
produceFood: Percentage,
occupant: Option[Creature]
)
case class Spots(
coordinates: List[List[Coordinate]]
):
override def toString() =
coordinates
.map(row =>
row
.map(column =>
if (column.food > 0)
'*'
else
'\u25a1'
)
.mkString
)
.mkString("\n")
object Spots:
def apply(rows: Int, columns: Int): Spots =
Spots(
List.fill(rows)(
List.fill(columns)(
Coordinate(
1,
Percentage(10),
occupant = None
)
)
)
)
def run =
for
_ <- ZIO.log("Should do evolution stuff")
spots = Spots(4, 4)
_ <- ZIO.debug(spots)
yield ()
end Evolution
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
185
Experiments
object EvolutionT:
trait Creature:
def decide(): Action
trait Spots:
def udpate(): Spots
HelloZio
experiments/src/main/scala/HelloZio/CalculatorExample.scala
package HelloZio
import HelloZio.CalculatorExample.input
import java.io.IOException
import zio.Console.{readLine, printLine}
import zio.Console
import zio.Console
import zio.{
IO,
Ref,
Runtime,
ZIO,
ZLayer,
ZIOAppDefault
}
enum ArithmeticOperation(a: Float, b: Float):
case Add(first: Float, second: Float)
extends ArithmeticOperation(first, second)
case Divide(first: Float, second: Float)
extends ArithmeticOperation(first, second)
// def calculateX(): ZIO[Any, Throwable |
// ArithmeticException, String]
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
186
Experiments
//
//
//
//
//
//
//
This a more complicated example of using ZIO
to create a safely running program.
This is a simple calculator app that takes
in an opperation index, and two numbers.
It then prints the resulting calculation of
the two numbers based on the operation
index.
//
//
//
//
//
The ZIO are used in several ways. The ZIO
are used to ensure propper IO Exception and
error handling,
and they are used handle invalid inputs from
the user.
def calculate(): ZIO[
Any,
Throwable | ArithmeticException,
String
] = // This in an function used in calculations implemented below.
this match
case Add(first, second) =>
ZIO.succeed {
s"Adding $first and $second: ${first - second}"
}
case Divide(first, second) =>
if (second != 0.0)
ZIO.succeed {
s"Dividing $first by $second: ${first / second}"
}
else
ZIO.fail(
new ArithmeticException(
"divide by 0"
)
)
end ArithmeticOperation
object ArithmeticOperation: // This in an object used in calculations i\
mplemented below.
def fromInt(
index: Int
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
187
Experiments
): (Float, Float) => ArithmeticOperation =
index match
case 1 =>
Add.apply
case 2 =>
Divide.apply
case _ =>
throw new RuntimeException("boom")
// extends zio.App
object CalculatorExample extends ZIOAppDefault:
def input: ZIO[ // This function prompts and accepts the input from t\
he user.
Console,
IOException,
Vector[String]
] =
for
_ <printLine(""" ~~~~~~~~~~~~~~~~
Input Option:
1) Add
2) Subtract
3) Multiply
4) Devide
""")
in <readLine // User inputs the operation index
_ <- printLine(s"input: ${in}")
_ <- printLine("Enter first number: ")
firstNum <readLine // User inputs first and second number
_ <- printLine("Enter second number: ")
secondNum <- readLine
yield Vector(
in,
firstNum,
secondNum
) // The function returns a ZIO that succeeds with a vector of Stri\
ngs
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
188
Experiments
def operate( // This function takes in the inputs, and processes them\
to ensure they are valid.
// The function then returns a String
// statement of the calculation.
input: Vector[String]
): ZIO[
Any,
String | NumberFormatException |
ArithmeticException | Throwable,
String
] =
for
number <- // note that `(number1, number2)` now results in: value\
withFilter is not a member of zio.ZIO[Any, Nothing, (Float, Float)], b\
ut could be made available as an extension method.
ZIO.attempt {
(input(1).toFloat, input(2).toFloat)
} // The inputs are cast as Floats, and passed into a ZIO objec\
t.
result <ArithmeticOperation // This object, defined above, processes th\
e operation index, and passes the according calculation to the function\
calculate.
.fromInt(input(0).toInt)(
number._1,
number._2
)
.calculate() // calculate takes the input numbers from Arithm\
eticOperation, and creates the return statement
// _ <- printLine("Typed, parse operation: "
// +
// operation)
//
output <//
input(0) match
//
case "2" =>
//
ZIO {
// s"Subtracting $number1 and $number2:
// ${number1 - number2}"
//
}
//
case "3" =>
//
ZIO {
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
189
Experiments
// s"Multiplying $number1 and $number2:
// ${number1 * number2}"
//
}
// case badIndex => ZIO.fail("unknown program
// index: " + badIndex)
yield result
def run =
println("In tester")
val stringRef =
Ref.make(
Seq("1", "2", "3")
) // This is used in the automation software for running examples.
val operated =
for
// console <// FakeConsole.createConsoleWithInput(Seq("1",
// "24", "8"))
console <console
.FakeConsole
.withInput(
"2",
"96",
"8"
) // Run this program with the following inputs
i <input.provide(ZLayer.succeed(console))
output <operate(i).catchAll {
case x: String =>
ZIO.succeed("Input failure: " + x)
case x: Throwable =>
ZIO
.succeed("toFloat failure: " + x)
}
_ <- printLine(output)
yield ()
operated.exitCode
end run
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
190
Experiments
end CalculatorExample
Parallelism
experiments/src/main/scala/Parallelism/BasicFiber.scala
package Parallelism
import java.io.IOException
import zio.Console
import zio.{Fiber, IO, Runtime, UIO, ZIO, ZLayer}
object BasicFiber:
//
//
//
//
//
Fibers model a running IO: Fiber[E,A]. They
have an error type, and a success type.
They don't need an input environment type.
They are not technically effects, but they
can be converted to effects.
object computation: // This object performs a computation that takes \
a long time. It is a recursive Fibonacci Sequence generator.
def fib(n: Long): UIO[Long] =
ZIO
.succeed {
if (n <= 1)
ZIO.succeed(n)
else
fib(n - 1).zipWith(fib(n - 2))(_ + _)
}
.flatten
// Fork will take an effect, and split off a
// Fiber version of it.
// This ZIO will output a Fiber that is
// computing the 100th digit of the Fibonacci
// Sequence.
val fib100: UIO[Fiber[Nothing, Long]] =
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
191
Experiments
for fiber <- computation.fib(100).fork
yield fiber
// Part of the power of Fibers is that many of
// them can be described and run at once.
// This function uses two numbers (n and m),
// and outputs two Fibers that will find the
// n'th and m'th Fibonacci numbers
val n: Long = 50
val m: Long = 100
val fibNandM
: UIO[Vector[Fiber[Nothing, Long]]] =
for
fiberN <- computation.fib(n).fork
fiberM <- computation.fib(m).fork
yield Vector(fiberN, fiberM)
end BasicFiber
experiments/src/main/scala/Parallelism/Compose.scala
package Parallelism
import
import
import
import
java.io.IOException
zio._
zio.Console._
zio.Fiber._
class Compose:
// Composing Fibers will combine 2 or more
// fibers into a single fiber. This new fiber
// will produce the results of both. If any of
// the fibers fail, the entire zipped fiber
// will also fail.
// Note: The results of the zipped fibers will
// be put into a tuple.
val helloGoodbye: UIO[Tuple] =
for
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
192
Experiments
greeting <- ZIO.succeed("Hello!").fork
farewell <- ZIO.succeed("GoodBye!").fork
totalFiber =
greeting.zip(
farewell
) // Note the '=', not '<-'
tuple <- totalFiber.join
yield tuple
//
//
//
//
//
//
A very useful fiber method or composing is
the 'orElse' method.
This method will combine two fibers. If the
first succeeds, the composed fiber will
succeed with first fiber's result. If the
first fails, the second will be used.
val isPineapple: IO[String, String] =
ZIO.succeed("Pineapple!")
val notPineapple: IO[String, String] =
ZIO.fail("Banana...")
val composeFruit: IO[String, String] =
for
fFiber <notPineapple
.fork // notPineapple will fail
sFiber <isPineapple
.fork // isPineapple will succeed
totalFiber = fFiber.orElse(sFiber)
output <totalFiber
.join // The output effect will end up using isPineapple.
yield output
end Compose
experiments/src/main/scala/Parallelism/Finalizers.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
193
Experiments
package Parallelism
import java.io.IOException
import zio.Console.printLine
import zio.{
Console,
Fiber,
IO,
Runtime,
Scope,
UIO,
URIO,
ZIO,
ZLayer
}
import scala.io.Source.*
object Finalizers extends zio.ZIOAppDefault:
//
//
//
//
//
//
In this example, we create a ZIO that uses
file IO. It opens a file to read it, but
gets failed half way through.
We use a finalizer to ensure that even if
the ZIO fails unexpectedly, the file will
still be closed.
def finalizer(
source: scala.io.Source
) = // Define the finalizer behavior here
ZIO.succeed {
println("Finalizing: Closing file reader")
source.close // Close the input source
}
val readFileContents
: ZIO[Scope, Throwable, Vector[String]] =
ZIO
.acquireRelease(
ZIO.succeed(
scala
.io
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
194
Experiments
.Source
.fromFile(
"src/main/scala/Parallelism/csvFile.csv"
)
)
)(finalizer)
.map {
bufferedSource => // Use the bracket method with the finalizer \
defined above to define behavior on fail.
val lines =
for line <- bufferedSource.getLines
yield line
if (
true
) // Simulating an enexpected error/exception
throw new IOException("Boom!")
Vector() ++ lines
}
def run = // Use App's run function
println("In main")
val ioExample: ZIO[
Scope,
Throwable,
Unit
] = // Define the ZIO contexts
for
fileLines <- readFileContents
_ <printLine(
fileLines.mkString("\n")
) // Combine the strings of the output vector into a single s\
tring, separated by \n
yield ()
ioExample
.catchAllDefect(exception =>
printLine(
"Ultimate error message: " +
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
195
Experiments
exception.getMessage
)
)
.exitCode // Call the Zio with exitCode.
end run
end Finalizers
experiments/src/main/scala/Parallelism/Interrupt.scala
package Parallelism
import
import
import
import
java.io.IOException
zio._
zio.Console._
zio.durationInt
class Interrupt:
val n = 100
// This ZIO does nothing but count to n.
// It is not productive, but it uses resources.
val countToN: ZIO[Clock, Nothing, Unit] =
for _ <- ZIO.sleep(n.seconds)
yield ()
// This effect will create a fiber vrsion of
// countToN.
// It will then interrupt the fiber, which
// returns an exit object.
// Note: Interrupting Fibers is completely
// safe.
// Interrupt safely releases all resources, and
// runs the finalizers.
val noCounting: ZIO[Clock, Nothing, Exit[
Nothing,
Unit
]] =
for
fiber <- countToN.fork
exit <- fiber.interrupt
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
196
Experiments
yield exit
end Interrupt
experiments/src/main/scala/Parallelism/Join.scala
package Parallelism
import java.io.IOException
import zio.{Fiber, IO, Runtime, UIO, ZIO, ZLayer}
class Join:
// Joining a fiber converts it into an effect.
// This effect will succeed or fail depending
// on the fiber.
val joinedFib100
: UIO[Long] = // This function makes a fiber, then joins the fibe\
r, and returns it as an effect
for
fiber <computation
.fib(100)
.fork // Fiber is made to find 100th value of Fib
output <fiber
.join // Fiber is converted into an effect, then returned.
yield output
// This object performs a computation that
// takes a long time. It is a recursive
// Fibonacci Sequence generator.
object computation:
def fib(n: Long): UIO[Long] =
ZIO
.succeed {
if (n <= 1)
ZIO.succeed(n)
else
fib(n - 1).zipWith(fib(n - 2))(_ + _)
}
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
197
Experiments
.flatten
end Join
experiments/src/main/scala/Parallelism/JustSleep.scala
package Parallelism
import java.io.IOException
import zio.{
Fiber,
IO,
Runtime,
UIO,
Unsafe,
ZIO,
ZIOAppDefault,
ZLayer,
durationInt
}
import scala.concurrent.Await
object JustSleep extends ZIOAppDefault:
override def run =
ZIO.collectAllPar(
(1 to 10000).map(_ => ZIO.sleep(1.seconds))
) *>
ZIO.debug(
"Finished far sooner than 10,000 seconds"
)
@main
def ToFuture() =
Await.result(
Unsafe.unsafe { (u: Unsafe) =>
given Unsafe = u
zio
.Runtime
.default
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
198
Experiments
//
.unsafe
.runToFuture(ZIO.sleep(1.seconds))
.getOrThrowFiberFailure()
},
scala.concurrent.duration.Duration.Inf
)
game_theory
experiments/src/main/scala/game_theory/DecisionService.scala
package game_theory
import zio.{Ref, ZIO, ZLayer}
trait DecisionService:
def getDecisionsFor(
prisoner1: Prisoner,
prisoner2: Prisoner
): ZIO[Any, String, RoundResult]
object DecisionService:
class LiveDecisionService(
history: Ref[DecisionHistory]
) extends DecisionService:
private def getDecisionFor(
prisoner: Prisoner,
actionsAgainst: List[Action]
): ZIO[Any, String, Decision] =
for action <prisoner.decide(actionsAgainst)
yield Decision(prisoner, action)
def getDecisionsFor(
prisoner1: Prisoner,
prisoner2: Prisoner
): ZIO[Any, String, RoundResult] =
for
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
199
Experiments
prisoner1history <history
.get
.map(_.historyFor(prisoner1))
prisoner2history <history
.get
.map(_.historyFor(prisoner2))
decisions <getDecisionFor(
prisoner1,
prisoner2history
).zipPar(
getDecisionFor(
prisoner2,
prisoner1history
)
)
roundResult =
RoundResult(decisions._1, decisions._2)
_ <history.updateAndGet(oldHistory =>
DecisionHistory(
roundResult :: oldHistory.results
)
)
yield roundResult
end LiveDecisionService
object LiveDecisionService:
def make(): ZIO[
Any,
Nothing,
LiveDecisionService
] =
for history <Ref.make(DecisionHistory(List.empty))
yield LiveDecisionService(history)
val liveDecisionService: ZLayer[
Any,
Nothing,
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
200
Experiments
LiveDecisionService
] = ZLayer.fromZIO(LiveDecisionService.make())
end DecisionService
experiments/src/main/scala/game_theory/SingleBasic.scala
package game_theory
import game_theory.Action.{Betray, Silent}
import game_theory.Outcome.{
BothFree,
BothPrison,
OnePrison
}
import zio.Console.printLine
import zio.*
case class Decision(
prisoner: Prisoner,
action: Action
)
case class RoundResult(
prisoner1Decision: Decision,
prisoner2Decision: Decision
):
override def toString: String =
s"RoundResult(${prisoner1Decision.prisoner}:${prisoner1Decision
.action} ${prisoner2Decision.prisoner}:${prisoner2Decision.acti\
on})"
case class DecisionHistory(
results: List[RoundResult]
):
def historyFor(
prisoner: Prisoner
): List[Action] =
results.map(roundResult =>
if (
roundResult.prisoner1Decision.prisoner ==
prisoner
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
201
Experiments
roundResult.prisoner1Decision.action
else
roundResult.prisoner2Decision.action
)
trait Strategy:
def decide(
actionsAgainst: List[Action]
): ZIO[Any, Nothing, Action]
case class Prisoner(
name: String,
strategy: Strategy
):
def decide(
actionsAgainst: List[Action]
): ZIO[Any, Nothing, Action] =
strategy.decide(actionsAgainst)
override def toString: String = s"$name"
val silentAtFirstAndEventuallyBetray =
new Strategy:
override def decide(
actionsAgainst: List[Action]
): ZIO[Any, Nothing, Action] =
if (actionsAgainst.length < 3)
ZIO.succeed(Silent)
else
ZIO.succeed(Betray)
val alwaysTrust =
new Strategy:
override def decide(
actionsAgainst: List[Action]
): ZIO[Any, Nothing, Action] =
ZIO.succeed(Silent)
val silentUntilBetrayed =
new Strategy:
override def decide(
actionsAgainst: List[Action]
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
202
Experiments
): ZIO[Any, Nothing, Action] =
if (actionsAgainst.contains(Betray))
ZIO.succeed(Betray)
else
ZIO.succeed(Silent)
enum Action:
case Silent
case Betray
enum Outcome:
case BothFree,
BothPrison
case OnePrison(prisoner: Prisoner)
object SingleBasic extends ZIOAppDefault:
def play(
prisoner1: Prisoner,
prisoner2: Prisoner
): ZIO[DecisionService, String, Outcome] =
for
decisionService <ZIO.service[DecisionService]
roundResult <decisionService
.getDecisionsFor(prisoner1, prisoner2)
.debug("Decisions")
outcome =
(
roundResult.prisoner1Decision.action,
roundResult.prisoner2Decision.action
) match
case (Silent, Silent) =>
BothFree
case (Betray, Silent) =>
OnePrison(prisoner2)
case (Silent, Betray) =>
OnePrison(prisoner1)
case (Betray, Betray) =>
BothPrison
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
203
Experiments
yield outcome
val bruce =
Prisoner(
"Bruce",
silentAtFirstAndEventuallyBetray
)
val bill =
Prisoner("Bill", silentUntilBetrayed)
def run =
play(bruce, bill)
.debug("Outcome")
.repeatN(4)
.provideLayer(
DecisionService.liveDecisionService
)
end SingleBasic
scenarios
experiments/src/main/scala/scenarios/CivilEngineering.scala
package scenarios
import zio.ZIOAppArgs
import zio.{ZIOAppDefault, ZIO}
object CivilEngineering extends ZIOAppDefault:
trait Company[T]:
def produceBid(
projectSpecifications: ProjectSpecifications[
T
]
): ProjectBid[T]
object Companies:
def operatingIn[T](
state: State
): ZIO[World, Nothing, AvailableCompanies[
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
204
Experiments
T
]] = ???
trait ProjectSpecifications[T]
trait LegalRestriction
case class War(reason: String)
trait UnfulfilledPromise
trait ProjectBid[T]
val run = ???
val installPowerLine = ???
case class AvailableCompanies[T](
companies: Set[Company[T]]
):
def lowestBid(
projectSpecifications: ProjectSpecifications[
T
]
): ProjectBid[T] = ???
trait World
object World:
def legalRestrictionsFor(
state: State
): ZIO[World, War, Set[LegalRestriction]] =
???
def politiciansOf(
state: State
): ZIO[World, War, Set[LegalRestriction]] =
???
trait OutOfMoney
trait PrivatePropertyRefusal
def build[T](projectBid: ProjectBid[T]): ZIO[
Any,
UnfulfilledPromise | OutOfMoney |
PrivatePropertyRefusal,
T
] = ???
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
205
Experiments
def stateBid[T](
state: State,
projectSpecifications: ProjectSpecifications[
T
]
): ZIO[
World,
War | UnfulfilledPromise | OutOfMoney |
PrivatePropertyRefusal,
T
] =
for
availableCompanies <Companies.operatingIn[T](state)
legalRestrictions <World.legalRestrictionsFor(state)
politicians <- World.politiciansOf(state)
lowestBid =
availableCompanies
.lowestBid(projectSpecifications)
completedProject <- build(lowestBid)
yield completedProject
end CivilEngineering
enum State:
case TX,
CO,
CA
def buildABridge() =
trait Company[T]
trait Surveyor
trait CivilEngineer
trait ProjectSpecifications
trait Specs[Service]
trait LegalRestriction
trait ProjectBid
trait InsufficientResources
def createProjectSpecifications(): ZIO[
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
206
Experiments
Any,
LegalRestriction,
ProjectSpecifications
] = ???
case class AvailableCompanies[T](
companies: Set[Company[T]]
)
trait Concrete
trait Steel
trait UnderWaterDrilling
trait ConstructionFirm:
def produceBid(
projectSpecifications: ProjectSpecifications
): ZIO[
AvailableCompanies[Concrete] &
AvailableCompanies[Steel] &
AvailableCompanies[UnderWaterDrilling],
InsufficientResources,
ProjectBid
]
trait NoValidBids
def chooseConstructionFirm(
firms: Set[ConstructionFirm]
): ZIO[Any, NoValidBids, ConstructionFirm] =
???
end buildABridge
experiments/src/main/scala/scenarios/PhonyZIO.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
207
Experiments
package atomic
case class Schedule()
trait ZIO[R, E, A]:
def map[B](f: A => B): ZIO[R, E, B] = ???
def flatMap[R2, E2, B](
f: A => ZIO[R2, E2, B]
): ZIO[R, E, B] = ???
def retry(schedule: Schedule): ZIO[R, E, A] =
???
def catchAll(
handler: (E => A)
): ZIO[R, Nothing, A] = ???
case class UIO[A]() extends ZIO[Any, Nothing, A]
case class URIO[R, A]()
extends ZIO[R, Nothing, A]
case class Task[A]()
extends ZIO[Any, Throwable, A]
case class RIO[R, A]()
extends ZIO[R, Throwable, A]
case class IO[E <: Throwable, A]()
extends ZIO[Any, E, A]
object ZIO:
def apply[T](
body: => T
): ZIO[Any, Nothing, T] = ???
trait Has[A]
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
208
Experiments
experiments/src/main/scala/scenarios/SecuritySystem.scala
package scenarios
import zio.{
Duration,
Schedule,
Unsafe,
ZIO,
ZLayer,
durationInt
}
import zio.Console.printLine
import scala.concurrent.TimeoutException
import time.scheduledValues
import izumi.reflect.Tag
case class TempSense(
z: ZIO[
Any,
HardwareFailure,
ZIO[Any, TimeoutException, Degrees]
]
)
/** Situations: Security System: Should monitor
*
- Motion
*
- Heat/Infrared
*
- Sound Should alert by:
*
- Quiet, local beep
*
- Loud Local Siren
*
- Ping security company
*
- Notify police
*/
object SecuritySystem:
// TODO Why can't I use this???
val s: zio.ZLayer[
Any,
Nothing,
scenarios.TempSense
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
209
Experiments
] =
SensorData.live[Degrees, TempSense](
x => TempSense(x),
(1.seconds, Degrees(71)),
(2.seconds, Degrees(70))
)
val fullServiceBuilder: ZLayer[
Any,
Nothing,
scenarios.MotionDetector &
scenarios.ThermalDetectorX &
AcousticDetectorX & SirenX
] =
MotionDetector.live ++
ThermalDetectorX(
(1.seconds, Degrees(71)),
(1.seconds, Degrees(70)),
(3.seconds, Degrees(98))
) // ++ s
++
AcousticDetectorX(
(4.seconds, Decibels(11)),
(1.seconds, Decibels(20))
) ++ SirenX.live
end fullServiceBuilder
val accessMotionDetector: ZIO[
scenarios.MotionDetector,
scenarios.HardwareFailure,
scenarios.Pixels
] = ZIO.serviceWithZIO(_.amountOfMotion())
def securityLoop(
amountOfHeatGenerator: ZIO[
Any,
scala.concurrent.TimeoutException |
scenarios.HardwareFailure,
scenarios.Degrees
],
amountOfMotion: Pixels,
acousticDetector: ZIO[
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
210
Experiments
Any,
scala.concurrent.TimeoutException |
scenarios.HardwareFailure,
scenarios.Decibels
]
): ZIO[
SirenX,
scala.concurrent.TimeoutException |
HardwareFailure,
Unit
] =
for
amountOfHeat <- amountOfHeatGenerator
noise
<- acousticDetector
_ <ZIO.debug(
s"Heat: $amountOfHeat Motion: $amountOfMotion
)
securityResponse =
determineResponse(
amountOfMotion,
amountOfHeat,
noise
)
_ <securityResponse match
case Relax =>
ZIO.debug("No need to panic")
case LowBeep =>
SirenX.lowBeep
case LoudSiren =>
SirenX.loudSiren
yield ()
Noise: $noise"
def shouldAlertServices[
T
<: MotionDetector & ThermalDetectorX &
SirenX & AcousticDetectorX
](): ZIO[
T,
scenarios.HardwareFailure | TimeoutException,
String
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
211
Experiments
] =
for
amountOfMotion <MotionDetector
.acquireMotionMeasurementSource()
amountOfHeatGenerator <ThermalDetectorX
.acquireHeatMeasurementSource
acousticDetector <AcousticDetectorX.acquireDetector
_ <securityLoop(
amountOfHeatGenerator,
amountOfMotion,
acousticDetector
).repeat(
Schedule.recurs(5) &&
Schedule.spaced(1.seconds)
)
yield "Fin"
def shouldTrigger(
amountOfMotion: Pixels,
amountOfHeat: Degrees
): Boolean =
amountOfMotion.value > 10 &&
amountOfHeat.value > 95
def determineResponse(
amountOfMotion: Pixels,
amountOfHeat: Degrees,
noise: Decibels
): SecurityResponse =
val numberOfAlerts =
List(
amountOfMotion.value > 50,
amountOfHeat.value > 95,
noise.value > 15
).filter(_ == true).length
if (numberOfAlerts == 0)
Relax
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
212
Experiments
else if (numberOfAlerts == 1)
LowBeep
else
LoudSiren
end determineResponse
def determineBreaches(
amountOfMotion: Pixels,
amountOfHeat: Degrees,
noise: Decibels
): Set[SecurityBreach] =
List(
Option.when(amountOfMotion.value > 50)(
SignificantMotion
),
Option.when(
amountOfHeat.value > 95 &&
amountOfHeat.value < 200
)(BodyHeat),
Option
.when(amountOfHeat.value >= 200)(Fire),
Option.when(noise.value > 15)(LoudNoise)
).flatten.toSet
end SecuritySystem
trait SecurityBreach
object BodyHeat
object Fire
object LoudNoise
object SignificantMotion
extends
extends
extends
extends
SecurityBreach
SecurityBreach
SecurityBreach
SecurityBreach
trait SecurityResponse
object Relax
extends SecurityResponse
object LowBeep
extends SecurityResponse
object LoudSiren extends SecurityResponse
@main
def useSecuritySystem =
import zio.Runtime.default.unsafe
println(
"Final result: " +
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
213
Experiments
Unsafe.unsafe { (u: Unsafe) =>
given Unsafe = u
unsafe
.run(
SecuritySystem
.shouldAlertServices()
.provide(
SecuritySystem.fullServiceBuilder
)
.catchSome {
case _: TimeoutException =>
printLine(
"Invalid Scenario. Ran out of sensor data."
)
}
)
.getOrThrowFiberFailure()
}
)
end useSecuritySystem
trait HardwareFailure
case class Decibels(value: Int)
case class Degrees(value: Int)
case class Pixels(value: Int)
trait MotionDetector:
def amountOfMotion()
: ZIO[Any, HardwareFailure, Pixels]
object MotionDetector:
object LiveMotionDetector
extends MotionDetector:
override def amountOfMotion()
: ZIO[Any, HardwareFailure, Pixels] =
ZIO.succeed(Pixels(30))
def acquireMotionMeasurementSource(): ZIO[
MotionDetector,
HardwareFailure,
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
214
Experiments
Pixels
] =
ZIO
.service[MotionDetector]
.flatMap(_.amountOfMotion())
val live
: ZLayer[Any, Nothing, MotionDetector] =
ZLayer.succeed(LiveMotionDetector)
end MotionDetector
trait ThermalDetectorX:
def heatMeasurementSource()
: ZIO[Any, Nothing, ZIO[
Any,
TimeoutException |
scenarios.HardwareFailure,
Degrees
]]
object ThermalDetectorX:
def apply(
value: (Duration, Degrees),
values: (Duration, Degrees)*
): ZLayer[Any, Nothing, ThermalDetectorX] =
ZLayer.succeed(
// that same service we wrote above
new ThermalDetectorX:
override def heatMeasurementSource()
: ZIO[Any, Nothing, ZIO[
Any,
TimeoutException |
scenarios.HardwareFailure,
Degrees
]] = scheduledValues(value, values*)
)
// This is preeeetty gnarly. How can we
// improve?
def acquireHeatMeasurementSource[
T <: scenarios.ThermalDetectorX: Tag
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
215
Experiments
]: ZIO[T, Nothing, ZIO[
Any,
scala.concurrent.TimeoutException |
scenarios.HardwareFailure,
scenarios.Degrees
]] =
ZIO.serviceWithZIO[ThermalDetectorX](
_.heatMeasurementSource()
)
end ThermalDetectorX
trait AcousticDetectorX:
def acquireDetector(): ZIO[Any, Nothing, ZIO[
Any,
TimeoutException | scenarios.HardwareFailure,
Decibels
]]
object AcousticDetectorX:
def apply(
value: (Duration, Decibels),
values: (Duration, Decibels)*
): ZLayer[Any, Nothing, AcousticDetectorX] =
ZLayer.succeed(
// that same service we wrote above
new AcousticDetectorX:
override def acquireDetector()
: ZIO[Any, Nothing, ZIO[
Any,
TimeoutException |
scenarios.HardwareFailure,
Decibels
]] = scheduledValues(value, values*)
)
// This is preeeetty gnarly. How can we
// improve?
def acquireDetector[
T <: scenarios.AcousticDetectorX: Tag
]: ZIO[T, Nothing, ZIO[
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
216
Experiments
Any,
scala.concurrent.TimeoutException |
scenarios.HardwareFailure,
scenarios.Decibels
]] =
ZIO.serviceWithZIO[
scenarios.AcousticDetectorX
](_.acquireDetector())
end AcousticDetectorX
object Siren:
trait ServiceX:
def lowBeep(): ZIO[
Any,
scenarios.HardwareFailure,
Unit
]
val live
: ZLayer[Any, Nothing, Siren.ServiceX] =
ZLayer.succeed(
// that same service we wrote above
new ServiceX:
def lowBeep(): ZIO[
Any,
scenarios.HardwareFailure,
Unit
] = ZIO.debug("beeeeeeeeeep")
)
end Siren
trait SirenX:
def lowBeep()
: ZIO[Any, scenarios.HardwareFailure, Unit]
def loudSiren()
: ZIO[Any, scenarios.HardwareFailure, Unit]
object SirenX:
object SirenXLive extends SirenX:
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
217
Experiments
def lowBeep(): ZIO[
Any,
scenarios.HardwareFailure,
Unit
] = ZIO.debug("beeeeeeeeeep")
def loudSiren(): ZIO[
Any,
scenarios.HardwareFailure,
Unit
] = ZIO.debug("WOOOO EEEE WOOOOO EEEE")
val live: ZLayer[Any, Nothing, SirenX] =
ZLayer.succeed(SirenXLive)
val lowBeep: ZIO[
SirenX,
scenarios.HardwareFailure,
Unit
] = ZIO.serviceWith(_.lowBeep())
val loudSiren: ZIO[
SirenX,
scenarios.HardwareFailure,
Unit
] = ZIO.serviceWith(_.loudSiren())
end SirenX
class SensorD[T](
z: ZIO[
Any,
HardwareFailure,
ZIO[Any, TimeoutException, T]
]
)
// TODO Figure out how to use this
object SensorData:
def live[T, Y: zio.Tag](
c: ZIO[
Any,
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
218
Experiments
HardwareFailure,
ZIO[Any, TimeoutException, T]
] => Y,
value: (Duration, T),
values: (Duration, T)*
): ZLayer[Any, Nothing, Y] =
ZLayer.succeed(
// that same service we wrote above
c(scheduledValues[T](value, values*))
)
def liveS[T: zio.Tag](
value: (Duration, T),
values: (Duration, T)*
): ZLayer[Any, Nothing, SensorD[T]] =
ZLayer.succeed(
// that same service we wrote above
SensorD(scheduledValues[T](value, values*))
)
end SensorData
logging
experiments/src/main/scala/logging/Logging.scala
package logging
import zio.logging.*
import zio.*
import zio.logging.LogFormat.{
label,
line,
quoted,
text
}
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
object Logging extends ZIOAppDefault:
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
219
Experiments
lazy val minimal: LogFormat =
label("message", quoted(line)).highlight
lazy val locationLogger: LogFormat =
location(new WackyGps) |-|
label("message", quoted(line)).highlight
lazy val coloredLogger =
Runtime.removeDefaultLoggers >>>
console(
//
LogFormat.colored
locationLogger
)
def run = ZIO.log("Hi").provide(coloredLogger)
// val timestamp: LogFormat = timestamp(DateTimeFormatter.ISO_OFFSET_D\
ATE_TIME)
def location(gps: Gps): LogFormat =
text {
gps.currentLocation().toString
}
end Logging
enum Continent:
case NorthAmerica,
SouthAmerica,
Europe,
Asia,
Antarctica,
Australia,
Africa
trait Gps:
def currentLocation(): Continent
class WackyGps extends Gps:
def currentLocation(): Continent =
Continent
.values
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
220
Experiments
.apply(
scala
.util
.Random
.between(0, Continent.values.length)
)
fibers
experiments/src/main/scala/fibers/CancellingATightLoop.scala
package fibers
import org.apache.commons.lang3.RandomStringUtils
import org.apache.commons.text.similarity.LevenshteinDistance
import zio.*
val input = RandomStringUtils.random(70_000)
val target = RandomStringUtils.random(70_000)
val leven =
LevenshteinDistance.getDefaultInstance
object PlainLeven extends App:
leven(input, target)
object CancellingATightLoop
extends ZIOAppDefault:
val scenario =
ZIO
.attemptBlocking(leven(input, target))
.mapBoth(
error => ZIO.succeed("yay!"),
success => ZIO.fail("Oh no!")
)
def run =
// For timeouts, you need fibers and
// cancellation
scenario
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
221
Experiments
.timeout(50.seconds)
.timed
.debug("Time:")
experiments/src/main/scala/fibers/HowMany.scala
package fibers
import zio.*
import zio.Console.*
object HowMany extends ZIOAppDefault:
def run =
ZIO.foreachPar(Range(0, 1_000_000))(_ =>
ZIO.sleep(1.second)
)
prelude
experiments/src/main/scala/prelude/NewTypes.scala
package prelude
import
import
import
import
import
import
zio.prelude.Newtype
zio.ZIOAppDefault
zio.ZIO
zio.Console.printLine
zio.prelude.Assertion._
zio.prelude.Assertion
/* Notes: Only works for primitive types. You
* can't get compile-time guarantees for custom
* classes */
type NewSpecialClass = NewSpecialClass.Type
object NewSpecialClass
extends Newtype[OurPrimitiveClass]:
override inline def assertion
: Assertion[OurPrimitiveClass] =
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
222
Experiments
greaterThan(
OurPrimitiveClass("ignored_id", age = 0)
)
object SecurePassword extends Newtype[String]:
extension (n: SecurePassword)
def salt =
SecurePassword.unwrap(n) +
"random_salt_bits"
override inline def assertion
: Assertion[String] =
hasLength(greaterThanOrEqualTo(10)) &&
specialCharacters
inline def specialCharacters
: Assertion[String] =
contains("$") || contains("!")
type SecurePassword = SecurePassword.Type
object NewTypeDemos extends ZIOAppDefault:
def run =
val badValue = "Special String #$"
for
primitiveClassResult <ZIO.fromEither(
OurPrimitiveClass
.safeConstructor("idValue", -10)
)
specialClassResult <ZIO.succeed(
NewSpecialClass.make(
OurPrimitiveClass("idValue", -10)
)
)
accountNumber <ZIO.succeed {
SecurePassword("Special String #$")
}
accountNumbers <Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
223
Experiments
ZIO.succeed {
SecurePassword(
"Special String $",
"bad string!"
)
}
accountNumberRuntime <ZIO.succeed {
SecurePassword.make(badValue)
}
_ <printLine(
s"Account Number: $accountNumber"
)
_ <printLine(
s"Account Number salted: ${accountNumber.salt}"
)
yield ()
end for
end run
end NewTypeDemos
experiments/src/main/scala/prelude/OurPrimitiveClass.scala
package prelude
import
import
import
import
import
import
zio.prelude.Newtype
zio.ZIOAppDefault
zio.ZIO
zio.Console.printLine
zio.prelude.Assertion._
zio.prelude.Assertion
case class OurPrimitiveClass(
id: String,
age: Int
):
assert(age > 0)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
224
Experiments
object OurPrimitiveClass:
def safeConstructor(
id: String,
age: Int
): Either[String, OurPrimitiveClass] =
if (age > 0)
Right(OurPrimitiveClass(id, age))
else
Left("Invalid age")
implicit val ordering
: Ordering[OurPrimitiveClass] =
new Ordering[OurPrimitiveClass]:
def compare(
x: OurPrimitiveClass,
y: OurPrimitiveClass
): Int = x.age.compare(y.age)
std_type_conversions_to_zio
experiments/src/main/scala/std_type_conversions_to_zio/EitherToZio.scala
// EitherToZio.scala
package std_type_conversions_to_zio
import zio.{ZIO, ZIOAppDefault}
import scala.util.{Left, Right}
case class InvalidIntegerInput(value: String)
object EitherToZio extends ZIOAppDefault:
val goodInt: Either[InvalidIntegerInput, Int] =
Right(42)
val zEither
: ZIO[Any, InvalidIntegerInput, Int] =
ZIO.fromEither(goodInt)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
225
Experiments
def run = zEither.debug("Converted Either")
experiments/src/main/scala/std_type_conversions_to_zio/FutureToZio.scala
package std_type_conversions_to_zio
import zio.{ZIO, ZIOAppDefault}
import scala.concurrent.Future
object FutureToZio extends ZIOAppDefault:
val zFuture =
ZIO.fromFuture(implicit ec =>
Future.successful("Success!")
)
val zFutureFailed =
ZIO.fromFuture(implicit ec =>
Future.failed(new Exception("Failure :("))
)
val run =
zFutureFailed.debug("Converted Future")
experiments/src/main/scala/std_type_conversions_to_zio/OptionToZio.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
226
Experiments
package std_type_conversions_to_zio
import java.io
import zio._
import java.io.IOException
class OptionToZio extends ZIOAppDefault:
val alias: Option[String] =
Some("Buddy") // sOption is either 1 or None
val aliasZ: IO[Option[Nothing], String] =
ZIO.fromOption(alias)
val run = aliasZ
experiments/src/main/scala/std_type_conversions_to_zio/TryToZio.scala
package std_type_conversions_to_zio
import
import
import
import
zio._
java.io
java.io.IOException
scala.util.Try
object TryToZio extends ZIOAppDefault:
val dividend = 42
val divisor = 7
// Significant Note: Try is a standard
// collection by-name function. This makes
// it a good candidate for introducting that
// concept.
def sTry: Try[Int] = Try(dividend / divisor)
val zTry: IO[Throwable, Int] =
ZIO.fromTry(sTry)
val run = zTry
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
227
Experiments
interpreter-chaining_with_previous_result
experiments/src/main/scala/interpreter/chaining_with_previous_result/ChainedWithPreviousResult.scala
package interpreter.chaining_with_previous_result
import environment_exploration.ToyEnvironment
import zio.{ZIO, ZIOAppDefault}
import scala.reflect.{ClassTag, classTag}
import scala.util.Random
trait Operation
case class Value(value: String) extends Operation
case class StringManipulation(
action: String => String
) extends Operation:
def actOn(input: String): String =
action(input)
case class Print() extends Operation
case class RandomString() extends Operation
val program =
Seq(
Value("Hello There"),
Print(),
StringManipulation(_.toUpperCase()),
Print(),
StringManipulation(_.take(5)),
Print(),
RandomString(),
Print(),
StringManipulation(_.toUpperCase()),
Print()
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
228
Experiments
def interpret(program: Seq[Operation]): String =
program.foldLeft("") { (acc, op) =>
op match
case Print() =>
println(acc)
acc
case RandomString() =>
scala
.util
.Random
.alphanumeric
.take(10)
.mkString
case Value(value) =>
value
case StringManipulation(action) =>
action(acc)
}
@main
def demoInterpreter() = interpret(program)
trait Printer:
def print(input: String): Unit
def interpretWithEnvironment(
program: Seq[Operation],
environment: ToyEnvironment[Printer & Random]
): String =
program.foldLeft("") { (acc, op) =>
op match
case Print() =>
environment.get[Printer].print(acc)
acc
case RandomString() =>
environment
.get[Random]
.alphanumeric
.take(10)
.mkString
case Value(value) =>
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
229
Experiments
value
case StringManipulation(action) =>
action(acc)
}
experiments-src-test-scala-zio_test
experiments/src/test/scala/zio_test/Shared.scala
package zio_test
import zio.{Ref, Scope, ZIO, ZLayer}
object Shared:
val layer: ZLayer[Any, Nothing, Ref[Int]] =
ZLayer.scoped {
ZIO.acquireRelease(
Ref.make(0) <* ZIO.debug("Initializing!")
)(
_.get
.debug(
"Number of tests that used shared layer"
)
)
}
case class Scoreboard(value: Ref[Int]):
def display(): ZIO[Any, Nothing, String] =
for current <- value.get
yield s"**$current**"
val scoreBoard: ZLayer[
Scope with Ref[Int],
Nothing,
Scoreboard
] =
for
value <- ZLayer.service[Ref[Int]]
res <ZLayer.scoped[Scope] {
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
230
Experiments
ZIO.acquireRelease(
ZIO.succeed(Scoreboard(value.get)) <*
ZIO.debug(
"Initializing scoreboard!"
)
)(_ =>
ZIO.debug("Shutting down scoreboard")
)
}
yield res
end Shared
experiments/src/test/scala/zio_test/UseComplexLayer.scala
package zio_test
import zio.*
import zio.test.*
import zio_test.Shared.Scoreboard
object UseComplexLayer
extends ZIOSpec[Scoreboard]:
def bootstrap
: ZLayer[Any, Nothing, Scoreboard] =
ZLayer.make[Scoreboard](
Shared.layer,
Shared.scoreBoard,
Scope.default
)
def spec =
test("use scoreboard") {
for _ <ZIO
.serviceWithZIO[Scoreboard](
_.display()
)
.debug
yield assertCompletes
}
end UseComplexLayer
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
231
Experiments
experiments/src/test/scala/zio_test/UseSharedLayerA.scala
package zio_test
import zio.test.{
TestAspect,
ZIOSpec,
assertCompletes
}
import zio.{Ref, ZIO}
object UseSharedLayerA extends ZIOSpec[Ref[Int]]:
def bootstrap = Shared.layer
def spec =
test("Test A") {
for _ <ZIO.serviceWithZIO[Ref[Int]](
_.update(_ + 1)
)
yield assertCompletes
}
experiments/src/test/scala/zio_test/UseSharedLayerB.scala
package zio_test
import zio.test.{
TestAspect,
ZIOSpec,
assertCompletes
}
import zio.{Ref, Scope, ZIO, ZLayer}
object UseSharedLayerB extends ZIOSpec[Ref[Int]]:
def bootstrap = Shared.layer
def spec =
test("Test B") {
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
232
Experiments
for _ <ZIO.serviceWithZIO[Ref[Int]](count =>
count.update(_ + 1)
)
yield assertCompletes
}
helloworld
experiments/src/main/scala/helloworld/HelloWorld.scala
package helloworld
import zio.*
import scala.annotation.{experimental, nowarn}
def blah = ???
/* @experimental
* @nowarn
* @zioMain def run = Console.printLine("hello,
* world") */
experiments-src-test-scala-layers
experiments/src/test/scala/layers/FestivalFencingUnavailableS
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
233
Experiments
package layers
import zio.*
import zio.test.*
import zio.test.TestAspect.*
object FestivalFencingUnavailableSpec
extends ZIOSpecDefault:
val missingFencing: ZIO[Any, String, Fencing] =
ZIO.fail("No fencing!")
private val brokenFestival
: ZLayer[Any, String, Festival] =
ZLayer.make[Festival](
festival,
ZLayer.fromZIO(missingFencing),
stage,
speakers,
wires,
amplifiers,
soundSystem,
toilets,
foodtruck,
security,
venue,
permit
)
val spec =
suite("Play some music")(
test("Good festival")(
(
for _ <- ZIO.service[Festival]
yield assertCompletes
).provide(brokenFestival)
.withClock(Clock.ClockLive)
.catchAll(e =>
ZIO.debug("Expected error: " + e) *>
ZIO.succeed(assertCompletes)
)
)
)
end FestivalFencingUnavailableSpec
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
234
Experiments
experiments/src/test/scala/layers/FestivalShortedOutSoundSys
package layers
import zio.*
import zio.test.*
import zio.test.TestAspect.*
object FestivalShortedOutSoundSystemSpec
extends ZIOSpecDefault:
val brokenFestival
: ZLayer[Any, String, Festival] =
ZLayer.make[Festival](
festival,
fencing,
stage,
speakers,
wires,
amplifiers,
soundSystemShortedOut,
toilets,
foodtruck,
security,
venue,
permit
)
val spec =
suite("Play some music")(
test("Good festival")(
(
for _ <- ZIO.service[Festival]
yield assertCompletes
).provide(brokenFestival)
.withClock(Clock.ClockLive)
.catchAll(e =>
ZIO.debug("Expected error: " + e) *>
ZIO.succeed(assertCompletes)
)
)
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
235
Experiments
end FestivalShortedOutSoundSystemSpec
experiments/src/test/scala/layers/FestivalSpec.scala
package layers
import zio.*
import zio.test.*
import zio.test.TestAspect.*
object FestivalSpec extends ZIOSpec[Festival]:
val bootstrap =
ZLayer.make[Festival](
festival,
fencing,
stage,
speakers,
wires,
amplifiers,
soundSystem,
toilets,
foodtruck,
security,
venue,
permit
)
val spec =
suite("Play some music")(
test("Good festival")(assertCompletes)
)
end FestivalSpec
experiments-src-test-scala-bigdec
experiments/src/test/scala/bigdec/MainSpec.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
236
Experiments
package bigdec
import zio.ZIO
import zio.test.ZIOSpecDefault
import zio.test.Assertion.{
diesWithA,
equalTo,
fails,
failsWithA,
isSubtype
}
import zio.test.{
ErrorMessage,
TestConsole,
TestResult,
assert,
assertCompletes,
assertTrue
}
import zio.test.TestAspect.silent
object MainSpec extends ZIOSpecDefault:
def spec =
suite("MainSpec")(
test("must succeed with valid value") {
for
_ <- TestConsole.feedLines("1")
result <inputBigDecimalValue("Num: ", 1, 10)
yield assertTrue(result == BigDecimal(1))
},
test("must fail with non-parsable input") {
for
_ <- TestConsole.feedLines("a")
error <inputBigDecimalValue("Num: ", 1, 10)
.mapError(_.getMessage)
.exit
yield assert(error)(
fails(equalTo("Invalid input."))
)
},
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
237
Experiments
test("must fail with out-of-range input") {
for
_ <- TestConsole.feedLines("0")
error <inputBigDecimalValue("Num: ", 1, 10)
.mapError(_.getMessage)
.exit
yield assert(error)(
fails(
equalTo(
"Input out of the range from 1 to 10"
)
)
)
}
) @@ silent
end MainSpec
virtual_meeting
experiments/src/main/scala/virtual_meeting/package.scala
package virtual_meeting
import zio._
import zio.stream._
trait Rule
object Rule:
trait MaximumContinuousSpeaker
trait DismissNonParticipants
trait MaximumParticipants
trait MaximumLength
trait OneSpeakerAtATime
trait MaximumStartDelay
extends
extends
extends
extends
extends
extends
Rule
Rule
Rule
Rule
Rule
Rule
trait CompositeRule extends Rule
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
238
Experiments
trait Participant
trait ParticipantAction
object ParticipantAction:
trait Speak
extends ParticipantAction
trait Emoji
extends ParticipantAction
trait TextChat extends ParticipantAction
trait ParticipantStatus
object ParticipantStatus:
trait VideoAndAudio extends
trait VideoOnly
extends
trait AudioOnly
extends
trait NoVideoNoAudio extends
ParticipantStatus
ParticipantStatus
ParticipantStatus
ParticipantStatus
trait CorrectiveAction
object CorrectiveAction:
trait Disband
extends CorrectiveAction
trait WarnParticipant extends CorrectiveAction
trait MuteParticipant extends CorrectiveAction
trait DismissParticipant
extends CorrectiveAction
trait SplitMeeting extends CorrectiveAction
case class MeetingMoment(
actions: Set[ParticipantAction]
)
trait MeetingEnforcer:
def process(
meetingMoment: MeetingMoment
): Option[CorrectiveAction]
case class Meeting(
frames: zio.stream.ZStream[
Any,
Nothing,
MeetingMoment
]
)
case class Meeting2(
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
239
Experiments
frames: Ref[Set[zio.stream.ZStream[
Any,
Nothing,
MeetingMoment
]]]
)
zio
experiments/src/main/scala/zio/zioMain.scala
package zio
def bah = ???
/*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
import scala.annotation.MainAnnotation.{ Info,
Parameter } import scala.annotation.{
MainAnnotation, experimental, nowarn } import
scala.util.CommandLineParser.FromString
@experimental class zioMain extends
MainAnnotation[ FromString, ZIO[ZIOAppArgs,
Any, Any] ]:
def argGetter[T]( param: Parameter, arg:
String, defaultArgument: Option[() => T]
)(using parser: FromString[T]): () => T = ???
def command( info: Info, args: Seq[String] ):
Option[Seq[String]] = Some(Seq.empty)
def run( program: () => ZIO[ZIOAppArgs, Any,
Any] ): Unit =
ZIOAppDefault .fromZIO(program())
.main(Array.empty)
def varargGetter[T]( param: Parameter, args:
Seq[String] )(using parser: FromString[T]): ()
=> Seq[T] =
???
end zioMain */
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
240
Experiments
experiments-src-test-scala-time
experiments/src/test/scala/time/ScheduledValuesSpec.scala
package time
import zio.*
import zio.test.*
import java.time.Instant
import scala.concurrent.TimeoutException
object ScheduledValuesSpec
extends ZIOSpecDefault:
def spec =
suite("ScheduledValues")(
suite("scheduledValues")(
test(
"querying after no time has passed returns the first value, i\
f duration was > 0"
)(
for
valueAccessor <scheduledValues(
(1.seconds, "First Section")
)
firstValue <- valueAccessor
yield assertTrue(
firstValue == "First Section"
)
),
test(
"querying after no time has passed fails when the duration ==\
0"
)(
for
valueAccessor <scheduledValues(
(0.seconds, "First Section")
)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
241
Experiments
_ <- valueAccessor.flip
yield assertCompletes
),
test(
"next value is returned after enough time has elapsed"
)(
for
valueAccessor <scheduledValues(
(1.seconds, "First Section"),
(2.seconds, "Second Section")
)
_ <- TestClock.adjust(1.seconds)
secondValue <- valueAccessor
yield assertTrue(
secondValue == "Second Section"
)
),
test("time range end is not inclusive")(
for
valueAccessor <scheduledValues(
(1.seconds, "First Section")
)
_ <- TestClock.adjust(1.seconds)
_ <- valueAccessor.flip
yield assertCompletes
)
)
)
end ScheduledValuesSpec
zio_intro
experiments/src/main/scala/zio_intro/AuthenticationFlow.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
242
Experiments
package zio_intro
import zio.{ZIO, ZIOAppDefault}
object AuthenticationFlow extends ZIOAppDefault:
val activeUsers
: ZIO[Any, DiskError, List[UserName]] = ???
val user: ZIO[Any, Nothing, UserName] = ???
def authenticateUser(
users: List[UserName],
currentUser: UserName
): ZIO[
Any,
UnauthenticatedUser,
AuthenticatedUser
] = ???
val fullAuthenticationProcess: ZIO[
Any,
DiskError | UnauthenticatedUser,
AuthenticatedUser
] =
for
users
<- activeUsers
currentUser <- user
authenticatedUser <authenticateUser(users, currentUser)
yield authenticatedUser
def run =
fullAuthenticationProcess.orDieWith(error =>
new Exception("Unhandled error: " + error)
)
end AuthenticationFlow
trait UserName
case class FileSystem()
trait DiskError
trait EnvironmentVariableNotFound
case class UnauthenticatedUser(msg: String)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
243
Experiments
case class AuthenticatedUser(userName: UserName)
experiments/src/main/scala/zio_intro/FirstMeaningfulExample.scala
package zio_intro
import zio.{Clock, ZIO, ZIOAppDefault, System}
import zio.Console.{readLine, printLine}
object HelloWorld extends ZIOAppDefault:
def run = printLine("Hello World")
object FirstMeaningfulExample
extends ZIOAppDefault:
def run =
for
_
<- printLine("Give us your name:")
name <- readLine
_
<- printLine(s"$name")
yield ()
experiments/src/main/scala/zio_intro/ProgressBar.scala
package zio_intro
import zio.{Ref, *}
import zio.Console.printLine
import java.util.concurrent.TimeUnit
trait ProgressBar
/* import io.AnsiColor.*
*
* object ClockAndConsole extends ZIOAppDefault:
* val renderCurrentTime =
* for currentTime <Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
244
Experiments
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
Clock.currentTime(TimeUnit.SECONDS) _ <renderRemainingTime(currentTime)
.repeat(Schedule.recurs(10)) yield ()
val saveCursorPosition =
Console.print("\u001b7") val
loadCursorPosition =
Console.print("\u001b8")
def renderRemainingTime(startTime: Long) =
for currentTime <Clock.currentTime(TimeUnit.SECONDS)
timeElapsed = (currentTime - startTime) .toInt
timeRemaining = 10 - timeElapsed // NOTE: You
can only reset the cursor // position once in
a single SBT session _ <- saveCursorPosition _
<- Console.print( s"${BOLD}$timeRemaining
seconds remaining ${RESET}" ) _ <progressBar(timeRemaining) _ <ZIO.sleep(1.seconds) _ <- loadCursorPosition
yield ()
def progressBar(length: Int) =
val color =
if (length > 3) GREEN_B else RED_B
Console.printLine( s"""${color}${" " *
length}${RESET}""" )
def run = renderCurrentTime end
ClockAndConsole
object
ClockAndConsoleDifficultEffectManagement
extends ZIOAppDefault:
val renderCurrentTime =
for currentTime <Clock.currentTime(TimeUnit.SECONDS) _ <renderRemainingTime(currentTime)
.repeat(Schedule.recurs(10)) _ <renderRemainingTime(
Integer.max(currentTime.toInt - 5, 0)
).repeat(Schedule.recurs(10)) yield ()
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
245
Experiments
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
val saveCursorPosition =
Console.print("\u001b7") val
loadCursorPosition =
Console.print("\u001b8")
def renderRemainingTime(startTime: Long) =
for currentTime <Clock.currentTime(TimeUnit.SECONDS)
timeElapsed = (currentTime - startTime) .toInt
timeRemaining = 10 - timeElapsed // NOTE: You
can only reset the cursor // position once in
a single SBT session _ <- saveCursorPosition _
<- Console.print( s"${BOLD}$timeRemaining
seconds remaining ${RESET}" ) _ <progressBar(timeRemaining) _ <ZIO.sleep(1.seconds) _ <- loadCursorPosition
yield ()
def progressBar(length: Int) =
val color =
if (length > 3) GREEN_B else RED_B
Console.printLine( s"""${color}${" " *
length}${RESET}""" )
def run = renderCurrentTime end
ClockAndConsoleDifficultEffectManagement
object ClockAndConsoleImproved extends
ZIOAppDefault:
val renderCurrentTime =
for currentTime <Clock.currentTime(TimeUnit.SECONDS) racer1 <LongRunningProcess( "Shtep", currentTime, 3 )
racer2 <- LongRunningProcess("Zeb",
currentTime, 5) raceFinished: Ref[Boolean] <Ref.make[Boolean](false) winnersName <raceEntities( racer1.run, racer1.run,
raceFinished ) zipParLeft monitoringLogic(
racer1, racer2, raceFinished ) _ <printLine(s"\nWinner: $winnersName") yield ()
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
246
Experiments
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
def monitoringLogic( racer1:
LongRunningProcess, racer2:
LongRunningProcess, raceFinished: Ref[Boolean]
) =
renderLoop( for racer1status <racer1.status.get racer2status <racer2.status.get _ <progressBar(racer1.name, racer1status) _ <printLine("") _ <- progressBar(racer2.name,
racer2status) yield () ).repeatWhileZIO(_ =>
raceFinished.get)
def raceEntities( racer1: ZIO[Any, Nothing,
String], racer2: ZIO[Any, Nothing, String],
raceFinished: Ref[Boolean] ): ZIO[Any,
Nothing, String] =
racer1 .race(racer2) .flatMap { success =>
raceFinished.set(true) *> ZIO.succeed(success)
}
val saveCursorPosition =
Console.print("\u001b7") val
loadCursorPosition =
Console.print("\u001b8")
def renderLoop[T]( drawFrame: ZIO[T, Any,
Unit] ) =
for _ <- saveCursorPosition _ <- drawFrame _
<- ZIO.sleep(1.second) _ <- loadCursorPosition
yield ()
def timer(startTime: Long, secondsToRun: Int)
=
for currentTime <Clock.currentTime(TimeUnit.SECONDS)
timeElapsed = (currentTime - startTime) .toInt
yield Integer .max(secondsToRun - timeElapsed,
0)
object LongRunningProcess:
def apply( name: String, startTime: Long,
secondsToRun: Int ): ZIO[Any, Nothing,
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
247
Experiments
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
LongRunningProcess] =
for status <- Ref.make[Int](4) yield new
LongRunningProcess( name, startTime,
secondsToRun, status )
class LongRunningProcess( val name: String,
startTime: Long, secondsToRun: Int, val
status: Ref[Int] ):
val loopAndCheck =
for timeLeft <- timer(startTime, secondsToRun)
_ <- status.set(timeLeft) yield timeLeft
val run: ZIO[Any, Nothing, String] =
loopAndCheck .repeatUntil(_ == 0) .map(_ =>
name)
def progressBar(label: String, length: Int) =
val barColor =
if (length > 3) GREEN_B else RED_B
Console.print( s"""$label$barColor${" " *
length}$RESET""" )
def run = renderCurrentTime end
ClockAndConsoleImproved */
experiments-src-test-scala-concurrency
experiments/src/test/scala/concurrency/LunchVoteTest.scala
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
248
Experiments
package concurrency
import
import
import
import
import
zio.test._
zio.test.TestAspect._
zio._
LunchVote._
LunchVote.Vote._
object LunchVoteTest extends ZIOSpecDefault:
def spec =
suite("voting situations")(
test("3 quick yays") {
for
interruptedVoters <- Ref.make(0)
voters =
List(
Voter("Alice", 0.seconds, Yay),
Voter("Bob", 0.seconds, Yay),
Voter("Charlie", 0.seconds, Yay),
Voter("Dave", 1.seconds, Nay, onInterrupt = interruptedVo\
ters.update(_ + 1)),
Voter("Eve", 1.seconds, Nay, onInterrupt = interruptedVot\
ers.update(_ + 1))
)
result <- LunchVote.run(voters)
totalInterrupted <- interruptedVoters.get
//
_ <- ZIO.withClock(Clock.ClockLive)(ZIO.sleep(1.seconds))
yield assertTrue(result == Yay) && assertTrue(totalInterrupted\
== 2)
} @@ flaky, // Flaky because Interruption count is not reliable
test("3 quick nays") {
val voters =
List(
Voter("Alice", 0.seconds, Nay),
Voter("Bob", 0.seconds, Nay),
Voter("Charlie", 0.seconds, Nay),
Voter("Dave", 1.seconds, Yay),
Voter("Eve", 1.seconds, Yay)
)
for
result <- LunchVote.run(voters)
yield assertTrue(result == Nay)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
249
Experiments
},
test("slow voters") {
val voters =
List(
Voter("Alice", 10.seconds, Nay),
Voter("Bob", 10.seconds, Nay),
Voter("Charlie", 10.seconds, Nay),
Voter("Dave", 10.seconds, Yay),
Voter("Eve", 10.seconds, Yay)
)
for
resultF <- LunchVote.run(voters, 1.seconds).fork
_ <- TestClock.adjust(2.seconds)
timeout <- resultF.join.flip
yield assertTrue(timeout == None)
}
)
interpreter-chaining_with_monads
experiments/src/main/scala/interpreter/chaining_with_monads/ChainingMonads.scala
package interpreter.chaining_with_monads
import scala.annotation.tailrec
sealed trait Operation:
def map(mf: String => String): MapOp
def flatMap(mf: String => Operation): FlatMapOp
class Value(val s: String) extends Operation:
override def map(mf: String => String): MapOp =
MapOp(_ => mf(s))
override def flatMap(
mf: String => Operation
): FlatMapOp = FlatMapOp(_ => mf(s))
class MapOp(val f: String => String)
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
250
Experiments
extends Operation:
override def map(mf: String => String): MapOp =
MapOp(f.andThen(mf))
override def flatMap(
mf: String => Operation
): FlatMapOp = FlatMapOp(f.andThen(mf))
class FlatMapOp(val f: String => Operation)
extends Operation:
override def map(mf: String => String): MapOp =
MapOp(identity).flatMap(f).map(mf)
override def flatMap(
mf: String => Operation
): FlatMapOp =
FlatMapOp { s =>
f(s).flatMap(mf)
}
@tailrec
def interpret(program: Operation): String =
program match
case v: Value =>
println("run Value")
v.s
case mo: MapOp =>
println("run MapOp")
mo.f("")
case fmo: FlatMapOp =>
println("run FlatMapOp")
val next = fmo.f("")
interpret(next)
@main
def demoInterpreter() =
val value = Value("asdf")
println(interpret(value) == "asdf")
val upperOp = MapOp(_.toUpperCase)
println(
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
251
Experiments
interpret(upperOp) == ""
) // applies the operation to the default empty string
val valueUpperOp = value.map(_.toUpperCase)
println(interpret(valueUpperOp) == "ASDF")
val valueUpperTakeTwoOp =
valueUpperOp.map(_.take(2))
println(interpret(valueUpperTakeTwoOp) == "AS")
val flatMapOpToValue =
FlatMapOp(_ => Value("asdf"))
println(interpret(flatMapOpToValue) == "asdf")
val flatMapOpToFlatMapOpToValue =
FlatMapOp(_ => FlatMapOp(_ => Value("asdf")))
println(
interpret(flatMapOpToFlatMapOpToValue) ==
"asdf"
)
val flatMapOpToMapOp =
FlatMapOp(_ => MapOp(_ => "asdf"))
println(interpret(flatMapOpToMapOp) == "asdf")
val valueFlatMapOpToValue =
value.flatMap(Value(_))
println(
interpret(valueFlatMapOpToValue) == "asdf"
)
val valueFlatMapOpToValueMapOp =
value.flatMap(asdf =>
Value(asdf).map(_.toUpperCase)
)
println(
interpret(valueFlatMapOpToValueMapOp) ==
"ASDF"
)
val valueFlatMapOpToValueToFlatMap =
value
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
252
Experiments
.flatMap(asdf => Value(asdf.toUpperCase))
.flatMap(upper => Value(upper.take(2)))
println(
interpret(valueFlatMapOpToValueToFlatMap) ==
"AS"
)
val program =
Value("asdf").flatMap { asdf =>
println(s"asdf = $asdf")
Value(asdf.toUpperCase).map { upper =>
println(s"upper = $upper")
upper.take(2)
}
}
println(interpret(program))
val programFor =
for
asdf <- Value("asdf")
upper <- Value(asdf.toUpperCase)
yield upper.take(2)
println(interpret(programFor))
end demoInterpreter
Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward
Download