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 2023-11-25 This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and many iterations to get reader feedback, pivot until you have the right book and build traction once you do. © 2021 - 2023 Bill Frasure, Bruce Eckel and James Ward CONTENTS Contents Copyright . . . . . . . . . . . . . . . Effect Oriented Programming Source Code . . . . . . . . . . . Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 5 Preface . . . . . . . . . . . . Who is the book is for Code examples . . . . Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 6 6 6 What Are Effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 8 Superpowers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Building a Resilient Process in stages . . . . . . . . . . . . . . . . . . . . . . . Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 10 14 Why Functional . . . . . . . . . . . . . . . . . . . . . A Different Goal . . . . . . . . . . . . . . . . . . Reuse . . . . . . . . . . . . . . . . . . . . . . . . . Pure Functions . . . . . . . . . . . . . . . . . . . Composability . . . . . . . . . . . . . . . . . . . . Effects . . . . . . . . . . . . . . . . . . . . . . . . Avoiding Recursion . . . . . . . . . . . . . . . . Core Differences Between OO and Functional Summary: Style vs Substance . . . . . . . . . . Edit This Chapter . . . . . . . . . . . . . . . . . . 15 15 18 19 20 21 21 22 22 23 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . CONTENTS Composability . . . . . . . . . . . . . . . Composability Explanation . . . . . Alternatives and their downsides . Universal Composability with ZIO Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 24 24 26 27 Dependency Injection . . . . . . . . . . . . . . . . . . . . . . . . . DI-Wow! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Intersections AKA Products AKA Case Classes AKA Ands Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 29 32 38 The ZIO Type . . . . . . . . . . . . . . . . . . R - The Environment . . . . . . . . . . . E - The Error . . . . . . . . . . . . . . . . A - The Result . . . . . . . . . . . . . . . . Conversions from standard Scala types Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 39 39 40 40 41 Built-in Services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . History - TODO Consider deleting. Not crucial to the reader. Overriding Builtin Services . . . . . . . . . . . . . . . . . . . . . Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 42 43 43 Running Effects . . . . . . . . . . . . . . . . . . . . ZIOs are not their result. . . . . . . . . . . . . The ZIO Interpreter . . . . . . . . . . . . . . . Building applications from scratch . . . . . . Testing code . . . . . . . . . . . . . . . . . . . . Interop with existing/legacy code via Unsafe Web Request Handler . . . . . . . . . . . . . . Processing streams of data . . . . . . . . . . . Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 44 45 45 47 49 50 50 50 Layers . . . . . . . . . . . Creating . . . . . . . Composing . . . . . . Historic Approaches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 51 51 52 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward CONTENTS Traits . . . . . . . . . . . . . . . . . . . . . . Edit This Chapter . . . . . . . . . . . . . . . Automatically attached experiments. . . . Testing Unpredictable Effects . . . . . . . Random . . . . . . . . . . . . . . . . . . . . Time . . . . . . . . . . . . . . . . . . . . . . assertTrue . . . . . . . . . . . . . . . . . . Test Aspects . . . . . . . . . . . . . . . . . . Defining a base test class for your project Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . 53 55 55 58 58 59 60 61 66 66 Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 Hello Failures . . . . . . . . . . . . . . . . . . . . . . . Historic approaches to Error-handling . . . . . . ZIO Error Handling . . . . . . . . . . . . . . . . . Unions AKA Sum Types AKA Enums AKA Ors Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 68 73 77 79 Concurrency High Level zipPar, zipWithPar . . validateWithPar? . . . withParallelism . . . . Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 81 81 81 81 Concurrency Low Level . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 82 Concurrency Interruption . . . . . . . . . . . . . . . . . . . Why Interruption Is Necessary Throughout the Stack . Timeout . . . . . . . . . . . . . . . . . . . . . . . . . . . . .withFinalizer . . . . . . . . . . . . . . . . . . . . . . . . . Uninterruptable . . . . . . . . . . . . . . . . . . . . . . . . Future Cancellation . . . . . . . . . . . . . . . . . . . . . Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 83 83 83 83 84 84 Concurrency State . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . CONTENTS Unreliable Counting Reliable Counting . Ref.Synchronized . . Edit This Chapter . . . . . . . . . . . . . . 86 86 89 89 Repeats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 90 Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 91 Logging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . . 92 92 92 Configuration . . . . . . . . CLI Params . . . . . . . Config Files . . . . . . . Environment Variables ZIO Config . . . . . . . . Historic Approach . . . Building a Better Way . Official ZIO Approach . Exercises . . . . . . . . . Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 . 94 . 94 . 94 . 94 . 94 . 96 . 100 . 101 . 103 Streams . . . . . . . . . . . . . . . . . . . . UI Interactions . . . . . . . . . . . . . Trend Recognition . . . . . . . . . . . Edit This Chapter . . . . . . . . . . . . Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 104 104 104 105 Appendix: Significant Indentation . . . . Motivations . . . . . . . . . . . . . . . . Concessions / Acknowledgements . . Rules / examples . . . . . . . . . . . . . Edge cases that are difficult to defend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 129 129 129 130 Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward CONTENTS Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131 Appendix: ZIO Direct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132 How does defer, etc relate to flatmaps, for comprehensions, etc . . . . . . . 132 Edit This Chapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 Experiments . . . . . . . . . . . . . . . . . . . . . experiments-src-test-scala-Hubs . . . . . . . Parallelism . . . . . . . . . . . . . . . . . . . . experiments-src-test-scala-random . . . . . Hubs . . . . . . . . . . . . . . . . . . . . . . . executing_external_programs . . . . . . . . experiments-src-test-scala-running_effects hello_failures . . . . . . . . . . . . . . . . . . scenarios . . . . . . . . . . . . . . . . . . . . . zio_helpers . . . . . . . . . . . . . . . . . . . . crypto . . . . . . . . . . . . . . . . . . . . . . . experiments-src-test-scala-test_aspects . . experiments-src-test-scala-scenarios . . . . cancellation . . . . . . . . . . . . . . . . . . . concurrency . . . . . . . . . . . . . . . . . . . rezilience . . . . . . . . . . . . . . . . . . . . . cause . . . . . . . . . . . . . . . . . . . . . . . environment . . . . . . . . . . . . . . . . . . . experiments-src-test-scala-zio_test . . . . . experiments-src-test-scala-layers . . . . . . experiments-src-test-scala-time . . . . . . . performance . . . . . . . . . . . . . . . . . . . resourcemanagement . . . . . . . . . . . . . experiments-src-test-scala-concurrency . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134 134 137 141 145 150 151 153 155 158 159 161 164 165 168 170 176 180 184 188 189 191 195 197 Copyright Effect Oriented Programming By Bill Frasure, Bruce Eckel and James Ward {{ These are the new ISBNs }} Copyright ©2021, MindView LLC. eBook ISBN 978-0-9818725-6-8 Print Book ISBN 978-0-9818725-7-5 The eBook ISBN covers the Leanpub eBook distribution in all formats, available through www.EffectOrientedProgramming.com. Please purchase this book through www.EffectOrientedProgramming.com, to support its continued maintenance and updates. All rights reserved. Printed in the United States of America. This publication is protected by copyright, and permission must be obtained from the publisher prior to any prohibited reproduction, storage in a retrieval system, or transmission in any form or by any means, electronic, mechanical, photocopying, recording, or likewise. For information regarding permissions, see EffectOrientedProgramming.com. Created in Crested Butte, Colorado, USA. Text printed in the United States Ebook: Version 1.0, Month Year First printing Month Year Cover design by Daniel Will-Harris, www.Will-Harris.com¹ ¹http://www.Will-Harris.com 2 Copyright Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and the publisher was aware of a trademark claim, the designations are printed with initial capital letters or in all capitals. The Scala trademark belongs to {{???}}. Java is a trademark or registered trademark of Oracle, Inc. in the United States and other countries. Windows is a registered trademark of Microsoft Corporation in the United States and other countries. All other product names and company names mentioned herein are the property of their respective owners. The authors and publisher have taken care in the preparation of this book, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein. Visit us at www.EffectOrientedProgramming.com. Source Code All the source code for this book is available as copyrighted freeware, distributed via Github². To ensure you have the most current version, this is the official code distribution site. You may use this code in classroom and other educational situations as long as you cite this book as the source. The primary goal of this copyright is to ensure that the source of the code is properly cited, and to prevent you from republishing the code without permission. (As long as this book is cited, using examples from the book in most media is generally not a problem.) In each source-code file you find a reference to the following copyright notice: ²https://github.com/EffectOrientedProgramming/EOPCode Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 3 Copyright // Copyright.txt This computer source code is Copyright ©2021 MindView LLC. All Rights Reserved. Permission to use, copy, modify, and distribute this computer source code (Source Code) and its documentation without fee and without a written agreement for the purposes set forth below is hereby granted, provided that the above copyright notice, this paragraph and the following five numbered paragraphs appear in all copies. 1. Permission is granted to compile the Source Code and to include the compiled code, in executable format only, in personal and commercial software programs. 2. Permission is granted to use the Source Code without modification in classroom situations, including in presentation materials, provided that the book "Effect Oriented Programming" is cited as the origin. 3. Permission to incorporate the Source Code into printed media may be obtained by contacting: MindView LLC, PO Box 969, Crested Butte, CO 81224 MindViewInc@gmail.com 4. The Source Code and documentation are copyrighted by MindView LLC. The Source code is provided without express or implied warranty of any kind, including any implied warranty of merchantability, fitness for a particular purpose or non-infringement. MindView LLC does not warrant that the operation of any program that includes the Source Code will be uninterrupted or error-free. MindView LLC makes no representation about the suitability of the Source Code or of any software that includes the Source Code for any purpose. The entire risk as to the quality and performance of any program that includes the Source Code is with the user of the Source Code. The user understands that the Source Code was developed for research and instructional purposes and is advised not to rely exclusively for any reason on the Source Code or any program that includes the Source Code. Should the Source Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 4 Copyright Code or any resulting software prove defective, the user assumes the cost of all necessary servicing, repair, or correction. 5. IN NO EVENT SHALL MINDVIEW LLC, OR ITS PUBLISHER BE LIABLE TO ANY PARTY UNDER ANY LEGAL THEORY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR ANY OTHER PECUNIARY LOSS, OR FOR PERSONAL INJURIES, ARISING OUT OF THE USE OF THIS SOURCE CODE AND ITS DOCUMENTATION, OR ARISING OUT OF THE INABILITY TO USE ANY RESULTING PROGRAM, EVEN IF MINDVIEW LLC, OR ITS PUBLISHER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. MINDVIEW LLC SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOURCE CODE AND DOCUMENTATION PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, WITHOUT ANY ACCOMPANYING SERVICES FROM MINDVIEW LLC, AND MINDVIEW LLC HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. Please note that MindView LLC maintains a Web site which is the sole distribution point for electronic copies of the Source Code, where it is freely available under the terms stated above: https://github.com/EffectOrientedProgramming/EOPCode If you think you've found an error in the Source Code, please submit a correction at: https://github.com/EffectOrientedProgramming/EOPCode/issues You may use the code in your projects and in the classroom (including your presentation materials) as long as the copyright notice that appears in each source file is retained. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 5 Copyright Edit This Chapter Edit This Chapter³ ³https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/00_Copyright.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Preface Who is the book is for Code examples Edit This Chapter Edit This Chapter⁴ ⁴https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/01_Preface.md What Are Effects Any real program has to interact with things outside the programmer’s control. All external systems are unpredictable. One of the biggest challenges in building systems that are more predictable / reliable / ??? is to isolate and manage the unpredictable parts. An approach that programmers may use to handle this is to delineate the parts of the program which use external systems. By delineating them, programmers then have tools to handle the unpredictable parts in more predictable ways. The interactions with external systems can be defined in terms of “Effects” which create a delineation between the parts of a program that interact with external systems and those that don’t. For example, a program that displays the current date requires something that actually knows the current date. The program needs to talk to an external system (maybe just the operating system) to get this information. These programs are unpredictable because the programmer has no control over what that external system will say or do. Effect Oriented systems allow us to apply strategies to mitigate the unpredictability of using external systems. Effects can not be un-done. Once a program has communicated with an external system, (i.e. executed an Effect), everything that happens on that external systems is out of the program’s control. (analogies on human communication) Imagine that a friend recently stayed in your home. 3 days after they leave, you realize that you are missing some money that What Are Effects 8 had been stored in the guestroom. Now you have a dilemma - do you ask them if they took the money? Simply by asking, you could permanently change, or even end, your relationship with this person. They could immediately admit fault, and ask for forgiveness. Now you know that they are capable of stealing from you - will you ever trust them in your home again? They could angrily deny the accusation, and resent you for making it. Or the conversation could go in a million different other ways that are impossible to predict. We know one thing for certain - you will never be able to un-ask that question. Even if you ultimately grow closer with this person after navigating this situation, you can’t go back to a world where you never asked. Regardless of any apology and forgiveness, your relationship is now different. The Effect is not what happens on the external system because there is no way to know the actual impact of what the program caused by the communication. Edit This Chapter Edit This Chapter⁵ ⁵https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/02_What_Are_Effects.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Superpowers 1. The superpowers are the strategies to deal with unpredictability 2. OpeningHook (1-6) 1. Note: Progressive enhancement through adding capabilities 3. Concurrency 1. Race 2. Hedge (to show the relationship to OpeningHook ie progressive enhancement) 4. Sequential 1. ZIO Direct 2. Note: Combine OpeningHook & Concurrency with ZIO Direct 5. And so much more 1. Note: And there are many capabilities you might want to express. In the future we will dive into these other capabilities. • • • • • • • • • • Racing Timeout Resource Safety Mutability that you can trust Human-readable Cross-cutting Concerns / Observability / Regular Aspects – timed – metrics – debug – logging Interruption/Cancellation Fibers Processor Utilization – Fairness – Work-stealing Resource Control/Management 10 Superpowers object DatabaseError object TimeoutError Building a Resilient Process in stages Successful Code // works runDemo: saveUser: "mrsdavis" // User saved Error Fallback Value // fails runDemo: saveUser: "mrsdavis" .orElseFail: "ERROR: User could not be saved" // DatabaseError // ERROR: User could not be saved Retry Upon Failure import zio.Schedule.{recurs, spaced} val aFewTimes = // TODO Restore original spacing when done // editing // recurs(3) && spaced(1.second) recurs(3) && spaced(1.millis) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 11 Superpowers runDemo: saveUser: "morty" .retry: aFewTimes .orElseSucceed: "ERROR: User could not be saved" // DatabaseError // DatabaseError // User saved Fallback after multiple failures // fails every time - with retry runDemo: saveUser: "morty" .retry: aFewTimes .orElseSucceed: "ERROR: User could not be saved" // DatabaseError // DatabaseError // DatabaseError // DatabaseError // ERROR: User could not be saved Timeouts Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 12 Superpowers // TODO Restore real value when done editing val timeLimit = 5.millis // timeLimit: Duration = PT0.005S // 5.seconds // first is slow - with timeout and retry runDemo: saveUser: "morty" .timeoutFail(TimeoutError)(timeLimit) .retry: aFewTimes .orElseSucceed: "ERROR: User could not be saved" // Interrupting slow request // Database Timeout // User saved Fallback Effect // fails - with retry and fallback runDemo: saveUser: "morty" .timeoutFail(TimeoutError)(timeLimit) .retry: aFewTimes .orElse: sendToManualQueue: "morty" .orElseSucceed: "ERROR: User could not be saved, even to the fallback system" // DatabaseError // DatabaseError // DatabaseError // DatabaseError // User sent to manual setup queue Concurrently Execute Effect Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 13 Superpowers // concurrently save & send analytics runDemo: saveUser: "morty" // todo: maybe this hidden extension method // goes too far with functionality that // doesn't really exist // TODO Should we fireAndForget before the // retries/fallbacks? .fireAndForget: userSignupInitiated: "morty" .timeoutFail(TimeoutError)(timeLimit) .retry: aFewTimes .orElse: sendToManualQueue: "morty" .orElseSucceed: "ERROR: User could not be saved" // User saved Ignore failures in Concurrent Effect Feeling a bit “meh” about this step. // concurrently save & send analytics, ignoring analytics failures runDemo: // TODO Consider ways to dedup morty // string saveUser: "mrsdavis" .timeoutFail(TimeoutError)(timeLimit) .retry: aFewTimes .orElse: sendToManualQueue: "mrsdavis" .tapBoth( error => userSignUpFailed("mrsdavis", error), Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 14 Superpowers success => userSignupSucceeded("mrsdavis", success) ) .orElseSucceed: "ERROR: User could not be saved" // Analytics sent for signup completion // User saved Edit This Chapter Edit This Chapter⁶ ⁶https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/03_Superpowers.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional 1. Functions & Specialized Data Types Are Great 2. 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 through the trouble of an effect-oriented API when plain vars, for loops, and :Unit exist? Sometimes it seems like functional programmers write code like this just to be fancy. To understand what’s really behind this different way of thinking about programming, 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 Why Functional 16 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 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 Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional 17 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 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 Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional 18 more reliable software. As a result, we have languages that quickly create unreliable software. And in many cases we’ve been able to get by with that. For one thing, this approach has greatly advanced testing technology, because it was necessary. Customers have learned to put up with buggy software. They’ve often been willing to accept buggy software when the alternative is no software at all. The world has changed. Back then, the drive was to speed up activities that humans were doing. Those humans could compensate for bugs. Now, however, more and more software is doing things that humans can’t do, so failures in software cannot be propped up by humans. Unreliable software is no longer an inconvenience, but a serious problem. Quickly creating unreliable software is no longer acceptable. We must delve into the reasons that software fails—either it doesn’t do what it’s supposed to, or it just breaks. Reuse How do we create software? When you first learned to program, you probably solved problems by writing code using the basic constructs of your language. But at some point you began realizing that you could only produce and debug so much code by yourself. If you could use code that was already written and debugged by other people, you could produce solutions faster. You might have gone through a cut-and-paste phase before discovering that formalized libraries were easier and more reliable. Even then, library ease of use depended on the sophistication of your language. For the reasons mentioned, using a C library could be tricky and difficult. C++ made this much easier and paved the way for the acceptance of languages like Java, Python, Scala, and Kotlin. Indeed, any new language that doesn’t support easy code reuse is not taken seriously. But code reuse in object-oriented languages was still limited. You could either use objects in a library directly, or you could add those library classes into new classes 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. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional 19 To compose systems rapidly and reliably, we return to first principles and figure out how to: 1. Create basic components that are completely reliable. 2. Combine those components in a way that 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. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional 20 What about the environment affecting the function? This is a bit more subtle, and requires that we think in more general terms than just basic imperative programming. In particular, we must consider concurrency. If multiple tasks are running in our program, then at any point another task might see variables in our function. A variable can change, so that means this other task might see different values at different points in the function’s execution. And if that variable is modified by some other task, we have no way of predicting the result, and we don’t get the reliable mathematical function that we want. We solve this problem through immutability. That is, instead of using variables, we create values that cannot change. This way, it doesn’t matter if an external task sees our values, because it will only see that one value and not something that is different from one moment to the next. And the external task cannot change the value and cause the function to produce a different result. Functions that behave mathematically, that always produce the same results from the same inputs and have no side effects, are called pure functions. When we add the additional constraint of immutability, we produce functions that compose without introducing points of breakage. We can reliably reason about such functions. Composability TODO Reconsider this formal math style. It doesn’t match our other examples, where we really stick to Scala code If functions g and h are pure, we should be able to combine them directly (assuming the types agree) to produce a new function f: f(a) = g(h(a)) This assumes that all functions involved are complete, meaning that they produce a legitimate result for every possible value of a. This is not always true. For example, dividing a number by zero is undefined, and so cannot produce a reasonable number as a result. Using a key to look up a value in a map is undefined if that key doesn’t exist in the map. <– TODO Weird wording An incomplete function requires more operations when using it, to handle the problematic inputs. You can think of the solution as stepwise composability. Instead Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional 21 of calling g(h(a)), we break the process into steps: x = h(a), then check the success of the operation. If successful, pass the result to g. These extra steps make composability sound like it could get tedious, and languages like Scala that provide more thorough support for functional programming provide syntax to make this kind of programming feasible. Effects Now we have created this perfect world of pure functions that behave just like the functions in theoretical mathematics. They have no side effects and cannot be affected by other functions, and can be neatly and safely composed. “But,” you wonder, “if all I can do with the result of one pure function is pass it as an argument to another pure function, what’s the point of all these pure function calls? If these functions have no effect on the world, they seem like an intellectual exercise that merely heats up the CPU.” This is absolutely true. A program that never affects the world is pointless. For a program to be useful, it must be affected by the world, and it must have effects upon the world. The phrase “side effect” implies an incidental or accidental impact on the world. What we need to do is formalize this idea and bring it under our control. We can then call it simply an “effect,” without the “side.” The solution is to manage these effects so they are under our control. This bridge between pure functions and practical programs with controlled and managed effects is the reason for the title of this book. Avoiding Recursion We will deliberately avoid recursion in most examples. Many functional programming tutorials begin by introducing recursion. While this resonates with some, it is an additional hurdle that makes the transition harder. We want to demonstrate powerful, functional code without user-facing recursion. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional 22 Core Differences Between OO and Functional An OO language worries about managing state. It “encapsulates” a data structure in privacy and surrounds it with custom methods (aka member functions) which are ideally the only way to access and modify the state of that data structure. This is important because an OO data structure is typically mutable. This OO ceremony attempts to create predictability by knowing how the data structure can be mutated. Functional programming abstracts common behavior into reusable functional components. These components are adapted to specific needs using other functions. This is why lambdas are so important, because you constantly need to adapt general code to specific purposes, They enable concise code that would otherwise be awkward and intrusive to right as a standalone function. Functions in a functional language don’t need to be tied to a particular data structure. Thus, they can often be written for more general use and to reduce duplication. Functional languages come with a general set of well-tested, reusable operations that can be applied 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. 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. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional 23 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: • a function’s ability to create other functions • transforming elemements in a collection using map Those are indeed important benefits, but they just dip into the possibilities. Adopting some of the styles found in functional programming does not make a language functional. In this book we want to get to the heart of what it means to be functional. In particular, we want to show what it takes to make reliable functional code that can be composed without propagating or amplifying flaws in its components. Edit This Chapter Edit This Chapter⁷ ⁷https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/04_Why_Functional.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Composability Composability Explanation 1. But Functions & Specialized Data Types Don’t Compose for Effects 2. Composability 1. Limitations of Functions & SDTs 1. Some intro to Universal Effect Data Types ie ZIO 1. The ways in which ZIOs compose (contrasted to limitations) 1. Note: Merge chapters: composability, Unit, The_ZIO_Type 1. Note: Avoid explicit anonymous sum & product types at this point Alternatives and their downsides Other framings/techniques and their pros/cons: Plain functions that throw Exceptions • We can’t union these error possibilities and track them in the type system • Cannot attach behavior to deferred functions Plain functions that block • We can’t indicate if they block or not • Too many concurrent blocking operations can prevent progress of other operations • Very difficult to manage • Blocking performance varies wildly between environments Composability 25 Functions that return Either/Option/Try/etc • We can manage the errors in the type system, but we can’t interrupt the code that is producing these values • All of these types must be manually transformed into the other types • Execution is not deferred Functions that return a Future • • • • • • • Can be interrupted example1[future_interrupted_1] two[future_interrupted_2] Cleanup is not guaranteed⁸ Manual management of cancellation Start executing immediately Must all fail with Exception Implicits Are not automatically managed by the compiler, you must explicitly add each one to your parent function • Resolving the origin of a provided implicit can be challenging Try-with-resources • These are statically scoped • Unclear who is responsible for acquisition & cleanup Each of these approaches gives you benefits, but you can’t assemble them all together. Instead of the best of all worlds, you get the pain of all worlds. eg Closeable[Future[Either[Throwable, A]]] The ordering of the nesting is significant, and not easily changed. The number of combinations is something like: PairsIn(numberOfConcepts) ⁸./15_Concurrency_Interruption.md#Future-Cancellation Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Composability 26 Universal Composability with ZIO ZIOs compose including errors, async, blocking, resource managed, cancellation, eitherness, environmental requirements. The types expand through generic parameters. ie composing a ZIO with an error of String with a ZIO with an error of Int results in a ZIO with an error of String | Int. With functions there is one way to compose. f(g(h)) will sequentially apply the functions from the inside out. Another term for this form of composition is called andThen in Scala. With ZIO you can use zio-direct to compose ZIOs sequentially with: runDemo: defer: val topStory = findTopNewsStory .run textAlert: topStory .run // Texting story: Battery Breakthrough // () There are many other ways you can compose ZIOs. The methods for composability depend on the desired behavior. For example, to compose a ZIO that can produce an error with a ZIO that logs the error and then produces a default value, you can use the catchAll like: Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Composability def logAndProvideDefault(e: Throwable) = Console .printLine: e.getMessage .as: "default value" runDemo: ZIO .attempt: ??? .catchAll: logAndProvideDefault // an implementation is missing // default value Edit This Chapter Edit This Chapter⁹ ⁹https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/05_Composability.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 27 Dependency Injection 1. Application startup uses the same tools that you utilize for the rest of your application General/Historic discussion One reason to modularize an application into “parts” is that the relationship between the parts can be expressed and also changed depending on the needs for a given execution path. Typically, this approach to breaking things into parts and expressing what they need, is called “Dependency Injection.” … Why is it called “Dependency Injection” ? Avoid having to explicitly pass things down the call chain. There is one way to express dependencies. Let’s consider an example: We want to write a function that fetches Accounts from a database The necessary parts might be a DatabaseService which provides database connections and a UserService which provides the access controls. By separating these dependencies our from the functionality of fetching accounts, tests can “fake” or “mock” the dependencies to simulate the actual dependency. In the world of Java these dependent parts are usually expressed through annotations (e.g. @Autowired in Spring). But these approaches are “impure” (require mutability), often rely on runtime magic (e.g. reflection), and require everything that uses the annotations to be created through a Dependency Injection manager, complicating construction flow. An alternative to this approach is to use “Constructor Injection” which avoids some of the pitfalls associated with “Field Injection” but doesn’t resolve some of the underlying issues, including the ability for dependencies to be expressed at compile time. Dependency Injection 29 If instead functionality expressed its dependencies through the type system, the compiler could verify that the needed parts are in-fact available given a particular path of execution (e.g. main app, test suite one, test suite two). What ZIO can provide us. With ZIO’s approach to dependencies, you get many desirable characteristics at compile-time, using standard language features. Your services are defined as classes with constructor arguments, just as in any vanilla Scala application. No annotations that kick off impenetrable wiring logic outside your normal code. For any given service in your application, you define what it needs in order to execute. Finally, when it is time to build your application, all of these pieces can be provided in one, flat space. Each component will automatically find its dependencies, and make itself available to other components that need it. To aid further in understanding your application architecture, you can visualize the dependency graph with a single line. You can also do things that simply are not possible in other approaches, such as sharing a single instance of a dependency across multiple test classes, or even multiple applications. DI-Wow! TODO Values to convey: - Layer Graph - Cycles are a compile error - Visualization with Mermaid - test implementations - Layer Resourcefulness - Layers can have setup & teardown (open & close) - “‘scala // Explain private constructor approach case class Dough private () object Dough: val letRise: ZIO[Dough, Nothing, Unit] = ZIO.debug(“Dough is rising”) val fresh: ZLayer[Any, Nothing, Dough] = ZLayer .derive[Dough] .tapWithMessage(“Making Fresh Dough”) “‘ Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Dependency Injection 30 Step 1: Effects can express dependencies Effects can’t be run until their dependencies have been fulfilled TODO: Decide what to do about the compiler error differences between these approaches object LetDoughRiseNoDough extends ZIOAppDefault: override def run = Dough.letRise // error: // // // ──── ZIO APP ERROR ─────────────────────────────────────────────────\ ── // // Your effect requires a service that is not in the environment. // Please provide a layer for the following type: // // 1. repl.MdocSession.MdocApp.Dough // // Call your effect's provide method with the layers you need. // You can read more about layers and providing services here: // // https://zio.dev/reference/contextual/ // // ────────────────────────────────────────────────────────────────────\ ── // // // defer: // ^ Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Dependency Injection 31 // TODO Consider weirdness of provide with no args runDemo: Dough.letRise.provide() // error: // // // ──── ZLAYER ERROR ──────────────────────────────────────────────────\ ── // // Please provide a layer for the following type: // // 1. repl.MdocSession.MdocApp.Dough // // ────────────────────────────────────────────────────────────────────\ ── // // // Bread.make.provide(Dough.fresh, Heat.oven) // ^ Step 2: Provide Dependencies to Effects Then the effect can be run. runDemo: Dough .letRise .provide: Dough.fresh // Making Fresh Dough // Dough is rising // () For code organization, and legibility at call sites, we are defining several layers within the Heat companion object. They will all be used soon. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Dependency Injection 32 case class Heat private () object Heat: val oven: ZLayer[Any, Nothing, Heat] = ZLayer .derive[Heat] .tapWithMessage: "Heating Oven" val toaster: ZLayer[Any, Nothing, Heat] = ZLayer .derive[Heat] .tapWithMessage: "Heating Toaster" val broken: ZLayer[Any, String, Nothing] = ZLayer.fail: "**Power Out**" Step 3: Effects can require multiple dependencies Note: The following is copy&pasted and might just need a slight diversion to &’d typed parameters Intersections AKA Products AKA Case Classes AKA Ands 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. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Dependency Injection 33 // Restore private constructor after failure scenario is dialed in case class Bread() object Bread: val make: ZIO[Heat & Dough, Nothing, Bread] = ZIO.succeed: Bread() // TODO Explain ZLayer.fromZIO in prose // immediately before/after this val homemade : ZLayer[Heat & Dough, Nothing, Bread] = ZLayer .fromZIO: make .tapWithMessage: "Making Homemade Bread" val storeBought: ZLayer[Any, Nothing, Bread] = ZLayer .derive[Bread] .tapWithMessage: "Buying Bread" val eat: ZIO[Bread, Nothing, String] = ZIO.succeed: "Eating bread!" end Bread runDemo: Bread.make.provide(Dough.fresh, Heat.oven) // Bread() Step 4: Dependencies can “automatically” assemble to fulfill the needs of an effect Something around how like typical DI, the “graph” of dependencies gets resolved “for you” This typically happens in some completely new/custom phase, that does follow standard code paths. Dependencies on effects propagate to effects which use effects. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Dependency Injection 34 // TODO Figure out why Bread.eat debug isn't showing up runDemo: Bread .eat .provide( // Highlight that homemade needs the other // dependencies. Bread.homemade, Dough.fresh, Heat.oven ) // Eating bread! Step 5: Different effects can require the same dependency Eventually, we grow tired of eating plain Bread and decide to start making Toast. Both of these processes require Heat. // Is it worth the complexity of making this private? // It would keep people from creating Toasts without using the make met\ hod case class Toast private () object Toast: val make: ZIO[Heat & Bread, Nothing, Toast] = ZIO.succeed: println: "Making toast" Toast() It is possible to also use the oven to provide Heat to make the Toast. The dependencies are based on the type, so in this case both Toast.make and Bread.make require heat, but Notice - Even though we provide the same dependencies in this example, Heat.oven is also required by Toast.make Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Dependency Injection 35 runDemo: Toast .make .provide( Bread.homemade, Dough.fresh, Heat.oven ) // Making toast // Toast() However, the oven uses a lot of energy to make Toast. It would be great if we can instead use our dedicated toaster! Step 6: Dependencies are based on types and must be uniquely provided runDemo: Toast .make .provide( Dough.fresh, Bread.homemade, Heat.oven, Heat.toaster ) // error: // // // ──── ZLAYER ERROR ──────────────────────────────────────────────────\ ── // // Ambiguous layers! I cannot decide which to use. // You have provided more than one layer for the following type: // // repl.MdocSession.MdocApp.Heat is provided by: // 1. Heat.oven // 2. Heat.toaster // // ────────────────────────────────────────────────────────────────────\ ── Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Dependency Injection 36 // // Unfortunately our program is now ambiguous. It cannot decide if we should be making Toast in the oven, Bread in the toaster, or any other combination. Step 7: Providing Dependencies at Different Levels This enables other effects that use them to provide their own dependencies of the same type runDemo: val bread = ZLayer.fromZIO: Bread.make.provide(Dough.fresh, Heat.oven) Toast.make.provide(bread, Heat.toaster) // Making toast // Toast() Step 8: Dependencies can fail TODO Explain .build before using it to demo layer construction Bread2.fromFriend: ZLayer[Any, String, Bread] runDemo: Bread .eat .provide: Bread2.fromFriend // **Power out** // **Power out Rez** Step 9: Dependency Retries Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Dependency Injection runDemo: val bread = Bread2 .fromFriend .retry: Schedule.recurs: 3 Bread .eat .provide: bread // **Power out** // **Power out** // Power is on // Eating bread! Step 10: Dependency Fallback runDemo: val bread = Bread2 .fromFriend .orElse: Bread.storeBought Toast.make.provide(bread, Heat.toaster) // **Power out** // Making toast // Toast() Step 11: Layer Retry + Fallback? Maybe retry on the ZLayer eg. (BreadDough.rancid, Heat.brokenFor10Seconds) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 37 Dependency Injection runDemo: Bread2 .fromFriend .retry: Schedule.recurs: 1 .orElse: Bread.storeBought .build // TODO Stop using build, if possible .debug // **Power out** // **Power out** // ZEnvironment(MdocSession::MdocApp::Bread -> Br Edit This Chapter Edit This Chapter¹⁰ ¹⁰https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/06_Dependency_Injection.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 38 The ZIO Type We need an Answer about this scenario. The scenario requires things and could produce an error. trait ZIO[Requirements, Error, Answer] The ZIO trait is at the center of our Effect-oriented world. trait ZIO[R, E, A] A trait with 3 type parameters can be intimidating, but each one serves a distinct, important purpose. R - 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 40 The ZIO Type import scala.concurrent.Future runDemo: ZIO.fromFuture(implicit ec => Future.successful("Success!") ) // Success! runDemo( ZIO.fromFuture(implicit ec => Future.failed(new Exception("Failure :(")) ) ) // java.lang.Exception: Failure :( Edit This Chapter Edit This Chapter¹¹ ¹¹https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/07_The_ZIO_Type.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 41 Built-in Services Why are Console, Clock, Random, System Built-in? ZIO[Any, _, _] - System effects are not included in E Examples of Using System Effects Some Services are considered fundamental/primitive by ZIO. They are built-in to the runtime and available to any program. History - TODO Consider deleting. Not crucial to the reader. 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 For 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. Built-in Services Overriding Builtin Services Note: This doesn’t work in ZIO 2 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") runDemo( leakSensitiveInfo.provide( ZLayer.succeed[Console](ConsoleSanitized) ) ) Edit This Chapter Edit This Chapter¹² ¹²https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/08_Builtin_Services.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 43 Running Effects ZIOs are not their result. They are something that can be executed, that might produce that result. If you have a ZIO Effect like: println("A") // A ZIO.debug("B") // res1: ZIO[Any, Nothing, Unit] = Sync( // trace = "repl.MdocSession.MdocApp.res1(09_Running_Effects.md:13)", // eval = zio.ZIOCompanionVersionSpecific$$Lambda$17027/0x00000008042\ 96440@332c4631 // ) println("C") // C We will not see the ZIO.debug output. It only describes something to be done. It is only data (in the ZIO data type), not instructions. To actually run a ZIO, your program must take the data types and interpret / run them, executing the logic . A common mistake when starting with ZIO is trying to return ZIO instances themselves rather than their result. println(Random.nextInt) // Stateful(repl.MdocSession.MdocApp.res3(09_Running_Effects.md:25),zio\ .FiberRef$unsafe$$anon$2$$Lambda$17104/0x000000080431c040@5a51f58d) This is a mistake because ZIO’s are not their result, they are descriptions of effects that produce the result. ZIOs are not automatically executed. The user must determine when/where that happens. Running Effects 45 An Option might have a value inside of it, but you can’t safely assume that it does. Similarly, a ZIO might produce a value, but you have to run it to find out. You can think of them as recipes for producing a value. You don’t want to return a recipe from a function, you can only return a value. If it is your friend’s birthday, they want a cake, not a list of instructions about mixing ingredients and baking. The defer/direct syntax makes this more explicit The ZIO Interpreter Scala compiles code to JVM bytecodes, Similarly ZIO has an interpreter that steps through and executes your code, much like the JVM interprets JVM bytecodes. The Zio interpreter is the hidden piece that allows Zio to understand so much more about the meaning of your code. This includes the ability to decide what to run concurrently and how to invisibly tune that concurrency–all at runtime. The interpreter is responsible for deciding when to context-switch between tasks, and is able to do this because it understands the ZIO code that it’s executing. The interpreter is also the mechanism that evaluates the various effects described in the generic type parameters for each ZIO object. The reason we have the defer directive(method?) in zio-direct is to indicate that this code will be evaluated by the interpreter later. Building applications from scratch One way to run ZIOs is to use a “main method” program (something you can start in the JVM). However, setting up the pieces needed for this is a bit cumbersome if done without helpers. ZIOAppDefault ZIO provides an easy way to do this with the ZIOAppDefault trait. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Running Effects 46 To use it create a new object that extends the ZIOAppDefault trait and implements the run method. That method returns a ZIO so you can now give it the example ZIO.debug data: scala object HelloWorld extends zio.ZIOAppDefault: def run = ZIO.debug: "hello, world" This can be run on the JVM in the same way as any other class that has a static void main method. The ZIOAppDefault trait sets up the ZIO runtime which interprets ZIOs and provides some out-of-the-box functionality, and then runs the provided data in that runtime. If you are learning ZIO, you should start your exploration with ZIOAppDefault. It is the standard, simplest way to start executing your recipes. // NOTE We cannot execute invoke main on this // because it crashes mdoc in the CI process object RunningZIOs extends ZIOAppDefault: def run = // TODO Console/debug don't work ZIO.attempt: println: "Hello World!" You can provide arbitrary ZIO instances to the run method, as long as you have provided every piece of the environment. In other words, it can accept ZIO[Any, _, _]. There is a more flexible ZIOApp that facilitates sharing layers between applications, but this is advanced and not necessary for most applications. runDemo While the ZIOApp* types are great for building real applications, they are not ideal for demonstrating code for a book. We created the runDemo function to streamline this use-case. It is a function that takes a ZIO and executes it in a runtime, returning the result. It uses most of the same techniques that are used in ZIOAppDefault, but is more single purpose, always immediately executing the ZIO provided to it. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Running Effects 47 runDemo: ZIO.debug: "hello, world" // hello, world // () Testing code - `runSpec` ? ZIOSpecDefault Similar to ZIOAppDefault, there is a ZIOSpecDefault that should be your starting point for testing ZIO applications. ZIOSpecDefault provides test-specific implementations built-in services, to make testing easier. When you run the same ZIO in these 2 contexts, the only thing that changes are the built-in services provided by the runtime. TODO - Decide which scenario to test import zio.test._ object TestingZIOs extends ZIOSpecDefault: def spec = test("Hello Tests"): defer: ZIO.console.run assertTrue: Random.nextIntBounded(10).run > 10 runSpec Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Running Effects 48 runSpec: defer: assertTrue: Random.nextIntBounded(10).run < 10 // Test: PASSED* TODO Justify defer syntax over for-comp for multi-statement assertions I think this example completes the objective TODO Change this to a Console app, where the logic & testing is more visceral scala runSpec: defer: assertTrue: Random.nextIntBetween(0, 10).run <= 10 && Random.nextIntBetween(10, 20).run <= 20 && Random.nextIntBetween(20, 30).run <= 30 // Test: PASSED* runSpec: for res1 <- Random.nextIntBetween(0, 10) res2 <- Random.nextIntBetween(10, 20) res3 <- Random.nextIntBetween(20, 30) yield assertTrue: res1 <= 10 && res2 <= 20 && res3 <= 30 // Test: PASSED* Consider a Console application: scala val logic = defer: val username = Console .readLine: "Enter your name\n" .run Console .printLine: s"Hello $username" .run .orDie If we try to run this code in the same way as most of the examples in this book, we encounter a problem. scala runDemo: logic.timeout(1.second) // Defect: scala.NotImplementedError: an implemen We cannot execute this code and render the results for the book because it requires interaction with a user. However, even if you are not trying to write demo code for a book, it is very limiting to need a user at the keyboard for your program to execute. Even for the smallest programs, it is slow, error-prone, and boring. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Running Effects 49 runSpec: defer: TestConsole .feedLines: "Zeb" .run logic.run val capturedOutput: String = TestConsole.output.run.mkString val expectedOutput = s"""|Enter your name |Hello Zeb |""".stripMargin assertTrue: capturedOutput == expectedOutput // Test: PASSED* Interop with existing/legacy code via Unsafe In some cases your ZIOs may need to be run outside a main program, for example when embedded into other programs. In this case you can use ZIO’s Unsafe utility which is called Unsafe to indicate that the code may perform side effects. To do the same ZIO.debug with Unsafe do: Unsafe.unsafe { implicit u: Unsafe => Runtime .default .unsafe .run: ZIO.debug: "hello, world" .getOrThrowFiberFailure() } // hello, world If needed you can even interop to Scala Futures through Unsafe, transforming the output of a ZIO into a Future. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Running Effects 50 Web Request Handler Different enough to warrant its own section? You only ever execute a top-level ZIO, even if it branches out to multiple other ZIOs Processing streams of data Edit This Chapter Edit This Chapter¹³ ¹³https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/09_Running_Effects.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Layers Creating Composing Managing and wiring dependencies has been a perennial challenge in software development. ZIO provides the ZLayer class to solve many of the problems in this space. If you pay the modest, consistent cost of constructing pieces of your application as ZLayers, you will get benefits that scale with the complexity of your project. Consistent with ZIO itself, ZLayer has 3 type parameters that represent: • What it needs from the environment • How it can fail • What it produces when successful. With the same type parameters, and many of the same methods, you might be wondering why we even need a separate data type - why not just use ZIO itself for our dependencies? The environment type parameter for ZLayer maps directly to unique, singleton services in your application. The environment type parameter for ZIO might have many possible instances. ZLayer provides additional behaviors that are valuable specifically in this domain. Typically, you only want a single instance of a dependency to exist across your application. This may be to reduce memory/resource usage, or even to ensure basic correctness. ZLayer output values are shared maximally by default. They also build in scope management that will ensure resource cleanup in asynchronous, fallible situations. ============== Imagine a ServiceX that is needed by 20 diverse functions across your stack. Usually ServiceX has exactly one instance/implementation should be used throughout your application. 52 Layers case class ServiceX(): val retrieveImportantData : ZIO[Any, Nothing, String] = ??? {{ TODO: Should we show a class-based approach, or just go straight to functions? }} “‘scala case class UserManagement(serviceX: ServiceX) case class StatisticsCalculator( serviceX: ServiceX ) case class SecurityModule(serviceX: ServiceX) case class LandingPage( statisticsCalculator: StatisticsCalculator ) “‘ Historic Approaches Manual Wiring case class Application( userManagment: UserManagement, securityModule: SecurityModule, landingPage: LandingPage ) def construct(): Application = val serviceX = ServiceX() Application( UserManagement(serviceX), SecurityModule(serviceX), LandingPage(StatisticsCalculator(serviceX)) ) Even in this tiny example, the downsides are already starting to show. • We have to copy/paste serviceX numerous times • We have to manage multiple levels of dependencies. LandingPage and ServiceImplentation have to be manually connected. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 53 Layers Annotations Pros - “Easy” in the sense that they do not require much code at the use-site Smoother refactoring, as the injection system will determine what needs to be passed around Cons - Does not follow normal control flow or composition - Typically, relies on some framework-level processing that is not easily controlled by the user Traits 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. 1. Create a trait with the needed functions. 2. Create an implementation of the trait. 3. (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 is less familiar, and might be harder to appreciate. We endeavor in the following chapters to make a compelling case for them. If we succeed, the reader will use them when creating their own Effects. One: Create the trait This trait represents effectful code that we need to interact with. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 54 Layers 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)) case class Logic(console: Console): val invoke: ZIO[Any, Nothing, Unit] = defer: console.printLine("Hello").run console.printLine("World").run However, providing dependencies to the logic is still tedious. runDemo: Logic: ConsoleLive .invoke // Hello // World // () Three: 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. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 55 Layers object Console: val live: ZLayer[Any, Nothing, Console] = ZLayer.succeed[Console]: ConsoleLive More important than removing repetition - using 1 unique Layer instance per type allows us to share it across our application. Now executing our code is as simple as describing it. runDemo: ZIO .serviceWithZIO[Logic]: _.invoke .provide( Console.live, ZLayer.fromFunction(Logic.apply _) ) // Hello // World // () Edit This Chapter Edit This Chapter¹⁴ 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/layers/Festival.scala ¹⁴https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/10_Layers.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 56 Layers package layers import zio.ZIO.debug case class Toilets() val toilets = activityLayer(entity = Toilets()) case class Stage() val stage: ZLayer[Any, Nothing, Stage] = activityLayer( entity = Stage(), setupSteps = ("Transporting", 2.seconds), ("Building", 4.seconds) ) case class Permit() val permit: ZLayer[Any, Nothing, Permit] = activityLayer( entity = Permit(), setupSteps = ("Legal Request", 5.seconds) ) def activityLayer[T: Tag]( entity: T, setupSteps: (String, Duration)* ) = ZLayer.scoped( ZIO.acquireRelease( defer: ZIO .debug: entity.toString + " ACQUIRE" .run ZIO .foreach(setupSteps): case (name, duration) => activity( entity.toString, name, duration ) .run Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 57 Layers entity )(_ => debug(entity.toString + " RELEASE")) ) def activity( entity: String, name: String, duration: Duration ) = defer: debug: s"$entity: BEGIN $name" .run debug: s"$entity: END $name" .delay(duration) .run case class Venue(stage: Stage, permit: Permit) val venue = ZLayer.fromFunction(Venue.apply) case class SoundSystem() val soundSystem : ZLayer[Any, Nothing, SoundSystem] = ZLayer.succeed(SoundSystem()) case class Festival( toilets: Toilets, venue: Venue, soundSystem: SoundSystem, security: Security ) val festival = ZLayer.scoped { ZIO.acquireRelease { defer: debug("FESTIVAL: We are all set!").run Festival( ZIO.service[Toilets].run, ZIO.service[Venue].run, ZIO.service[SoundSystem].run, Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 58 Layers ZIO.service[Security].run ) } { _ => debug( "FESTIVAL: Good job, everyone. Close it down!" ) } } case class Security(toilets: Toilets) val security : ZLayer[Toilets, Nothing, Security] = ZLayer.scoped { ZIO.acquireRelease { defer: debug("SECURITY: Ready").run Security(ZIO.service[Toilets].run) } { _ => debug("SECURITY: Going home") } } Testing Unpredictable Effects Effects need access to external systems thus are unpredictable. Tests are ideally predictable so how do we write tests for effects that are predictable? With ZIO we can replace the external systems with predictable ones when running our tests. With ZIO Test we can use predictable replacements for the standard systems effects (Clock, Random, Console, etc). Random An example of this is Random numbers. Randomness is inherently unpredictable. But in ZIO Test, without changing our Effects we can change the underlying systems with something predictable: Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 59 Layers import zio.test.TestRandom import zio.test.assertTrue runSpec: defer: TestRandom.feedInts(1, 2).run val result1 = Random.nextInt.run val result2 = Random.nextInt.run assertTrue(result1 == 1, result2 == 2) // Test: PASSED* The Random Effect uses an injected something which when running the ZIO uses the system’s unpredictable random number generator. In ZIO Test the Random Effect uses a different something which can predictably generate “random” numbers. TestRandom provides a way to define what those numbers are. This example feeds in the Ints 1 and 2 so the first time we ask for a random number we get 1 and the second time we get 2. Anything an effect needs (from the system or the environment) can be substituted in tests for something predictable. For example, an effect that fetches users from a database can be simulated with a predictable set of users instead of having to setup a test database with predictable users. Time Even time can be simulated as using the clock is an effect. import zio.test.* runSpec: val thingThatTakesTime = ZIO.sleep(2.seconds) defer: val fork = thingThatTakesTime .timeout(1.second) .fork .run TestClock.adjust(2.seconds).run Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 60 Layers val result = fork.join.run assertTrue(result.isEmpty) // Test: PASSED* By default in ZIO Test, the clock does not change unless instructed to. Calling a time based effect like timeout would hang indefinitely with a warning like: Warning: A test is using time, but is not advancing the test clock, which may result in the test hanging. Use TestClock.adjust to manually advance the time. To test time based effects we need to fork those effects so that then we can adjust the clock. After adjusting the clock, we can then join the effect where in this case the timeout has then been reached causing the effect to return a None. Using a simulated Clock means that we no longer rely on real-world time for time. So this example runs in milliseconds of real-world time instead of taking an actual 1 second to hit the timeout. This way our time-based tests run much more quickly since they are not based on actual system time. They are also more predictable as the time adjustments are fully controlled by the tests. Targeting Error-Prone Time Bands Using real-world time also can be error prone because effects may have unexpected results in certain time bands. For instance, if you have code that gets the time and it happens to be 23:59:59, then after some operations that take a few seconds, you get some database records for the current day, those records may no longer be the day associated with previously received records. This scenario can be very hard to test for when using real-world time. When using a simulated clock in tests, you can write tests that adjust the clock to reliably reproduce the condition. Todo: The example could be clarified. assertTrue In this example we utilize ZIO Test’s assertTrue which provides a non-DSL approach to writing assertions while preserving the negative condition error messages. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 61 Layers Typically using assertTrue doesn’t give helpful errors, ie true != false, but ZIO Test provides helpful details for why the assertion was false. Todo: Can we display with mdoc the nice assertTrue fail message? Todo: More compelling assertTrue failure runSpec: assertTrue(Some("asdf") == None) // Test: FAILED Test Aspects We have seen how to add capabilities and behaviors ZIO’s enabled by manipulating them as values. We can add behaviors to ZSpecs that are more specific to testing. Overriding Builtin Services When testing ZIOs we can provide user-defined Environment types by using .provide. However, the Built-in Services are not part of the Environment, so we need a different way to override them. By default, tests will get Test versions of the Built-in Services. runSpec: defer: val thingThatTakesTime = ZIO.sleep(2.seconds) val result = defer: val fork = thingThatTakesTime .fork .run TestClock.adjust(10.seconds).run fork.join.run .timed .run println(result) assertCompletes // (PT10S,()) // Test: PASSED* Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 62 Layers runSpec( defer: val thingThatTakesTime = ZIO.sleep(2.seconds) val result = defer: val fork = thingThatTakesTime .fork .run TestClock.adjust(10.seconds).run fork.join.run .timed .run println(result) assertCompletes , TestAspect.withLiveClock ) // (PT2.000875687S,()) // Test: PASSED* Injecting Behavior before/after/around runSpec( defer: println("During test") assertCompletes , TestAspect.around( ZIO.debug("ZIO IO, before"), ZIO.succeed(println("plain IO, after")), ) ) // During test // plain IO, after // Test: PASSED* Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 63 Layers Flakiness Commonly, as a project grows, the supporting tests become more and more flaky. This can be caused by a number of factors: • The code is using shared, live services Shared resources, such as a database or a file system, might be altered by other processes. These could be other tests in the project, or even unrelated processes running on the same machine. • The code is not thread safe Other processes running simultaneously might alter the expected state of the system. • Resource limitations A team of engineers might be able to successfully run the entire test suite on their personal machines. However, the CI/CD system might not have enough resources to run the tests triggered by everyone pushing to the repository. Your tests might be occasionally failing due to timeouts or lack of memory. runSpec( defer: assertTrue: Random.nextBoolean.run , TestAspect.withLiveRandom, TestAspect.flaky ) // Test: PASSED* Forbidding • nonflaky We might have sections of the code that absolutely must be reliable, and we want to express that in our tests. By using nonFlaky we can ensure that the test will fail if it is flaky, by hammering it with repeated executions. You can dial up the number of iterations to match your reliability expectations. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 64 Layers runSpec( defer: assertTrue: Random.nextInt.run != 42 , TestAspect.withLiveRandom, TestAspect.nonFlaky ) // Test: PASSED* Tolerating/Flagging In a perfect world, we would fix the underlying issues immediately. However, under real world constraints, we may need to tolerate flakiness for a time. ZIO Test provides a few ways to do this. • flaky This is your goto, easily-applied solution for accommodating legacy flakiness in your codebase. For the average, undiagnosed “This test fails sometimes” circumstance, this is the right starting point. • eventually When you have a test that is flaky, but you don’t know what a reasonable retry behavior is, use eventually. It’s tolerant of any number of failures, and will just keep retrying until interrupted by other mechanisms. Platform concerns Configuration / Environment • TestAspect.ifEnv/ifProp Time #### Measuring Time Since there is already a .timed method available directly on ZIO instances, it might seem redundant to have a timed TestAspect. However, they are distinct enough to justify their existence. ZIOs .timed methods changes the result type of your code by adding the duration to a tuple in the result. This is useful, but requires the calling code to handle this new result type. TestAspect.timed is a non-invasive way to measure the duration of a test. The timing information will be managed behind the scenes, and printed in the test output, without changing any other behavior. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 65 Layers Restricting Time Sometimes, it’s not enough to simply track the time that a test takes. If you have specific Service Level Agreements (SLAs) that you need to meet, you want your tests to help ensure that you are meeting them. However, even if you don’t have contracts bearing down on you, there are still good reasons to ensure that your tests complete in a timely manner. Services like GitHub Actions will automatically cancel your build if it takes too long, but this only happens at a very coarse level. It simply kills the job and won’t actually help you find the specific test responsible. A common technique is to define a base test class for your project that all of your tests extend. In this class, you can set a default upper limit on test duration. When a test violates this limit, it will fail with a helpful error message. This helps you to identify tests that have completely locked up, or are taking an unreasonable amount of time to complete. For example, if you are running your tests in a CI/CD pipeline, you want to ensure that your tests complete quickly, so that you can get feedback as soon as possible. you can use TestAspect.timeout to ensure that your tests complete within a certain time frame. What should run? It would be great if all our tests could run & pass at every moment in time, but there are times when it’s not feasible. If you are doing Test-Driven Development, you don’t want the build to be broken until you are completely finished implementing the feature. If you are rewriting a significant part of your project, you already know there are going to be test failures until you are finished. Traditionally, we comment out the tests in these situations. However, this can lead to a lot of noise in the codebase, and it’s easy to forget to uncomment the tests when you are done. TestAspects provide a better way to handle this. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 66 Layers runSpec( defer: assertNever: "Not implemented. Do not run" ) // Test: FAILED runSpec( defer: assertNever: "Not implemented. Do not run" , TestAspect.ignore ) // Test: PASSED* Defining a base test class for your project Edit This Chapter Edit This Chapter¹⁵ ¹⁵https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/11_Testing_Effects.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Errors 1. Creating & Handling 2. Error composability 3. Retry In a language that cannot throw, following the execution path is simple, following 2 basic rules: - At a branch, execute only the first match - Otherwise, Read everything from left-to-right, top-to-bottom, Once you add throw, the rules are more complicated t At a branch, execute only the first match Otherwise, Read everything from left-to-right, top-to-bottom, Unless we `throw`, which means immediately jumping through a differen\ dimension away from the code you're viewing 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 render(value: String) = s"Temperature: $value" def calculateTemp(behavior: Scenario): String = behavior match case Scenario.GPSError => throw GpsException() case Scenario.NetworkError => throw NetworkException() case Scenario.Success => "35 degrees" Hello Failures 69 def currentTemperatureUnsafe( behavior: Scenario ): String = render: calculateTemp: behavior currentTemperatureUnsafe: Scenario.Success // res0: String = "Temperature: 35 degrees" On the happy path, everything looks as desired. If the network is unavailable, what is the behavior for the caller? This can take many forms. If we don’t make any attempt to handle our problem, the whole program blows up and shows the gory details to the user. // Note - Can't make this output prettier/simpler because it's *not* us\ ing ZIO currentTemperatureUnsafe: Scenario.NetworkError // repl.MdocSession$MdocApp$NetworkException // at repl.MdocSession$MdocApp.calculateTemp(12_Errors.md:28) // at repl.MdocSession$MdocApp.currentTemperatureUnsafe(12_Errors.md:4\ 0) // at repl.MdocSession$MdocApp.$init$$$anonfun$1(12_Errors.md:53) We could take the bare-minimum approach of catching the Exception and returning null: def currentTemperatureNull( behavior: Scenario ): String = render: try calculateTemp: behavior catch case ex: RuntimeException => null currentTemperatureNull: Scenario.NetworkError // res1: String = "Temperature: null" Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures 70 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? def currentTemperature( behavior: Scenario ): String = render: try calculateTemp: behavior catch case ex: RuntimeException => "-1 degrees" currentTemperature: Scenario.NetworkError // res2: String = "Temperature: -1 degrees" Clearly, this isn’t acceptable, as both of these common sentinel values are valid temperatures. We can take a more honest and accurate approach in this situation. def currentTemperature( behavior: Scenario ): String = render: try calculateTemp: behavior catch case ex: RuntimeException => "Unavailable" currentTemperature: Scenario.NetworkError // res3: String = "Temperature: Unavailable" We have improved the failure behavior significantly; is it sufficient for all cases? Imagine our network connection is stable, but we have a problem in our GPS hardware. In this situation, do we show the same message to the user? Ideally, we would show the user a distinct message for each scenario. The Network issue is transient, but the GPS problem is likely permanent. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures 71 def currentTemperature( behavior: Scenario ): String = try render: calculateTemp: behavior catch case ex: NetworkException => "Network Unavailable" case ex: GpsException => "GPS problem" currentTemperature: Scenario.NetworkError // res4: String = "Network Unavailable" currentTemperature: Scenario.GPSError // res5: String = "GPS problem" Wonderful! We have specific messages for all relevant error cases. However, this still suffers from downsides that become more painful as the codebase grows. • The signature of currentTemperature does not alert us that it might fail • If we realize it can fail, we must dig through the implementation to discover the multiple failure values • We never have certainty about the failure paths of our full application, or any subset of it. {{ TODO Tear apart exceptions more }} Encountering an error during a function call generally means two things: 1. You can’t continue executing the function in the normal fashion. 2. You can’t return a normal result. Many languages use exceptions for handling errors. An exception throws out of the current execution path to locate a user-written handler to deal with the error. There are two goals for exceptions: Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures 72 1. Separate error-handling code from “success-path” code, so the success-path code is easier to understand and reason about. 2. Reduce redundant error-handling code by handling associated errors in a single place. Exceptions have problems: 1. They can be “swallowed.” Just because code throws an exception, there’s no guarantee that issue will be dealt with. 2. They can lose important information. Once an exception is caught, it is considered to be “handled,” and the program doesn’t need to retain the failure information. 3. They aren’t typed. Java’s checked exceptions provide a small amount of type information, but it’s not that helpful compared to a full type system. Unchecked exceptions provide no information at all. 4. Because they are handled dynamically, the only way to ensure your program won’t crash is by testing it through all possible execution paths. A staticallytyped error management solution can ensure—at compile time—that all errors are handled. 5. They don’t scale. {{Need to think about this more to make the case.}} 6. Hard to reason about. {{Also need to make this case}} 7. Difficult or impossible to retry an operation if it fails. Java {{and Scala?}} use the “termination” model of exception handling. This assumes the error is so critical there’s no way to get back to where the exception occurred. If you’re performing an operation that you’d like to retry if it fails, exceptions don’t help much. Exceptions were a valiant attempt to produce a consistent error-reporting interface, and they are definitely better than what’s in C. But they don’t end up solving the problem very well, and you just don’t know what you’re going to get when you use exceptions. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures What’s wrong with Try? ADTS as another step forward ZIO Error Handling Now we will explore how ZIO enables more powerful, uniform error-handling. TODO {{Update verbiage now that ZIO section is first}} • ZIO Error Handling • Wrapping Legacy Code ZIO-First Error Handling def getTemperatureZ(behavior: Scenario): ZIO[ Any, GpsException | NetworkException, String ] = behavior match case Scenario.GPSError => ZIO.fail: GpsException() case Scenario.NetworkError => // TODO Use a non-exceptional error ZIO.fail: NetworkException() case Scenario.Success => ZIO.succeed: "35 degrees" runDemo: getTemperatureZ: Scenario.Success // 35 degrees Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 73 Hello Failures 74 // TODO make MDoc:fail adhere to line limits? runDemo: getTemperatureZ: Scenario.Success .catchAll: case ex: NetworkException => ZIO.succeed: "Network Unavailable" // error: // match may not be exhaustive. // // It would fail on pattern case: _: GpsException // TODO Demonstrate ZIO calculating the error types without an explicit annotation being provided runDemo: getTemperatureZ: Scenario.GPSError // repl.MdocSession$MdocApp$GpsException Wrapping Legacy Code If we are unable to re-write the fallible function, we can still wrap the call We are re-using the calculateTemp {{TODO }} def calculateTempWrapped( behavior: Scenario ): ZIO[Any, Throwable, String] = ZIO.attempt: calculateTemp: behavior Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures 75 def displayTemperatureZWrapped( behavior: Scenario ): ZIO[Any, Nothing, String] = calculateTempWrapped: behavior .catchAll: case ex: NetworkException => ZIO.succeed: "Network Unavailable" case ex: GpsException => ZIO.succeed: "GPS problem" runDemo: displayTemperatureZWrapped: Scenario.Success // 35 degrees runDemo: displayTemperatureZWrapped: Scenario.NetworkError // Network Unavailable This is decent, but does not provide the maximum possible guarantees. Look at what happens if we forget to handle one of our errors. def getTemperatureZGpsGap( behavior: Scenario ): ZIO[Any, Nothing, String] = calculateTempWrapped: behavior .catchAll: case ex: NetworkException => ZIO.succeed: "Network Unavailable" Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures 76 runDemo: getTemperatureZGpsGap: Scenario.GPSError // Defect: GpsException The compiler does not catch this bug, and instead fails at runtime. Take extra care when interacting with legacy code, since we cannot automatically recognize these situations at compile time. We have 2 options in these situations. First, we can provide a fallback case that will report anything we missed: def getTemperatureZWithFallback( behavior: Scenario ): ZIO[Any, Nothing, String] = calculateTempWrapped: behavior .catchAll: case ex: NetworkException => ZIO.succeed: "Network Unavailable" case other => ZIO.succeed: "Error: " + other runDemo: getTemperatureZWithFallback: Scenario.GPSError // Error: repl.MdocSession$MdocApp$GpsException This lets us avoid the most egregious gaps in functionality, but it does not take full advantage of ZIO’s type-safety. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures 77 def getTemperatureZAndFlagUnhandled( behavior: Scenario ): ZIO[Any, GpsException, String] = calculateTempWrapped: behavior .catchSome: case ex: NetworkException => ZIO.succeed: "Network Unavailable" // TODO Eh, find a better version of this. .mapError(_.asInstanceOf[GpsException]) runDemo: getTemperatureZAndFlagUnhandled: Scenario.GPSError // repl.MdocSession$MdocApp$GpsException {{TODO show catchSome}} Note: The following is copy&pasted and needs work Unions AKA Sum Types AKA Enums AKA Ors Note - Avoid official terminology in most prose. Just say “And”/”Or” where appropriate. Scala 3 automatically aggregates the error types by synthesizing an anonymous sum type from the combined errors. Functions usually transform the Answer from one type to another type. Errors often aggregate. trait Error1 trait Error2 def failableFunction() : ZIO[Any, Error1 | Error2, Unit] = ??? Consider 2 error types Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures 78 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 graph TD; UserNotFound-->UserNotFound_OR_PermissionError; PermissionError-->UserNotFound_OR_PermissionError; UserNotFound-->Nothing; PermissionError-->Nothing; Often, you do not care that Nothing is involved at all. The mental model can be simply: graph TD; UserNotFound-->UserNotFound_OR_PermissionError; PermissionError-->UserNotFound_OR_PermissionError; case class UserService() trait User trait SuperUser def getUser( userId: String ): ZIO[UserService, UserNotFound, User] = ??? def getSuperUser( user: User ): ZIO[UserService, PermissionError, SuperUser] = ??? Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures def loginSuperUser(userId: String): ZIO[ UserService, UserNotFound | PermissionError, SuperUser ] = defer: val basicUser = getUser(userId).run getSuperUser(basicUser).run trait Status trait NetworkService def statusOf( user: User ): ZIO[NetworkService, UserNotFound, Status] = ??? def check(userId: String): ZIO[ UserService & NetworkService, UserNotFound, Status ] = defer: val user = getUser: userId .run statusOf: user .run Edit This Chapter Edit This Chapter¹⁶ ¹⁶https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/12_Errors.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 79 Concurrency High Level 1. forEachPar, collectAllPar TODO Prose scala def sleepThenPrint( d: Duration ): ZIO[Any, java.io.IOException, Duration] = defer: ZIO.sleep(d).run ZIO.debug(s"${d.render} elapsed").run d runDemo: ZIO.foreach(Seq(2, 1)): i => sleepThenPrint(i.seconds) // List(PT2S, PT1S) runDemo: ZIO.foreachPar(Seq(2, 1)): i => sleepThenPrint(i.seconds) // List(PT2S, PT1S) runDemo: defer: val durations = ZIO .collectAllPar: Seq( sleepThenPrint(2.seconds), sleepThenPrint(1.seconds) ) .run val total = durations.fold(Duration.Zero)(_ + _).render Console .printLine: total .run // () Concurrency High Level def slowFailableRandom(duration: Duration) = defer: val randInt = Random.nextIntBetween(0, 100).run ZIO.sleep(duration).run ZIO .when(randInt < 10)( ZIO.fail("Number is too low") ) .run duration // Massive example runDemo: defer: val durations = ZIO .collectAllSuccessesPar: Seq .fill(1_000)(1.seconds) .map(duration => slowFailableRandom(duration) ) .run durations.fold(Duration.Zero)(_ + _).render // 14 m 59 s zipPar, zipWithPar validateWithPar? withParallelism Edit This Chapter Edit This Chapter¹⁷ ¹⁷https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/13_Concurrency_High_Level.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 81 Concurrency Low Level 1. Fork join 2. Throwaway reference to STM TODO Prose scala def sleepThenPrint( d: Duration ): ZIO[Any, java.io.IOException, Duration] = defer { ZIO.sleep(d).run println(s"${d.render} elapsed") d } runDemo( defer { val f1 = sleepThenPrint(2.seconds).fork.run val f2 = sleepThenPrint(1.seconds).fork.run f1.join.run f2.join.run } ) // 1 s elapsed // 2 s elapsed // PT1S Edit This Chapter Edit This Chapter¹⁸ ¹⁸https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/14_Concurrency_Low_Level.md Concurrency Interruption Why Interruption Is Necessary Throughout the Stack In order for the Runtime to operate and provide the super powers of ZIO, it needs to be able to interrupt running workflows without resource leaks. Timeout ## Race .withFinalizer ## .zipWithPar ## .aquireRelease effects are uninterruptable ## .fromFutureInterrupt Uninterruptable // This is duplicate code def sleepThenPrint( d: Duration ): ZIO[Any, java.io.IOException, Duration] = defer: ZIO .sleep: d .run println: s"${d.render} elapsed" d Concurrency Interruption runDemo: sleepThenPrint: 2.seconds .race: sleepThenPrint: 1.seconds // 1 s elapsed // PT1S Future Cancellation We show that Future’s are killed with finalizers that never run import scala.concurrent.Future runDemo: ZIO .fromFuture: Future: try println: "Starting operation" Thread.sleep: 500 println: "Ending operation" finally println: "Cleanup" .timeout: 25.millis // Starting operation // None Edit This Chapter Edit This Chapter¹⁹ ¹⁹https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/15_Concurrency_Interruption.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 84 Concurrency State 1. Ref 2. Thundering Herds Functional programmers often sing the praises of immutability. The advantages are real and numerous. However, it is easy to find situations that are intrinsically mutable. • How many people are currently inside a building? • How much fuel is in your car? • How much money is in your bank account? Rather than avoiding mutability entirely, we want to avoid unprincipled, unsafe mutability. If we codify and enumerate everything that we need from Mutability, then we can wield it safely. Required Operations: • Update the value • Read the current value These are both effectful operations. trait RefZ[A]: def get: ZIO[Any, Nothing, A] def update(a: A => A): ZIO[Any, Nothing, Unit] 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 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: Concurrency State 86 object RefZ: def make[A](a: A): ZIO[Any, Nothing, RefZ[A]] = ??? Unreliable Counting val unreliableCounting = var counter = 0 val increment = ZIO.succeed: counter = counter + 1 defer: ZIO .foreachParDiscard(Range(0, 100000)): _ => increment .run // It's not obvious to the reader why // we need to wrap counter in .succeed "Final count: " + ZIO.succeed(counter).run runDemo: unreliableCounting // Final count: 98239 Due to the unpredictable nature of shared mutable state, we do not know exactly what the final count above is. Each time we publish a copy of this book, the code is reexecuted and a different wrong result is generated. However, conflicts are extremely likely, so some of our writes get clobbered by others, and we end up with less than the expected 100,000. Ultimately, we lose information with this approach. TODO Consider making a diagram parallel writes Performing our side effects inside ZIO’s does not magically make them safe. We need to fully embrace the ZIO components, utilizing Ref for correct mutation. Reliable Counting Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Concurrency State 87 lazy val reliableCounting = def incrementCounter(counter: Ref[Int]) = counter.update: _ + 1 defer: val counter = Ref.make(0).run ZIO .foreachParDiscard(Range(0, 100000)): _ => incrementCounter: counter .run "Final count: " + counter.get.run runDemo: reliableCounting // Final count: 100000 Now we can say with full confidence that our final count is 100000. Additionally, these updates happen without blocking. This is achieved through a strategy called “Compare & Swap”, which we will not cover in detail. TODO Link/reference supplemental reading Although there are significant advantages; a basic Ref is not the solution for everything. We can only pass pure functions into update. The API of the plain Atomic Ref steers you in the right direction by not accepting ZIOs as parameters to any of its methods. To demonstrate why this restriction exists, we will deliberately undermine the system by sneaking in a side effect. First, we will create a helper function that imitates a long-running calculation. def expensiveCalculation() = Thread.sleep: 35 Our side effect will be a mock alert that is sent anytime our count is updated: scala def sendNotification() = println: "Alert: updating count!" Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Concurrency State 88 def update(counter: Ref[Int]) = counter.update: previousValue => expensiveCalculation() sendNotification() previousValue + 1 runDemo: defer: val counter = Ref.make(0).run ZIO .foreachParDiscard(Range(0, 4)): _ => update(counter) .run "Final count: " + counter.get.run // Alert: updating count! // Alert: updating count! // Alert: updating count! // Alert: updating count! // Alert: updating count! // Alert: updating count! // Alert: updating count! // Alert: updating count! // Alert: updating count! // Alert: updating count! // Final count: 4 What is going on?! Previously, we were losing updates because of unsafe mutability. Now, we have the opposite problem! We are sending far more alerts than intended, even though we can see that our final count is 4. TODO This section will need significant attention and polish Now we must consider the limitations of the “Compare & Swap” system. It achieves lock-free performance by letting each fiber freely make their updates, and then doing a last-second check to see if the underlying value changed during its update. If the value has not changed, the update is made. If it has changed, then the entire function that was passed into update is re-executed until it completes with a stable value. The higher the parallelism, or the longer the operation takes, the higher the likelihood of a compare-and-swap retry. This retry behavior is safe with pure functions, which can be executed an arbitrary number of times. However, it is completely inappropriate for effects, which should Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Concurrency State 89 only be executed a single time. For these situations, we need a specialized variation of Ref Ref.Synchronized Ref.Synchronized guarantees only a single execution of the update body and any of the effects contained inside. The only change required is replacing Ref.make with Ref.Synchronized.make val sideEffectingUpdatesSync = defer: val counter = Ref.Synchronized.make(0).run ZIO .foreachParDiscard(Range(0, 4)): _ => counter.update: previousValue => expensiveCalculation() sendNotification() previousValue + 1 .run "Final count: " + counter.get.run runDemo: sideEffectingUpdatesSync // Alert: updating count! // Alert: updating count! // Alert: updating count! // Alert: updating count! // Final count: 4 Now we see exactly the number of alerts that we expected. This correctness comes with a cost though, as the name of this type implies. Each of your updates will run sequentially, despite initially launching them all in parallel. This is the only known way to avoid retries. Try to structure your code to minimize the coupling between effects and updates, and use this type only when necessary. Edit This Chapter Edit This Chapter²⁰ ²⁰https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/16_Concurrency_State.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Repeats ## Number of occurances ## Spacing/timing of repeats ## Until a condition is met ## While a condition is met ## Until a failure occurs Edit This Chapter Edit This Chapter²¹ ²¹https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/17_Repeats.md Resources 1. Open / Close around an Effect 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. Edit This Chapter Edit This Chapter²² ²²https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/18_Resources.md Logging Edit This Chapter Edit This Chapter²³ 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/logging/Logging.scala package logging import zio.logging.* import zio.logging.LogFormat.{ label, line, quoted, text } object Logging extends ZIOAppDefault: lazy val minimal: LogFormat = label("message", quoted(line)).highlight lazy val locationLogger: LogFormat = location(new WackyGps) |-| label("message", quoted(line)).highlight ²³https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/19_Logging.md 93 Logging lazy val coloredLogger = Runtime.removeDefaultLoggers >>> consoleLogger( ConsoleLoggerConfig( // LogFormat.colored locationLogger, LogFilter.logLevel(LogLevel.Info) ) ) def run = ZIO.log("Hi").provide(coloredLogger) 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 .apply( scala .util .Random .between(0, Continent.values.length) ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Configuration Changing things based on the running environment. CLI Params Config Files Environment Variables ZIO Config 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: case class Hotel(name: String) case class Error(msg: String) To augment the built-in environment function, we will create a wrapper. Configuration 95 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: 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(HotelApi()) // res0: Either[Error, Hotel] = Right( // value = Hotel(name = "Eddy's Roach Motel") // ) Collaborator’s Machine: fancyLodgingUnsafe(HotelApi()) // res2: Either[Error, Hotel] = Left( // value = Error(msg = "Invalid API Key") // ) Continuous Integration Server: Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Configuration 96 fancyLodgingUnsafe(HotelApi()) // 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. Building a Better Way ZIO has a full System implementation of, but we will consider just 1 function for the moment. def envZ( variable: String ): ZIO[Any, Nothing, Option[String]] = ZIO.succeed(sys.env.get("API_KEY")) This merely wraps our original function call. This is safe, but it is not the easiest code to use or read. We want to convert an empty Option into an error state. object SystemStrict: val live: ZLayer[Any, Nothing, SystemStrict] = ZLayer.fromZIO( defer { SystemStrict() } ) case class SystemStrict(): def envRequired( variable: String ): ZIO[Any, Error, String] = defer { val variableAttempt = envZ(variable).run ZIO Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Configuration .fromOption(variableAttempt) .mapError(_ => Error("Missing value for: " + variable) ) .run } Similarly, we wrap our API in one that leverages ZIO. case class HotelApiZ( system: SystemStrict, hotelApi: HotelApi ): def cheapest( zipCode: String ): ZIO[Any, Error, Hotel] = defer { val apiKey = system.envRequired("API_KEY").run ZIO .fromEither( hotelApi.cheapest(zipCode, apiKey) ) .run } object HotelApiZ: val live: ZLayer[ SystemStrict with HotelApi, Nothing, HotelApiZ ] = ZLayer.fromZIO( defer { HotelApiZ( ZIO.service[SystemStrict].run, ZIO.service[HotelApi].run ) } ) This helps us keep a flat Error channel when we write our domain logic. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 97 Configuration 98 This was quite a process; where did it get us? Our fully ZIO-centric, side-effect-free logic looks like this: // TODO This produces large, wide output that does not adhere to the wi\ dth of the page. // TODO This has fallen out of sync with the "identical" code below // TODO Make this a case class? def fancyLodging( hotelApiZ: HotelApiZ ): ZIO[Any, Error, Hotel] = hotelApiZ.cheapest("90210") Original, unsafe: def fancyLodgingUnsafe( hotelApi: HotelApi ): Either[Error, Hotel] = hotelApi.cheapest("90210") // error: // missing argument for parameter apiKey of method cheapest in class Ho\ telApi: (zipCode: String, apiKey: String): // Either[repl.MdocSession.MdocApp.Error, repl.MdocSession.MdocApp.Ho\ tel] The logic is identical to our original implementation! The only difference is the result type. This is what it looks like in action: Your Machine: // TODO Do this for CI environment too // TODO "originalAuthor" Don't know why it's called that? val originalAuthor = HotelApiZ.live Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Configuration 99 val logic = defer { fancyLodging(ZIO.service[HotelApiZ].run) } // logic: ZIO[HotelApiZ, Nothing, ZIO[Any, Error, Hotel]] = OnSuccess( // trace = "zio.direct.ZioMonad.Success.$anon.map(ZioMonad.scala:18)", // first = OnSuccess( // trace = "repl.MdocSession.MdocApp.<local MdocApp>.logic(20_Confi\ guration.md:233)", // first = Sync( // trace = "repl.MdocSession.MdocApp.<local MdocApp>.logic(20_Con\ figuration.md:233)", // eval = zio.ZIOCompanionVersionSpecific$$Lambda$17027/0x0000000\ 804296440@3f8ce416 // ), // successK = zio.ZIO$$$Lambda$17035/0x00000008042a8040@3724614b // ), // successK = zio.ZIO$$Lambda$17083/0x000000080430f040@1e8e292a // ) runDemo( logic.provide( SystemStrict.live, ZLayer.succeed(HotelApi()), originalAuthor ) ) // OnSuccess(zio.direct.ZioMonad.Success.$anon.fl Collaborator’s Machine: // TODO Do this for CI environment too val collaborater = HotelApiZ.live val colaboraterLayer = collaborater Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Configuration 100 runDemo( defer { fancyLodging(ZIO.service[HotelApiZ].run) }.provide( SystemStrict.live, ZLayer.succeed(HotelApi()), collaborater ) ) // OnSuccess(zio.direct.ZioMonad.Success.$anon.fl Continuous Integration Server: val ci = HotelApiZ.live runDemo( defer { fancyLodging(ZIO.service[HotelApiZ].run) }.provide( SystemStrict.live, ZLayer.succeed(HotelApi()), ci ) ) // OnSuccess(zio.direct.ZioMonad.Success.$anon.fl 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. TODO{{Reorder things so that the official ZIO TestSystem is used.}} Official ZIO Approach ZIO provides a more complete System API in the zio.System. This is always available as a standard service from the ZIO runtime. TODO Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Configuration def fancyLodgingBuiltIn( hotelApiZ: HotelApiZ ): ZIO[Any, SecurityException | Error, Hotel] = defer { val apiKey = zio.System.env("API_KEY").run hotelApiZ .cheapest( apiKey.get // unsafe! TODO Use either ) .run } Exercises import zio.test.TestSystem import zio.test.TestSystem.Data // TODO Use real tests once Scala3 & ZIO2 are // updated 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 101 Configuration object Exercise1Solution extends Exercise1: def envOrFail(variable: String): ZIO[ zio.System, SecurityException | NoSuchElementException, String ] = // TODO Direct instead of flatmap zio .System .env(variable) .flatMap( _.fold( ZIO.fail(new NoSuchElementException()) )(ZIO.succeed(_)) ) import zio.test.* runSpec( defer { val res = Exercise1Solution .envOrFail("key") .provide( TestSystem.live( Data(envs = Map("key" -> "value")) ) ) .run assertTrue(res == "value") } ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 102 Configuration import zio.test.* runSpec( defer { val res = Exercise1Solution .envOrFail("key") .catchSome { case _: NoSuchElementException => ZIO.succeed("Expected Error") } .provide( TestSystem.live(Data(envs = Map())) ) .run assertTrue(res == "Expected Error") } ) Exercise 2: Create a function will attempt to parse a value as an Integer and report errors as a NumberFormatException. trait Exercise2: def envInt(variable: String): ZIO[ Any, NoSuchElementException | NumberFormatException, Int ] = ??? Edit This Chapter Edit This Chapter²⁴ ²⁴https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/20_Configuration.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 103 Streams If you want to get all items in a defined range, eg July 1st - July 3rd, then you might not need a Stream. However, if you want to get all items in a range that is not bounded, eg From July 1st onward, then you need a Stream. UI Interactions UI events are a great use-case for streams. When you present a UI to a user, it is impossible to know how they will interact with it. They might click a few buttons, and finish in a few minutes. They might never interact with it at all. They might leave the page open for days, only occasionally interacting with it. It is impossible to run a function at any point in time that returns a List[Event] that represents all interactions, because it is an unbounded, ongoing concept. In addition, it’s unlikely that you want to hold all of UI events at one time. It is enough to process them individually, or in small batches, as they occur. Trend Recognition Possible Scenarios: - Fraud Prevention - Self-harm prevention - Anti-terrorism Surge pricing / smoothing - Disease spread tracking Edit This Chapter Edit This Chapter²⁵ ²⁵https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/21_Streams.md 105 Streams 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/streams/Alphabet.scala package streams import zio.stream.* object Alphabet1 extends ZIOAppDefault: override def run = ZStream .fromIterable('a' to 'z') .debug .runDrain object Alphabet2 extends ZIOAppDefault: override def run = ZStream .fromIterable('a' to 'z') .forever .debug .runDrain object Alphabet3 extends ZIOAppDefault: override def run = ZStream .fromIterable('a' to 'z') .mapZIO { c => defer { val d = Random.nextIntBounded(5).run ZIO.sleep(d.seconds).run ZIO.debug(c).run }.fork Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 106 Streams } .runDrain // exits before all forks are completed object Alphabet4 extends ZIOAppDefault: override def run = ZStream .fromIterable('a' to 'z') .schedule( Schedule.fixed(1.second).jittered ) .aggregateAsyncWithin( ZSink.collectAll, Schedule.fixed(3.seconds) ) .debug("Elements in past 3 seconds") .map(_.length) .debug("Rate per 3 seconds") .runDrain // doesn't chunk into time-oriented groups as we'd expect object Alphabet5 extends ZIOAppDefault: override def run = ZStream .fromIterable('a' to 'z') .schedule(Schedule.spaced(10.millis)) .throttleShape(1, 1.second) { chunk => println(chunk) 1 } .debug .runDrain // grouping as many items as can fit in one second, with a cap of 1000 // Note: Int.MaxValue causes OOM object Alphabet6 extends ZIOAppDefault: def run = ZStream .fromIterable('a' to 'z') .schedule(Schedule.spaced(100.millis)) .groupedWithin(1000, 1.second) .debug .runDrain Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 107 Streams experiments/src/main/scala/streams/CommitStream.scala package streams import zio.stream.* trait CommitStream: def commits: Stream[Nothing, Commit] case class Commit( project: Project, author: Author, message: String, added: Int, removed: Int ) object CommitStream: object Live extends CommitStream: def commits: Stream[Nothing, Commit] = ZStream.repeatZIO(randomCommit) private val randomCommit = defer { val author = Author.random.run val project = Project.random.run val message = Message.random.run val linesAdded = Random.nextIntBounded(500).run val linesRemoved = Random.nextIntBounded(500).run Commit( project, author, message, linesAdded, -linesRemoved ) } end CommitStream Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 108 Streams object Message: private val generic = List( "Refactor code", "Add documentation", "Update dependencies", "Format code", "Fix bug", "Add feature", "Add tests", "Remove unused code" ) def random: ZIO[Any, Nothing, String] = randomElementFrom(generic) case class Project( name: String, language: Language ) object Project: private val entries = List( Project("ZIO", Language.Scala), Project("Tapir", Language.Scala), Project("Kafka", Language.Java), Project("Flask", Language.Python), Project("Linux", Language.C) ) val random: ZIO[Any, Nothing, Project] = randomElementFrom(entries) enum Language: case Scala, Java, C, CPlusPlus, Go, Rust, Python, Unison, Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 109 Streams Ruby enum Author: case Kit, Adam, Bruce, James, Bill object Author: val random: ZIO[Any, Nothing, Author] = randomElementFrom(Author.values.toList) experiments/src/main/scala/streams/Counter.scala package streams import zio.{Ref, ZIO} case class Counter(count: Ref[Int]): val get: ZIO[Any, Nothing, Int] = count.getAndUpdate(_ + 1) object Counter: val make = Ref.make(0).map(Counter(_)) experiments/src/main/scala/streams/DataFountain.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 110 Streams package streams case class DataFountain( tweets: TweetStream, commitStream: CommitStream, httpRequestStream: HttpRequestStream, rate: Schedule[Any, Nothing, Long] = Schedule.spaced(1.second) ): def withRate(newValue: Int) = copy(rate = Schedule .spaced(1.second.dividedBy(newValue)) ) object DataFountain: def userFriendlyConstructor(rate: Int) = DataFountain( TweetStream.Live, CommitStream.Live, HttpRequestStream.Live, Schedule.spaced(1.second.dividedBy(rate)) ) val live = DataFountain( TweetStream.Live, CommitStream.Live, HttpRequestStream.Live ) // // TODO More throttle investigation tweets.throttleEnforce(1, 1.second, 1)(_.length) experiments/src/main/scala/streams/DeliveryCenter.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 111 Streams package streams import zio.stream.* case class Order() /** Possible stages to demo: * 1. Ship individual orders as they come 2. * Queue up multiple items and then send 3. * Ship partially-filled truck if it has * been waiting too long */ object DeliveryCenter extends ZIOAppDefault: sealed trait Truck case class TruckInUse( queued: List[Order], fuse: Promise[Nothing, Unit], capacity: Int = 3 ) extends Truck: val isFull: Boolean = queued.length == capacity val waitingTooLong = fuse.isDone.map(done => !done) def handle( order: Order, staged: Ref[Option[TruckInUse]] ) = def shipIt(reason: String) = defer: ZIO .debug(reason + " Ship the orders!") .run staged .get .flatMap(_.get.fuse.succeed(())) .run staged.set(None).run val loadTruck = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 112 Streams defer { val latch = Promise.make[Nothing, Unit].run val truck = staged .updateAndGet(truck => truck match case Some(t) => Some( t.copy(queued = t.queued :+ order ) ) case None => Some( TruckInUse( List(order), latch ) ) ) .map(_.get) .run ZIO .debug( "Loading order: " + truck.queued.length + "/" + truck.capacity ) .run truck } def shipIfWaitingTooLong(truck: TruckInUse) = ZIO .whenZIO(truck.waitingTooLong)( shipIt(reason = "Truck has bit sitting half-full too long." ) ) .delay(4.seconds) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 113 Streams defer { val truck = loadTruck.run if (truck.isFull) shipIt(reason = "Truck is full.").run else ZIO .when(truck.queued.length == 1)( ZIO.debug("Adding timeout daemon") *> shipIfWaitingTooLong(truck) ) .forkDaemon .run } end handle def run = defer { val stagedItems = Ref.make[Option[TruckInUse]](None).run val orderStream = ZStream.repeatWithSchedule( Order(), Schedule .exponential(1.second, factor = 1.8) ) orderStream .foreach(handle(_, stagedItems)) .timeout(12.seconds) .run } end DeliveryCenter experiments/src/main/scala/streams/DemoDataFountain.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 114 Streams package streams import zio.* import zio.stream.* def specifyStream[A]( f: DataFountain => Stream[Nothing, A] ) = f(DataFountain.live) .take(10) .foreach(ZIO.debug(_)) object DemoDataFountainTweets extends ZIOAppDefault: def run = specifyStream: _.tweets.tweets object DemoDataFountainHttpRequests extends ZIOAppDefault: def run = specifyStream: _.httpRequestStream.requests object DemoDataFountainCommits extends ZIOAppDefault: def run = specifyStream: _.commitStream.commits object RecognizeBurstOfBadRequests extends ZIOAppDefault: def run = DataFountain .live .httpRequestStream .requests .groupedWithin(10, 1.second) .debug .foreach(requests => ZIO.when( requests Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 115 Streams .filter: r => r.response == Code.Forbidden .length > 2 )(ZIO.debug("Too many bad requests")) ) .timeout: 5.seconds end RecognizeBurstOfBadRequests experiments/src/main/scala/streams/HelloStreams.scala package streams import zio.stream.* object HelloStreams extends ZIOAppDefault: def run = for _ <- ZIO.debug("Stream stuff!") greetingStream = ZStream.repeatWithSchedule( "Hi", Schedule.spaced(1.seconds) ) insultStream = ZStream.repeatWithSchedule( "Dummy", Schedule.spaced(2.seconds) ) combinedStream = ZStream.mergeAllUnbounded()( greetingStream, insultStream ) aFewElements = combinedStream.take(6) res <- aFewElements.runCollect _ <- ZIO.debug("Res: " + res) yield () end HelloStreams Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 116 Streams experiments/src/main/scala/streams/HttpRequestStream.scala package streams import zio.stream.* case class Request(response: Code, path: Path) trait HttpRequestStream: def requests: Stream[Nothing, Request] object HttpRequestStream: object Live extends HttpRequestStream: override def requests : Stream[Nothing, Request] = ZStream .repeatZIO(randomRequest) .schedule(Schedule.spaced(100.millis)) private val randomRequest = defer { val code = Code.random.run val path = Path.random.run Request(code, path) } enum Code: case Ok, BadRequest, Forbidden object Code: val random = randomElementFrom(Code.values.toList) case class Path(segments: Seq[String]): override def toString: String = segments.mkString("/") object Path: val random: ZIO[Any, Nothing, Path] = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 117 Streams defer { val generator = randomElementFrom(Random.generators).run generator.run } def apply(first: String, rest: String*): Path = Path(Seq(first) ++ rest) private object Random: private val generic : ZIO[Any, Nothing, Path] = val genericPaths = List( "login", "preferences", "settings", "home", "latest", "logout" ) defer { val section = randomElementFrom(genericPaths).run Path(s"/$section") } private val user: ZIO[Any, Nothing, Path] = val userSections = List( "activity", "status", "collaborators" ) defer { val userId = zio.Random.nextIntBounded(1000).run val section = randomElementFrom(userSections).run Path(s"/user/$userId/$section") Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 118 Streams } val generators : List[ZIO[Any, Nothing, Path]] = List(generic, user) end Random end Path private[streams] def randomElementFrom[T]( collection: List[T] ): ZIO[Any, Nothing, T] = for idx <Random.nextIntBounded(collection.length) yield collection(idx) experiments/src/main/scala/streams/MultipleConcurrentStrea package streams import zio.stream.* import java.io.File object MultipleConcurrentStreams extends ZIOAppDefault: val userActions = ZStream( "login", "post:I feel happy", "post: I want to buy something!", "updateAccount", "logout", "post:I want to buy something expensive" ).mapZIO(action => ZIO.succeed(action).delay(1.seconds) ) // .throttleShape(1, 1.seconds, 2)(_.length) // Note: I tried to bake this into the mapZIO Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 119 Streams // call above, but that resulted in additional // printing // for every consumer of the stream. // Surprising, but I'm sure there's good // reasoning behind it. val userActionAnnouncements = userActions.mapZIO(action => ZIO.debug("Incoming event: " + action) ) val actionBytes: ZStream[Any, Nothing, Byte] = userActions.flatMap(action => ZStream .fromIterable((action + "\n").getBytes) ) val filePipeline : ZPipeline[Any, Throwable, Byte, Long] = ZPipeline.fromSink( ZSink.fromFile(new File("target/output")) ) val writeActionsToFile = actionBytes >>> filePipeline val marketingData = userActions .filter(action => action.contains("buy")) val marketingActions = marketingData.mapZIO(marketingDataPoint => ZIO.debug( " $$ info: " + marketingDataPoint ) ) val accountAuthentication = userActions.filter(action => action == "login" || action == "logout" ) val auditingReport = accountAuthentication.mapZIO(event => ZIO.debug(" Security info: " + event) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 120 Streams ) def run = ZStream .mergeAllUnbounded()( userActionAnnouncements, marketingActions, auditingReport, writeActionsToFile ) .runDrain end MultipleConcurrentStreams experiments/src/main/scala/streams/Scanning.scala package streams import zio.* import zio.stream.* object Scanning extends ZIOAppDefault: enum GdpDirection: case GROWING, SHRINKING enum EconomicStatus: case GOOD_TIMES, RECESSION import EconomicStatus.* import GdpDirection.* case class EconomicHistory( quarters: Seq[GdpDirection], economicStatus: EconomicStatus ) object EconomicHistory: def apply( quarters: Seq[GdpDirection] ): EconomicHistory = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 121 Streams EconomicHistory( quarters, if ( quarters .sliding(2) .toList .lastOption .contains(List(SHRINKING, SHRINKING)) ) RECESSION else GOOD_TIMES ) val gdps = ZStream( GROWING, SHRINKING, GROWING, SHRINKING, SHRINKING ) val economicSnapshots = gdps.scan(EconomicHistory(List.empty))( (history, gdp) => EconomicHistory(history.quarters :+ gdp) ) def run = economicSnapshots.runForeach(snapShot => ZIO.debug(snapShot) ) end Scanning experiments/src/main/scala/streams/TweetFactory.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 122 Streams package streams case class TweetFactory(counter: Counter): val randomTweet : ZIO[Any, Nothing, SimpleTweet] = defer { val subject = TweetFactory.randomSubject.run val adjective = TweetFactory.randomAdjective.run val id = counter.get.run SimpleTweet( id, s"$subject is the $adjective thing ever!" ) } private object TweetFactory: val make: ZIO[Any, Nothing, TweetFactory] = Counter.make.map(TweetFactory(_)) val superlatives = List("best", "greatest", "most awesome") val derogatory = List("worst", "most terrible", "most awful") val allAdjectives = superlatives ++ derogatory val allSubjects = List( "Ice cream", "The sunrise", "Rain", "ZIO", "PHP", "Skiing", "Music" ) val randomAdjective = defer { val index = Random Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 123 Streams .nextIntBounded(allAdjectives.size) .run allAdjectives(index) } val randomSubject = defer { val index = Random .nextIntBounded(allSubjects.size) .run allSubjects(index) } end TweetFactory experiments/src/main/scala/streams/TweetStream.scala package streams import zio.stream.* case class SimpleTweet(id: Int, text: String) trait TweetStream: def tweets: Stream[Nothing, SimpleTweet] val slowTweetStream: Stream[ Nothing, SimpleTweet ] object TweetStream: object Live extends TweetStream: private val tweetService = ZLayer.fromZIO(TweetFactory.make) private val tweetsPerSecond = 6000 private val tweetRate = Schedule.spaced( 1.second.dividedBy(tweetsPerSecond) ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 124 Streams val tweets : ZStream[Any, Nothing, SimpleTweet] = ZStream .repeatZIO( ZIO.serviceWithZIO[TweetFactory]( _.randomTweet ) ) .schedule(tweetRate) .provideLayer(tweetService) val slowTweetStream = tweets.throttleShape(1, 1.second)(_.length) end Live end TweetStream experiments/src/main/scala/streams/TwitterCustomerSupport package streams import zio.stream.* import java.nio.file.{Files, Paths} // This currently runs against the dataset available here: // https://www.kaggle.com/datasets/thoughtvector/customer-support-on-tw\ itter?resource=download object TwitterCustomerSupport extends ZIOAppDefault: val fileName = // "../datasets/sample.csv" // "../datasets/twcs/twcs.csv" // "small" "medium" // "twcs_tiny.csv" def isHappy(tweet: Tweet): Boolean = List( "fantastic", Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 125 Streams "awesome", "great", "wonderful" ).exists(tweet.text.toLowerCase.contains(_)) def isAngry(tweet: Tweet): Boolean = List("stupid", "dumb", "idiot", "shit") .exists(tweet.text.toLowerCase.contains(_)) def trackActiveCompanies( tweets: ZStream[Any, Throwable, Tweet] ) = defer { val activeCompanies = Ref.make[Map[String, Int]](Map.empty).run val mostActiveCompanyAtEachMoment = tweets.mapZIO(tweet => defer { val companies = activeCompanies .updateAndGet( incrementCompanyActivity( _, tweet ) ) .run companies .map(x => x) .toList .sortBy(x => -x._2) } ) val res = mostActiveCompanyAtEachMoment.runLast.run res.get } end trackActiveCompanies override def run = defer { val dataset = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 126 Streams ZIOAppArgs .getArgs .map(_.headOption.getOrElse(fileName)) .run val tweets = ZStream .fromJavaStream( Files.lines( Paths.get( // "..", "datasets", "twcs", dataset + ".csv" ) ) ) .map(l => Tweet(l)) .filter(_.isRight) .map(_.getOrElse(???)) val happyTweetFilter: ZPipeline[ Any, Nothing, Tweet, Tweet ] = ZPipeline.filter(isHappy) val angryTweetFilter: ZPipeline[ Any, Nothing, Tweet, Tweet ] = ZPipeline.filter(isAngry) (tweets >>> happyTweetFilter) .runCount .debug("Number of happy tweets") .run (tweets >>> angryTweetFilter) .runCount .debug("Number of angry tweets") Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 127 Streams .run // gatherHappyTweets // .timed // .map(_._1) // .debug("Happy duration") <&> // gatherAngryTweets <&> trackActiveCompanies(tweets) .map(_.take(3).mkString(" : ")) .debug("ActiveCompanies") .timed .map(_._1) .debug("Active Company duration") .run } end run // .timeout(60.seconds) private def incrementCompanyActivity( value1: Map[String, Int], tweet: Tweet ): Map[String, Int] = value1.updatedWith(tweet.author_id) { case Some(value) => Some(value + 1) case None => Some(1) } case class ParsingError(msg: String) case class Tweet( tweet_id: String, author_id: String, inbound: Boolean, created_at: String, text: String, response_tweet_id: Option[String], in_response_to_tweet_id: Option[String] ) object Tweet: def apply( csvLine: String Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 128 Streams ): Either[ParsingError, Tweet] = val pieces = csvLine.split(",") Either.cond( pieces.length == 7, pieces match case Array( tweet_id, author_id, inbound, created_at, text, response_tweet_id, in_response_to_tweet_id ) => Tweet( tweet_id, author_id, inbound == "True", created_at, text, Some(response_tweet_id), Some(in_response_to_tweet_id) ) case _ => ??? , ParsingError("Bad value: " + pieces) ) end apply end Tweet end TwitterCustomerSupport Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Appendix: Significant Indentation We have taken a bit of a risk with the style used in this book. We are embracing significant indentation nearly to the max. These syntastic features were some of the most contentious changes from Scala 3 Motivations • Reduce the space taken up by small demos – Often, closing parens/braces would double the vertical space • Avoid getting lost scanning a long line Concessions / Acknowledgements • This style will be new/unfamiliar to many programmers - even seasoned Scala 2 users! • The syntax is not yet fully supported by all editors // TODO Confirm this • Sometimes this style actually increases the vertical space Rules / examples • Generally, when providing a block as an argument, use a colon and place the argument on the following line Appendix: Significant Indentation 130 def sendMessage(msg: String) = println("Sent: " + msg) sendMessage: val name = "Alice" val greeting = "Hello" s"$greeting, $name" // Sent: Hello, Alice However, when the function accepts: • Multiple parameters • Multiple lists of parameters We use parentheses to group/collect/etc the arguments multiply(7, 6) // res1: Int = 42 This is necessary, because the alternative would just be evaluated as one block. The first parameter is flagged as an unused value, and we only provide the 2nd parameter to the function. multiply: 7 6 // error: // A pure expression does nothing in statement position; you may be omi\ tting necessary parentheses // "Hello" // ^ Edge cases that are difficult to defend Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Appendix: Significant Indentation 131 runDemo: defer: ZIO.debug("Hello").run ZIO.debug("World").run // Hello // World // () VS runDemo: defer: ZIO .debug: "Hello" .run ZIO .debug: "World" .run // Hello // World // () Edit This Chapter Edit This Chapter²⁶ md ²⁶https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/22_Appendix_Significant_Indentation. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Appendix: ZIO Direct How does defer, etc relate to flatmaps, for comprehensions, etc We don’t need to talk about monads, etc as zio-direct will just be “the way” so we don’t need to explain why zio-direct or what it is. It just is. — We have been experimenting with the zio-direct style of writing ZIO applications. Our theory is that it is easier to teach this style of code to beginners. “Program as values” is a core concept when using ZIO. ZIOs are just unexecuted values until they are run. If not using zio-direct, you must explain many language details in order to write ZIO code. - If you want to sequence ZIO operations, you need to `flatmap` them - To avoid indentation mountain, you should use a `for comprehension` - How a `for` comprehension turns into `flatMap`s followed by a final `\ map` - If you want to create a `val` in the middle of this, you need to use \ a `=` instead of `<-` After you have accomplished all of that, you have trained your student to write concise, clean code… that only makes sense to those versed in this style. With zio-direct, you can write ZIO code that looks like imperative code. Here are the concepts you need to understand for zio-direct Appendix: ZIO Direct 133 - If you want to sequence ZIO operations, you need to write them inside\ of a `defer` block - Code in `defer` will be captured in a ZIO - Inside defer you can use `.run` to indicate when effects should be ex\ ecuted Note- `.run` calls are *only* allowed inside `defer` blocks. After you have accomplished that, you have trained your student to write slightly less concise code… that most programmers will be comfortable with. Gotchas -Something about mutable collection operations. TODO More info from James - Cannot end a defer block with a ZIO[_,_,Nothing] It currently fails with a very cryptic missing argument message Edit This Chapter Edit This Chapter²⁷ ²⁷https://github.com/EffectOrientedProgramming/book/edit/main/Chapters/23_Appendix_ZIO_Direct.md Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Experiments These experiments are not currently attached to a chapter, but are included for previewing. Before publication, we should not have any lingering experiments here. experiments-src-test-scala-Hubs experiments/src/test/scala/Hubs/QuizGameSpec.scala package Hubs import Hubs.QuizGame.cahootGame import zio.test.* object QuizGameSpec extends ZIOSpecDefault: val val val val frop zeb shtep cheep = = = = Player("Frop") Player("Zeb") Player("Shtep") Player("Cheep") val players: List[Player] = List(frop, zeb, shtep, cheep) def spec = suite("QuizGameSpec")( test("roundWithMultipleCorrectAnswers") { val roundWithMultipleCorrectAnswers = RoundDescription( Question( "What is the southern-most European country?", "Spain" ), Seq( Answer(zeb, "Germany", 2.seconds), Answer(frop, "Spain", 1.seconds), 135 Experiments Answer(cheep, "Spain", 3.seconds), Answer(shtep, "Spain", 4.seconds) ) ) val rounds = Seq(roundWithMultipleCorrectAnswers) defer { val results = QuizGame .cahootGame(rounds, players) .run assertTrue( results == List( RoundResults(List(frop, cheep)) ) ) } }, test("roundWithOnly1CorrectAnswer") { 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 rounds = Seq(roundWithOnly1CorrectAnswer) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 136 Experiments defer { val results = QuizGame .cahootGame(rounds, players) .run assertTrue( results == List(RoundResults(List(zeb))) ) } }, test("roundWhereEverybodyIsWrong") { val roundWhereEverybodyIsWrong = 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(roundWhereEverybodyIsWrong) defer { val results = QuizGame .cahootGame(rounds, players) .run assertTrue( results == List(RoundResults(List())) ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 137 Experiments } } ) @@ TestAspect.withLiveClock end QuizGameSpec Parallelism experiments/src/main/scala/Parallelism/Finalizers.scala package Parallelism import zio.Console.printLine import java.io.IOException 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 Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 138 Experiments .io .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 = bufferedSource.getLines 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 defer { val fileLines = readFileContents.run printLine(fileLines.mkString("\n")) .run // Combine the strings of the output vector into a singl\ e string, separated by \n } ioExample .catchAllDefect(exception => printLine( "Ultimate error message: " + exception.getMessage ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 139 Experiments ) .exitCode // Call the Zio with exitCode. end run end Finalizers experiments/src/main/scala/Parallelism/ParallelSleepers.scala package Parallelism val sleepNow = defer: ZIO .debug: "Yawn, going to sleep" .run ZIO .sleep: 1.seconds .run ZIO .debug: "Okay, I am awake!" .run import zio_helpers.timedSecondsDebug @main def quick = runDemo: defer: for _ <- 1 to 3 do sleepNow.run .timedSecondsDebug("Serial Sleepers") object SerialSleepers extends ZIOAppDefault: override def run = defer: for _ <- 1 to 3 do sleepNow.run .timedSecondsDebug("Serial Sleepers") Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 140 Experiments object ParallelSleepers extends ZIOAppDefault: override def run = defer(Use.withParallelEval): for _ <- 1 to 3 do sleepNow.run .timedSecondsDebug("AllSleepers") val sleepers = Seq( 1.seconds, 2.seconds, 3.seconds, 4.seconds, 5.seconds ) object ParallelSleepers2 extends ZIOAppDefault: override def run = ZIO .foreach(sleepers)(ZIO.sleep(_)) .timed .debug object ParallelSleepers3 extends ZIOAppDefault: override def run = ZIO .foreachPar(sleepers)(ZIO.sleep(_)) .timed .debug object ParallelSleepers4 extends ZIOAppDefault: override def run = val racers = sleepers.map(ZIO.sleep(_)) ZIO .raceAll(racers.head, racers.tail) .timed .debug object ParallelSleepers5 extends ZIOAppDefault: override def run = ZIO.withParallelism(2) { Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 141 Experiments ZIO .foreachPar(sleepers)(ZIO.sleep(_)) .timed .debug } experiments/src/main/scala/Parallelism/PrimeSeeker.scala package Parallelism object PrimeSeeker extends ZIOAppDefault: override def run = ZIO .foreachPar(1 to 16): _ => ZIO .succeed: crypto.nextPrimeAfter: 100_000_000 .debug: "Found prime:" .timed .debug: "Found a bunch of primes" experiments-src-test-scala-random experiments/src/test/scala/random/OfficialZioRandomSpec.sca Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 142 Experiments package random import zio.test.* object OfficialZioRandomSpec extends ZIOSpecDefault: def spec = suite("random")( test("hello random")( defer { // Note: Once the test exhausts these // values, it goes // back to true random values. TestRandom .feedInts( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ) .run val result = Random.nextIntBounded(1000).run assertTrue(result == 1) val result2 = Random.nextIntBounded(1000).run assertTrue(result2 == 2) } ), test( "rosencrants and guildenstern are dead" )( defer { val coinToss = defer { if (Random.nextBoolean.run) ZIO .debug("ROSENCRANTZ: Heads.") .run else ZIO .fail( "Tails encountered. Ending performance." ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 143 Experiments .run } TestRandom .feedBooleans(Seq.fill(100)(true)*) .run coinToss.repeatN(4).run ZIO .debug( "GUILDENSTERN: There is an art to the building up of susp\ ense." ) .run coinToss.run ZIO .debug( "GUILDENSTERN: Though it can be done by luck alone." ) .run coinToss.run assertCompletes } ), test("asdf")( defer: TestRandom.feedInts(1, 2).run val result1 = Random.nextInt.run val result2 = Random.nextInt.run // val result3 = Random.nextInt.run // // this falls back to system Random assertTrue( result1 == 1, result2 == 2 // result3 == 5 ) ), test("timeout"): val thingThatTakesTime = ZIO.sleep(2.seconds) defer: Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 144 Experiments val fork = thingThatTakesTime .timeout(1.second) .fork .run TestClock.adjust(2.seconds).run val result = fork.join.run assertTrue(result.isEmpty) // test("failure"): // assertTrue(Some("asdf") == None) ) end OfficialZioRandomSpec experiments/src/test/scala/random/RunEffectfulGuessingGame package random import zio.test.* import zio.internal.stacktracer.SourceLocation object RunEffectfulGuessingGameSpec extends ZIOSpecDefault: def spec = suite("GuessingGame")( suite("Effectful")( test("Untestable randomness")( defer { TestConsole .feedLines(Seq.fill(100)("3")*) .run val res = sideEffectingGuessingGame.run assertTrue(res == "You got it!") } ) @@ TestAspect .flaky, // Highlight that we shouldn't need this TestAspect. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 145 Experiments test("Testable")( defer { TestConsole.feedLines("3").run TestRandom.feedInts(3).run val res = effectfulGuessingGame.run assertTrue(res == "You got it!") } ) @@ TestAspect.nonFlaky(10) ) ) end RunEffectfulGuessingGameSpec Hubs experiments/src/main/scala/Hubs/BasicHub.scala package Hubs // 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) => defer { Hub.publish("Hub message").run val leftItem = left.take.run Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 146 Experiments Console .printLine( "Left item: " + leftItem ) .run val rightItem = right.take.run Console .printLine( "Right item: " + rightItem ) .run } } } } def run = logic1.exitCode end BasicHub experiments/src/main/scala/Hubs/QuizGame.scala package Hubs import zio.Console.printLine import java.io.IOException case class Player(name: String) case class Question( text: String, correctResponse: String ) case class Answer( player: Player, text: String, delay: Duration Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 147 Experiments ) case class RoundDescription( question: Question, answers: Seq[Answer] ) case class RoundResults( correctRespondents: List[Player] ) object QuizGame: // TODO Return result that can be tested def cahootGame( rounds: Seq[RoundDescription], players: List[Player] ) = defer { val questionHub = Hub.bounded[Question](1).run val answerHub: Hub[Answer] = Hub.bounded[Answer](players.size).run val ( questions: Dequeue[Question], answers: Dequeue[Answer] ) = questionHub .subscribe .zip(answerHub.subscribe) .run ZIO .foreach(rounds)(roundDescription => questionHub.publish( roundDescription.question ) *> playARound( roundDescription, questions, answerHub, answers ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 148 Experiments ) .run } private[Hubs] def playARound( roundDescription: RoundDescription, questions: Dequeue[Question], answerHub: Hub[Answer], answers: Dequeue[Answer] ): ZIO[Any, IOException, RoundResults] = defer { val correctRespondents = Ref.make[List[Player]](List.empty).run printLine( "Question for round: " + roundDescription.question.text ).run // TODO This should happen *before* // playARound is invoked questions.take.run 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 149 Experiments .timeout(4.second) .run RoundResults(correctRespondents.get.run) } private def untilWinnersAreFound( correctRespondents: Ref[List[Player]] ) = Schedule.recurUntilZIO(_ => correctRespondents.get.map(_.size == 2) ) private def submitAnswersAfterDelay( answerHub: Hub[Answer], answers: Seq[Answer] ) = ZIO.foreachParDiscard(answers) { answer => defer { ZIO.sleep(answer.delay).run answerHub.publish(answer).run } } private def recordCorrectAnswers( correctAnswer: String, answers: Dequeue[Answer], correctRespondents: Ref[List[Player]] ) = defer: val answer = answers.take.run val output = if (answer.text == correctAnswer) correctRespondents .update(_ :+ answer.player) .run "Correct response from: " + answer.player else "Incorrect response from: " + answer.player printLine(output).run Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 150 Experiments end QuizGame executing_external_programs experiments/src/main/scala/executing_external_programs/Gource.scala package executing_external_programs import zio.process.Command /* 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) = defer { val run1 = gource(repoDir).run.run ZIO.sleep(5.seconds).run run1.killForcibly.run Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 151 Experiments } def randomProjectActivity = defer { val idx = Random .nextIntBounded(projects.length) .run showActivityForAWhile(projects(idx)).run } def run = randomProjectActivity.repeatN(2) end GourceDemo experiments/src/main/scala/executing_external_programs/Say.scala package executing_external_programs import zio.process.Command def say(message: String) = Command("say", message) object SayDemo extends ZIOAppDefault: def run = say("Hello, world!").run experiments-src-test-scala-running_effects experiments/src/test/scala/running_effects/ExampleConsoleSpec.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 152 Experiments package running_effects import zio.test.* object FeedLinesDemo extends ZIOSpecDefault: def spec = test("feedSomeLines"): defer: TestConsole.feedLines("Zep").run assertCompletes object ExampleConsoleSpec extends ZIOSpecDefault: val promptForUsername = ZIO.succeed("Zeb") def notificationFor(username: String) = ZIO.succeed("Meeting @ 9") def spec = test("console IO"): defer: TestConsole.feedLines("Zeb").run val username = Console .readLine("Enter your name\n") .run Console.printLine(s"Hello $username").run val notification = notificationFor(username).run Console.printLine(notification).run val capturedOutput: Vector[String] = TestConsole.output.run val expectedOutput = s"""|Enter your name Hello Zeb Meeting @ 9 """.stripMargin assertTrue( capturedOutput.mkString("") == expectedOutput ) end ExampleConsoleSpec Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 153 Experiments hello_failures experiments/src/main/scala/hello_failures/KeepSuccesses.scala package hello_failures import zio.Console.printLine 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 moreStructuredLogic = defer: val results = ZIO .partition(allCalls)( fastUnreliableNetworkCall ) .run results match case (failures, successes) => defer: printFailures(failures).run val recoveries = attemptFallbackFor(failures).run successes ++ recoveries .debug: "All successes" .run def run = moreStructuredLogic Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 154 Experiments private def printFailures( failures: Iterable[BadResponse] ) = ZIO.foreach(failures): e => printLine: "Error: " + e + ". Should retry on other server." private def attemptFallbackFor( failures: Iterable[BadResponse] ) = ZIO.collectAllSuccesses: failures.map: failure => slowMoreReliableNetworkCall: failure.payload .tapError: e => printLine: "Giving up on: " + e 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 Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 155 Experiments scenarios experiments/src/main/scala/scenarios/SecuritySystem.scala package scenarios import izumi.reflect.Tag import time.scheduledValues import scala.concurrent.TimeoutException case class SecuritySystemX( motionDetector: MotionDetector, acousticDetectorX: AcousticDetectorX ): val securityLoop: ZIO[ Any, scala.concurrent.TimeoutException, Unit ] = defer: val noise = acousticDetectorX.monitorNoise.run val motion = motionDetector.amountOfMotion.run ZIO .debug: s"Motion: $motion Noise: $noise" .run val securityResponse = determineResponse(motion, noise) securityResponse match case Relax => ZIO .debug: "No need to panic" .run case LoudSiren => ZIO Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 156 Experiments .debug: "WOOOO EEEE WOOOOO EEEE" .run @annotation.nowarn def shouldAlertServices() : ZIO[Any, TimeoutException, String] = defer: securityLoop .repeat: Schedule.recurs(5) && Schedule.spaced(1.seconds) .run "Fin" def determineResponse( amountOfMotion: Pixels, noise: Decibels ): SecurityResponse = val numberOfAlerts = determineBreaches(amountOfMotion, noise) .size if (numberOfAlerts == 0) Relax else LoudSiren def determineBreaches( amountOfMotion: Pixels, noise: Decibels ): Set[SecurityBreach] = List( Option.when(amountOfMotion.value > 50)( SignificantMotion ), Option.when(noise.value > 15)(LoudNoise) ).flatten.toSet end SecuritySystemX object SecuritySystemX: Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 157 Experiments val live = ZLayer.fromFunction(SecuritySystemX.apply _) trait SecurityBreach object LoudNoise extends SecurityBreach object SignificantMotion extends SecurityBreach trait SecurityResponse object Relax extends SecurityResponse object LoudSiren extends SecurityResponse case class Decibels(value: Int) case class Pixels(value: Int) trait MotionDetector: val amountOfMotion: ZIO[Any, Nothing, Pixels] object MotionDetector: object LiveMotionDetector extends MotionDetector: override val amountOfMotion : ZIO[Any, Nothing, Pixels] = ZIO.succeed(Pixels(30)) val amountOfMotion : ZIO[MotionDetector, Nothing, Pixels] = ZIO .service[MotionDetector] .flatMap(_.amountOfMotion) val live : ZLayer[Any, Nothing, MotionDetector] = ZLayer.succeed(LiveMotionDetector) case class AcousticDetectorX( valueProducer: ZIO[ Any, TimeoutException, Decibels ] ): Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 158 Experiments val monitorNoise : ZIO[Any, TimeoutException, Decibels] = valueProducer object AcousticDetectorX: def apply( value: (Duration, Decibels), values: (Duration, Decibels)* ): ZLayer[Any, Nothing, AcousticDetectorX] = ZLayer.fromZIO: defer: val valueProducer: ZIO[ Any, TimeoutException, Decibels ] = scheduledValues(value, values*).run // that same service we wrote above AcousticDetectorX(valueProducer) zio_helpers experiments/src/main/scala/zio_helpers/Helpers.scala package zio_helpers extension (z: ZIO.type) def repeatNPar[R, E, A]( numTimes: Int )(op: ZIO[R, E, A]): ZIO[R, E, Seq[A]] = z.foreachPar(0 until numTimes)((_: Int) => op ) extension [R, E, A](z: ZIO[R, E, A]) def timedSecondsDebug( message: String ): ZIO[R, E, A] = z.timed Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 159 Experiments .tap: (duration, res) => res match // Don't bother printing Unit results case () => ZIO.debug: message + " [took " + duration.getSeconds + "s]" case _ => ZIO.debug: message + ": " + res + " [took " + duration.getSeconds + "s]" .map(_._2) crypto experiments/src/main/scala/crypto/Mining.scala package crypto import zio.Random.nextIntBetween import zio.ZIO.debug import scala.annotation.tailrec object Mining extends ZIOAppDefault: def run = defer: val chain = Ref.make[BlockChain](BlockChain()).run raceForNextBlock(chain).repeatN(5).run chain.get.debug("Final").run private val miners = Seq("Zeb", "Frop", "Shtep") .flatMap(minerName => Range(1, 50) .map(i => new Miner(minerName + i)) ) def raceForNextBlock( Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 160 Experiments chain: Ref[BlockChain] ): ZIO[Any, Nothing, Unit] = defer: val raceResult = findNextBlock(miners).run val (winner, winningPrime) = raceResult chain .update(chainCurrent => chainCurrent.copy(blocks = chainCurrent.blocks :+ winningPrime ) ) .run debug( s"$winner mined block: $winningPrime" ).run case class BlockChain( blocks: List[Int] = List.empty ) class Miner(val name: String): def mine( num: Int ): ZIO[Any, Nothing, (String, Int)] = defer { val duration = nextIntBetween(1, 4).run ZIO.sleep(duration.second).run (name, nextPrimeAfter(num)) } def findNextBlock( miners: Seq[Miner] ): ZIO[Any, Nothing, (String, Int)] = defer: val startNum = nextIntBetween(80000000, 160000000).run ZIO .raceAll( miners.head.mine(startNum), miners.tail.map(_.mine(startNum)) ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 161 Experiments .run end Mining // TODO Consider putting math functions somewhere else to avoid clutter\ ing example def isPrime(num: Int): Boolean = (2 until num) .forall(divisor => num % divisor != 0) @tailrec def nextPrimeAfter(num: Int): Int = if (isPrime(num)) num else nextPrimeAfter(num + 1) experiments-src-test-scala-test_aspects experiments/src/test/scala/test_aspects/DemoBaseSpec.scala package test_aspects import zio.* import zio.test.* trait DemoBaseSpec extends ZIOSpecDefault { val trackStats = aroundAllWith(ZIO.debug("Starting"))( (_: Unit) => ZIO.debug("Finis\ hing")) override def aspects: Chunk[TestAspectAtLeastR[TestEnvironment]] = if (TestPlatform.isJVM) Chunk(TestAspect.timeout(10.seconds), TestAspect.timed, trackStat\ s) else Chunk(TestAspect.timeout(10.seconds), TestAspect.sequential, Test\ Aspect.timed, trackStats) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 162 Experiments def aroundWith[R0, E0, A0]( before: ZIO[R0, E0, A0] )(after: A0 => ZIO[R0, Nothing, Any]): Test\ Aspect[Nothing, R0, E0, Any] = new TestAspect.PerTest[Nothing, R0, E0, Any] { def perTest[R <: R0, E >: E0](test: ZIO[R, TestFailure[E], TestSu\ ccess])(implicit \ trace: Trace ): ZIO[R, TestFailure[E], TestSuccess] = ZIO.acquireReleaseWith(before.catchAllCause(c => ZIO.fail(TestF\ ailure.Runtime(c))))(after)(_ => test) } def walk[R, E](spec: Spec[R, E], labels: Chunk[String] = Chunk.empty)\ : Unit = println("Walking") spec.caseValue match case Spec.ExecCase(exec, spec) => () case Spec.LabeledCase(label, spec) => println("Walk Label: " + label) walk(spec, labels.appended(label)) case Spec.ScopedCase(scoped) => () case Spec.MultipleCase(specs) => println("Multi case") specs.foreach(s => walk(s, labels)) case Spec.TestCase(test, annotations) => println("test labels: " + labels.mkString(" - ")) () def aroundAllWith[R0, E0, A0]( before: ZIO[R0, E0, A0] )(after: A0 => ZIO[R0, Nothing, Any]): T\ estAspect[Nothing, R0, E0, Any] = new TestAspect[Nothing, R0, E0, Any] { def some[R <: R0, E >: E0](spec: Spec[R, E])(implicit trace: Trac\ e): Spec[R, E] = walk(spec) Spec.scoped[R]( ZIO.acquireRelease(before)(after).mapError(TestFailure.fail).\ as(spec) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 163 Experiments ) } } object Demo1Spec extends DemoBaseSpec: def spec = suite("Demo1Spec")( test("test1") { assertCompletes }, test("test2") { assertCompletes } ) experiments/src/test/scala/test_aspects/WithLiveSpec.scala package test_aspects import zio.test.* import zio.test.TestAspect.* object WithLiveSpec extends ZIOSpecDefault: def halfFlaky[A](a: A): ZIO[Any, String, A] = defer { val b = zio.Random.nextBoolean.debug.run ZIO .cond(b, a, "failed") .tapError(ZIO.logError(_)) .run } val song = defer { halfFlaky("works").debug.run assertCompletes } val song1: Spec[Any, String] = test("Song 1")(song) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 164 Experiments 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 experiments-src-test-scala-scenarios experiments/src/test/scala/scenarios/SecuritySystemSpec.scala package scenarios import zio.test.* import zio.Console.printLine import scala.concurrent.TimeoutException object SecuritySystemSpec extends ZIOSpecDefault: def spec = suite("SecuritySystemSpec")( suite("Module pattern version")( test("runs out of data")( defer { val system = ZIO.service[SecuritySystemX].run val res = system .shouldAlertServices() .catchSome { case _: TimeoutException => printLine( "Invalid Scenario. Ran out of sensor data." Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 165 Experiments ) } .run ZIO.debug("Final result: " + res).run assertCompletes } ).provide( SecuritySystemX.live, MotionDetector.live ++ AcousticDetectorX( (2.seconds, Decibels(11)), (1.seconds, Decibels(20)) ) ) @@ TestAspect.withLiveClock @@ TestAspect.tag("important", "slow") @@ TestAspect.flaky @@ TestAspect.silent @@ TestAspect.timed ) ) end SecuritySystemSpec cancellation experiments/src/main/scala/cancellation/FutureCancellation.s package cancellation import scala.concurrent.Future // We show that Future's are killed with finalizers that never run object FutureNaiveCancellation extends ZIOAppDefault: def run = ZIO .fromFuture: Future: try Thread.sleep(500) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 166 Experiments finally println("Cleanup") "Success!" .timeout(25.millis) .debug experiments/src/main/scala/cancellation/ZIOCancellation.scala package cancellation val longRunning = createProcess( "LongRunning", ZIO.sleep(5.seconds) ) object HelloCancellation extends ZIOAppDefault: def run = longRunning.timeout(2.seconds) def createProcess( label: String, innerProcess: ZIO[Any, Nothing, Unit] ) = defer: ZIO.debug(s"Started $label").run innerProcess.run ZIO.debug(s"Finished $label").run // TODO Consider rewriting to avoid // dot-chaining on block .onInterrupt(ZIO.debug(s"Interrupted $label")) object HelloCancellation2 extends ZIOAppDefault: val complex = createProcess("Complex", longRunning) def run = complex.timeout(2.seconds) object CancellationWeb extends ZIOAppDefault: def spawnLevel( level: Int, Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 167 Experiments limit: Int, parent: String ): ZIO[Any, Nothing, Unit] = ZIO .foreachPar(List("L", "R"))(label => createProcess( " " * (level + 1 * 2) + parent + s"-$label", ZIO .when(level < limit)( spawnLevel( level + 1, limit, " " * (level + 1 * 2) + parent + s"-$label" ) ) .unit ) ) .delay(level.seconds) .unit def run = spawnLevel(0, 3, "Root").timeout(3.seconds) end CancellationWeb object FailureDuringFork extends ZIOAppDefault: def run = defer: val fiber1 = createProcess( "Fiber 1", ZIO.sleep(5.seconds) ).fork.run val fiber2 = createProcess( "Fiber 2", ZIO.sleep(5.seconds) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 168 Experiments ).fork.run // Once we fail here, the fibers will be // interrupted. ZIO.fail("Youch!").run fiber1.join.run fiber2.join.run end FailureDuringFork import org.apache.commons.lang3.RandomStringUtils import org.apache.commons.text.similarity.LevenshteinDistance 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.attempt(leven(input, target)) def run = // For timeouts, you need fibers and // cancellation scenario // TODO This is running for 16 seconds // nomatter what. .timed.debug("Time:").timeout(2.seconds) concurrency experiments/src/main/scala/concurrency/ServiceThatCanHand Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 169 Experiments package concurrency import zio.cache.{Cache, Lookup} import java.nio.file.Path import zio.Console.printLine // TODO Move this all to concurrency_state prose when we can bring test\ s over in a decent way case class FileContents(contents: List[String]) trait FileService: def retrieveContents( name: Path ): ZIO[Any, Nothing, FileContents] // These are just for demos val hits: ZIO[Any, Nothing, Int] val misses: ZIO[Any, Nothing, Int] // TODO Figure if these functions belong in the object instead. trait FileSystem: def readFileExpensive( name: Path ): ZIO[Any, Nothing, FileContents] = defer: printLine("Reading from FileSystem") .orDie .run ZIO.sleep(2.seconds).run FileSystem.hardcodedFileContents object FileSystem: val hardcodedFileContents = FileContents( List("viralImage1", "viralImage2") ) val live = ZLayer.succeed(new FileSystem {}) case class ServiceThatCanHandleThunderingHerds( Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 170 Experiments cache: Cache[Path, Nothing, FileContents] ) extends FileService: override def retrieveContents( name: Path ): ZIO[Any, Nothing, FileContents] = cache.get(name) override val hits: ZIO[Any, Nothing, Int] = defer { cache.cacheStats.run.hits.toInt } override val misses: ZIO[Any, Nothing, Int] = defer { cache.cacheStats.run.misses.toInt } object ServiceThatCanHandleThunderingHerds: val make = defer: val retrievalFunction = ZIO .service[FileSystem] .map(_.readFileExpensive) .run val cache : Cache[Path, Nothing, FileContents] = Cache .make( capacity = 100, timeToLive = Duration.Infinity, lookup = Lookup(retrievalFunction) ) .run ServiceThatCanHandleThunderingHerds(cache) end ServiceThatCanHandleThunderingHerds rezilience experiments/src/main/scala/rezilience/Bulkhead.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 171 Experiments package rezilience import nl.vroste.rezilience.* import nl.vroste.rezilience.Bulkhead.BulkheadError /** In this demo, we can visualize all the * requests that are currently in flight */ // TODO - Demonstrate when maxQueueing is reached val makeBulkhead: ZIO[Scope, Nothing, Bulkhead] = Bulkhead .make(maxInFlightCalls = 3, maxQueueing = 32) object BulkheadDemo extends ZIOAppDefault: def run = defer: val currentRequests = Ref.make[List[Int]](List.empty).run val bulkhead = makeBulkhead.run val statefulResource = StatefulResource(currentRequests) ZIO .foreachPar(1 to 10): _ => bulkhead(statefulResource.request) .debug("All requests done: ") .run case class StatefulResource( currentRequests: Ref[List[Int]] ): def request: ZIO[Any, Throwable, Int] = defer: val res = Random.nextIntBounded(1000).run // Add the request to the list of current // requests currentRequests .updateAndGet(res :: _) .debug("Current requests: ") .run // Simulate a long-running request Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 172 Experiments ZIO.sleep(1.second).run removeRequest(res).run res private def removeRequest(i: Int) = currentRequests.update(_ diff List(i)) end StatefulResource experiments/src/main/scala/rezilience/CircuitBreakerDemo.sca package rezilience import nl.vroste.rezilience.* import nl.vroste.rezilience.CircuitBreaker.* object Scenario: enum Step: case Success, Failure import rezilience.Scenario.Step object CircuitBreakerDemo extends ZIOAppDefault: case class ExternalSystem( requests: Ref[Int], steps: List[Step] ): // TODO: Better error type than Throwable def call(): ZIO[Any, Throwable, Int] = defer: val requestCount = requests.getAndUpdate(_ + 1).run steps.apply(requestCount) match case Scenario.Step.Success => ZIO .succeed: Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 173 Experiments requestCount .run case Scenario.Step.Failure => ZIO .fail: Exception: "Something went wrong" .run .tapError: e => ZIO.debug(s"External failed: $e") end ExternalSystem val makeCircuitBreaker : ZIO[Scope, Nothing, CircuitBreaker[ Any ]] = CircuitBreaker.make( trippingStrategy = TrippingStrategy .failureCount(maxFailures = 2), resetPolicy = Retry .Schedules .exponentialBackoff( min = 1.second, max = 1.minute ) ) def callProtectedSystem( cb: CircuitBreaker[Any], system: ExternalSystem ) = defer { ZIO.sleep(500.millis).run cb(system.call()) .catchSome: case CircuitBreakerOpen => ZIO.debug: "Circuit breaker blocked the call to our external system" case WrappedError(e) => ZIO.debug: Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 174 Experiments s"External system threw an exception: $e" .tap: result => ZIO.debug: s"External system returned $result" .run } def run = defer: val cb = makeCircuitBreaker.run val requests = Ref.make[Int](0).run import Scenario.Step.* val steps = List(Success, Failure, Failure, Success) val system = ExternalSystem(requests, steps) callProtectedSystem(cb, system) .repeatN(5) .run end CircuitBreakerDemo experiments/src/main/scala/rezilience/RateLimiter.scala package rezilience import nl.vroste.rezilience.* /** This is useful for scenarios such as: * - Making sure you don't suddenly spike your * AWS bill * - Not accidentally DDOSing a service */ val makeRateLimiter : ZIO[Scope, Nothing, RateLimiter] = RateLimiter.make(max = 1, interval = 1.second) // We use Throwable as error type in this example def rsaKeyGenerator: ZIO[Any, Throwable, Int] = Random.nextInt Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 175 Experiments object RateLimiterDemo extends ZIOAppDefault: def run = defer: val rateLimiter = makeRateLimiter.run rateLimiter(rsaKeyGenerator) // Repeats as fast as the limiter allows .repeatN(5).debug("Result").run object RateLimiterDemoWithLogging extends ZIOAppDefault: // TODO Put in book-side ZIO helpers? extension [R, E, A](z: ZIO[R, E, A]) def timedSecondsDebug( message: String ): ZIO[R, E, A] = z.timed .tap: (duration, res) => ZIO.debug: message + ": " + res + " [took " + duration.getSeconds + "s]" .map(_._2) def run = defer: val rateLimiter = makeRateLimiter.run rateLimiter(rsaKeyGenerator) // Print the time to generate each key: .timedSecondsDebug("Generated key") // Repeat as fast as the limiter allows: .repeatN(5) // Print the last result .timedSecondsDebug("Result").run end RateLimiterDemoWithLogging object RateLimiterDemoGlobal extends ZIOAppDefault: // TODO Put in book-side ZIO helpers? extension (z: ZIO.type) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 176 Experiments def repeatNPar[R, E, A](numTimes: Int)( op: Int => ZIO[R, E, A] ): ZIO[R, E, Seq[A]] = z.foreachPar(0 until numTimes)(op) def run = defer: val rateLimiter = makeRateLimiter.run ZIO .repeatNPar(4): i => rateLimiter( rsaKeyGenerator.debug(i.toString) ) // Repeats as fast as allowed .repeatN(5).debug(s"Result $i") .run end RateLimiterDemoGlobal cause experiments/src/main/scala/cause/MalcomInTheMiddle.scala package cause object MalcomInTheMiddle extends ZIOAppDefault: @annotation.nowarn 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() Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 177 Experiments 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 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?!" Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 178 Experiments // // ) .exitCode end run /** try { turnOnLights } catch { case * burntLightBulb => try { */ end MalcomInTheMiddle experiments/src/main/scala/cause/MalcomInTheMiddleZ.scala package cause 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")) defer { turnOnLights() .catchAllCause(originalError => getNewBulb().catchAllCause(bulbError => grabScrewDriver() .mapErrorCause(screwDriverError => (originalError ++ bulbError) ++ screwDriverError ) ) ) .run Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 179 Experiments ZIO.debug("Preserve failures!").run }.catchAllCause(bigError => ZIO.debug( "Final error: " + simpleStructureAlternative(bigError) ) ) 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 _ => Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 180 Experiments ??? environment experiments/src/main/scala/environment/DatabaseConnection package environment opaque type UserId = String object UserId: def apply(str: String): UserId = str case class DbConnection( actionLog: Ref[Seq[String]] ): def execute(label: String) = actionLog.update(_.appended(label)) object DbConnection: val make: ZLayer[Any, Nothing, DbConnection] = ZLayer.scoped( ZIO.acquireRelease( defer { val actionLog: Ref[Seq[String]] = Ref.make(Seq.empty[String]).run val connection = DbConnection(actionLog) connection.execute("OPEN").run connection } )(connection => defer { connection.execute("CLOSE").run pprint.apply(connection) }.debug ) ) end DbConnection Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 181 Experiments object DatabaseConnectionSimple extends ZIOAppDefault: def executeUserQueries( userId: UserId ): ZIO[DbConnection, Nothing, Unit] = // TODO Consider alternative version of this // where the defer happens inside of a // serviceWithZIO call defer { val connection = ZIO.service[DbConnection].run connection .execute( s"QUERY for $userId preferences" .stripMargin ) .run ZIO.sleep(1.second).run connection .execute( s"QUERY for $userId accountHistory" .stripMargin ) .run } def run = defer { executeUserQueries(UserId("Alice")).run executeUserQueries(UserId("Bob")).run }.provide(DbConnection.make) end DatabaseConnectionSimple object DatabaseConnectionInterleavedQueries extends ZIOAppDefault: def executeUserQueries( userId: UserId ): ZIO[DbConnection, Nothing, Unit] = // TODO Consider alternative version of this // where the defer happens inside of a Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 182 Experiments // serviceWithZIO call defer { val connection = ZIO.service[DbConnection].run connection .execute( s"QUERY for $userId preferences" .stripMargin ) .run ZIO.sleep(1.second).run connection .execute( s"QUERY for $userId accountHistory" .stripMargin ) .run } def run = ZIO .foreachPar( List(UserId("Alice"), UserId("Bob")) )(executeUserQueries) .provide(DbConnection.make) end DatabaseConnectionInterleavedQueries experiments/src/main/scala/environment/ToyEnvironment.sca Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 183 Experiments package environment 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] @annotation.nowarn @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[ String & DBService & List[String] ] = env2.add(List("a", "b")) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 184 Experiments 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-test-scala-zio_test experiments/src/test/scala/zio_test/Shared.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 185 Experiments package zio_test 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] = defer { val current = value.get.run s"**$current**" } val scoreBoard: ZLayer[ Scope with Ref[Int], Nothing, Scoreboard ] = ZLayer.fromZIO { defer { val value = ZIO.service[Ref[Int]].run ZIO .acquireRelease( ZIO.succeed(Scoreboard(value)) <* ZIO.debug( "Initializing scoreboard!" ) )(_ => ZIO.debug("Shutting down scoreboard") ) .run } } Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 186 Experiments end Shared experiments/src/test/scala/zio_test/UseComplexLayer.scala package zio_test 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") { defer { ZIO .serviceWithZIO[Scoreboard]( _.display() ) .debug .run assertCompletes } } end UseComplexLayer experiments/src/test/scala/zio_test/UseSharedLayerA.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 187 Experiments package zio_test import zio.test.{ZIOSpec, assertCompletes} 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.{ZIOSpec, assertCompletes} import zio.{Ref, ZIO} object UseSharedLayerB extends ZIOSpec[Ref[Int]]: def bootstrap = Shared.layer def spec = test("Test B") { for _ <ZIO.serviceWithZIO[Ref[Int]](count => count.update(_ + 1) ) yield assertCompletes } Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 188 Experiments experiments-src-test-scala-layers experiments/src/test/scala/layers/FestivalShortedOutSoundSys package layers import zio.test.* // TODO Replace with some toilet issue? object FestivalShortedOutSoundSystemSpec extends ZIOSpecDefault: val brokenFestival : ZLayer[Any, String, Festival] = ZLayer.make[Festival]( festival, stage, soundSystem, toilets, 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 FestivalShortedOutSoundSystemSpec Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 189 Experiments experiments/src/test/scala/layers/FestivalSpec.scala package layers import zio.test.* object FestivalSpec extends ZIOSpecDefault: val spec = suite("Play some music")( test("Good festival")( ZIO.service[Festival].as(assertCompletes) ) ).provide( festival, stage, soundSystem, toilets, security, venue, permit // ZLayer.Debug.mermaid ) @@ TestAspect.withLiveClock experiments-src-test-scala-time experiments/src/test/scala/time/ScheduledValuesSpec.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 190 Experiments package time import zio.test.* 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" )( defer { val valueAccessor = scheduledValues( (1.seconds, "First Section") ).run assertTrue( valueAccessor.run == "First Section" ) } ), test( "querying when no time has passed fails when the duration == \ 0" )( defer { val valueAccessor = scheduledValues( (0.seconds, "First Section") ).run valueAccessor.flip.run assertCompletes } ), test( "next value is returned after enough time has elapsed" )( Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 191 Experiments defer { val valueAccessor = scheduledValues( (1.seconds, "First Section"), (2.seconds, "Second Section") ).run TestClock.adjust(1.seconds).run val secondValue = valueAccessor.run assertTrue( secondValue == "Second Section" ) } ), test("time range end is not inclusive")( defer { val valueAccessor = scheduledValues( (1.seconds, "First Section") ).run TestClock.adjust(1.seconds).run valueAccessor.flip.run assertCompletes } ) ) ) end ScheduledValuesSpec performance experiments/src/main/scala/performance/Hedging.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 192 Experiments package performance import zio_helpers.repeatNPar object Hedging extends ZIOAppDefault: extension [R, E, A](z: ZIO[R, E, A]) def hedge( wait: zio.Duration, depth: Int = 1 ): ZIO[R, E, A] = depth match case 0 => z case other => z.delay: wait .race: hedge(wait, depth - 1) def hedgedRequestNarrow = handleRequest .race(handleRequest.delay(25.millis)) .race(handleRequest.delay(25.millis)) .race(handleRequest.delay(25.millis)) def hedgedRequestGeneral = handleRequest.hedge(25.millis, 3) def run = defer: val timeBuckets = Ref .make[Map[Percentile, RequestStats]]: Map() .run ZIO .repeatNPar(50_000): demoRequest: timeBuckets .run Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 193 Experiments pprint.pprintln( timeBuckets.get.run, width = 47 ) def demoRequest( timeBuckets: Ref[ Map[Percentile, RequestStats] ] ) = hedgedRequestGeneral.tap(requestTime => timeBuckets.update(results => results.updatedWith( Percentile .fromDuration(requestTime.duration) ): case Some(value) => Some( value.copy( count = value.count + 1, totalTime = value .totalTime .plus(requestTime.duration) ) ) case None => Some( RequestStats( count = 1, totalTime = requestTime.duration ) ) ) ) end Hedging val handleRequest = defer { Percentile.random.run match Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 194 Experiments case Percentile._50 => ResponseTimeCutoffs.Fast case Percentile._95 => ResponseTimeCutoffs.Acceptable case Percentile._999 => ResponseTimeCutoffs.BreachOfContract }.tap(dimension => ZIO.sleep(dimension.duration) ) // TODO Fix name enum ResponseTimeCutoffs(val duration: Duration): case Fast extends ResponseTimeCutoffs(21.millis) case Acceptable extends ResponseTimeCutoffs(70.millis) case BreachOfContract extends ResponseTimeCutoffs(1800.millis) enum Percentile: case _50, _95, _999 object Percentile: def random = defer: val int = Random.nextIntBounded(1_000).run if (int < 950) Percentile._50 else if (int < 999) Percentile._95 else Percentile._999 def fromDuration(d: Duration) = d match case ResponseTimeCutoffs.Fast.duration => Percentile._50 case ResponseTimeCutoffs .Acceptable .duration => Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 195 Experiments Percentile._95 case ResponseTimeCutoffs .BreachOfContract .duration => Percentile._999 case _ => ??? end Percentile case class RequestStats( count: Int, totalTime: Duration ) resourcemanagement experiments/src/main/scala/resourcemanagement/ChatSlots.s package resourcemanagement import zio.Console.printLine case class Slot(id: String) case class Player(name: String, slot: Slot) object ChatSlots extends zio.ZIOAppDefault: enum SlotState: case Closed, Open def run = @annotation.nowarn def acquire(ref: Ref[SlotState]) = defer: printLine: "Took a speaker slot" .run ref Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 196 Experiments .set: SlotState.Open .run "Use Me" def release(ref: Ref[SlotState]) = defer: printLine: "Freed up a speaker slot" .orDie .run ref .set: SlotState.Closed .run defer { val ref = Ref .make[SlotState]: SlotState.Closed .run val managed = ZIO.acquireRelease(acquire(ref))(_ => release: ref ) val reusable = managed.map: printLine(_) reusable.run reusable.run ZIO .scoped: // TODO Get rid of flatmap if // possible... managed.flatMap: s => defer: printLine: s .run printLine: Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 197 Experiments "Blowing up" .run if (true) throw Exception: "Arggggg" .run } end run end ChatSlots experiments-src-test-scala-concurrency experiments/src/test/scala/concurrency/ThunderingHerdsSpec package concurrency import zio.Console.printLine import zio.test.* import java.nio.file.Path object ThunderingHerdsSpec extends ZIOSpecDefault: val testInnards = defer { val users = List("Bill", "Bruce", "James") val herdBehavior = defer { val fileService = ZIO.service[FileService].run val fileResults = ZIO .foreachPar(users)(user => fileService.retrieveContents( Path.of("awesomeMemes") ) ) .run Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 198 Experiments ZIO.debug("=========").run fileService .retrieveContents( Path.of("awesomeMemes") ) .run fileResults } printLine("Capture?").run val logicFork = herdBehavior.fork.run TestClock.adjust(2.seconds).run val res = logicFork.join.run val misses = ZIO .serviceWithZIO[FileService](_.misses) .run ZIO.debug("Eh?").run assertTrue( misses == 1, res.forall(singleResult => singleResult == FileSystem.hardcodedFileContents ) ) } end testInnards override def spec = suite("ThunderingHerdsSpec")( test( "classic happy path using zio-cache library" ) { testInnards }.provide( FileSystem.live, ZLayer.fromZIO( ServiceThatCanHandleThunderingHerds .make ) ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 199 Experiments ) end ThunderingHerdsSpec Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward