Effect-Oriented Programming Creating Reliable Systems with Scala 3 and ZIO 2 Bill Frasure, Bruce Eckel and James Ward Effect-Oriented Programming Creating Reliable Systems with Scala 3 and ZIO 2 Bill Frasure, Bruce Eckel and James Ward This book is for sale at http://leanpub.com/effect-oriented-programming This version was published on 2022-12-23 This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and many iterations to get reader feedback, pivot until you have the right book and build traction once you do. © 2021 - 2022 Bill Frasure, Bruce Eckel and James Ward CONTENTS Contents Copyright . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Effect Oriented Programming . . . . . . . . . . . . . . . . . . . . . . . . . . . Source Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 Superpowers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Underlying . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Introduction . . . . . . . . Untangling the Chaos The State of Software The Software Crisis . Reliability . . . . . . . What is an Effect? . . Managing Effects . . . Who This Book Is For How to Use This Book Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 8 9 10 11 12 12 13 13 Why Functional? . . . . . . . . . . . . . . . . . . . . A Different Goal . . . . . . . . . . . . . . . . . . Reuse . . . . . . . . . . . . . . . . . . . . . . . . . Pure Functions . . . . . . . . . . . . . . . . . . . Composability . . . . . . . . . . . . . . . . . . . . Effects . . . . . . . . . . . . . . . . . . . . . . . . Immutability During Repetition . . . . . . . . . Core Differences Between OO and Functional Summary: Style vs Substance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 14 17 18 19 20 20 22 23 Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward CONTENTS Effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Effects VS Side-Effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 25 26 Unit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 The ZIO Type . . . . . . . . . . . . . . . . . . R - The Environment . . . . . . . . . . . E - The Error . . . . . . . . . . . . . . . . A - The Result . . . . . . . . . . . . . . . . Conversions from standard Scala types . . . . . 29 29 29 30 30 And / Or . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Unions AKA Sum Types AKA Enums AKA Ors . . . . . . . . . . . . . . . . Intersections AKA Products AKA Case Classes AKA Ands . . . . . . . . . 32 32 34 Built-in Services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.x . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.x . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 36 36 Console . . . . . . . . . . . . . . . . . . . . The Unprincipled Way . . . . . . . . Building a Better Way . . . . . . . . . Official ZIO Approach . . . . . . . . . ZIO Super-Powers . . . . . . . . . . . Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 37 37 41 41 42 Mutability . . . . . . . . . . . . . . . . . . . Unreliable Counting . . . . . . . . . . Reliable Counting . . . . . . . . . . . Ref.Synchronized . . . . . . . . . . . . Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 45 46 49 50 Time . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . . 52 52 Environment Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Historic Approach . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 59 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward . . . . . . . . . . . . . . . CONTENTS Building a Better Way . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Official ZIO Approach . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 66 67 Random . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . . 70 70 Hello Failures . . . . . . . . . . . . . . . . . Historic approaches to Error-handling ZIO Error Handling . . . . . . . . . . . Automatically attached experiments. . . . . . 79 79 83 87 Cause . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Avoided Technique - Throwing Exceptions . . . . . . . . . . . . . . . . . . . Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . . 96 96 98 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . New Effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 Location . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 Environment Exploration . . . . . . . . . . . Dependency Injection . . . . . . . . . . . ZEnvironment: Powered by a TypeMap ZLayer . . . . . . . . . . . . . . . . . . . . Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 108 108 109 109 Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 STM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . . 115 Executing External Programs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 Basic shell tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 Automatically attached experiments. . . . . . . . . . . . . . . . . . . . . . . . 121 Experiments . . . . . . . . resourcemanagement energygrid . . . . . . . microcontrollers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward . . . . . . . . . . . . . . . . 124 124 127 128 CONTENTS diningphilosophers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . interpreter-chaining_previous_result_and_environmental_dependency . experiments-src-test-scala-energrygrid . . . . . . . . . . . . . . . . . . . . . layers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . javawrappers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . interpreter-level1_nochaining . . . . . . . . . . . . . . . . . . . . . . . . . . bigdec . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . typeclasses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ZIOFromNothing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . experiments-src-test-scala-test_aspects . . . . . . . . . . . . . . . . . . . . testcontainers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . interpreter-level2_chaining . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hubs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . TicTacToe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . runtime . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . experiments-src-test-scala-testcontainers . . . . . . . . . . . . . . . . . . . crypto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . environment_exploration-opaque . . . . . . . . . . . . . . . . . . . . . . . . simulations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . HelloZio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Parallelism . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . game_theory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . scenarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . logging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . fibers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . prelude . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . std_type_conversions_to_zio . . . . . . . . . . . . . . . . . . . . . . . . . . . interpreter-chaining_with_previous_result . . . . . . . . . . . . . . . . . . experiments-src-test-scala-zio_test . . . . . . . . . . . . . . . . . . . . . . . helloworld . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . experiments-src-test-scala-layers . . . . . . . . . . . . . . . . . . . . . . . . experiments-src-test-scala-bigdec . . . . . . . . . . . . . . . . . . . . . . . . virtual_meeting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . zio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . experiments-src-test-scala-time . . . . . . . . . . . . . . . . . . . . . . . . . zio_intro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 132 135 138 142 143 144 146 147 149 151 159 162 172 174 174 180 182 183 185 190 198 203 218 220 221 224 227 229 232 232 235 237 239 240 241 CONTENTS experiments-src-test-scala-concurrency . . . . . . . . . . . . . . . . . . . . . 247 interpreter-chaining_with_monads . . . . . . . . . . . . . . . . . . . . . . . . 249 Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Copyright Effect Oriented Programming By Bill Frasure, Bruce Eckel and James Ward {{ These are the new ISBNs }} Copyright ©2021, MindView LLC. eBook ISBN 978-0-9818725-6-8 Print Book ISBN 978-0-9818725-7-5 The eBook ISBN covers the Leanpub eBook distribution in all formats, available through www.EffectOrientedProgramming.com. Please purchase this book through www.EffectOrientedProgramming.com, to support its continued maintenance and updates. All rights reserved. Printed in the United States of America. This publication is protected by copyright, and permission must be obtained from the publisher prior to any prohibited reproduction, storage in a retrieval system, or transmission in any form or by any means, electronic, mechanical, photocopying, recording, or likewise. For information regarding permissions, see EffectOrientedProgramming.com. Created in Crested Butte, Colorado, USA. Text printed in the United States Ebook: Version 1.0, Month Year First printing Month Year Cover design by Daniel Will-Harris, www.Will-Harris.com¹ ¹http://www.Will-Harris.com 2 Copyright Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and the publisher was aware of a trademark claim, the designations are printed with initial capital letters or in all capitals. The Scala trademark belongs to {{???}}. Java is a trademark or registered trademark of Oracle, Inc. in the United States and other countries. Windows is a registered trademark of Microsoft Corporation in the United States and other countries. All other product names and company names mentioned herein are the property of their respective owners. The authors and publisher have taken care in the preparation of this book, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein. Visit us at www.EffectOrientedProgramming.com. Source Code All the source code for this book is available as copyrighted freeware, distributed via Github². To ensure you have the most current version, this is the official code distribution site. You may use this code in classroom and other educational situations as long as you cite this book as the source. The primary goal of this copyright is to ensure that the source of the code is properly cited, and to prevent you from republishing the code without permission. (As long as this book is cited, using examples from the book in most media is generally not a problem.) In each source-code file you find a reference to the following copyright notice: ²https://github.com/EffectOrientedProgramming/EOPCode Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 3 Copyright // Copyright.txt This computer source code is Copyright ©2021 MindView LLC. All Rights Reserved. Permission to use, copy, modify, and distribute this computer source code (Source Code) and its documentation without fee and without a written agreement for the purposes set forth below is hereby granted, provided that the above copyright notice, this paragraph and the following five numbered paragraphs appear in all copies. 1. Permission is granted to compile the Source Code and to include the compiled code, in executable format only, in personal and commercial software programs. 2. Permission is granted to use the Source Code without modification in classroom situations, including in presentation materials, provided that the book "Effect Oriented Programming" is cited as the origin. 3. Permission to incorporate the Source Code into printed media may be obtained by contacting: MindView LLC, PO Box 969, Crested Butte, CO 81224 MindViewInc@gmail.com 4. The Source Code and documentation are copyrighted by MindView LLC. The Source code is provided without express or implied warranty of any kind, including any implied warranty of merchantability, fitness for a particular purpose or non-infringement. MindView LLC does not warrant that the operation of any program that includes the Source Code will be uninterrupted or error-free. MindView LLC makes no representation about the suitability of the Source Code or of any software that includes the Source Code for any purpose. The entire risk as to the quality and performance of any program that includes the Source Code is with the user of the Source Code. The user understands that the Source Code was developed for research and instructional purposes and is advised not to rely exclusively for any reason on the Source Code or any program that includes the Source Code. Should the Source Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 4 Copyright Code or any resulting software prove defective, the user assumes the cost of all necessary servicing, repair, or correction. 5. IN NO EVENT SHALL MINDVIEW LLC, OR ITS PUBLISHER BE LIABLE TO ANY PARTY UNDER ANY LEGAL THEORY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR ANY OTHER PECUNIARY LOSS, OR FOR PERSONAL INJURIES, ARISING OUT OF THE USE OF THIS SOURCE CODE AND ITS DOCUMENTATION, OR ARISING OUT OF THE INABILITY TO USE ANY RESULTING PROGRAM, EVEN IF MINDVIEW LLC, OR ITS PUBLISHER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. MINDVIEW LLC SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOURCE CODE AND DOCUMENTATION PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, WITHOUT ANY ACCOMPANYING SERVICES FROM MINDVIEW LLC, AND MINDVIEW LLC HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. Please note that MindView LLC maintains a Web site which is the sole distribution point for electronic copies of the Source Code, where it is freely available under the terms stated above: https://github.com/EffectOrientedProgramming/EOPCode If you think you've found an error in the Source Code, please submit a correction at: https://github.com/EffectOrientedProgramming/EOPCode/issues You may use the code in your projects and in the classroom (including your presentation materials) as long as the copyright notice that appears in each source file is retained. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Superpowers • Racing • Timeout • Error-handling – Fallback – Retry • Repeat • Parallelism • Resource Safety • Mutability that you can trust • Human-readable • Cross-cutting Concerns / Observability / Regular Aspects – timed – metrics – debug – logging Underlying • • • • • Composability Success VS failure Interruption/Cancellation Fibers Processor Utilization – Fairness – Work-stealing • Resource Control/Management • Programs as values Introduction 08:07 AM January 13, 2018 Televisions, Radios, and Cell Phones across Hawaii suddenly flash an alert: “BALLISTIC MISSILE INBOUND THREAT TO HAWAII. SEEK IMMEDIATE SHELTER. THIS IS NOT A DRILL” Local communities sound alarms. Calls to 911 jam the phone lines. Panicked internet searches overwhelm data networks. Hundreds of students sprint from their classrooms to fallout shelters. Parents say goodbye to their children. Untangling the Chaos Thankfully, no missiles were launched that day. During what should have been a quiet system test, an employee at the Hawaii Emergency Management Agency accidentally pushed the wrong button. From the Washington Post³: “He clicked the button to send out an actual notification on Hawaii’s emergency alert interface during what was intended to be a test of the state’s ballistic missile preparations computer program.” The employee was prompted to choose between the options “test missile alert” and “missile alert”, had selected the latter, initiating the alert sent out across the state. ³https://www.washingtonpost.com/news/post-nation/wp/2018/01/14/hawaii-missile-alert-how-one-employeepushed-the-wrong-button-and-caused-a-wave-of-panic/ 8 Introduction Here is the system’s control screen: This cluster of inconsistently named links increased the likelihood of mistakes. Basic changes would drastically simplify proper use of the alerts. Imagine the earlier mishaps that moved “False Alarm” to the top of the list. We believe the system was doomed long before the interface was created. The fatal flaw was that both “live” and “test” alerts were available in the running application. A safe system makes these behaviors mutually exclusive. The effects of this system were: • Sending messages to Cell Phones • Playing warnings on Radio frequencies • Displaying banners on Television stations The State of Software There are many other examples of carefully-built software systems failing disastrously: • The Ariane 5 rocket self-destructed on 4 June 1996 because of a malfunction in the control software (the program tried to stuff a 64-bit number into a 16-bit space). • The American Northeast Power Blackout, August 14 2003. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 9 Introduction • The NASA Mars Climate Orbiter, September 23, 1999. The orbiter was programmed for metric but ground control software used non-metric English. The list goes on; just search for something like “Famous Software Failures” to see more. And consider security; all the applications you use that are constantly being updated with security patches (what about those that aren’t? Are they that good, or is security being ignored?). How did things get so bad? The Software Crisis In the 70’s and 80’s, the idea of the Software Crisis emerged. This can be summarized as: “We can’t create software fast enough.” One of the most popular attempts to solve this problem was Structured Analysis & Design, which was a way to understand a problem and design a solution using existing imperative languages. The real problem that Structured Analysis & Design set out to solve was big monolithic pieces of code. When one programmer was able to solve the entire problem, the structure of the program didn’t matter as much. But as software needs grew, this approach didn’t scale. In particular, you couldn’t finish a project more quickly by adding more programmers, because there wasn’t a way to hand off portions of a program to multiple programmers. To do that, teams needed some way to break down the complexity of the program into individual functions—functions that might someday be reused. This was seen as the reason for the Software Crisis. Structured Analysis was an attempt to discover the individual functions in a program. But it was a top-down approach, and it assumed these functions could be determined before any code is written. Structured Analysis & Design continued the approach of “big up-front design.” The analyst produced the structure, and then the programmers implemented it. Experienced programmers know that a design that cannot evolve during development is doomed to failure: both programmers and stakeholders learn things during development. You discover much of your structure as you’re building the program, and not on a whiteboard. Building a program reveals things you didn’t know were important when you designed the solution. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 10 Introduction From this book’s perspective, the most fundamental problem with Structured Analysis & Design was that it only paid lip service to the idea of reliability. There was nothing about reliability truly integrated into Structured Analysis & Design. Structured Analysis & Design was motivated by a business problem: “how do we create software faster?” Virtually every language that came out in its aftermath focused on development speed. Not reliability. So we produced a lot of languages to quickly create unreliable software. Reliability A reliable system does not break. // TODO Discuss If you’ve been programming for a while, this sounds far-fetched or even impossible. Most existing languages are built for rapid development. You create a system as quickly as possible, then begin isolating areas of failure, finding and fixing bugs until the system is tolerable and can be delivered. Throughout the lifetime of the system, bugs are regularly discovered and fixed. There is no realistic expectation that you will ever achieve a completely bug-free system, just one that seems to work well enough to meet the requirements. This is the reality programmers have come to accept. If each piece of a traditional system is unreliable, when you combine these pieces you get a multiplicative effect – the resulting parts are significantly less reliable than their component pieces. What if we could change our thinking around the problem of building software systems? Imagine building small pieces that can each be reasoned about and made rock-solid. Now suppose there is a way to combine these reliable pieces to make bigger parts that are just as reliable. Each time you combine smaller parts to create a bigger part, the result inherits the reliability of its components. Instead of multiplying unreliability, you combine reliability. The resulting system is as reliable as any of its components. This is what functional programming together with effects management can achieve. This is what we want to teach you in this book. The biggest impact on you as a programmer is the requirement for patience. With most languages, the first thing you want to do is figure out how to write “Hello, Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 11 Introduction World!”, then start accumulating the other language features as standalone concepts. In functional programming we start by examining the impact of each concept on reliability. We then combine the smaller concepts, ensuring reliability at each step. A reliable system isolates parts that are always the same (pure functions) from the parts that can change (effects). This mathematical rigor produces a reliable system. It can seem like a painfully long process before you begin writing working programs. // TODO Discuss Most of us are used to the more immediate feedback and satisfaction of getting something working, so this can be challenging. But would you rather create an unreliable system quickly? We assume you are reading this book because you do not. What is an Effect? An effect is an interaction with the world outside your CPU. An application might generate any number of effects, which fall into two categories: • Observing the World • Changing the World Effects cannot be undone. If you 3D-print a figurine, you cannot reclaim that material. Once you send a Tweet, you can delete it but people might have already read it. Even if you provide database DELETE statements paired with INSERT statements, it must still be considered effectful: Another program might read your data before you delete it, or a database trigger might activate during an INSERT. TODO {{Explain: Optionality, Asynchronicity, Blocking – In a later chapter. }} Observing the World Observation can be very basic: • Accepting user input from the console • Getting the current time from the system clock • Taking the output of a random number generator Observations can also be complex and domain-specific: Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 12 Introduction • Sensing slippage in an anti-lock braking system • Getting the current price of a stock • Detecting the current from a pacemaker • Checking the temperature of a nuclear reactor We explore similar scenarios throughout the book. Changing the World Just as with observations, changes can be basic: • Displaying on the console • Writing to a file • Mutating a variable • Saving to a database They can be advanced: • 3D printing a model • Triggering an alarm • Stabilizing an airplane • Detonating explosives Managing Effects {{ A very high-level overview of what an effects-management system does }} Who This Book Is For • Your background • What to expect This is not a comprehensive Scala 3 book. For that we recommend Programming in Scala 3⁴. We expect the kind of basic programming knowledge that allows you to effectively guess at the meaning of basic Scala code. We explain more complex Scala syntax as it appears in the book. However, we avoid the use of complex Scala syntax, as our goal is to teach ZIO in as simple a fashion as possible. ⁴https://www.TODO.com Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 13 Introduction How to Use This Book This book is designed to be a self-study guide together with exercises and solutions. All examples and exercise solutions are available as copyrighted freeware, distributed via Github⁵. To ensure you have the most current version, this is the official code distribution site. The README for this repository contains thorough, step-by-step instructions for setting up your computer to compile and run the examples and exercise solutions. Teaching With This Book You may use the examples, exercises and solutions in classroom and other educational situations as long as you cite this book as the source. See the Copyright⁶ page for further details. The primary goal of the copyright is to ensure that the source of the code is properly cited, and to prevent you from republishing the code without permission. As long as this book is cited, using examples from the book in most media is generally not a problem. Acknowledgements Kit Langton, for being a kindred spirit in software interests and inspiring contributor to the open source world. Wyett Considine, for being an enthusiastic intern and initial audience. ⁵https://github.com/EffectOrientedProgramming/EOPCode ⁶\protect\char”007B\relax\protect\char”007B\relax???\protect\char”007D\relax\protect\char”007D\relax Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional? In your journey to this book, you have undoubtedly learned at least one way to think about programming, and possibly several. You might have had reasonable success using that approach. You might also have encountered programming constructs inspired by functional programming. For example, Java 8 introduced lambdas along with library support for streams and functional primitives like map. Python has always allowed functions to be passed into, be created by, and be returned from other functions, and includes other functional support. C++, especially more recent versions, has added a number of features that support functional-style programming. If, however, you are coming from an imperative programming background, these functional-style devices can seem arbitrarily complicated. Why go to the trouble to use something like fold or reduce when a simple for loop will do the job, and be much easier to understand? Sometimes it seems like functional programmers write code like this just to be fancy. To understand what’s really behind this different way of thinking about programming, it helps to start with some history. A Different Goal In the early days of programming, most code was written in an assembly language for a particular machine. Assembly language had primitive function-like constructs called subroutines, but you had to do the work of setting up a call (storing arguments in registers or on the stack), then inside the subroutine you had to access those arguments, and then before returning from the subroutine you had to somehow pass the return value back to the caller (again, typically using either a register or the stack). You had to do all this by hand. It was worth it if you knew you were going to reuse that subroutine, but if you thought you were only ever going to use that code once Why Functional? 15 then it was easier and more efficient to just insert the code inline. Even if you wanted to create reusable code, it was often easier to just “goto” a piece of code and use global variables, rather than bothering with passing arguments and returning results. In those early days, a “high level language” meant a language like C that passed arguments and returned results for you. This suddenly made writing functions much easier, safer, and faster. But the habits of assembly-language programmers didn’t vanish overnight, and people were still prone to writing obtuse monolithic programs—often a single function for the whole program—and jumping around in their code using gotos. Many programs were written and maintained by a small number of programmers, or even a single programmer, for whom that code made sense. Anyone else reading the code would be baffled. In addition, the idea of calling code written by other people was fairly foreign. If you didn’t write the code yourself, how would you know if it does what you want? (Documentation and testing were also primitive, if they existed at all). Thus “code reuse” was a big hurdle; there were many attempts within companies to create cultures of code reuse because many programmers were rewriting the same functionality from scratch, over and over within the same organization. Monolithic programs that didn’t reuse code were also a maintenance nightmare. It was not uncommon for such programs to be thrown away and rewritten just to add some new features. Not surprisingly, writing everything from scratch also took a lot longer than reusing common functionality. The new question became: “how do we make code reuse easier?” Because languages like C and Pascal made it easy to not only write functions but to call them, they were a big improvement over assembly language, although the monolithic habits of assembly-language programmers persisted into those new languages. Libraries grew bigger and more complex, and using those libraries was not easy. In C, for example, you’d often have to call malloc to allocate memory before calling a library function, and later free to release the memory when that function was done with it. You also had to learn how to pass information from one library function to another. You had to learn how a each library reported errors, which typically varied in strategy from one library to the next. Code could be reused, but it wasn’t easy. At this point, object-oriented programming seemed like a good idea, because it combined a common data structure, automatic initialization and cleanup of that Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional? 16 data structure, together with all the functions that act upon that data. If you wanted to reuse some code, you just created an object with some initialization values and then sent messages to that object to produce the desired results. This did make code reuse easier, and helped speed up program creation. It also came with the distraction of inheritance polymorphism and an entire education and consulting industry explaining how to cram every design into an inheritance hierarchy (inheritance polymorphism does sometimes prove useful, but not everywhere, all the time). C++ added object-oriented features from the Simula language while maintaining backward compatibility with the C language. C++ had a strong emphasis on static type checking. Java was created as a counterpoint to C++ and was heavily inspired by the Smalltalk language. Smalltalk’s success came from its ability to rapidly create systems by adding functionality to existing objects. This introduced a conundrum, because Smalltalk is a dynamic language, and Java, like C++, is statically typed. Smalltalk can be thought of as supporting an experimental style of programming: you send a message to an object and discover at runtime whether the object knows what to do with that message. But C++ and Java ensure everything is valid, at compile time (along with escape mechanisms that effectively disable that type checking). This conundrum is exemplified by the Liskov Substitution Principle, which says that you shouldn’t add new methods to an inherited type—and yet that activity is the foundation of Smalltalk. The Agile methodologies that began in the early 2000’s were another attempt to produce software faster, but through a more bottom-up lens. Agile was primarily focused on improving communication between stakeholders and developers, and producing more rapid round trips between needs and experiments. This improves the chance that the stakeholders will get what they need, faster. Agile helped the process of software development, but again, the focus is on developing software quickly, not on developing reliable software. The most important thing to take away from this language history is that the fundamental goal of the various techniques was speed of creation. There seems to be an underlying assumption that these approaches will somehow automatically create more reliable software. As a result, we have languages that quickly create unreliable software. And in many cases we’ve been able to get by with that. For one thing, this approach has greatly advanced testing technology, because it was necessary. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional? 17 Customers have learned to put up with buggy software. They’ve often been willing to accept buggy software when the alternative is no software at all. The world has changed. Back then, the drive was to speed up activities that humans were doing. Those humans could compensate for bugs. Now, however, more and more software is doing things that humans can’t do, so failures in software cannot be propped up by humans. Unreliable software is no longer an inconvenience, but a serious problem. Quickly creating unreliable software is no longer acceptable. We must delve into the reasons that software fails—either it doesn’t do what it’s supposed to, or it just breaks. Reuse How do we create software? When you first learned to program, you probably solved problems by writing code using the basic constructs of your language. But at some point you began realizing that you could only produce and debug so much code by yourself. If you could use code that was already written and debugged by other people, you could produce solutions faster. You might have gone through a cut-and-paste phase before discovering that formalized libraries were easier and more reliable. Even then, library ease of use depended on the sophistication of your language. For the reasons mentioned, using a C library could be tricky and difficult. C++ made this much easier and paved the way for the acceptance of languages like Java, Python, Scala, and Kotlin. Indeed, any new language that doesn’t support easy code reuse is not taken seriously. But code reuse in object-oriented languages was still limited. You could either use objects in a library directly, or you could add those library classes into new classes using composition. This was a big step and it helped a lot. In contrast, composing C libraries wasn’t particularly realistic—it was just too messy and complicated. The problem is reliability. If you create a new class using composition, you combine problems with the existing class(es) with any bugs you introduced in your new class. As you build up bigger systems, the problem of bugs multiplies. To compose systems rapidly and reliably, we return to first principles and figure out how to: Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional? 18 1. Create basic components that are completely reliable. 2. Combine those components in a way that doesn’t introduce new bugs. To achieve these goals we must examine the fundamentals of how we think about software. Pure Functions Composition in an object-oriented language doesn’t attempt to manage bugs, so it ends up amplifying them. If we want to compose pieces of software, we must discover what creates a fundamentally unbreakable piece, then how to assemble those pieces without producing a broken result. First, what constitutes a reliable, unbreakable piece of software? We’ve already seen that objects are not inherently unbreakable, so we’ll move back to a more basic software component: the function. What are the characteristics of an unbreakable function? What we want is the same kind of function we have in math. This means that the function does nothing except produce a result from its arguments. And given the same arguments, it always produces the same result. This behavior imposes additional constraints: The function cannot affect its environment, and the environment cannot affect the function—otherwise, the function has a history and behaves differently at one point in time vs. another. Running that function doesn’t necessarily produce the same results from one call to the next. If a function affects its environment, we call that a side effect. It’s “on the side” because it’s something other than just producing a result from the function. Many programming languages have side effects built in, in the form of statements. A statement doesn’t return a result, so the only reason to execute a statement is for its side effect. For example, “print” is typically a statement that returns nothing but causes the side effect of displaying text on a console. On the other hand, an expression produces (“expresses”) a result. A functional language avoids statements and attempts to make everything an expression that produces a result. What about the environment affecting the function? This is a bit more subtle, and requires that we think in more general terms than just basic imperative programming. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional? 19 In particular, we must consider concurrency. If multiple tasks are running in our program, then at any point another task might see variables in our function. A variable can change, so that means this other task might see different values at different points in the function’s execution. And if that variable is modified by some other task, we have no way of predicting the result, and we don’t get the reliable mathematical function that we want. We solve this problem through immutability. That is, instead of using variables, we create values that cannot change. This way, it doesn’t matter if an external task sees our values, because it will only see that one value and not something that is different from one moment to the next. And the external task cannot change the value and cause the function to produce a different result. Functions that behave mathematically, that always produce the same results from the same inputs and have no side effects, are called pure functions. When we add the additional constraint of immutability, we produce functions that compose without introducing points of breakage. We can reliably reason about such functions. Composability If functions g and h are pure, we should be able to combine them directly (assuming the types agree) to produce a new function f: f(a) = g(h(a)) This assumes that all functions involved are complete, meaning that they produce a legitimate result for every possible value of a. This is not always true. For example, dividing a number by zero is undefined, and so cannot produce a reasonable number as a result. Using a key to look up a value in a map is undefined if that key doesn’t exist in the map. An incomplete function requires more operations when using it, to handle the problematic inputs. You can think of the solution as stepwise composability. Instead of calling g(h(a)), we break the process into steps: x = h(a), then check the success of the operation. If successful, pass the result to g. These extra steps make composability sound like it could get tedious, and languages like Scala that provide more thorough support for functional programming provide syntax to make this kind of programming feasible. We will look at this support in the [Monads]{{???}} chapter. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional? 20 Effects Now we have created this perfect world of pure functions that behave just like the functions in theoretical mathematics. They have no side effects and cannot be affected by other functions, and can be neatly and safely composed. “But,” you wonder, “if all I can do with the result of one pure function is pass it as an argument to another pure function, what’s the point of all these pure function calls? If these functions have no effect on the world, they seem like an intellectual exercise that merely heats up the CPU.” This is absolutely true. A program that never affects the world is pointless. For a program to be useful, it must be affected by the world, and it must have effects upon the world. The phrase “side effect” implies an incidental or accidental impact on the world. What we need to do is formalize this idea and bring it under our control. We can then call it simply an “effect,” without the “side.” The solution is to manage these effects so they are under our control. This bridge between pure functions and practical programs with controlled and managed effects is the reason for the title of this book. Immutability During Repetition {{ This might be moved somewhere else… }} Many functional programming tutorials begin by introducing recursion. Such tutorials assume you will just accept that recursion is important. This can make the reader wonder whether the entire language will be filled with what seems like theoretical exercises. Any time you perform a repetitive task, you could use recursion, but why would you? It’s much easier to think about an ordinary looping construct. You just count through the elements in a sequence and perform operations upon them. Recursion seems to add needless complexity to an otherwise simple piece of code. The problem is that recursion is not properly motivated in such tutorials. You must first understand the need for immutability, then encounter the problem of repetition Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional? 21 and see that your loop variable(s) mutate. How do you get rid of this mutation? By initializing values but never changing them. To achieve this when you iterate through a sequence, you can create a new frame for each iteration, and what was originally a loop variable becomes a value that is initialized to the next step for each frame. The stack frame of a function call is already set up to hold arguments and return the result. Thus, by creating a stack frame for each iteration, we can initialize the next count of the loop value and never change it within that frame. A recursive function is an excellent solution for the problem of iterating without a mutating loop variable. This solution comes with its own problem. By calling itself and creating a new stack frame for each recursion, a recursive function always has the possibility that it will exhaust (overflow) the stack. The capacity of the stack depends on the size required for that particular function along with the hardware, operating system and often the load on the computer—all factors that make it effectively unpredictable. Having an unpredictable error occur during recursion does not meet our goal of reliability. The fix to this issue is a hack called tail recursion, which typically requires the programmer to organize their code such that the return expression for the recursive function does not perform any additional calculations, but simply returns a finished result. When this criterion is met, the compiler is able to rewrite the recursive code so that it becomes simple imperative code, without the function calls that can lead to a stack overflow. This produces code that is reliable from the safety standpoint (it doesn’t overflow the stack) and from an immutability standpoint (there’s no mutating loop variable). At this point you might be wondering, “Wait, are you telling me that every time I want to perform some kind of operation on a sequence, I’m supposed to write recursive code rather than just an imperative loop?” Although you would certainly get better at recursion with practice, it does sound exhausting. Fortunately, functional programming goes one step further by implementing basic repetitive operations for you, using recursion. This is why you see operations like map, reduce, fold, etc., instead of loops, in functional languages, or even languages that support a functional style of programming. These operations allow you to benefit from the purity of recursion without implementing your own recursive functions except on rare occasions. There’s another fascinating factor that recursion exposes. Under the covers, tail Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional? 22 recursion uses mutation—which seems like a violation of functional programming’s immutability goal. However, because tail recursion is implemented by the compiler, it can be completely (and provably) invisible. No other code can even know about any mutable state used to implement tail recursion, much less read or change it. The concept of immutability only requires that storage be effectively immutable—if something is mutated (often for efficiency), it’s OK as long as no other part of the program can be affected by that mutation. Core Differences Between OO and Functional An OO language worries about managing state. It “encapsulates” a data structure in privacy and surrounds it with custom methods (aka member functions) which are ideally the only way to access and modify the state of that data structure. This is important because an OO data structure is typically mutable. This OO ceremony attempts to create predictability by knowing how the data structure can be mutated. Functional programming abstracts common behavior into reusable functional components. These components are adapted to specific needs using other functions. This is why lambdas are so important, because you constantly need to adapt general code to specific purposes, often with a brief amount of code that would otherwise be awkward and intrusive to right as a standalone function. Functions in a functional language don’t need to be tied to a particular data structure. Thus, they can often be written for more general use and to reduce duplication. Functional languages come with a general set of well-tested, reusable operations that can be applied almost mathematically in many situations. A functional language relies on immutability. An immutable data structure doesn’t need privacy because it is safe for any task to read, and it cannot be written (only initialized). Because immutability dramatically simplifies everything, objects in functional languages are simply naked data structures along with constructors. When everything is immutable, there is no need for private properties or methods to maintain the state of an object. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Why Functional? 23 Summary: Style vs Substance Functional programming abstracts common behavior into reusable functional components. These components are adapted to specific needs using other functions. This is why lambdas are so important, because you constantly need to adapt general code to specific purposes, often with a brief amount of code that would otherwise be awkward and intrusive to write as a standalone function. The two things we do with functions is compose them to make more complex functions, and adapt to them to our specific problem. We assume that many readers are attracted to this book because they have some experience with functional programming constructs in other languages such as Java (version 8 or newer), Kotlin, Python or some other language that provides a modicum of support. However, we also assume you have heard—or you have a sense—that there could be significantly more than, for example, a function’s ability to create other functions, or putting elements into a stream and acting upon that stream with map, or parallelizing stream operations. Those are indeed important benefits, but they just dip into the possibilities. Adopting some of the styles found in functional programming does not make a language functional. In this book we want to get to the heart of what it means to be functional. In particular, we want to show what it takes to make reliable functional code that can be composed without propagating or amplifying flaws in its components. A core way this is accomplished in ZIO is through the use of monads, which we gently introduce in the next chapter. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Effects A pure function accepts arguments and produces a result. Nothing more. Because of this we can combine pure functions and the result will also be a pure function. Pure functions are basically mathematical functions, and so obey the laws of math. This means we can reason about pure functions in a mathematical manner. For example, identical arguments produce the same result every time. It’s as if a pure function is only a lookup table. Because of all the benefits of pure functions, we clearly want to use them everywhere we can. A program composed of pure functions would be very reliable indeed. It would also have no interactions with the outside world, neither reading from nor writing to. A program comprised solely of pure functions has no effect upon the world. Without that, the only evidence that you ran the program is the additional warmth generated by your computer. A program that does interact with the world introduces a lot of uncertainty. In particular, the world is a completely uncertain state, as far as the program is concerned. The output effects of the program depend on the input effects when the program runs. No longer are identical inputs producing the same result, every time. {{???}} With the addition of all this uncertainty, errors become a serious possibility. Errors also have an affect on the world (if they do not there is no point to their existence). The problem is this: functions that affect or are affected by the world do so by directly contacting the world. In the previous chapter we solved the problem of needing to return extra information by boxing all necessary information into the function’s result value, as a monad. In this chapter we solve the problem of effects by boxing further information into the result monad. Doing so captures the effects so we can keep an eye on them. We write as many pure functions as possible, and when we need to do something effect-full, we isolate that in its own function, and limit the spread of uncertainty. The result is not deterministic, but we have isolated the effects. This allows us to much more effectively reason about the behavior of our program. 25 Effects Basics Consider a function that affects its surroundings: trait X object X: var x: Int = 0 def combine(a: Int, b: Int): Int = X.x += 1 a + b + X.x combine(1, 2) // res0: Int = 4 combine(1, 2) // res1: Int = 5 Because combine both writes to and reads from the global variable X.x, identical arguments will not produce the same result every time. combine modifies X.x and also depends on it to produce its result. combine is not pure. We want to manage this effect X. We repeat the trick we used in [Monads] but instead of packaging the return value with failure information, we package it with the type X: trait XIO[IO, R] case class IntXIO(i: Int) extends XIO[X, Int] def combine2(a: Int, b: Int): XIO[X, Int] = X.x += 1 IntXIO(a + b + X.x) combine2(1, 2) // res2: XIO[X, Int] = IntXIO(i = 6) At first glance this doesn’t seem to fix anything. combine2 returns an XIO instead of the simple Int produced by combine. The call to combine2 shows that we still see the side effect. What have we achieved? Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 26 Effects We haven’t prevented the side effect produced by the accesses to X.x, but that is presumably an essential part of the function. What we have done is tracked that effect by tagging it inside the XIO result. The fact that a side effect occurs is now tagged inside the type of XIO, and this information persists at runtime. What can we do with this information? We need to interpret this effect information at runtime. To achieve this we delay the evaluation of the program and hand it to an interpreter, which knows what to do with the effects. {{ This seems challenging (albeit illuminating). If the example were extremely specific (say, an interpreter that only knows about IntXIO) perhaps it could work.}} Effects VS Side-Effects The distinction between the terms effects and side-effects are important. Each represents a fundamentally different way of modeling a program. Side-effecting code observes or changes the world in some way that is not apparent in the type signature. Effectful code signals this in the type signature. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Unit The bare minimum of effect tracking Consider a simple function def saveInformation(info: Any): Unit = ??? If we consider only the types, this function is an Any=>Unit. Unit is the single, blunt tool to indicate effectful functions in plain Scala. When we see it, we know that some type of side-effect is being performed, but without any specificity. When a function returns Unit, we know that the result is an effect. Alternatively, if there are no arguments to the function, then the input is Unit, indicating that an effect is used to produce the result. Consider a simple WeatherService API: trait WeatherService: def forecast(): String If we do not have access to the implementation source code, there is no way to discern what effects are needed at compile time. The only way to figure it out is to run the code and see what happens. ClosedSourceWeatherService().forecast() // READ GPS SIGNAL // NETWORK CALL // res0: String = "Sunny" It is possible that we are using entirely open-source or in-house code throughout our entire application. That means that we could theoretically dig into every function involved in a complex path and note every effect. In practice this quickly becomes impossible. 28 Unit object OpenSourceLibrary: def sendToService(payload: String): Unit = println(s"NETWORK: Sending payload") save(payload) private def save(userData: String): Unit = Analytics.demographicsFrom(userData) println(s"DATABASE: Saving data") object Analytics: def demographicsFrom(userData: String): Unit = println(s"LOGGER: Key demographic found") def logic(): Unit = // ...Other calls... OpenSourceLibrary .sendToService("Network Payload") // ...Other calls... logic() // NETWORK: Sending payload // LOGGER: Key demographic found // DATABASE: Saving data Here our simple program performs 3 very different side-effects, but everything is boiled down to the same Unit type. If we extrapolate this is to a production application with hundreds and thousands of functions, it is overwhelming. Ideally, we could leverage the type system and the compiler to track the requirements for arbitrarily complex pieces of code. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward The ZIO Type We need an Answer about this scenario. The scenario requires things and could produce an error. trait ZIO[Requirements, Error, Answer] One downside of these type parameters The ZIO trait is at the center of our Effect-oriented world. trait ZIO[R, E, A] import zio.ZIO A trait with 3 type parameters can be intimidating, but each one serves a distinct, important purpose. R - The Environment This is the piece that distinguishes the ZIO monad. It indicates which pieces of the world we will be observing or changing. import zio.Console def print( msg: String ): ZIO[Console, Nothing, Unit] = ??? This type signature tells us that print needs a Console in its environment to execute. E - The Error This parameter tells us how this operation might fail. The ZIO Type def parse( contents: String ): ZIO[Any, IllegalArgumentException, Unit] = ??? A - The Result This is what our code will return if it completes successfully. def defaultGreeting() : ZIO[Any, Nothing, String] = ??? Conversions from standard Scala types ZIO provides simple interop with may of the built-in Scala data types, namely • • • • • Option Either Try scala.concurrent.Future Promise And even some Java types • java.util.concurrent.Future • AutoCloseable Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 30 The ZIO Type 31 import zio.{ZIO, ZIOAppDefault} import scala.concurrent.Future import mdoc.unsafeRunPrettyPrint val zFuture = ZIO.fromFuture(implicit ec => Future.successful("Success!") ) // zFuture: ZIO[Any, Throwable, String] = Stateful( // trace = "repl.MdocSession.MdocApp.zFuture(06_The_ZIO_Type.md:47)", // onState = zio.ZIO$$$Lambda$14201/99502783@74ce015f // ) val zFutureFailed = ZIO.fromFuture(implicit ec => Future.failed(new Exception("Failure :(")) ) // zFutureFailed: ZIO[Any, Throwable, Nothing] = Stateful( // trace = "repl.MdocSession.MdocApp.zFutureFailed(06_The_ZIO_Type.md\ :54)", // onState = zio.ZIO$$$Lambda$14201/99502783@599410f9 // ) unsafeRunPrettyPrint(zFuture) // Success! unsafeRunPrettyPrint(zFutureFailed) // java.lang.Exception: Failure :( Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward And / Or Unions AKA Sum Types AKA Enums AKA Ors Note - Avoid official terminology in most prose. Just say “And”/”Or” where appropriate. Scala 3 automatically aggregates the error types by synthesizing an anonymous sum type from the combined errors. Functions usually transform the Answer from one type to another type. Errors often aggregate. import zio.ZIO trait Error1 trait Error2 def failableFunction() : ZIO[Any, Error1 | Error2, Unit] = ??? Consider 2 error types trait UserNotFound trait PermissionError In the type system, the most recent ancestor between them is Any. Unfortunately, you cannot make any meaningful decisions based on this type. graph TD; UserNotFound-->Nothing; PermissionError-->Nothing; We need a more specific way to indicate that our code can fail with either of these types. The | (or) tool provides maximum specificity without the need for inheritance. TODO Figure out how to use pipe symbol in Mermaid 33 And / Or graph TD; UserNotFound-->UserNotFound_OR_PermissionError; PermissionError-->UserNotFound_OR_PermissionError; UserNotFound-->Nothing; PermissionError-->Nothing; Often, you do not care that Nothing is involved at all. The mental model can be simply: graph TD; UserNotFound-->UserNotFound_OR_PermissionError; PermissionError-->UserNotFound_OR_PermissionError; trait User trait SuperUser def getUser( userId: String ): ZIO[UserService, UserNotFound, User] = ??? def getSuperUser( user: User ): ZIO[UserService, PermissionError, SuperUser] = ??? def loginSuperUser(userId: String): ZIO[ UserService, UserNotFound | PermissionError, SuperUser ] = for basicUser <- getUser(userId) superUser <- getSuperUser(basicUser) yield superUser trait Status trait NetworkService def statusOf( user: User ): ZIO[NetworkService, UserNotFound, Status] = ??? Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 34 And / Or def check(userId: String): ZIO[ UserService & NetworkService, UserNotFound, Status ] = for user <- getUser(userId) status <- statusOf(user) yield status Intersections AKA Products AKA Case Classes AKA Ands graph TD; Any-->User; Any-->Account; trait Piece1 trait Piece2 def needyFunction() : ZIO[Piece1 & Piece1, Nothing, Unit] = ??? For your Answer, it can be desirable to give a clear name that is relevant to your domain. The requirements for each ZIO are combined as an anonymous product type denoted by the & symbol. trait AccountService trait UserService Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 35 And / Or import zio.ZIO trait Account trait AccountError def userToAccount( user: User ): ZIO[AccountService, AccountError, Account] = ??? def getAccount(userId: String): ZIO[ UserService & AccountService, AccountError | UserNotFound, Account ] = for user <- getUser(userId) account <- userToAccount(user) yield account case class SomeServices(userService: UserService, accountService: Accou\ ntService) //trait SomeServices extends UserService with AccountService You have the ability to handle all the possible errors from your logic without needing to create a new name that encompasses all of them. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Built-in Services Some Services are considered fundamental/primitive by ZIO. They are built-in to the runtime and available to any program. 1.x Originally, ZIO merely provided default implementations of these services. There was nothing else special about them. If you wanted to write to the console in your code, you needed a ZIO[Console, _, _]. If you wanted to write random, timestamped numbers, accompanied by some system information to the console, you needed a ZIO[Random with Clock with System with Console, _, _]. This is maximally informative, but it is also a lot of boilerplate code. 2.x Late in the development of ZIO 2.0, the team decided to bake these deeper into the runtime. Now you can use any of these services without an impact on your method signatures. This reduces boilerplate, with a trade-off. You can no longer discern which piece of the Environment/Runtime is being accessed by reading the signature. Console NON-MDOC examples throughout this file after 2.0.0 upgrade. TODO Fix before release The Unprincipled Way This is generally the first effect that we will want as we learn to construct functional programs. It is so basic that most languages do not consider it as anything special. The typical first scala program is something like: println("Hi there.") // Hi there. Simple enough, and familiar to anyone that has programmed before. Take a look at the signature of this function in the Scala Predef object: def println(x: Any): Unit = ??? Based on the name, it is likely that the Console is involved. Unfortunately the type signature does not indicate that. If we do not have access to the implementation source code, this is a surprise to us at runtime. Building a Better Way Before looking at the official ZIO implementation, we will create a simpler version. TODO: Decide whether explaining this pattern belongs in a standalone section. It is important in isolation, but probably hard to appreciate without a use-case, and Console is likely the simplest example. The pattern used here is fundamental to designing composable, ergonomic ZIO Services. 38 Console 1. 2. 3. 4. Create a trait with the needed functions. Create an implementation of the trait. (Optional) Put “accessor” methods in trait companion object. (Optional) Provide implementation instance in a Layer as a object field - live. We will go through each of these steps in detail in this chapter, and more concisely in the rest. Steps 1 and 2 steps will be familiar to many programmers. Steps 3 and 4 are less familiar, and a bit harder to appreciate. We endeavor in the following chapters to make a compelling case for them. If we succeed, the reader will add them when creating their own Effects. One: Create the trait This trait represents a piece of the Environment that our codes need to interact with. It contains the methods for effectful interactions. import zio.ZIO trait Console: def printLine( output: String ): ZIO[Any, Nothing, Unit] Two: Create the implementation object ConsoleLive extends Console: def printLine( output: String ): ZIO[Any, Nothing, Unit] = // TODO Get this working without Predef ZIO.succeed(Predef.println(output)) TODO{Determine how to best split the 2 pieces we need to add to the same object for these steps} Three: Create Accessor Methods in Companion The first two steps are enough for us to track Effects in our system, but the ergonomics are not great. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 39 Console val logicClunky: ZIO[Console, Nothing, Unit] = for _ <ZIO.serviceWithZIO[Console]( _.printLine("Hello") ) _ <ZIO.serviceWithZIO[Console]( _.printLine("World") ) yield () import mdoc.unsafeRunPrettyPrint import zio.ZLayer unsafeRunPrettyPrint( logicClunky.provide( ZLayer.succeed[Console](ConsoleLive) ) ) // Hello // World // () The caller has to handle the ZIO environment access, which is a distraction from the logic they want to implement. // TODO Consider deleting this entirely // TODO remove alt companions and make top-level // functions object ConsoleWithAccessor: def printLine( variable: => String ): ZIO[Console, Nothing, Unit] = ZIO.serviceWith(_.printLine(variable)) With this function, our callers have a much nicer experience. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 40 Console val logic: ZIO[Console, Nothing, Unit] = for _ <- ConsoleWithAccessor.printLine("Hello") _ <- ConsoleWithAccessor.printLine("World") yield () However, providing dependencies to the logic is still tedious. import zio.ZLayer import zio.Runtime.default.unsafe unsafeRunPrettyPrint( logic.provide( ZLayer.succeed[Console](ConsoleLive) ) ) // () Four: Create object Effect.live field Rather than making each caller wrap our instance in a Layer, we can do that a single time in our companion. import zio.ZLayer object ConsoleWithLayer: val live: ZLayer[Any, Nothing, Console] = ZLayer.succeed[Console](ConsoleLive) Now executing our code is as simple as describing it. unsafeRunPrettyPrint( logic.provide(ConsoleWithLayer.live) ) // () In real application, both of these will go in the companion object directly. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 41 Console import zio.ZLayer object Console: def printLine( variable: => String ): ZIO[Console, Nothing, Unit] = ZIO.serviceWith(_.printLine(variable)) val live: ZLayer[Any, Nothing, Console] = ZLayer.succeed[Console](ConsoleLive) Official ZIO Approach TODO ZIO Super-Powers Single expression debugging When debugging code, we often want to stick a println among our logic. def crunch(a: Int, b: Int) = (a * 2) / (a * 10) Historically, this has caused friction for chained expressions. We must surround our expression in braces, in order to add this statement before it. TODO Disclaimer that this is less compelling in a “fewer braces” world def crunchDebugged(a: Int, b: Int) = println("") a * a Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 42 Console unsafeRunPrettyPrint( ZIO.debug("ping") *> ConsoleLive.printLine("Normal logic") ) // ping // Normal logic // () object ConsoleSanitized extends Console: private val socialSecurity = "\\d{3}-\\d{2}-\\d{4}" def printLine( output: String ): ZIO[Any, Nothing, Unit] = val sanitized = output.replaceAll( socialSecurity, "***-**-****" ) ConsoleLive.printLine(sanitized) val leakSensitiveInfo : ZIO[Console, java.io.IOException, Unit] = zio .Console .printLine("Customer SSN is 000-00-0000") unsafeRunPrettyPrint( leakSensitiveInfo.provide( ZLayer.succeed[Console](ConsoleSanitized) ) ) // Customer SSN is 000-00-0000 // () Automatically attached experiments. These are included at the end of this chapter because their package in the experiments directory matched the name of this chapter. Enjoy working on the code with full editor capabilities :D Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 43 Console experiments/src/main/scala/console/FakeConsole.scala package console import zio._ import zio.Console import zio.Console._ import java.io.IOException object FakeConsole: val name: Console = single("(default name)") val word: Console = single("Banana") val number: Console = single("1") def single(hardcodedInput: String) = new Console: def print(line: => Any)(implicit trace: zio.Trace ): zio.IO[java.io.IOException, Unit] = ZIO.succeed(print("Hard-coded: " + line)) def printError(line: => Any)(implicit trace: zio.Trace ): zio.IO[java.io.IOException, Unit] = ??? def printLine(line: => Any)(implicit trace: zio.Trace ): zio.IO[java.io.IOException, Unit] = ZIO.succeed( println("Hard-coded: " + line) ) def printLineError(line: => Any)(implicit trace: zio.Trace ): zio.IO[java.io.IOException, Unit] = ??? def readLine(implicit trace: zio.Trace ): zio.IO[java.io.IOException, String] = ZIO.succeed(hardcodedInput) def withInput( Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 44 Console hardcodedInput: String* ): ZIO[Any, Nothing, Console] = for inputVariable <Ref.make(hardcodedInput.toSeq) yield inputConsole(inputVariable) private def inputConsole( hardcodedInput: Ref[Seq[String]] ) = new Console: def print(line: => Any)(implicit trace: zio.Trace ): zio.IO[java.io.IOException, Unit] = ZIO.succeed(print(line)) def printError(line: => Any)(implicit trace: zio.Trace ): zio.IO[java.io.IOException, Unit] = ??? def printLine(line: => Any)(implicit trace: zio.Trace ): zio.IO[java.io.IOException, Unit] = ZIO .succeed(println("Automated: " + line)) def printLineError(line: => Any)(implicit trace: zio.Trace ): zio.IO[java.io.IOException, Unit] = ??? def readLine(implicit trace: zio.Trace ): zio.IO[java.io.IOException, String] = for curInput <- hardcodedInput.get _ <- hardcodedInput.set(curInput.tail) yield curInput.head end FakeConsole Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Mutability Functional programmers often sing the praises of immutability. The advantages are real and numerous. However, it is easy to find situations that are intrinsically mutable. • How many people are currently inside a building? • How much fuel is in your car? • How much money is in your bank account? Rather than avoiding mutability entirely, we want to avoid unprincipled, unsafe mutability. If we codify and enumerate everything that we need from Mutability, then we can wield it safely. Required Operations: • Update the value • Read the current value These are both effectful operations. import zio.UIO trait RefZ[A]: def get: UIO[A] def update(a: A => A): UIO[Unit] Less obviously, we also need to create the Mutable reference itself. We are changing the world, by creating a space that we can manipulate. This operation can live in the companion object: object RefZ: def make[A](a: A): UIO[RefZ[A]] = ??? In order to confidently use this, we need certain guarantees about the behavior: • The underlying value cannot be changed during a read • Multiple writes cannot happen concurrently, which would result in lost updates Unreliable Counting 46 Mutability import zio.{Ref, ZIO} import mdoc.unsafeRunPrettyPrint object UnreliableCounting: var counter = 0 val increment = ZIO.succeed { counter = counter + 1 } val logic = for _ <ZIO.foreachParDiscard(Range(0, 100000))( _ => increment ) yield "Final count: " + counter unsafeRunPrettyPrint(UnreliableCounting.logic) // Final count: 99971 Due to the unpredictable nature of shared mutable state, we do not know exactly what the final count above is. Each time we publish a copy of this book, the code is reexecuted and a different wrong result is generated. However, conflicts are extremely likely, so some of our writes get clobbered by others, and we end up with less than the expected 100,000. Ultimately, we lose information with this approach. TODO Demo/diagram parallel writes Performing our side effects inside ZIO’s does not magically make them safe. We need to fully embrace the ZIO components, utilizing Ref for correct mutation. Reliable Counting Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 47 Mutability object ReliableCounting: def incrementCounter(counter: Ref[Int]) = counter.update(_ + 1) val logic = for counter <- Ref.make(0) _ <ZIO.foreachParDiscard(Range(0, 100000))( _ => incrementCounter(counter) ) finalResult <- counter.get yield "Final count: " + finalResult unsafeRunPrettyPrint(ReliableCounting.logic) // Final count: 100000 Now we can say with full confidence that our final count is 100000. Additionally, these updates happen without blocking. This is achieved through a strategy called “Compare & Swap”, which we will not cover in detail. TODO Link/reference supplemental reading Although there are significant advantages; a basic Ref is not the solution for everything. We can only pass pure functions into update. The API of the plain Atomic Ref steers you in the right direction by not accepting ZIOs as parameters to any of its methods. To demonstrate why this restriction exists, we will deliberately undermine the system by sneaking in a side effect. First, we will create a helper function that imitates a long-running calculation. def expensiveCalculation() = Thread.sleep(35) Our side effect will be a mock alert that is sent anytime our count is updated: scala def sendNotification() = println("Alert: We have updated our count!") Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 48 Mutability object SideEffectingUpdates: val logic = for counter <- Ref.make(0) _ <ZIO.foreachParDiscard(Range(0, 4))(_ => counter.update { previousValue => expensiveCalculation() sendNotification() previousValue + 1 } ) finalResult <- counter.get yield "Final count: " + finalResult unsafeRunPrettyPrint(SideEffectingUpdates.logic) // Alert: We have updated our count! // Alert: We have updated our count! // Alert: We have updated our count! // Alert: We have updated our count! // Alert: We have updated our count! // Alert: We have updated our count! // Alert: We have updated our count! // Alert: We have updated our count! // Alert: We have updated our count! What is going on?! Previously, we were losing updates because of unsafe mutability. Now, we have the opposite problem! We are sending far more alerts than intended, even though we can see that our final count is 4. TODO This section will need significant attention and polish Now we must consider the limitations of the “Compare & Swap” system. It achieves lock-free performance by letting each fiber freely make their updates, and then doing a last-second check to see if the underlying value changed during its update. If the value has not changed, the update is made. If it has changed, then the entire function that was passed into update is re-executed until it completes with a stable value. The higher the parallelism, or the longer the operation takes, the higher the likelihood of a compare-and-swap retry. This retry behavior is safe with pure functions, which can be executed an arbitrary number of times. However, it is completely inappropriate for effects, which should Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 49 Mutability only be executed a single time. For these situations, we need a specialized variation of Ref Ref.Synchronized Ref.Synchronized guarantees only a single execution of the update body and any of the effects contained inside. The only change required is replacing Ref.make with Ref.Synchronized.make object SideEffectingUpdatesSync: val logic = for counter <- Ref.Synchronized.make(0) _ <ZIO.foreachParDiscard(Range(0, 4))(_ => counter.update { previousValue => expensiveCalculation() sendNotification() previousValue + 1 } ) finalResult <- counter.get yield "Final count: " + finalResult unsafeRunPrettyPrint( SideEffectingUpdatesSync.logic ) // Alert: We have updated our count! // Alert: We have updated our count! // Alert: We have updated our count! // Alert: We have updated our count! Now we see exactly the number of alerts that we expected. This correctness comes with a cost though, as the name of this type implies. Each of your updates will run sequentially, despite initially launching them all in parallel. This is the only known way to avoid retries. Try to structure your code to minimize the coupling between effects and updates, and use this type only when necessary. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 50 Mutability Automatically attached experiments. These are included at the end of this chapter because their package in the experiments directory matched the name of this chapter. Enjoy working on the code with full editor capabilities :D experiments/src/main/scala/mutability/ComplexRefs.scala package mutability import zio.{Ref, ZIO, ZIOAppDefault} object ComplexRefs extends ZIOAppDefault: class Sensor(lastReading: Ref[SensorData]): def read: ZIO[Any, Nothing, SensorData] = zio .Random .nextIntBounded(10) .map(SensorData(_)) object Sensor: val make: ZIO[Any, Nothing, Sensor] = for lastReading <- Ref.make(SensorData(0)) yield Sensor(lastReading) case class SensorData(value: Int) case class World(sensors: List[Sensor]) val readFromSensors = for sensors <ZIO.foreach(List.fill(100)(0))(_ => Sensor.make ) world = World(sensors) _ <ZIO Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 51 Mutability .foreach(world.sensors)(_.read) .debug("Current data: ") yield () def run = readFromSensors end ComplexRefs Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Time Time based functions are effectful because they rely on a variable that is constantly changing. Your program displays 2 sections: Summary -Time range -totalNumberOfTransactions -All Participants Details - List[Transaction] Show how these can be out of sync with unprincipled Clock access .now() How often it is overlooked/minimized “Race Condition” vs “race operation” Example possibilities - Progress bar - query(largeRange) followed by query(smallRange), and getting new results in the 2nd call Automatically attached experiments. These are included at the end of this chapter because their package in the experiments directory matched the name of this chapter. Enjoy working on the code with full editor capabilities :D experiments/src/main/scala/time/OutOfSync.scala 53 Time package time import java.time.{Instant, Period} import zio.{IO, UIO, ZIO, ZIOAppDefault} object OutOfSync // TODO Consider deduping User throughout the book case class Post(content: String) case class Summary(numberOfPosts: Int) case class TransactionDetails( transactions: Seq[Post] ) object User: case class User(name: String) val frop = User("Frop") val zeb = User("Zeb") val shtep = User("Shtep") val cheep = User("Cheep") import time.User.* case class UserUI( user: User, summary: Summary, transactionDetails: Seq[Post] ) object TimeIgnorant: private var summaryCalledTime : Option[Instant] = None def summaryFor( participant: User ): UIO[Summary] = summaryCalledTime match case Some(value) => () case None => summaryCalledTime = Some(Instant.now()) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 54 Time ZIO.succeed(Summary(1)) def postsBy( participant: User ): IO[String, Seq[Post]] = val executionTimeStamp = Instant.now() for _ <ZIO .getOrFailWith( "Must call summary before posts" )(summaryCalledTime) .flatMap(timeStamp => ZIO.debug( "Summary called: " + timeStamp ) ) _ <ZIO.debug( "Getting posts: " + executionTimeStamp ) yield Seq(Post("Hello!"), Post("Goodbye!")) end postsBy end TimeIgnorant object DemoSyncIssues extends ZIOAppDefault: def run = for summary <- TimeIgnorant.summaryFor(shtep) transactions <- TimeIgnorant.postsBy(shtep) uiContents = UserUI(shtep, summary, transactions) _ <- zio.Console.printLine(uiContents) yield () experiments/src/main/scala/time/ScheduledValues.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 55 Time package time import import import import import import import zio.Duration zio.Clock zio.ZIO zio.URIO zio.Schedule zio.ExitCode zio.durationInt import java.util.concurrent.TimeUnit import java.time.Instant import scala.concurrent.TimeoutException import javawrappers.InstantOps.plusZ /* * * * * Goal: If I accessed this from: 0-1 seconds, I would get "First Value" 1-4 seconds, I would get "Second Value" 4-14 seconds, I would get "Third Value" 14+ seconds, it would fail */ // TODO Consider TimeSequence as a name def scheduledValues[A]( value: (Duration, A), values: (Duration, A)* ): ZIO[ Any, // construction time Nothing, ZIO[ Any, // access time TimeoutException, A ] ] = for startTime <- Clock.instant timeTable = createTimeTableX( startTime, value, values* // Yay Scala3 :) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 56 Time ) yield accessX(timeTable) // TODO Some comments, tests, examples, etc to // make this function more obvious private[time] def createTimeTableX[A]( startTime: Instant, value: (Duration, A), values: (Duration, A)* ): Seq[ExpiringValue[A]] = values.scanLeft( ExpiringValue( startTime.plusZ(value._1), value._2 ) ) { case ( ExpiringValue(elapsed, _), (duration, value) ) => ExpiringValue( elapsed.plusZ(duration), value ) } /** Input: (1 minute, "value1") (2 minute, * "value2") * * Runtime: Zero value: (8:00 + 1 minute, * "value1") * * case ((8:01, _) , (2.minutes, "value2")) => * (8:01 + 2.minutes, "value2") * * Output: ( ("8:01", "value1"), ("8:03", * "value2") ) */ private[time] def accessX[A]( timeTable: Seq[ExpiringValue[A]] ): ZIO[Any, TimeoutException, A] = for Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 57 Time now <- Clock.instant result <ZIO.getOrFailWith( new TimeoutException("TOO LATE") ) { timeTable .find(_.expirationTime.isAfter(now)) .map(_.value) } yield result private case class ExpiringValue[A]( expirationTime: Instant, value: A ) experiments/src/main/scala/time/TimedTapTap.scala package time import zio.* import zio.Console.* val longRunning = ZIO.sleep(5.seconds) *> printLine("done") val runningNotifier = ( ZIO.sleep(1.seconds) *> printLine("Still running") ).onInterrupt { printLine("finalized").orDie } object TimedTapTapJames extends ZIOAppDefault: def run = for lr <- longRunning.fork _ <- runningNotifier.fork _ <- lr.join Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 58 Time yield () object TimedTapTapBill extends ZIOAppDefault: def run = longRunning .race(runningNotifier *> ZIO.never) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Environment Variables Historic Approach Environment Variables are a common way of providing dynamic and/or sensitive data to your running application. A basic use-case looks like this: val apiKey = sys.env.get("API_KEY") // apiKey: Option[String] = Some(value = "SECRET_API_KEY") This seems rather innocuous; however, it can be an annoying source of problems as your project is built and deployed across different environments. Given this API: trait HotelApi: def cheapest( zipCode: String, apiKey: String ): Either[Error, Hotel] case class Hotel(name: String) case class Error(msg: String) To augment the built-in environment function, we will create a wrapper. def envRequiredUnsafe( variable: String ): Either[Error, String] = sys .env .get(variable) .toRight(Error("Unconfigured Environment")) toRight is an Option method that turns the Option into an Either. Our business logic now looks like this: Environment Variables 60 def fancyLodgingUnsafe( hotelApi: HotelApi ): Either[Error, Hotel] = for apiKey <- envRequiredUnsafe("API_KEY") hotel <- hotelApi.cheapest("90210", apiKey) yield hotel When you look up an Environment Variable, you are accessing information that was not passed into your function as an explicit argument. Now we will simulate running the function with the same arguments in 3 different environments. Your Machine: fancyLodgingUnsafe(HotelApiImpl) // res0: Either[Error, Hotel] = Right( // value = Hotel(name = "Eddy's Roach Motel") // ) Collaborator’s Machine: fancyLodgingUnsafe(HotelApiImpl) // res2: Either[Error, Hotel] = Left( // value = Error(msg = "Invalid API Key") // ) Continuous Integration Server: fancyLodgingUnsafe(HotelApiImpl) // res4: Either[Error, Hotel] = Left( // value = Error( // msg = "Unconfigured Environment" // ) // ) On your own machine, everything works as expected. However, your collaborator has a different value stored in this variable, and gets a failure when they execute this code. Finally, the CI server has not set any value, and fails at runtime. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Environment Variables 61 Building a Better Way Before looking at the official ZIO implementation of System, we will create a less-capable version. We need a trait that will indicate what is needed from the environment. The real implementation is a bit more complex, to handle corner cases. import zio.ZIO trait System: def env( variable: String ): ZIO[Any, Nothing, Option[String]] Now, our live implementation will wrap our original, unsafe function call. For easier usage by the caller, we also create an accessor. import zio.ZLayer object System: object Live extends System: def env( variable: String ): ZIO[Any, Nothing, Option[String]] = ZIO.succeed(sys.env.get("API_KEY")) val live: ZLayer[Any, Nothing, System] = ZLayer.succeed(Live) def env( variable: => String ): ZIO[System, Nothing, Option[String]] = ZIO.serviceWithZIO[System](_.env(variable)) Now if we use this code, our caller’s type tells us that it requires a System to execute. This is safe, but it is not the easiest code to use or read. We then build on first accessor to flatten out the function signature. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Environment Variables trait SystemStrict: def envRequired( variable: String ): ZIO[Any, Error, String] object SystemStrict: val live : ZLayer[System, Nothing, SystemStrict] = ZLayer .fromZIO(ZIO.service[System].map(Live(_))) def envRequired( variable: String ): ZIO[SystemStrict, Error, String] = ZIO.serviceWithZIO[SystemStrict]( _.envRequired(variable) ) case class Live(system: System) extends SystemStrict: def envRequired( variable: String ): ZIO[Any, Error, String] = for variableAttempt <- system.env(variable) res <ZIO .fromOption(variableAttempt) .mapError(_ => Error("Unconfigured Environment") ) yield res end SystemStrict Similarly, we wrap our API in one that leverages ZIO. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 62 Environment Variables 63 trait HotelApiZ: def cheapest( zipCode: String ): ZIO[Any, Error, Hotel] object HotelApiZ: def cheapest(zipCode: String): ZIO[ SystemStrict with HotelApiZ, Error, Hotel ] = ZIO.serviceWithZIO[HotelApiZ]( _.cheapest(zipCode) ) case class Live(system: SystemStrict) extends HotelApiZ: def cheapest( zipCode: String ): ZIO[Any, Error, Hotel] = for apiKey <- system.envRequired("API_KEY") res <ZIO.fromEither( HotelApiImpl .cheapest(zipCode, apiKey) ) yield res val live: ZLayer[ SystemStrict, Nothing, HotelApiZ ] = ZLayer.fromZIO( ZIO.service[SystemStrict].map(Live(_)) ) end HotelApiZ This helps us keep a flat Error channel when we write our domain logic. This was quite a process; where did it get us? Our fully ZIO-centric, side-effect-free Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Environment Variables 64 logic looks like this: val fancyLodging: ZIO[ SystemStrict with HotelApiZ, Error, Hotel ] = for hotel <- HotelApiZ.cheapest("90210") yield hotel // fancyLodging: ZIO[SystemStrict & HotelApiZ, Error, Hotel] = OnSucces\ s( // trace = "repl.MdocSession.MdocApp.fancyLodging(12_Environment_Vari\ ables.md:262)", // first = OnSuccess( // trace = "repl.MdocSession.MdocApp.HotelApiZ.cheapest(12_Environm\ ent_Variables.md:226)", // first = Sync( // trace = "repl.MdocSession.MdocApp.HotelApiZ.cheapest(12_Enviro\ nment_Variables.md:226)", // eval = zio.ZIOCompanionVersionSpecific$$Lambda$14266/136541955\ 2@2c0f4f23 // ), // successK = zio.ZIO$$$Lambda$14242/2025822708@2b1a7c5c // ), // successK = zio.ZIO$$Lambda$14229/567850394@7c462daa // ) Original, unsafe: def fancyLodgingUnsafe( hotelApi: HotelApi ): Either[Error, Hotel] = for apiKey <- envRequiredUnsafe("API_KEY") hotel <- hotelApi.cheapest("90210", apiKey) yield hotel The logic is identical to our original implementation! The only difference is the result type. It now reports the System and HotelApiZ dependencies of our function. This is what it looks like in action: Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Environment Variables import zio.ZLayer import mdoc.unsafeRunPrettyPrint import mdoc.unsafeRunPrettyPrintValue Your Machine: // TODO Do this for CI environment too val originalAuthor = HotelApiZ.live unsafeRunPrettyPrint( fancyLodging.provideLayer( System.live >>> SystemStrict.live >+> originalAuthor ) ) // Hotel(Eddy's Roach Motel) Collaborator’s Machine: // TODO Do this for CI environment too val collaborater = HotelApiZ.live val colaboraterLayer = collaborater ++ System.live unsafeRunPrettyPrint( fancyLodging.provideLayer( System.live >>> SystemStrict.live >+> collaborater ) ) // Error(Invalid API Key) Continuous Integration Server: val ci = HotelApiZ.live Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 65 Environment Variables 66 unsafeRunPrettyPrint( fancyLodging.provideLayer( System.live >>> SystemStrict.live >+> ci ) ) // Error(Unconfigured Environment) TODO{{The actual line looks the same, which I highlighted as a problem before. How should we indicate that the Environment is different?}} When constructed this way, it becomes very easy to test. We create a second implementation that accepts test values and serves them to the caller. case class SystemHardcoded( environmentVars: Map[String, String] ) extends System: def env( variable: String ): ZIO[Any, Nothing, Option[String]] = ZIO.succeed(environmentVars.get(variable)) We can now provide this to our logic, for testing both the success and failure cases. val testApiLayer = ZLayer.succeed[System]( SystemHardcoded( Map("API_KEY" -> "Invalid Key") ) ) >>> SystemStrict.live >+> HotelApiZ.live import mdoc.unsafeRunPrettyPrint unsafeRunPrettyPrint( fancyLodging.provide(testApiLayer) ) // Error(Invalid API Key) Official ZIO Approach ZIO provides a more complete System API in the zio.System TODO Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Environment Variables import zio.System def fancyLodgingZ(): ZIO[ zio.System, SecurityException, Either[Error, Hotel] ] = for apiKey <- zio.System.env("API_KEY") yield HotelApiImpl.cheapest( "90210", apiKey.get // unsafe! TODO Use either ) Exercises import zio.test.TestSystem import zio.test.TestSystem.Data Exercise 1: Create a function will report missing Environment Variables as NoSuchElementException failures, instead of an Option success case. trait Exercise1: def envOrFail(variable: String): ZIO[ zio.System, SecurityException | NoSuchElementException, String ] Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 67 Environment Variables val exercise1case1 = unsafeRunPrettyPrintValue( Exercise1Solution .envOrFail("key") .provide( TestSystem.live( Data(envs = Map("key" -> "value")) ) ) ) // value // exercise1case1: String = "value" assert(exercise1case1 == "value") val exercise1case2 = unsafeRunPrettyPrintValue( Exercise1Solution .envOrFail("key") .catchSome { case _: NoSuchElementException => ZIO.succeed("Expected Error") } .provide( TestSystem.live(Data(envs = Map())) ) ) // Expected Error // exercise1case2: String = "Expected Error" assert(exercise1case2 == "Expected Error") Exercise 2: Create a function will attempt to parse a value as an Integer and report errors as a NumberFormatException. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 68 Environment Variables trait Exercise2: def envInt(variable: String): ZIO[ Any, NoSuchElementException | NumberFormatException, Int ] Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 69 Random There {{Subject Dependencies: Console, ZIO.serviceWith}} TODO All the prose to justify these hoops NOTE Moved code to experiments/src/main/scala/random due to dependency on code not in Chapters Automatically attached experiments. These are included at the end of this chapter because their package in the experiments directory matched the name of this chapter. Enjoy working on the code with full editor capabilities :D experiments/src/main/scala/random/Examples.scala package random import scala.util.Random def rollDice(): Int = Random.nextInt(6) + 1 @main def randNumEx = println(rollDice()) println(rollDice()) enum GameState: case InProgress(roundResult: String) case Win case Lose 71 Random def scoreRound(input: Int): GameState = input match case 6 => GameState.Win case 1 => GameState.Lose case _ => GameState.InProgress("Attempt: " + input) def fullRound(): GameState = val roll = rollDice() scoreRound(roll) @main def playASingleRound() = println(fullRound()) import zio.ZIO val rollDiceZ : ZIO[RandomBoundedInt, Nothing, Int] = RandomBoundedInt.nextIntBetween(1, 7) import zio.{ZIO, ZIOAppDefault} object RollTheDice extends ZIOAppDefault: val logic = for roll <- rollDiceZ _ <- ZIO.debug(roll) yield () def run = logic.provideLayer(RandomBoundedInt.live) val fullRoundZ : ZIO[RandomBoundedInt, Nothing, GameState] = for roll <- rollDiceZ yield scoreRound(roll) // The problem above is that you can test the winner logic completely s\ eparate from the random number generator. // The next example cannot be split so easily. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 72 Random import zio.Ref val threeChances = for remainingChancesR <- Ref.make(3) finalGameResult <( for roll <- rollDiceZ remainingChances <remainingChancesR.getAndUpdate(_ - 1) yield if (remainingChances == 0) GameState.Lose else scoreRound(roll) ).repeatWhile { case GameState.InProgress(_) => true case _ => false } _ <ZIO.debug( "Final game result: " + finalGameResult ) yield () object ThreeChances extends ZIOAppDefault: def run = threeChances.provide( RandomBoundedIntFake.apply(Seq(2, 5, 6)) ) object LoseInTwoChances extends ZIOAppDefault: def run = threeChances.provide( RandomBoundedIntFake.apply(Seq(2, 1)) ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 73 Random experiments/src/main/scala/random/RandomBoundedInt.scala package random import zio.{Tag, UIO, ZIO, ZIOAppArgs} import scala.util.Random trait RandomBoundedInt: def nextIntBetween( minInclusive: Int, maxExclusive: Int ): UIO[Int] import zio.{UIO, ZIO, ZLayer} import scala.util.Random object RandomBoundedInt: def nextIntBetween( minInclusive: Int, maxExclusive: Int ): ZIO[RandomBoundedInt, Nothing, Int] = ZIO.serviceWithZIO[RandomBoundedInt]( _.nextIntBetween( minInclusive, maxExclusive ) ) object RandomBoundedIntLive extends RandomBoundedInt: override def nextIntBetween( minInclusive: Int, maxExclusive: Int ): UIO[Int] = ZIO.succeed( Random .between(minInclusive, maxExclusive) ) val live Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 74 Random : ZLayer[Any, Nothing, RandomBoundedInt] = ZLayer.succeed(RandomBoundedIntLive) end RandomBoundedInt experiments/src/main/scala/random/RandomBoundedIntFake. package random import zio.{Ref, UIO, ZIO, ZLayer} class RandomBoundedIntFake private ( values: Ref[Seq[Int]] ) extends RandomBoundedInt: def nextIntBetween( minInclusive: Int, maxExclusive: Int ): UIO[Int] = for remainingValues <- values.get nextValue <if (remainingValues.isEmpty) ZIO.die( new Exception( "Did not provide enough values!" ) ) else ZIO.succeed(remainingValues.head) _ <- values.set(remainingValues.tail) yield remainingValues.head end RandomBoundedIntFake object RandomBoundedIntFake: def apply( values: Seq[Int] ): ZLayer[Any, Nothing, RandomBoundedInt] = ZLayer.fromZIO( for valuesR <- Ref.make(values) yield new RandomBoundedIntFake(valuesR) ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 75 Random experiments/src/main/scala/random/RandomGuessingGame.sc package random import zio.{Console, UIO, Unsafe, ZIO, ZLayer} //import console.FakeConsole import zio.Runtime.default.unsafe val low = 1 /* val high = 10 * * val prompt = * s"Pick a number between $low and $high: " * * // TODO Determine how to handle .toInt failure * // possibility def checkAnswer( answer: Int, * guess: String ): String = * if answer == guess.toInt then "You got it!" * else s"BZZ Wrong!! Answer was $answer" * * val sideEffectingGuessingGame = * for _ <- Console.print(prompt) answer = * scala.util.Random.between(low, high) guess <* Console.readLine response = * checkAnswer(answer, guess) yield prompt + * guess + "\n" + response * * @main def runSideEffectingGuessingGame = * Unsafe.unsafe { (u: Unsafe) => given Unsafe = * u unsafe .run( * sideEffectingGuessingGame.provideLayer( * ZLayer.succeed(FakeConsole.single("3")) ) ) * .getOrThrowFiberFailure() } * * import zio.Console.printLine * * val effectfulGuessingGame = * for _ <- Console.print(prompt) answer <* RandomBoundedInt.nextIntBetween(low, high) * guess <- Console.readLine response = * checkAnswer(answer, guess) yield prompt + Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 76 Random * * * * * * * * * guess + "\n" + response @main def runEffectfulGuessingGame = Unsafe.unsafe { (u: Unsafe) => given Unsafe = u unsafe .run( effectfulGuessingGame.provideLayer( ZLayer .succeed(FakeConsole.single("3")) ++ RandomBoundedInt.live ) ) .getOrThrowFiberFailure() } */ experiments/src/main/scala/random/RandomZIOFake.scala package random import zio.{ BuildFrom, Chunk, Console, Random, UIO, ZIO, ZLayer, Trace } import zio.Console.printLine import java.util.UUID class RandomZIOFake(i: Int) extends Random: def nextUUID(implicit trace: Trace ): UIO[UUID] = ??? def nextBoolean(implicit trace: zio.Trace ): zio.UIO[Boolean] = ??? def nextBytes(length: => Int)(implicit trace: zio.Trace ): zio.UIO[zio.Chunk[Byte]] = ??? def nextDouble(implicit trace: zio.Trace Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 77 Random ): zio.UIO[Double] = ??? def nextDoubleBetween( minInclusive: => Double, maxExclusive: => Double )(implicit trace: zio.Trace): zio.UIO[Double] = ??? def nextFloat(implicit trace: zio.Trace ): zio.UIO[Float] = ??? def nextFloatBetween( minInclusive: => Float, maxExclusive: => Float )(implicit trace: zio.Trace): zio.UIO[Float] = ??? def nextGaussian(implicit trace: zio.Trace ): zio.UIO[Double] = ??? def nextInt(implicit trace: zio.Trace ): zio.UIO[Int] = ??? def nextIntBetween( minInclusive: => Int, maxExclusive: => Int )(implicit trace: zio.Trace): zio.UIO[Int] = ??? def nextIntBounded(n: => Int)(implicit trace: zio.Trace ): zio.UIO[Int] = ??? def nextLong(implicit trace: zio.Trace ): zio.UIO[Long] = ??? def nextLongBetween( minInclusive: => Long, maxExclusive: => Long )(implicit trace: zio.Trace): zio.UIO[Long] = ??? def nextLongBounded(n: => Long)(implicit trace: zio.Trace ): zio.UIO[Long] = ??? def nextPrintableChar(implicit trace: zio.Trace ): zio.UIO[Char] = ??? Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 78 Random def nextString(length: => Int)(implicit trace: zio.Trace ): zio.UIO[String] = ??? def setSeed(seed: => Long)(implicit trace: zio.Trace ): zio.UIO[Unit] = ??? def shuffle[A, Collection[+Element] <: Iterable[Element]]( collection: => Collection[A] )(implicit bf: BuildFrom[Collection[A], A, Collection[ A ]], trace: Trace ): UIO[Collection[A]] = ??? end RandomZIOFake Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures If you are not interested in the discouraged ways to handle errors, and just want to see the ZIO approach, jump down to ZIO Error Handling Historic approaches to Error-handling In the past, some programs have thrown exceptions to indicate failures. Imagine a program that displays the local temperature the user based on GPS position and a network call. There are distinct levels of problems in any given program. They require different types of handling by the programmer. Temperature: 30 degrees class GpsException() extends RuntimeException class NetworkException() extends RuntimeException enum Scenario: case Success, NetworkError, GPSError def displayTemperature( behavior: Scenario ): String = if (behavior == Scenario.GPSError) throw new GpsException() else if (behavior == Scenario.NetworkError) throw new NetworkException() else "35 degrees" Hello Failures 80 def currentTemperatureUnsafe( behavior: Scenario ): String = "Temperature: " + displayTemperature(behavior) currentTemperatureUnsafe(Scenario.Success) // res0: String = "Temperature: 35 degrees" On the happy path, everything looks as desired. If the network is unavailable, what is the behavior for the caller? This can take many forms. If we don’t make any attempt to handle our problem, the whole program blows up and shows the gory details to the user. // Note - Can't make this output prettier/simpler because it's *not* us\ ing ZIO currentTemperatureUnsafe(Scenario.NetworkError) // repl.MdocSession$MdocApp$NetworkException // at repl.MdocSession$MdocApp.displayTemperature(14_Hello_Failures.md\ :25) // at repl.MdocSession$MdocApp.currentTemperatureUnsafe(14_Hello_Failu\ res.md:35) // at repl.MdocSession$MdocApp.$init$$$anonfun$1(14_Hello_Failures.md:\ 46) We could take the bare-minimum approach of catching the Exception and returning null: def currentTemperatureNull( behavior: Scenario ): String = try "Temperature: " + displayTemperature(behavior) catch case (ex: RuntimeException) => "Temperature: " + null currentTemperatureNull(Scenario.NetworkError) // res1: String = "Temperature: null" This is slightly better, as the user can at least see the outer structure of our UI element, but it still leaks out code-specific details world. Maybe we could fallback to a sentinel value, such as 0 or -1 to indicate a failure? Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures 81 def currentTemperature( behavior: Scenario ): String = try "Temperature: " + displayTemperature(behavior) catch case (ex: RuntimeException) => "Temperature: -1 degrees" currentTemperature(Scenario.NetworkError) // res2: String = "Temperature: -1 degrees" Clearly, this isn’t acceptable, as both of these common sentinel values are valid temperatures. We can take a more honest and accurate approach in this situation. def currentTemperature( behavior: Scenario ): String = try "Temperature: " + displayTemperature(behavior) catch case (ex: RuntimeException) => "Temperature Unavailable" currentTemperature(Scenario.NetworkError) // res3: String = "Temperature Unavailable" We have improved the failure behavior significantly; is it sufficient for all cases? Imagine our network connection is stable, but we have a problem in our GPS hardware. In this situation, do we show the same message to the user? Ideally, we would show the user a distinct message for each scenario. The Network issue is transient, but the GPS problem is likely permanent. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures 82 def currentTemperature( behavior: Scenario ): String = try "Temperature: " + displayTemperature(behavior) catch case (ex: NetworkException) => "Network Unavailable" case (ex: GpsException) => "GPS problem" currentTemperature(Scenario.NetworkError) // res4: String = "Network Unavailable" currentTemperature(Scenario.GPSError) // res5: String = "GPS problem" Wonderful! We have specific messages for all relevant error cases. However, this still suffers from downsides that become more painful as the codebase grows. • The signature of currentTemperature does not alert us that it might fail • If we realize it can fail, we must dig through the implementation to discover the multiple failure values • We never have certainty about the failure paths of our full application, or any subset of it. {{ TODO Tear apart exceptions more }} Encountering an error during a function call generally means two things: 1. You can’t continue executing the function in the normal fashion. 2. You can’t return a normal result. Many languages use exceptions for handling errors. An exception throws out of the current execution path to locate a user-written handler to deal with the error. There are two goals for exceptions: 1. Separate error-handling code from “success-path” code, so the success-path code is easier to understand and reason about. 2. Reduce redundant error-handling code by handling associated errors in a single place. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures 83 Exceptions have problems: 1. They can be “swallowed.” Just because code throws an exception, there’s no guarantee that issue will be dealt with. 2. They can lose important information. Once an exception is caught, it is considered to be “handled,” and the program doesn’t need to retain the failure information. 3. They aren’t typed. Java’s checked exceptions provide a small amount of type information, but it’s not that helpful compared to a full type system. Unchecked exceptions provide no information at all. 4. Because they are handled dynamically, the only way to ensure your program won’t crash is by testing it through all possible execution paths. A staticallytyped error management solution can ensure—at compile time—that all errors are handled. 5. They don’t scale. {{Need to think about this more to make the case.}} 6. Hard to reason about. {{Also need to make this case}} 7. Difficult or impossible to retry an operation if it fails. Java {{and Scala?}} use the “termination” model of exception handling. This assumes the error is so critical there’s no way to get back to where the exception occurred. If you’re performing an operation that you’d like to retry if it fails, exceptions don’t help much. Exceptions were a valiant attempt to produce a consistent error-reporting interface, and they are definitely better than what’s in C. But they don’t end up solving the problem very well, and you just don’t know what you’re going to get when you use exceptions. What’s wrong with Try? ADTS as another step forward ZIO Error Handling Now we will explore how ZIO enables more powerful, uniform error-handling. TODO {{Update verbiage now that ZIO section is first}} Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures 84 • ZIO Error Handling • Wrapping Legacy Code ZIO-First Error Handling import zio.ZIO import mdoc.unsafeRunPrettyPrint def getTemperatureZ(behavior: Scenario): ZIO[ Any, GpsException | NetworkException, String ] = if (behavior == Scenario.GPSError) ZIO.fail(new GpsException()) else if (behavior == Scenario.NetworkError) // TODO Use a non-exceptional error ZIO.fail(new NetworkException()) else ZIO.succeed("30 degrees") unsafeRunPrettyPrint( getTemperatureZ(Scenario.Success) ) // 30 degrees // TODO make MDoc:fail adhere to line limits? unsafeRunPrettyPrint( getTemperatureZ(Scenario.Success).catchAll { case ex: NetworkException => ZIO.succeed("Network Unavailable") } ) // error: // match may not be exhaustive. // // It would fail on pattern case: _: GpsException // TODO Demonstrate ZIO calculating the error types without an explicit annotation being provided Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures 85 unsafeRunPrettyPrint( getTemperatureZ(Scenario.GPSError) ) // repl.MdocSession$MdocApp$GpsException Wrapping Legacy Code If we are unable to re-write the fallible function, we can still wrap the call We are re-using the displayTemperature {{TODO }} import zio.{Task, ZIO} def displayTemperatureZWrapped( behavior: Scenario ): ZIO[Any, Nothing, String] = ZIO .attempt(displayTemperature(behavior)) .catchAll { case ex: NetworkException => ZIO.succeed("Network Unavailable") case ex: GpsException => ZIO.succeed("GPS problem") } unsafeRunPrettyPrint( displayTemperatureZWrapped(Scenario.Success) ) // 35 degrees unsafeRunPrettyPrint( displayTemperatureZWrapped( Scenario.NetworkError ) ) // Network Unavailable This is decent, but does not provide the maximum possible guarantees. Look at what happens if we forget to handle one of our errors. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures 86 def getTemperatureZGpsGap( behavior: Scenario ): ZIO[Any, Nothing, String] = ZIO .attempt(displayTemperature(behavior)) .catchAll { case ex: NetworkException => ZIO.succeed("Network Unavailable") } unsafeRunPrettyPrint( getTemperatureZGpsGap(Scenario.GPSError) ) // Defect: GpsException The compiler does not catch this bug, and instead fails at runtime. Take extra care when interacting with legacy code, since we cannot automatically recognize these situations at compile time. We have 2 options in these situations. First, we can provide a fallback case that will report anything we missed: scala def getTemperatureZWithFallback( behavior: Scenario ): ZIO[Any, Nothing, String] = ZIO .attempt(displayTemperature(behavior)) .catchAll { case ex: NetworkException => ZIO.succeed("Network Unavailable") case other => ZIO.succeed("Error: " + other) } unsafeRunPrettyPrint( getTemperatureZWithFallback(Scenario.GPSError) ) // Error: repl.MdocSession$MdocApp$GpsException This lets us avoid the most egregious gaps in functionality, but it does not take full advantage of ZIO’s type-safety. scala def getTemperatureZAndFlagUnhandled( behavior: Scenario ): ZIO[Any, GpsException, String] = ZIO .attempt(displayTemperature(behavior)) .catchSome { case ex: NetworkException => ZIO.succeed("Network Unavailable") } // TODO Eh, find a better version of this. .mapError(_.asInstanceOf[GpsException]) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures 87 unsafeRunPrettyPrint( getTemperatureZAndFlagUnhandled( Scenario.GPSError ) ) // repl.MdocSession$MdocApp$GpsException {{TODO show catchSome}} Automatically attached experiments. These are included at the end of this chapter because their package in the experiments directory matched the name of this chapter. Enjoy working on the code with full editor capabilities :D experiments/src/main/scala/hello_failures/BadTypeManagement.scala package hello_failures import zio.ZIO object BadTypeManagement extends zio.ZIOAppDefault: val logic: ZIO[Any, Exception, String] = for _ <- ZIO.debug("ah") result <failable(1).catchAll { case ex: Exception => ZIO.fail(ex) case ex: String => ZIO.succeed( "recovered string error: " + ex ) } _ <- ZIO.debug(result) yield result def run = logic Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures def failable( path: Int ): ZIO[Any, Exception | String, String] = if (path < 0) ZIO.fail(new Exception("Negative path")) else if (path > 0) ZIO.fail("Too big") else ZIO.succeed("just right") end BadTypeManagement experiments/src/main/scala/hello_failures/KeepSuccesses.scala package hello_failures import zio.Console.printLine import zio.ZIO object KeepSuccesses extends zio.ZIOAppDefault: val allCalls = List("a", "b", "large payload", "doomed") case class GoodResponse(payload: String) case class BadResponse(payload: String) val initialRequests = allCalls.map(fastUnreliableNetworkCall) val logic = for results <ZIO.collectAllSuccesses( initialRequests.map( _.tapError(e => printLine("Error: " + e) ) ) ) _ <- printLine(results) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 88 Hello Failures yield () val moreStructuredLogic = for results <ZIO.partition(allCalls)( fastUnreliableNetworkCall ) _ <results match case (failures, successes) => for _ <ZIO.foreach(failures)(e => printLine( "Error: " + e + ". Should retry on other server." ) ) recoveries <ZIO.collectAllSuccesses( failures.map(failure => slowMoreReliableNetworkCall( failure.payload ).tapError(e => printLine( "Giving up on: " + e ) ) ) ) _ <printLine( "All successes: " + (successes ++ recoveries) ) yield () yield () val logicSpecific = ZIO.collectAllWith(initialRequests)( _.payload.contains("a") Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 89 Hello Failures 90 ) def run = // logic moreStructuredLogic def fastUnreliableNetworkCall(input: String) = if (input.length < 5) ZIO.succeed(GoodResponse(input)) else ZIO.fail(BadResponse(input)) def slowMoreReliableNetworkCall( input: String ) = if (input.contains("a")) ZIO.succeed(GoodResponse(input)) else ZIO.fail(BadResponse(input)) end KeepSuccesses experiments/src/main/scala/hello_failures/OrDie.scala package hello_failures import zio.ZIO object OrDie extends zio.ZIOAppDefault: val logic = for _ <- failable(-1).orDie yield () def run = logic def failable( path: Int ): ZIO[Any, Exception, String] = if (path < 0) ZIO.fail(new Exception("Negative path")) // else if (path > 0) // ZIO.fail("Too big") Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures else ZIO.succeed("just right") experiments/src/main/scala/hello_failures/catching.scala package hello_failures import import import import zio.* zio.Console.* hello_failures.file java.io.IOException def standIn: ZIO[Any, IOException, Unit] = printLine("Im a stand-in") object catching extends zio.ZIOAppDefault: val logic = loadFile("TargetFile") def run = logic .catchAll(_ => println("Error Caught") loadBackupFile() ) .exitCode // standIn.exitCode experiments/src/main/scala/hello_failures/fallback.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 91 Hello Failures package hello_failures import zio.* import zio.Console import scala.util.Random // A useful way of dealing with errors is by // using the // `orElse()` method. case class file(name: String) def loadFile(fileName: String) = if (Random.nextBoolean()) println("First Attempt Successful") ZIO.succeed(file(fileName)) else println("First Attempt Not Successful") ZIO.fail("File not found") def loadBackupFile() = println("Backup file used") ZIO.succeed(file("BackupFile")) object fallback extends zio.ZIOAppDefault: // orElse is a combinator that can be used to // handle // effects that can fail. def run = val loadedFile: UIO[file] = loadFile("TargetFile") .orElse(loadBackupFile()) loadedFile.exitCode experiments/src/main/scala/hello_failures/folding.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 92 Hello Failures 93 package hello_failures import import import import zio.* zio.Console.* hello_failures.file hello_failures.standIn object folding extends ZIOAppDefault: // When applied to ZIO, fold() allows the // programmer to handle both failure // and success at the same time. // ZIO's fold method can be broken into two // pieces: fold(), and foldM() // fold() supplied a non-effectful handler, why // foldM() applies an effectful handler. val logic = loadFile("targetFile") def run = logic .foldZIO( _ => loadBackupFile(), _ => printLine( "The file opened on first attempt!" ) ) // Effectful handling .exitCode end folding experiments/src/main/scala/hello_failures/value.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Hello Failures package hello_failures import zio.* object value: // Either and Absolve take ZIO types and // 'surface' or 'submerge' // the error. // // // // Either takes an ZIO[R, E, A] and produces an ZIO[R, Nothing, Either[E,A]] The error is 'surfaced' by making a non-failing ZIO that returns an Either. // // // // Absolve takes an ZIO[R, Nothing, Either[E,A]], and returns a ZIO[R,E,A] The error is 'submerged', as it is pushed from an either into a ZIO. val zEither: UIO[Either[String, Int]] = ZIO.fail("Boom").either // IO.fail("Boom") is naturally type // ZIO[R,String,Int], but is // converted into type UIO[Either[String, Int] def sqrt( input: UIO[Double] ): IO[String, Double] = ZIO.absolve( input.map(value => if (value < 0.0) Left("Value must be >= 0.0") else Right(Math.sqrt(value)) ) ) end value // The Left-Right statements naturally from an // 'either' of type either[String, Double]. // the ZIO.absolve changes the either into an Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 94 Hello Failures // ZIO of type IO[String, Double] Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 95 Cause Cause will track all errors originating from a single call in an application, regardless of concurrency and parallelism. import zio._ import mdoc.unsafeRunPrettyPrint val logic = ZIO .die(new Exception("Connection lost")) .ensuring( ZIO.die( throw new Exception("Release Failed") ) ) unsafeRunPrettyPrint(logic) // Defect: java.lang.Exception: Connection lost Cause allows you to aggregate multiple errors of the same type &&/Both represents parallel failures ++/Then represents sequential failures Cause.die will show you the line that failed, because it requires a throwable Cause.fail will not necessarily, because it can be any arbitrary type Avoided Technique - Throwing Exceptions Now we will highlight the deficiencies of throwing Exceptions. The previous code might be written in this style: 97 Cause import zio._ import mdoc.unsafeRunPrettyPrint val thrownLogic = ZIO.attempt( try throw new Exception( "Client connection lost" ) finally try () // Cleanup finally throw new Exception("Release Failed") ) // thrownLogic: ZIO[Any, Throwable, Nothing] = Stateful( // trace = "repl.MdocSession.MdocApp.thrownLogic(15_Cause.md:49)", // onState = zio.ZIOCompanionVersionSpecific$$Lambda$14256/1397083455\ @4903320a // ) unsafeRunPrettyPrint(thrownLogic) // java.lang.Exception: Release Failed We will only see the later pool problem. If we throw an Exception in our logic, and then throw another while cleaning up, we simply lose the original. This is because thrown Exceptions cannot be composed. In a language that cannot throw, following the execution path is simple, following 2 basic rules: - At a branch, execute only the first match - Otherwise, Read everything from left-to-right, top-to-bottom, Once you add throw, the rules are more complicated t At a branch, execute only the first match Otherwise, Read everything from left-to-right, top-to-bottom, Unless we `throw`, which means immediately jumping through a differen\ dimension away from the code you're viewing Linear reporting Everything must be reported linearly, even in systems that are executing on different fibers, across several threads, amongst multiple cores. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 98 Cause Automatically attached experiments. These are included at the end of this chapter because their package in the experiments directory matched the name of this chapter. Enjoy working on the code with full editor capabilities :D experiments/src/main/scala/cause/CauseBasics.scala package cause import zio._ object CauseBasics extends App: // ZIO.fail(Cause.fail("Blah")) println( ( Cause.die(Exception("1")) ++ (Cause.fail(Exception("2a")) && Cause.fail(Exception("2b"))) ++ Cause .stackless(Cause.fail(Exception("3"))) ).prettyPrint ) object CauseZIO extends ZIOAppDefault: val x: ZIO[Any, Nothing, Nothing] = ZIO.die(Exception("Blah")) def run = ZIO.die(Exception("Blah")) object LostInfo extends ZIOAppDefault: def run = ZIO.attempt( try throw new Exception( "Client connection lost" ) finally try () // Cleanup finally Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 99 Cause throw new Exception( "Problem relinquishing to pool" ) ) experiments/src/main/scala/cause/MalcomInTheMiddle.scala package cause import zio.{ZIO, ZIOAppDefault} object MalcomInTheMiddle extends ZIOAppDefault: def run = def turnOnLights() = throw new BurntBulb() class BurntBulb() extends Exception def getNewBulb() = throw new WobblyShelf() class WobblyShelf() extends Exception def grabScrewDriver() = throw new SqueakyDrawer() class SqueakyDrawer() extends Exception def sprayWD40() = throw new EmptyCan() class EmptyCan() extends Exception def driveToStore() = throw new DeadCar() class DeadCar() extends Exception def repairCar() = throw new Nagging() class Nagging() extends Exception try turnOnLights() catch case burntBulb: BurntBulb => try getNewBulb() catch Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 100 Cause // // // // // // case wobblyShelf: WobblyShelf => try grabScrewDriver() catch case squeakyDrawer: SqueakyDrawer => try sprayWD40() catch case emptyCan: EmptyCan => try driveToStore() catch case deadCar: DeadCar => try repairCar() finally ZIO .debug( "What does it look like I'm doing?!" ) .exitCode finally println end try finally ZIO .debug( "What does it look like I'm doing?!" ) .exitCode end run /** try { turnOnLights } catch { case * burntLightBulb => try { */ end MalcomInTheMiddle experiments/src/main/scala/cause/MalcomInTheMiddleZ.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 101 Cause package cause import zio.* object MalcomInTheMiddleZ extends ZIOAppDefault: def run = def turnOnLights() = ZIO.fail(BurntBulb()) class BurntBulb() extends Exception("Burnt Bulb!") def getNewBulb() = ZIO.attempt( throw new Exception("Wobbly Shelf!") ) def grabScrewDriver() = ZIO.fail(Exception("SqueakyDrawer")) ( for _ <turnOnLights() .catchAllCause(originalError => getNewBulb() .catchAllCause(bulbError => grabScrewDriver() .mapErrorCause( screwDriverError => (originalError ++ bulbError) ++ screwDriverError ) ) ) _ <- ZIO.debug("Preserve failures!") yield () ).catchAllCause(bigError => ZIO.debug( "Final error: " + simpleStructureAlternative(bigError) ) ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 102 Cause end run end MalcomInTheMiddleZ def simpleStructure( cause: Cause[Throwable] ): String = cause match case Cause.Empty => ??? case Cause.Fail(value, trace) => value.getMessage case Cause.Die(value, trace) => ??? case Cause.Interrupt(fiberId, trace) => ??? case Cause.Stackless(cause, stackless) => ??? case Cause.Then(left, right) => "Then(" + simpleStructure(left) + ", " + simpleStructure(right) + ")" case Cause.Both(left, right) => ??? def simpleStructureAlternative( cause: Cause[Throwable] ): String = cause match case Cause.Fail(value, trace) => value.getMessage case Cause.Then(left, right) => simpleStructureAlternative(left) + " => " + simpleStructureAlternative(right) case Cause.Both(left, right) => ??? case _ => ??? experiments/src/main/scala/cause/MutationTracking.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 103 Cause package cause import zio.{Cause, IO, UIO, ZIO} import zio.Console.* class MutationTracking: enum Stage: case Hominini, Chimpanzee, Human object TimelineFinally extends App: try throw new Exception("Straightened Spine") finally try throw new Exception("Less Hair") finally throw new Exception("Fine Voice Control") object Timeline extends zio.ZIOAppDefault: val mutation1: UIO[Nothing] = ZIO.die(Exception("Straightened Spine")) val mutation2 = ZIO.die(Exception("Less Hair")) val mutation3 = ZIO.die(Exception("Fine voice control")) val timeline = mutation1 .ensuring(mutation2) .ensuring(mutation3) def run = timeline.sandbox Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward New Effects So far, we have justified, rebuilt, and examined the built-in ZIO Services. While this should aid you in creating basic ZIO applications, we now want to explore some custom Services that could facilitate more complex programs. Location Consider the term Environment. In common speech, this often indicates where something happens. Previously, we have examined this in terms of “Which Machine?” However, it is equally valid to treat this as a spatial location at which our code is executed. import zio.{ZIO} trait HardwareFailure case class GpsCoordinates( latitude: Double, longitude: Double ) trait TimeZone trait Location: def gpsCoords : ZIO[Any, HardwareFailure, GpsCoordinates] def timezone: ZIO[Any, Nothing, TimeZone] object Location: def gpsCoords: ZIO[ Location, HardwareFailure, GpsCoordinates ] = ZIO.service[Location].flatMap(_.gpsCoords) Now that we have basic Location-awareness, we can build more domain-specific logic on top of it. 106 Location trait FloodStatus object Safe extends FloodStatus object Threatened extends FloodStatus trait FloodWarning: def seaLevelStatus : ZIO[Any, Nothing, FloodStatus] case class Slope(degrees: Float) trait Topography: def slope: ZIO[Location, Nothing, Slope] case class Rainfall(inches: Int) trait Almanac: def averageAnnualRainfail : ZIO[Location, Nothing, Rainfall] case class Country(name: String) trait CountryService: def currentCountry : ZIO[Location, HardwareFailure, Country] object CountryService: def currentCountry : ZIO[Location, HardwareFailure, Country] = for gpsCords <- Location.gpsCoords yield Country("USA") Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 107 Location trait LegalStatus object Legal extends LegalStatus object Illegal extends LegalStatus trait GeoPolitcalState trait CurrentWar enum Issue: case OnlineGambling, Alcohol trait LawLibrary: def status( country: Country, issue: Issue ): ZIO[ GeoPolitcalState, CurrentWar, LegalStatus ] class LegalService( countryService: CountryService, lawLibrary: LawLibrary ): def status(issue: Issue): ZIO[ Location & GeoPolitcalState, CurrentWar | HardwareFailure, LegalStatus ] = for country <- countryService.currentCountry status <- lawLibrary.status(country, issue) yield status Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Environment Exploration The Environment type parameter distinguishes ZIO from most other IO monads. At first, it might seem like a complicated way of passing values to your ZIO instances why can’t they just be normal function arguments? • The developer does not need to manually plug an instance of type T into every ZIO[T, _, _] in order to run them. • ZIO instances can be freely composed, flatmapped, etc before ever providing the required environment. It’s only needed at the very end when we want to execute the code! • Environments can be arbitrarily complex, without requiring a super-container type to hold all of the fields. • Compile time guarantees that you have 1. Provided everything required 2. Have not provided multiple, conflicting instances of the same type Dependency Injection TODO Decide if this requires a tangent, or just a single mention to let people know we’re solving the same type of problem. TODO ZEnvironment: Powered by a TypeMap ZIO is able to accomplish all this through the ZEnvironment class. TODO Figure out how/where to include the disclaimer that we’re stripping out many of the implementation details TODO Environment Exploration 109 ZEnvironment[+R](map: Map[LightTypeTag, (Any, Int)]) The crucial data structure inside is a Map[LightTypeTag, (Any)]. TODO Decide how much to dig into LightTypeTag vs Tag[A] TODO Seeing Any here might be confusing - ZEnvironment is supposed to give us type-safety when executing ZIOs! Looking at the get method, we see specic, typed results. def get[A >: R](implicit tagged: Tag[A]): A How is this possible when all of our Map values are Anys? add holds the answer. def add[A](a: A): ZEnvironment[R with A] Even though each new entry is stored as an Any, we store knowledge of our new entry in the R type parameter. We append the type of our new A to the R type parameter, and get back a brand-new environment that we know contains all types from the original and the type of the instance we just added. Now look at the get implementation to see how this is used. def get[A](tag: Tag[A]): A = val lightTypeTag = taggedTagType(tag) self.map.get(lightTypeTag) match: case Some(a) => a.asInstanceOf[A] case None => throw new Error(s"Defect: ${tag} not inside ${self}") private def taggedTagType[A](tagged: Tag[A]): LightTypeTag ZLayer • Better Ergonomics than ZEnvironment • Shared by default Automatically attached experiments. These are included at the end of this chapter because their package in the experiments directory matched the name of this chapter. Enjoy working on the code with full editor capabilities :D Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Environment Exploration experiments/src/main/scala/environment_exploration/ToyEnvironment.scala package environment_exploration import scala.reflect.{ClassTag, classTag} case class DBService(url: String) // Yada yada yada lets talk about the environment trait ToyEnvironmentT[+R]: def add[A: ClassTag]( a: A ): ToyEnvironmentT[R & A] def get[A >: R: ClassTag]: A class ToyEnvironment[+R]( typeMap: Map[ClassTag[_], Any] ) extends ToyEnvironmentT[R]: def add[A: ClassTag]( a: A ): ToyEnvironment[R & A] = ToyEnvironment(typeMap + (classTag[A] -> a)) def get[A >: R: ClassTag]: A = typeMap(classTag[A]).asInstanceOf[A] @main def demoToyEnvironment = val env: ToyEnvironment[_] = ToyEnvironment(Map.empty) val env1: ToyEnvironment[String] = env.add("hi") val env2: ToyEnvironment[String & DBService] = env1.add(DBService("blah")) val env3: ToyEnvironment[ Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 110 Environment Exploration 111 String & DBService & List[String] ] = env2.add(List("a", "b")) println(env3.get[String]) println(env3.get[DBService]) println(env3.get[List[String]]) // We get some amount of compile time safety // here, but not much // println(env.get(classOf[List[DBService]])) // Downside of the current approach is that it // doesn't prevent duplicate types env3.add("hi") // is accepted end demoToyEnvironment // Consider this runtime de-duping class ToyEnvironmentRuntimeDeduplication[+R]( typeMap: Map[ClassTag[_], Any] ): def add[A: ClassTag]( a: A ): ToyEnvironment[R & A] = if (typeMap.contains(classTag[A])) throw new IllegalArgumentException( s"Cannot add ${classTag[A]} to environment, it already exists" ) else ToyEnvironment( typeMap + (classTag[A] -> a) ) experiments/src/main/scala/environment_exploration/TupledEnvironmentZio.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Environment Exploration package environment_exploration // trait TypeTag // TODO Or ClassTag? // trait TypeInstance // case class TypeMap( // typeMap: Map[TypeTag, TypeInstance] // ) case class TupledEnvironmentZio[ENV, RESULT]( run: ENV => RESULT ): def unsafeRun(env: ENV): RESULT = run(env) // The tuple here is a step towards the // full-featured TypeMap that ZIO uses def flatMap[ENV2, RESULT2]( f: RESULT => TupledEnvironmentZio[ ENV2, RESULT2 ] ): TupledEnvironmentZio[(ENV, ENV2), RESULT2] = TupledEnvironmentZio((env, env2) => f(run(env)).run(env2) ) @main def demoSingleEnvironmentInstance = val customTypeMapZio : TupledEnvironmentZio[Int, String] = TupledEnvironmentZio(env => val result = env * 10 s"result: $result" ) println(customTypeMapZio.unsafeRun(5)) val repeatMessage : TupledEnvironmentZio[Int, String] = TupledEnvironmentZio(env => s"Message \n" * env ) println(repeatMessage.unsafeRun(5)) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 112 Environment Exploration case class BigResult(message: String) @main def demoTupledEnvironment = val squared: TupledEnvironmentZio[Int, Unit] = TupledEnvironmentZio(env => println( "Environment integer squared: " + env * env ) ) val repeatMessage : TupledEnvironmentZio[String, BigResult] = TupledEnvironmentZio(message => BigResult(s"Environment message: $message") ) val composedRes: TupledEnvironmentZio[ (Int, String), BigResult ] = squared.flatMap(_ => repeatMessage) val finalResult = composedRes.unsafeRun((5, "Hello")) println(finalResult) end demoTupledEnvironment import zio.ZIO Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 113 Resources Resources are finite / large overhead allocatable pools of things: - Connections File handles - Player Slots (Rule based or Fractional Computational Power - The usefulness of the game degrades at some scale) - STM? Is it based on ZManaged? Connection with dining philosophers Externalizes the resource management so that the logic that acts on the resource can be reused, refactored, composed. Assembly of resources works the same as a single resource. If a resource is more than 1 resource, the logic acting on any / all resources doesn’t have to know what cleanup. Similarly, the logic is unconcerned with the ability for all needed resources to be available. Logic is only ever to be applied when all resources are available. STM This is going to be a tough one. Automatically attached experiments. These are included at the end of this chapter because their package in the experiments directory matched the name of this chapter. Enjoy working on the code with full editor capabilities :D experiments/src/main/scala/stm/SimpleTransfers.scala package stm import import import import zio.Console.printLine zio.stm.{STM, TRef} zio.Runtime.default.unsafe zio.Unsafe def transfer( from: TRef[Int], to: TRef[Int], amount: Int ): STM[Throwable, Unit] = for senderBalance <- from.get _ <if (amount > senderBalance) STM.fail( new Throwable("insufficient funds") ) else from.update(_ - amount) *> 116 STM to.update(_ + amount) yield () @main def stmDemo() = val logic = for fromAccount <- TRef.make(100).commit toAccount <- TRef.make(0).commit _ <transfer(fromAccount, toAccount, 20) .commit // _ <- transferTransaction.commit toAccountFinal <- toAccount.get.commit _ <printLine( "toAccountFinal: " + toAccountFinal ) yield () Unsafe.unsafe { (u: Unsafe) => given Unsafe = u unsafe.run(logic).getOrThrowFiberFailure() } end stmDemo experiments/src/main/scala/stm/TownResources.scala package stm import import import import import zio.stm.STM zio.stm.TRef zio.Runtime.default.unsafe zio.Console.printLine zio.Unsafe case class Cash(value: Int) extends Resource[Cash] case class Lumber(value: Int) extends Resource[Lumber] Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 117 STM case class Grain(value: Int) extends Resource[Grain] sealed trait Resource[A]: val value: Int def <=(other: Resource[A]): Boolean = value <= other.value // TODO Consider other names: Commodity case class TownResources( cash: Cash, lumber: Lumber, grain: Grain ): def +[A](resource: Resource[A]) = resource match case c: Cash => copy(cash = Cash(cash.value + c.value)) case g: Grain => copy(grain = Grain(grain.value + g.value) ) case l: Lumber => copy(lumber = Lumber(lumber.value + l.value) ) def -[A](resource: Resource[A]) = resource match case c: Cash => copy(cash = Cash(cash.value - c.value)) case g: Grain => copy(grain = Grain(grain.value - g.value) ) case l: Lumber => copy(lumber = Lumber(lumber.value - l.value) ) def canSend[A](resource: Resource[A]) = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 118 STM resource match case c: Cash => c <= cash case l: Lumber => l <= lumber case g: Grain => g <= grain end TownResources /** Goal: Demonstrate a useful 3 party trade. */ @main def resourcesDemo() = val logic = for treeTown <TRef .make( TownResources( Cash(10), Lumber(100), Grain(0) ) ) .commit grainVille <TRef .make( TownResources( Cash(0), Lumber(0), Grain(300) ) ) .commit _ <tradeResources( treeTown, Cash(3), grainVille, Grain(30) ).commit Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 119 STM finalTreeTownResources <treeTown.get.commit finalGrainVilleResources <grainVille.get.commit _ <- printLine(finalTreeTownResources) _ <- printLine(finalGrainVilleResources) yield () Unsafe.unsafe { (u: Unsafe) => given Unsafe = u unsafe.run(logic).getOrThrowFiberFailure() } end resourcesDemo def tradeResources[ A <: Resource[A], B <: Resource[B] ]( town1: TRef[TownResources], town1Offering: A, town2: TRef[TownResources], town2Offering: B ): STM[Throwable, Unit] = for _ <- send(town1, town2, town1Offering) _ <- send(town2, town1, town2Offering) yield () def send[A <: Resource[A], B <: Resource[B]]( from: TRef[TownResources], to: TRef[TownResources], resource: A ): STM[Throwable, Unit] = for senderBalance <- from.get canSend = senderBalance.canSend(resource) _ <if (canSend) from.update(_ - resource) *> to.update(_ + resource) else STM.fail( Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 120 STM new Throwable( "Not enough resources to send: " + resource ) ) extraTransaction = from.update(fResources => fResources.copy(cash = Cash(fResources.cash.value + 1) ) ) party2Balance <- to.get yield () Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Executing External Programs Most of this book focuses on executing Scala code all confined within a single JVM. However, there are times when you need to execute external programs. As a rule, we must treat these programs as side-effecting, because there is no practical way of ensuring they are pure. We will explore this using zio-process This chapter will cover how to do that. Basic shell tools ### Say We could start with things like echo or ls, but those are easily done within Scala itself, so they are not very interesting. Top ### Git ## Advanced tools ### Gource ### Shpotify ## Running other programming languages ### Python ### Scala Automatically attached experiments. These are included at the end of this chapter because their package in the experiments directory matched the name of this chapter. Enjoy working on the code with full editor capabilities :D experiments/src/main/scala/executing_external_programs/Gource.scala Executing External Programs 122 package executing_external_programs import zio._ import zio.Console.printLine import zio.process.{ Command, ProcessInput, ProcessOutput } /* Possibilities: * - Show a certain time period * - More recent activity * - Cycle between different repositories */ object GourceDemo extends ZIOAppDefault: def gource(repoDir: String) = Command( "gource", // "--follow-user", "bfrasure", // Highlights user, but still show\ s others "--user-show-filter", "bfrasure|Bill Frasure", // Only shows user repoDir ) val projects = List( "/Users/bfrasure/Repositories/book", "/Users/bfrasure/Repositories/TestFrameworkComparison" ) def showActivityForAWhile(repoDir: String) = for run1 <- gource(repoDir).run _ <- ZIO.sleep(5.seconds) _ <- run1.killForcibly yield () def randomProjectActivity = for idx <Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward Executing External Programs Random.nextIntBounded(projects.length) _ <- showActivityForAWhile(projects(idx)) yield () def run = for _ <- randomProjectActivity.repeatN(2) yield () end GourceDemo experiments/src/main/scala/executing_external_programs/Say.scala package executing_external_programs import zio.process.{ Command, ProcessInput, ProcessOutput } import zio._ def say(message: String) = Command("say", message) object SayDemo extends ZIOAppDefault: def run = say("Hello, world!").run Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 123 Experiments These experiments are not currently attached to a chapter, but are included for previewing. Before publication, we should not have any lingering experiments here. resourcemanagement experiments/src/main/scala/resourcemanagement/ChatSlots.s package resourcemanagement import zio.Console.printLine import zio.{Ref, ZIO} case class Slot(id: String) case class Player(name: String, slot: Slot) case class Game(a: Player, b: Player) object ChatSlots extends zio.ZIOAppDefault: enum SlotState: case Closed, Open def run = def acquire(ref: Ref[SlotState]) = for _ <printLine { "Took a speaker slot" } _ <- ref.set(SlotState.Open) yield "Use Me" 125 Experiments def release(ref: Ref[SlotState]) = for _ <printLine("Freed up a speaker slot") .orDie _ <- ref.set(SlotState.Closed) yield () for ref <Ref.make[SlotState](SlotState.Closed) managed = ZIO.acquireRelease(acquire(ref))(_ => release(ref) ) reusable = managed.map( printLine(_) ) // note: Can't just do (Console.printLine) here _ <- reusable _ <- reusable _ <ZIO.scoped { managed.flatMap { s => for _ <- printLine(s) _ <- printLine("Blowing up") _ <- ZIO.fail("Arggggg") yield () } } yield () end for end run end ChatSlots experiments/src/main/scala/resourcemanagement/Trivial.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 126 Experiments package resourcemanagement import zio.Console import zio.{Ref, ZIO} object Trivial extends zio.ZIOAppDefault: enum ResourceState: case Closed, Open def run = def acquire(ref: Ref[ResourceState]) = for _ <Console.printLine("Opening Resource") _ <- ref.set(ResourceState.Open) yield "Use Me" def release(ref: Ref[ResourceState]) = for _ <Console .printLine("Closing Resource") .orDie _ <- ref.set(ResourceState.Closed) yield () def releaseSymbolic( ref: Ref[ResourceState] ) = Console .printLine("Closing Resource") .orDie *> ref.set(ResourceState.Closed) // // // // // // // This combines creating a managed resource with using it. In normal life, users just get a managed resource from a library and so they don't have to think about acquire & release logic. Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 127 Experiments for ref <Ref.make[ResourceState]( ResourceState.Closed ) managed = ZIO.acquireRelease(acquire(ref))(_ => release(ref) ) reusable = ZIO.scoped { managed.map(Console.printLine(_)) } // note: Can't just do (Console.printLine) here _ <- reusable _ <- reusable _ <ZIO.scoped { managed.flatMap { s => for _ <- Console.printLine(s) _ <Console.printLine("Blowing up") _ <- ZIO.fail("Arggggg") yield () } } yield () end for end run end Trivial energygrid experiments/src/main/scala/energygrid/Grid.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 128 Experiments package energygrid object Grid {} microcontrollers experiments/src/main/scala/microcontrollers/Arduino.scala package microcontrollers import zio.Console.{printLine, readLine} import zio.{ Clock, Console, Fiber, IO, Ref, Runtime, Schedule, UIO, URIO, ZIO, ZLayer, durationInt } import zio.Clock.{currentTime, instant} import zio.Duration.* import java.io.IOException import java.util.concurrent.TimeUnit case class DigitalPin private (active: Boolean) object DigitalPin: object ON extends DigitalPin(true) object OFF extends DigitalPin(false) case class Arduino(pin1: DigitalPin): Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 129 Experiments def passSignalToLight() = if (pin1.active) ZIO .succeed("Sending current to light bulb") else ZIO.succeed("Leaving the lights off") object MicroControllerExample extends zio.ZIOAppDefault: def turnOnPinAtRightTime( inSeconds: Long, startTime: Long ) = if ((inSeconds - startTime) % 4 > 2) DigitalPin.ON else DigitalPin.OFF def loopLogic( startTime: Long, arduino: Ref[Arduino] ): ZIO[Any, IOException, Unit] = for inSeconds <- currentTime(TimeUnit.SECONDS) originalArduino <- arduino.get originalLightStatus <originalArduino.passSignalToLight() signalOnPin = turnOnPinAtRightTime( inSeconds, startTime ) _ <- arduino.set(Arduino(signalOnPin)) updatedArduino <- arduino.get updatedLightStatus <updatedArduino.passSignalToLight() _ <if ( originalLightStatus != updatedLightStatus ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 130 Experiments printLine(updatedLightStatus) else ZIO.succeed(1) yield () def run = for arduino <Ref.make(Arduino(pin1 = DigitalPin.OFF)) inSeconds <- currentTime(TimeUnit.SECONDS) _ <loopLogic(inSeconds, arduino).repeat( // Can we calculate how long this is // using Schedule APIs? Schedule.recurs(60) && Schedule.spaced(100.milliseconds) ) yield () end MicroControllerExample diningphilosophers experiments/src/main/scala/diningphilosophers/Philosophers.s package diningphilosophers /** * * * * * * * * * * * * Problem statement Five silent philosophers sit at a round table with bowls of spaghetti. Forks are placed between each pair of adjacent philosophers. Each philosopher must alternately think and eat. However, a philosopher can only eat spaghetti when they have both left and right forks. Each fork can be held by only one philosopher at a time and so a philosopher can use the fork only if it is not being used by another philosopher. After an individual Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 131 Experiments * philosopher finishes eating, they need to put * down both forks so that the forks become * available to others. A philosopher can only * take the fork on their right or the one on * their left as they become available and they * cannot start eating before getting both * forks. * * Eating is not limited by the remaining * amounts of spaghetti or stomach space; an * infinite supply and an infinite demand are * assumed. * * The problem is how to design a discipline of * behavior (a concurrent algorithm) such that * no philosopher will starve; i.e., each can * forever continue to alternate between eating * and thinking, assuming that no philosopher * can know when others may want to eat or * think. */ class Fork() case class Philosopher( left: Option[Fork] = None, right: Option[Fork] = None ): def pickupLeftFork(): Philosopher = ??? def pickupRightFork(): Philosopher = ??? def eat(): Philosopher = ??? /** Table: F1 <-> P1 <-> F2 <-> P2 <-> F3 <-> P3 * <-> F4 <-> P4 <-> F5 <-> P5 <-> F1 */ class Table( forks: List[Option[Fork]], philosophers: List[Philosopher] ): val circularForks = forks :+ forks.head val alternateRep: Iterator[ ((Option[Fork], Option[Fork]), Philosopher) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 132 Experiments ] = circularForks .sliding(2) .map(l => (l(0), l(1))) .zip(philosophers) object Dinner: val table = Table( List( Some(Fork()), Some(Fork()), Some(Fork()), Some(Fork()), Some(Fork()) ), List( Philosopher(), Philosopher(), Philosopher(), Philosopher(), Philosopher() ) ) interpreter-chaining_previous_result_and_environmental_dependency experiments/src/main/scala/interpreter/chaining_previous_result_and_environmental_dependency/ChainedWithPreviousResultAndEnvironmentDependency.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 133 Experiments package interpreter.chaining_previous_result_and_environmental_dependen\ cy import environment_exploration.ToyEnvironment import zio.{ZIO, ZIOAppDefault} import scala.reflect.{ClassTag, classTag} import scala.util.Random trait Operation[ Dependencies <: Service: ClassTag ]: val dep: ClassTag[Dependencies] = classTag[Dependencies] case class Value(value: String) extends Operation[AnyService] case class StringManipulation( action: String => String ) extends Operation[AnyService]: def actOn(input: String): String = action(input) case class Print() extends Operation[Printer] case class RandomString() extends Operation[RandomService] trait Service case class AnyService() extends Service case class RandomService() extends Service val program = Seq( Value("Hello There"), Print(), StringManipulation(_.toUpperCase()), Print(), StringManipulation(_.take(5)), Print(), RandomString(), Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 134 Experiments Print(), StringManipulation(_.toUpperCase()), Print() ) trait Printer extends Service: def print(input: String): Unit def interpretWithEnvironment( program: Seq[Operation[_]], environment: ToyEnvironment[Service] ): String = program.foldLeft("") { (acc, op) => // environment.get(op.dep) op match case Print() => // environment.get[Printer].print(acc) acc case RandomString() => scala .util .Random .alphanumeric .take(10) .mkString case Value(value) => value case StringManipulation(action) => action(acc) } @main def demoInterpreter() = val env = ToyEnvironment(Map.empty) .add(RandomService()) .add(AnyService()) interpretWithEnvironment(program, env) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 135 Experiments experiments-src-test-scala-energrygrid experiments/src/test/scala/energrygrid/GridSpec.scala package energrygrid import import import import zio.* zio.test.* energrygrid.GridErrors.* zio.test.TestAspect.ignore /* TODO Simulate a home-level grid managing power * needs and production Why? * - This is code that interacts with TheWorld in * numerous ways * - It's something I've wanted a more visceral * understanding of */ trait EnergyParticipant trait EnergyProvider: val producingPriority: Int sealed trait EnergyProducer extends EnergyParticipant with EnergyProvider: object SolarPanels extends EnergyProducer with EnergyProvider: override val producingPriority = 10 object Generator extends EnergyProducer with EnergyProvider: override val producingPriority = 1 def sendPowerTo( consumer: EnergyConsumer | EnergyBidirectional ): ZIO[Any, Overheat, Unit] = ??? sealed trait EnergyConsumer Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 136 Experiments extends EnergyParticipant: val consumerPriority: Int object EnergyConsumer: object Dishwasher extends EnergyConsumer: override val consumerPriority: Int = 3 object Wifi extends EnergyConsumer: override val consumerPriority: Int = 4 object Refrigerator extends EnergyConsumer: override val consumerPriority: Int = 5 def drawPowerFrom( producer: EnergyProducer | EnergyBidirectional ): ZIO[Any, InsufficientPower, Unit] = ??? sealed trait EnergyBidirectional extends EnergyProvider with EnergyConsumer: def sendPowerTo( consumer: EnergyConsumer | EnergyBidirectional ): ZIO[Any, Overheat, Unit] = ??? def drawPowerFrom( producer: EnergyProducer | EnergyBidirectional ): ZIO[Any, InsufficientPower, Unit] = ??? object EnergyBidirectional: object HomeBattery extends EnergyBidirectional: override val producingPriority: Int = 3 override val consumerPriority: Int = 2 object ExternalGrid extends EnergyBidirectional: override val producingPriority: Int = 1 override val consumerPriority: Int = 1 case class Grid( participants: Set[EnergyParticipant] ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 137 Experiments trait MunicipalGrid case class User(): val live: ZIO[Clock, UnsatisfiedNeeds, Unit] = ??? case class Home(family: User, grid: Grid): val provide = for _ <- family.live yield () sealed trait GridErrors object GridErrors: trait Unpowered trait UnsatisfiedNeeds trait InsufficientPower trait Overheat object GridSpec extends ZIOSpecDefault: def spec = suite("GridSpec")( test("recognizes grid input")( for _ <- ZIO.unit yield assertNever("Need a test!") ), test("runs through an energy scenario")( /* We start by running our dishwasher * before the sun is hitting our panels, * so we are drawing power fully from the * grid. Once the panels are active, they * provide most of the power, but we * still need some from the grid. Once * the dishes finished, we start feeding * the solar power back into the grid. * * 8:00 8:30 9:00 Dishwasher -1.5kw * -1.5kw 0kw Solar Panels 0kw +1.0kw * +1.0kw CityGrid +1.5kw +0.5kw -1.0kw */ for _ <- ZIO.unit yield assertNever("Need a test!") Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 138 Experiments ) ) @@ ignore end GridSpec layers experiments/src/main/scala/layers/Festival.scala package layers import zio.{ZIO, ZLayer, Duration} import zio.ZIO.debug import zio.durationInt case class Toilets() val toilets = ZLayer.scoped( ZIO.acquireRelease( debug("TOILETS: Setting up") *> ZIO.succeed(Toilets()) )(_ => debug("TOILETS: Removing")) ) case class Stage() val stage = ZLayer.scoped( ZIO.acquireRelease( activity( "STAGE", "Transporting", 2.seconds ) *> activity( "STAGE", "Building", 4.seconds ) *> ZIO.succeed(Stage()) )(_ => debug("STAGE: Tearing down")) ) case class Permit() Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 139 Experiments val permit = ZLayer.scoped( ZIO.acquireRelease( activity( "PERMIT", "Legal Request", 5.seconds ) *> ZIO.succeed(Permit()) )(_ => debug("PERMIT: Relinquished")) ) case class Venue(stage: Stage, permit: Permit) val venue = ZLayer.fromFunction(Venue.apply) case class Speakers() val speakers = ZLayer.scoped( ZIO.acquireRelease( debug("SPEAKERS: Positioning") *> ZIO.succeed(Speakers()) )(_ => debug("SPEAKERS: Packing up")) ) case class Amplifiers() val amplifiers = ZLayer.scoped( ZIO.acquireRelease( debug("AMPLIFIERS: Positioning") *> ZIO.succeed(Amplifiers()) )(_ => debug("AMPLIFIERS: Putting away")) ) case class Wires() val wires = ZLayer.scoped( ZIO.acquireRelease( debug("WIRES: Unrolling") *> ZIO.succeed(Wires()) )(_ => debug("WIRES: Spooling up")) ) case class Fencing() val fencing = ZLayer.scoped( ZIO.acquireRelease( Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 140 Experiments debug("FENCING: Surrounding the area") *> ZIO.succeed(Fencing()) )(_ => debug("FENCING: Tearing down")) ) case class SoundSystem( speakers: Speakers, amplifiers: Amplifiers, wires: Wires ) val soundSystem = for layer <ZLayer.fromFunction(SoundSystem.apply) scoped <ZLayer.scoped { ZIO.acquireRelease( debug( "SOUNDSYSTEM: Hooking up speakers, amplifiers, and wires" ) *> ZIO.succeed(layer.get) )(_ => debug( "SOUNDSYSTEM: Disconnecting speakers, amplifiers, and wires" ) ) } yield scoped val soundSystemShortedOut: ZLayer[ Speakers with Amplifiers with Wires, String, SoundSystem ] = for layer <ZLayer.fromFunction(SoundSystem.apply) scoped <ZLayer.scoped { ZIO.acquireRelease( debug( "SOUNDSYSTEM: Hooking up speakers, amplifiers, and wires" ) *> ZIO.fail("BZZZZ") *> ZIO.succeed(layer.get) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 141 Experiments )(_ => debug( "SOUNDSYSTEM: Disconnecting speakers, amplifiers, and wires" ) ) } yield scoped case class FoodTruck() val foodtruck = ZLayer.scoped( ZIO.acquireRelease( debug("FOODTRUCK: Driving in ") *> activity( "FOODTRUCK", "Fueling", 2.seconds ) *> ZIO.succeed(FoodTruck()) )(_ => debug("FOODTRUCK: Driving out ")) ) case class Festival( toilets: Toilets, venue: Venue, soundSystem: SoundSystem, fencing: Fencing, foodTruck: FoodTruck, security: Security ) val festival = for layer <- ZLayer.fromFunction(Festival.apply) scoped <ZLayer.scoped { ZIO.acquireRelease( debug("FESTIVAL: We are all set!") *> ZIO.succeed(layer.get) )(_ => debug( "FESTIVAL: Good job, everyone. Close it down!" ) ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 142 Experiments } yield scoped case class Security( toilets: Toilets, foodTruck: FoodTruck ) val security = for layer <- ZLayer.fromFunction(Security.apply) _ <ZLayer.scoped( ZIO.acquireRelease( debug("SECURITY: Ready") )(_ => debug("SECURITY: Going home")) ) yield layer def activity( entity: String, name: String, duration: Duration ) = debug(s"$entity: BEGIN $name") *> debug(s"$entity: END $name").delay(duration) javawrappers experiments/src/main/scala/javawrappers/InstantEOP.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 143 Experiments package javawrappers import zio.duration2DurationOps import java.time.Instant // TODO Consider deleting object InstantOps: extension (i: Instant) def plusZ(duration: zio.Duration): Instant = i.plus(duration.asJava) interpreter-level1_nochaining experiments/src/main/scala/interpreter/level1_nochaining/1_singleOperation.scala package interpreter.level1_nochaining /* Programs with no chained operations. * The interpreter only handles known types. */ trait Operation case class Print(s: String) extends Operation case class Random(f: Int => Unit) extends Operation object NoOp extends Operation def interpret(operation: Operation): Unit = operation match case p: Print => println(p.s) case r: Random => r.f(scala.util.Random.nextInt()) @main def m1 = val p1 = Print("hello") val r1 = Random(println) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 144 Experiments interpret(p1) interpret(r1) experiments/src/main/scala/interpreter/level1_nochaining/2_InterpretSequence.scala package interpreter.level1_nochaining def interpretSequence( prints: Seq[Operation] ): Unit = prints match case Nil => () case head :: tail => interpret(head) interpretSequence(tail) @main def demoSequence = val program = Seq( Print("asdf"), Print("hello"), Random(println) ) interpretSequence(program) bigdec experiments/src/main/scala/bigdec/Main.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 145 Experiments package bigdec import zio.{ ZIO, ZIOAppDefault, Console, Schedule } def inputBigDecimalValue( prompt: String, min: BigDecimal, max: BigDecimal ): ZIO[Any, Exception, BigDecimal] = for _ <- Console.printLine(prompt) input <- Console.readLine result <ZIO .attempt(BigDecimal(input)) .mapError(_ => Exception("Invalid input.") ) _ <ZIO.unless(min <= result && result <= max)( ZIO.fail( Exception( s"Input out of the range from $min to $max" ) ) ) yield result object Main extends ZIOAppDefault: def run = inputBigDecimalValue("Enter a number", 1, 10) .tapError(e => Console.printLineError(e.getMessage) ) .retry(Schedule.forever) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 146 Experiments typeclasses experiments/src/main/scala/typeclasses/PolymorphismUnboun package typeclasses import zio.* class Dog(): def bark() = println("woof") class Person(): def greet() = println("hello") trait Communicate[T]: extension (t: T) def communicate(): Unit trait Eater[T]: extension (t: T) def eat(): Unit given Communicate[Person] with extension (t: Person) override def communicate(): Unit = t.greet() given Communicate[Dog] with extension (t: Dog) override def communicate(): Unit = t.bark() class Cat() object PolymorphismUnbound extends App: def demo[T](instance: T)(using Communicate[T] ) = instance.communicate() demo( // Person() Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 147 Experiments Dog() ) ZIOFromNothing experiments/src/main/scala/ZIOFromNothing/ZIOFromScratch. package ZIOFromNothing class XEnvironment(): def increment(y: Int): Int = XEnvironment.x += y XEnvironment.x object XEnvironment: private var x: Int = 0 case class IO(behavior: () => Unit): def compose(other: IO) = IO(() => behavior() println("New behavior from compose") other.behavior() ) object Interpreter: def run(io: IO) = io.behavior() @main def runEffects = val hi = IO(() => println("hi ")) val there = IO(() => println("there!")) val fullApp = hi.compose(there) Interpreter.run(fullApp) case class XIO[A](behavior: (x: String) => A): def compose(other: XIO[A]) = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 148 Experiments XIO[A]((x: String) => println(s"Executing with Environment: $x") behavior(x) other.behavior(x) ) object XInterpreter: def run[A](io: XIO[A], x: String) = io.behavior(x) @main def XrunEffects = val hi = XIO(x => println("hi ")) val there = XIO(x => println("there!")) val x = "Planet Z" val fullApp = hi.compose(there) XInterpreter.run(fullApp, x) // 3 import zio._ import zio.Console.printLine val zioLogic = for _ <- printLine("Accessing the environment") state1 <ZIO.serviceWithZIO[XEnvironment](env => ZIO.succeed(env.increment(1)) ) _ <- printLine("state1: " + state1) state2 <ZIO.serviceWithZIO[XEnvironment](env => ZIO.succeed(env.increment(1)) ) _ <- printLine("state2: " + state2) yield () object RealZIOEnvironmentPassingExplicitlyProvided extends ZIOAppDefault: def run = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 149 Experiments zioLogic.provideLayer( ZLayer.succeed(XEnvironment()) ) object RealZIOEnvironmentPassingProvidingSome extends ZIOAppDefault: def run = zioLogic.provideSomeLayer( ZLayer.succeed(XEnvironment()) ) case class XIO3[X, A](behavior: (x: X) => A): def compose(other: XIO3[X, A]) = XIO3[X, A]((x: X) => behavior(x) println("New behavior from compose") other.behavior(x) ) object XIO3Interpreter: def run[X, A](io: XIO3[X, A], x: X) = io.behavior(x) @main def XIO3runEffects = val hi = XIO3(x => println("hi ")) val there = XIO3(x => println("there!")) val fullApp = hi.compose(there) val z = "Planet Z" XIO3Interpreter.run(fullApp, z) val magicNumber = "42" XIO3Interpreter.run(fullApp, magicNumber) experiments-src-test-scala-test_aspects experiments/src/test/scala/test_aspects/WithLiveSpec.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 150 Experiments package test_aspects import zio.* import zio.test.* import zio.test.TestAspect.* object WithLiveSpec extends ZIOSpecDefault: def halfFlaky[A](a: A): ZIO[Any, String, A] = for b <- zio.Random.nextBoolean.debug o <ZIO .cond(b, a, "failed") .tapError(ZIO.logError(_)) yield o val song = for _ <- halfFlaky("works").debug yield assertCompletes val song1: Spec[Any, String] = test("Song 1")(song) val songFlaky : Spec[Live & Annotations, String] = test("Song Flaky")(song) @@ flaky(10) @@ withLiveRandom val spec = suite("Play some music")( song1, songFlaky, test("Song 2")(assertCompletes) ) end WithLiveSpec Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 151 Experiments testcontainers experiments/src/main/scala/testcontainers/InteractWithDatab package testcontainers import io.github.scottweaver.zio.testcontainers.postgres.ZPostgreSQLCon\ tainer import zio.* object InteractWithDatabase extends ZIOAppDefault: // val logic = // for { // _ <// // } def run = UserService .get("blah") .provide( UserServiceLive.layer, // UserActionServiceLive.layer, QuillContext.dataSourceLayer ) experiments/src/main/scala/testcontainers/QuillContext.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 152 Experiments package testcontainers import com.typesafe.config.ConfigFactory import io.getquill.context.ZioJdbc.DataSourceLayer import io.getquill.jdbczio.Quill import io.getquill.{ NamingStrategy, PluralizedTableNames, PostgresZioJdbcContext, SnakeCase } import zio.* import javax.sql.DataSource import scala.jdk.CollectionConverters.MapHasAsJava /** QuillContext houses the datasource layer * which initializes a connection pool. This has * been slightly complicated by the way Heroku * exposes its connection details. Database URL * will only be defined when run from Heroku in * production. */ object QuillContext extends PostgresZioJdbcContext( NamingStrategy( PluralizedTableNames, SnakeCase ) ): val dataSourceLayer : ZLayer[Any, Nothing, DataSource] = ZLayer { for _ <- ZIO.debug("Hi") herokuURL <System.env("DATABASE_URL").orDie _ <- ZIO.debug("Bye") localDBConfig = Map( "dataSource.user" -> "postgres", "dataSource.password" -> "", Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 153 Experiments "dataSource.url" -> "jdbc:postgresql://localhost:5432/postgres" ) configMap = herokuURL .map(parseHerokuDatabaseUrl(_).toMap) .getOrElse(localDBConfig) config = ConfigFactory.parseMap( configMap .updated( "dataSourceClassName", "org.postgresql.ds.PGSimpleDataSource" ) .asJava ) yield Quill .DataSource .fromConfig(config) .orDie }.flatten /** HerokuConnectionInfo is a wrapper for the * datasource information to make it * compatible with Heroku */ final case class HerokuConnectionInfo( username: String, password: String, host: String, port: String, dbname: String ): def toMap: Map[String, String] = Map( "dataSource.user" -> username, "dataSource.password" -> password, "dataSource.url" -> s"jdbc:postgresql://$host:$port/$dbname" ) /** Parses the necessary information out of the Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 154 Experiments * Heroku formatted URL */ def parseHerokuDatabaseUrl( string: String ): HerokuConnectionInfo = string match case s"postgres://$username:$password@$host:$port/$dbname" => HerokuConnectionInfo( username, password, host, port, dbname ) end QuillContext experiments/src/main/scala/testcontainers/UserActionService. package testcontainers import io.getquill.{Query, Quoted} import zio.* import java.sql.SQLException import java.time.{Instant, LocalDateTime} import javax.sql.DataSource enum ActionType: case LogIn, LogOut, UpdatePreferences case class UserAction( userId: String, actionType: ActionType, timestamp: LocalDateTime ) trait UserActionService: def get( Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 155 Experiments userId: String ): ZIO[Any, UserNotFound, List[UserAction]] def insert( user: UserAction ): ZIO[Any, Nothing, Long] object UserActionService: def get( userId: String ): ZIO[UserActionService, UserNotFound, List[ UserAction ]] = ZIO.serviceWithZIO[UserActionService](x => x.get(userId) ) // use .option ? def insert( user: UserAction ): ZIO[UserActionService, Nothing, Long] = ZIO.serviceWithZIO[UserActionService](x => x.insert(user) ) final case class UserActionServiceLive( dataSource: DataSource ) extends UserActionService: import io.getquill._ // SnakeCase turns firstName -> first_name val ctx = new PostgresZioJdbcContext( NamingStrategy( PluralizedTableNames, SnakeCase ) ) import ctx._ inline def runWithSourceQuery[T]( inline quoted: Quoted[Query[T]] ): ZIO[Any, SQLException, List[T]] = run(quoted).provideEnvironment( ZEnvironment(dataSource) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 156 Experiments ) inline def runWithSourceInsert[T]( inline quoted: Quoted[Insert[T]] ): ZIO[Any, SQLException, Long] = run(quoted).provideEnvironment( ZEnvironment(dataSource) ) import java.util.UUID implicit val encodeUserAction : MappedEncoding[ActionType, String] = MappedEncoding[ActionType, String]( _.toString ) implicit val decodeUserAction : MappedEncoding[String, ActionType] = MappedEncoding[String, ActionType]( ActionType.valueOf(_) ) implicit val encodeUUID : MappedEncoding[Instant, String] = MappedEncoding[Instant, String](_.toString) implicit val decodeUUID : MappedEncoding[String, Instant] = MappedEncoding[String, Instant]( Instant.parse(_) ) def get( userId: String ): ZIO[Any, UserNotFound, List[UserAction]] = inline def somePeople = quote { query[UserAction] .filter(_.userId == lift(userId)) } runWithSourceQuery(somePeople).orDie def insert( Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 157 Experiments user: UserAction ): ZIO[Any, Nothing, Long] = inline def insert = quote { query[UserAction].insertValue(lift(user)) } runWithSourceInsert(insert).orDie end UserActionServiceLive object UserActionServiceLive: val layer : URLayer[DataSource, UserActionService] = ZLayer.fromFunction( UserActionServiceLive.apply _ ) experiments/src/main/scala/testcontainers/UserService.scala package testcontainers import io.getquill.{Query, Quoted} import zio.* import io.getquill._ import java.sql.SQLException import javax.sql.DataSource trait UserNotFound case class User(userId: String, name: String) trait UserService: def get( userId: String ): ZIO[Any, UserNotFound, User] def insert(user: User): ZIO[Any, Nothing, Long] // TODO update(user) object UserService: def get(userId: String): ZIO[ UserService with DataSource, Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 158 Experiments UserNotFound, User ] = ZIO.serviceWithZIO[UserService]( _.get(userId) ) // use .option ? def insert(user: User): ZIO[ UserService with DataSource, Nothing, Long ] = // TODO Um? Why Nothing????? ZIO.serviceWithZIO[UserService]( _.insert(user) ) final case class UserServiceLive( dataSource: DataSource ) extends UserService: // SnakeCase turns firstName -> first_name val ctx = new PostgresZioJdbcContext( NamingStrategy( PluralizedTableNames, SnakeCase ) ) import ctx.{run, lift, _} def get( userId: String ): ZIO[Any, UserNotFound, User] = inline def somePeople = quote { query[User] .filter(_.userId == lift(userId)) } run(somePeople) .provideEnvironment( ZEnvironment(dataSource) ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 159 Experiments .orDie .map(_.head) def insert( user: User ): ZIO[Any, Nothing, Long] = inline def insert = quote { query[User].insertValue(lift(user)) } run(insert) .provideEnvironment( ZEnvironment(dataSource) ) .orDie end UserServiceLive object UserServiceLive: val layer = ZLayer.fromZIO( for datasource <- ZIO.service[DataSource] yield UserServiceLive(datasource) ) interpreter-level2_chaining experiments/src/main/scala/interpreter/level2_chaining/3_chainedOperations.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 160 Experiments package interpreter.level2_chaining trait Operation: val nextAction: String => Operation object DoNothing extends Operation: override val nextAction = s => DoNothing case class Print( s: String, nextAction: String => Operation = _ => DoNothing ) extends Operation //case class Pure( // value: String //) extends Operation def interpreter3(doSomething: Operation): Unit = doSomething match case DoNothing => () case Print(s, nextAction) => println(s) // It's weird that we're giving this value // during interpretation interpreter3(nextAction("hello")) @main def m1 = val conditionallyExecuteAnotherOperation : String => Operation = s => if s == "hello" then Print(s) else DoNothing val program3: Operation = Print( "asdf", conditionallyExecuteAnotherOperation Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 161 Experiments ) interpreter3(Print("some thing")) interpreter3(program3) experiments/src/main/scala/interpreter/level2_chaining/4_chainedRandom.scala package interpreter.level2_chaining case class ToyRandom( nextAction: String => Operation ) extends Operation val program: Operation = ToyRandom(s => Print(s)) def interpreter(doSomething: Operation): Unit = doSomething match case DoNothing => () case r: ToyRandom => val i = scala.util.Random.nextInt() interpreter(r.nextAction(i.toString)) case p: Print => println(p.s) interpreter(p.nextAction("")) @main def m4 = interpreter(program) experiments/src/main/scala/interpreter/level2_chaining/5_useEnvironmentInstances.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 162 Experiments package interpreter.level2_chaining import environment_exploration.ToyEnvironment def interpret( env: ToyEnvironment[ scala.util.Random & scala.Console.type ], doSomething: Operation ): Unit = doSomething match case DoNothing => () case r: ToyRandom => val i = env.get[scala.util.Random].nextInt() interpret(env, r.nextAction(i.toString)) case p: Print => env.get[scala.Console.type].println(p.s) interpret(env, p.nextAction("")) @main def demoInterpretWithEnvironment() = val env = ToyEnvironment(Map.empty) .add[scala.util.Random](scala.util.Random) .add(scala.Console) interpret(env, program) Hubs experiments/src/main/scala/Hubs/BasicHub.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 163 Experiments package Hubs import import import import zio.* zio.Duration zio.Clock zio.Console // The purpose of this example to to create a // very basic hub that displays small // capabilities. object BasicHub extends zio.ZIOAppDefault: // This example makes a hub, and publishes a // String. Then, two entities take the // published string and print it. val logic1 = Hub .bounded[String](2) .flatMap { Hub => ZIO.scoped { Hub .subscribe .zip(Hub.subscribe) .flatMap { case (left, right) => for _ <Hub.publish( "This is from Hub left!" ) _ <left .take .flatMap( Console.printLine(_) ) _ <right .take .flatMap( Console.printLine(_) ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 164 Experiments yield () } } } /* * * * * * * * * * * * * * * case class entity(name:String) case class question(ques:String) case class response(rep:String, ent:entity) val entities = List(entity("Bob"), entity("Smith")) //This example sends out a question in the form of a string. Then, two //entities respond with different reponses. val logic2 = for questHub <- Hub.bounded[question](1) repHub <Hub.bounded[response](entities.size) _ <questHub.subscribe.zip(repHub.subscribe).use { case ( Quest, Resp ) = } */ def run = logic1.exitCode end BasicHub experiments/src/main/scala/Hubs/QuizGame.scala package Hubs import console.FakeConsole import java.io.IOException import zio.{ Clock, Console, Dequeue, Duration, Hub, Ref, Schedule, ZIO, durationInt Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 165 Experiments } import zio.Console.printLine object QuizGame extends zio.ZIOAppDefault: case class Player(name: String) case class Question( text: String, correctResponse: String ) case class Answer( player: Player, text: String, delay: Duration ) case class RoundDescription( question: Question, answers: Seq[Answer] ) def run = // Use App's run function /* Teacher --> Questions --> Student1 --> * Answers --> Teacher Student2 Student3 */ val val val val frop zeb shtep cheep = = = = Player("Frop") Player("Zeb") Player("Shtep") Player("Cheep") val students: List[Player] = List(frop, zeb, shtep, cheep) def submitAnswersAfterDelay( answerHub: Hub[Answer], answers: Seq[Answer] ) = ZIO .collectAllPar( answers.map { answer => Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 166 Experiments for _ <- ZIO.sleep(answer.delay) _ <- answerHub.publish(answer) yield () } ) .unit def recordCorrectAnswers( correctAnswer: String, answers: Dequeue[Answer], correctRespondents: Ref[List[Player]] ) = for // gather answers until there's a winner answer <- answers.take output <if (answer.text == correctAnswer) for currentCorrectRespondents <correctRespondents.get _ <correctRespondents.set( currentCorrectRespondents :+ answer.player ) yield "Correct response from: " + answer.player else ZIO.succeed( "Incorrect response from: " + answer.player ) _ <- printLine(output) yield () def untilWinnersAreFound( correctRespondents: Ref[List[Player]] ) = Schedule.recurUntilZIO(_ => correctRespondents.get.map(_.size == 2) ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 167 Experiments def printRoundResults( winners: List[Player] ) = val finalOutput = if (winners.isEmpty) "Nobody submitted a correct response" else if (winners.size == 2) "Winners: " + winners.mkString(",") else "Winners of incomplete round: " + winners.mkString(",") printLine(finalOutput) val roundWithMultipleCorrectAnswers = RoundDescription( Question( "What is the southern-most European country?", "Spain" ), Seq( Answer(zeb, "Germany", 2.seconds), Answer(frop, "Spain", 1.seconds), Answer(cheep, "Spain", 3.seconds), Answer(shtep, "Spain", 4.seconds) ) ) val roundWithOnly1CorrectAnswer = RoundDescription( Question( "What is the lightest element?", "Hydrogen" ), Seq( Answer(frop, "Lead", 2.seconds), Answer(zeb, "Hydrogen", 1.seconds), Answer(cheep, "Gold", 3.seconds), Answer(shtep, "Hydrogen", 10.seconds) ) ) val roundWhereEverybodyIsWrong = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 168 Experiments RoundDescription( Question( "What is the average airspeed of an unladen swallow?", "INSUFFICIENT DATA FOR MEANINGFUL ANSWER" ), Seq( Answer(frop, "3.0 m/s", 1.seconds), Answer(zeb, "Too fast", 1.seconds), Answer( cheep, "Not fast enough", 1.seconds ), Answer(shtep, "Scary", 1.seconds) ) ) val rounds = Seq( roundWithMultipleCorrectAnswers, roundWithOnly1CorrectAnswer, roundWhereEverybodyIsWrong ) val cahootSingleRound = for questionHub <- Hub.bounded[Question](1) answerHub: Hub[Answer] <Hub.bounded[Answer](students.size) correctRespondents: Ref[List[Player]] <Ref.make[List[Player]](List.empty) _ <questionHub .subscribe .zip(answerHub.subscribe) .flatMap { case ( questions, answers: Dequeue[Answer] ) => def playARound( roundDescription: RoundDescription Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 169 Experiments ) = for _ <printLine( "===============================" ) _ <printLine( "Question for round: " + roundDescription .question .text ) _ <correctRespondents .set(List.empty) _ <questionHub.publish( roundDescription.question ) question <- questions.take _ <ZIO .collectAllPar( Seq( submitAnswersAfterDelay( answerHub, roundDescription .answers ), recordCorrectAnswers( roundDescription .question .correctResponse, answers, correctRespondents ).repeat( untilWinnersAreFound( correctRespondents ) ) ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 170 Experiments ) .timeout(4.second) winners <correctRespondents.get _ <printRoundResults(winners) _ <printLine( "===============================" ) yield () ZIO.foreach(rounds)(playARound) } yield () cahootSingleRound.exitCode end run end QuizGame experiments/src/main/scala/Hubs/ReadIntAndMultiply.scala package Hubs import import import import import import console.FakeConsole zio.ZIO zio.* zio.Duration.* zio.Clock.* zio.Console.* object ReadIntAndMultiply extends zio.ZIOAppDefault: def run = // Use App's run function val logic = for hub <- Hub.bounded[Int](2) _ <ZIO.scoped { Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 171 Experiments hub .subscribe .flatMap { hubSubscription => val getAndStoreInput = for _ <Console.printLine( "Please provide an int" ) input <- Console.readLine nextInt = input.toInt _ <- hub.publish(nextInt) yield () val processNextIntAndPrint = for nextInt <hubSubscription.take _ <Console.printLine( "Multiplied Int: " + nextInt * 5 ) yield () val reps = 5 for _ <ZIO .collectAllPar( Set( getAndStoreInput .repeatN(reps), processNextIntAndPrint .forever ) ) .timeout(5.seconds) yield () } } yield () Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 172 Experiments ( for fakeConsole <FakeConsole.withInput( "3", "5", "7", "9", "11", "13" ) _ <- logic.withConsole(fakeConsole) yield () ).exitCode end run end ReadIntAndMultiply TicTacToe experiments/src/main/scala/TicTacToe/TicTacToe.scala // From: https://scalac.io/blog/write-command-line-application-with-zio/ package TicTacToe import zio.{Console, ZIOAppDefault, ZIO} enum MenuCommand: case NewGame, Resume, Quit, Invalid object TicTacToe extends ZIOAppDefault: val program : ZIO[Any, java.io.IOException, Unit] = Console.printLine("TicTacToe game!") def run = program.foldZIO( Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 173 Experiments error => Console.printLine( s"Execution failed with: $error" ) *> ZIO.succeed(1), _ => ZIO.succeed(0) ) // ----------------------------------abstract case class Name private (name: String) object Name: def make(name: String) = new Name(name) {} // Can't inherit because constructor is private: // class FirstName(fn: String) extends Name(fn) def bar = val name: Name = Name.make("Bob") // val name2 = new Name("Joe") {} // val jan = name.copy("Janet") // ----------------------------------trait X object X: var x: Int = 0 trait XIO[IO, R] case class IntXIO(i: Int) extends XIO[X, Int] def combine2(a: Int, b: Int): XIO[X, Int] = X.x += 1 IntXIO(a + b + X.x) def foo = combine2(1, 2) //trait YIO[ENV, F, R] extends Either[F, R] Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 174 Experiments runtime experiments/src/main/scala/runtime/BuiltInServices.scala package runtime import zio.{Console, ZIO, ZIOAppDefault} import java.time.Instant object BuiltInServices extends ZIOAppDefault: val logic : ZIO[Any, java.io.IOException, Instant] = for now <ZIO.clockWith(clock => clock.instant) _ <- Console.printLine("Now: " + now) yield now def run = logic experiments-src-test-scala-testcontainers experiments/src/test/scala/testcontainers/DbMigration.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 175 Experiments package testcontainers import import import import import io.github.scottweaver.models.JdbcInfo org.flywaydb.core.Flyway org.flywaydb.core.api.configuration.FluentConfiguration zio.* zio.test.TestAspect.{before, beforeAll} object DbMigration: type ConfigurationCallback = (FluentConfiguration) => FluentConfiguration private def doMigrate( jdbcInfo: JdbcInfo, configureCallback: ConfigurationCallback, locations: String* ) = ZIO.attempt { val flyway = configureCallback { val flyway = Flyway .configure() .dataSource( jdbcInfo.jdbcUrl, jdbcInfo.username, jdbcInfo.password ) if (locations.nonEmpty) flyway.locations(locations: _*) else flyway }.load() flyway.migrate } def migrate(mirgationLocations: String*)( configureCallback: ConfigurationCallback = identity ) = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 176 Experiments ZIO .service[JdbcInfo] .flatMap(jdbcInfo => doMigrate( jdbcInfo, configureCallback, mirgationLocations: _* ) ) .orDie def migratedLayer( jdbcInfo: ZEnvironment[JdbcInfo] ): ZLayer[Any, Throwable, Unit] = ZLayer.fromZIO( DbMigration .migrate("db")() .provideEnvironment(jdbcInfo) .unit ) end DbMigration experiments/src/test/scala/testcontainers/SharedDbLayer.scal package testcontainers import io.github.scottweaver.models.JdbcInfo import io.github.scottweaver.zio.testcontainers.postgres.ZPostgreSQLCon\ tainer import zio.ZLayer import javax.sql.DataSource object SharedDbLayer: val layer = for layer <ZLayer.make[DataSource & JdbcInfo]( ZPostgreSQLContainer.live, ZPostgreSQLContainer.Settings.default Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 177 Experiments ) _ <- DbMigration.migratedLayer(layer) yield layer experiments/src/test/scala/testcontainers/TestContainerLayer package testcontainers import com.zaxxer.hikari.{ HikariConfig, HikariDataSource } import io.github.scottweaver.models.JdbcInfo import zio.* import java.util.Properties import javax.sql.DataSource import scala.jdk.CollectionConverters.MapHasAsJava object TestContainerLayers: val dataSourceLayer : ZLayer[JdbcInfo, Nothing, DataSource] = ZLayer { for jdbcInfo <- ZIO.service[JdbcInfo] datasource <ZIO .attemptBlocking( unsafeDataSourceFromJdbcInfo( jdbcInfo ) ) .orDie yield datasource } private def unsafeDataSourceFromJdbcInfo( jdbcInfo: JdbcInfo ): DataSource = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 178 Experiments val props = new Properties() props.putAll( Map( "driverClassName" -> jdbcInfo.driverClassName, "jdbcUrl" -> jdbcInfo.jdbcUrl, "username" -> jdbcInfo.username, "password" -> jdbcInfo.password ).asJava ) println("JdbcInfo: " + jdbcInfo) new HikariDataSource(new HikariConfig(props)) end TestContainerLayers experiments/src/test/scala/testcontainers/UserActionSpec.scal package testcontainers import com.dimafeng.testcontainers.PostgreSQLContainer import io.github.scottweaver.models.JdbcInfo import io.github.scottweaver.zio.aspect.DbMigrationAspect import io.github.scottweaver.zio.testcontainers.postgres.ZPostgreSQLCon\ tainer import io.github.scottweaver.zio.testcontainers.postgres.ZPostgreSQLCon\ tainer.{ Settings, live } import org.postgresql.ds.PGSimpleDataSource import zio.* import zio.test.* import java.sql.Connection import javax.sql.DataSource object UserActionSpec /* extends ZIOSpec[DataSource & JdbcInfo]: * val bootstrap = SharedDbLayer.layer * * def spec = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 179 Experiments * * * * * * suite("UserActionService")( test("inserts a user") { for _ <- UserActionService .get("uuid_hard_coded") .debug("Actions") yield assertCompletes } ).provideSomeShared[DataSource]( UserActionServiceLive.layer ) */ experiments/src/test/scala/testcontainers/UserServiceSpec.sca package testcontainers import com.dimafeng.testcontainers.PostgreSQLContainer import io.github.scottweaver.models.JdbcInfo import zio.test.* import zio.* import io.github.scottweaver.zio.testcontainers.postgres.ZPostgreSQLCon\ tainer.live import io.github.scottweaver.zio.testcontainers.postgres.ZPostgreSQLCon\ tainer.Settings import io.github.scottweaver.zio.testcontainers.postgres.ZPostgreSQLCon\ tainer import io.github.scottweaver.zio.aspect.DbMigrationAspect import org.postgresql.ds.PGSimpleDataSource import zio.test.TestAspect.ignore import java.sql.Connection import javax.sql.DataSource object UserServiceSpec /* extends ZIOSpec[DataSource & JdbcInfo]: * val bootstrap = SharedDbLayer.layer def spec = * suite("UserService")( test("retrieves an * existin user")( for user <- UserService * .get("uuid_hard_coded") .debug yield * assertCompletes ), test("inserts a user") { * val newUser = * User("user_id_from_app", "Appy") for _ <* UserService.insert(newUser) user <* UserService.get(newUser.userId) yield * assertTrue(newUser == user) } Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 180 Experiments * ).provideSomeShared[DataSource]( * UserServiceLive.layer ) end UserServiceSpec */ crypto experiments/src/main/scala/crypto/Mining.scala package crypto import zio._ import ZIO.debug import zio.Random.nextIntBetween import java.io.IOException import scala.annotation.tailrec object Mining extends ZIOAppDefault: def run = for chain <- Ref.make[BlockChain](BlockChain()) _ <- raceForNextBlock(chain).repeatN(5) _ <- chain.get.debug("Final") yield () private val miners = Seq("Zeb", "Frop", "Shtep") .flatMap(minerName => Range(1, 50) .map(i => new Miner(minerName + i)) ) def raceForNextBlock( chain: Ref[BlockChain] ): ZIO[Any, Nothing, Unit] = for raceResult <- findNextBlock(miners) (winner, winningPrime) = raceResult _ <chain.update(chainCurrent => chainCurrent.copy(blocks = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 181 Experiments chainCurrent.blocks :+ winningPrime ) ) _ <debug( s"$winner mined block: $winningPrime" ) yield () case class BlockChain( blocks: List[Int] = List.empty ) class Miner(val name: String): def mine( num: Int ): ZIO[Any, Nothing, (String, Int)] = for duration <- nextIntBetween(1, 4) _ <- ZIO.sleep(duration.second) yield (name, nextPrimeAfter(num)) def findNextBlock( miners: Seq[Miner] ): ZIO[Any, Nothing, (String, Int)] = for startNum <- nextIntBetween(2000, 4000) result <ZIO.raceAll( miners.head.mine(startNum), miners.tail.map(_.mine(startNum)) ) yield result end Mining // TODO Consider putting math functions somewhere else to avoid clutter\ ing example private def isPrime(num: Int): Boolean = (2 until num) .forall(divisor => num % divisor != 0) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 182 Experiments @tailrec private def nextPrimeAfter(num: Int): Int = if (isPrime(num)) num else nextPrimeAfter(num + 1) environment_exploration-opaque experiments/src/main/scala/environment_exploration/opaque/ToyEnvironment.scala package environment_exploration.opaque import scala.reflect.{ClassTag, classTag} case class DBService(url: String) opaque type ToyEnvironment[R] = Map[ClassTag[_], _] object ToyEnvironment: def apply[A: ClassTag]( a: A ): ToyEnvironment[A] = Map(classTag[A] -> a) extension [R](env: ToyEnvironment[R]) def add[A: ClassTag]( a: A ): ToyEnvironment[R & A] = env + (classTag[A] -> a) def get[A >: R: ClassTag]: A = env(classTag[A]).asInstanceOf[A] @main def demoToyEnvironment = val env1: ToyEnvironment[String] = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 183 Experiments ToyEnvironment("hi") val env2: ToyEnvironment[String & DBService] = env1.add(DBService("blah")) val env3: ToyEnvironment[ String & DBService & List[String] & List[Int] ] = env2.add(List("a", "b")).add(List(1, 2)) println(env3.get[String]) println(env3.get[DBService]) println(env3.get[List[String]]) println(env3.get[List[Int]]) // We get some amount of compile time safety here, but not much // println(env.get(classOf[List[DBService]])) simulations experiments/src/main/scala/simulations/Evolution.scala package simulations import zio._ enum Action: case Stay case Move(x: Int, y: Int) object Evolution extends ZIOAppDefault: case class Percentage(value: Int): assert(value >= 0 & value <= 100) case class Creature( energy: Int, explore: Percentage ) case class Coordinate( food: Int, Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 184 Experiments produceFood: Percentage, occupant: Option[Creature] ) case class Spots( coordinates: List[List[Coordinate]] ): override def toString() = coordinates .map(row => row .map(column => if (column.food > 0) '*' else '\u25a1' ) .mkString ) .mkString("\n") object Spots: def apply(rows: Int, columns: Int): Spots = Spots( List.fill(rows)( List.fill(columns)( Coordinate( 1, Percentage(10), occupant = None ) ) ) ) def run = for _ <- ZIO.log("Should do evolution stuff") spots = Spots(4, 4) _ <- ZIO.debug(spots) yield () end Evolution Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 185 Experiments object EvolutionT: trait Creature: def decide(): Action trait Spots: def udpate(): Spots HelloZio experiments/src/main/scala/HelloZio/CalculatorExample.scala package HelloZio import HelloZio.CalculatorExample.input import java.io.IOException import zio.Console.{readLine, printLine} import zio.Console import zio.Console import zio.{ IO, Ref, Runtime, ZIO, ZLayer, ZIOAppDefault } enum ArithmeticOperation(a: Float, b: Float): case Add(first: Float, second: Float) extends ArithmeticOperation(first, second) case Divide(first: Float, second: Float) extends ArithmeticOperation(first, second) // def calculateX(): ZIO[Any, Throwable | // ArithmeticException, String] Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 186 Experiments // // // // // // // This a more complicated example of using ZIO to create a safely running program. This is a simple calculator app that takes in an opperation index, and two numbers. It then prints the resulting calculation of the two numbers based on the operation index. // // // // // The ZIO are used in several ways. The ZIO are used to ensure propper IO Exception and error handling, and they are used handle invalid inputs from the user. def calculate(): ZIO[ Any, Throwable | ArithmeticException, String ] = // This in an function used in calculations implemented below. this match case Add(first, second) => ZIO.succeed { s"Adding $first and $second: ${first - second}" } case Divide(first, second) => if (second != 0.0) ZIO.succeed { s"Dividing $first by $second: ${first / second}" } else ZIO.fail( new ArithmeticException( "divide by 0" ) ) end ArithmeticOperation object ArithmeticOperation: // This in an object used in calculations i\ mplemented below. def fromInt( index: Int Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 187 Experiments ): (Float, Float) => ArithmeticOperation = index match case 1 => Add.apply case 2 => Divide.apply case _ => throw new RuntimeException("boom") // extends zio.App object CalculatorExample extends ZIOAppDefault: def input: ZIO[ // This function prompts and accepts the input from t\ he user. Console, IOException, Vector[String] ] = for _ <printLine(""" ~~~~~~~~~~~~~~~~ Input Option: 1) Add 2) Subtract 3) Multiply 4) Devide """) in <readLine // User inputs the operation index _ <- printLine(s"input: ${in}") _ <- printLine("Enter first number: ") firstNum <readLine // User inputs first and second number _ <- printLine("Enter second number: ") secondNum <- readLine yield Vector( in, firstNum, secondNum ) // The function returns a ZIO that succeeds with a vector of Stri\ ngs Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 188 Experiments def operate( // This function takes in the inputs, and processes them\ to ensure they are valid. // The function then returns a String // statement of the calculation. input: Vector[String] ): ZIO[ Any, String | NumberFormatException | ArithmeticException | Throwable, String ] = for number <- // note that `(number1, number2)` now results in: value\ withFilter is not a member of zio.ZIO[Any, Nothing, (Float, Float)], b\ ut could be made available as an extension method. ZIO.attempt { (input(1).toFloat, input(2).toFloat) } // The inputs are cast as Floats, and passed into a ZIO objec\ t. result <ArithmeticOperation // This object, defined above, processes th\ e operation index, and passes the according calculation to the function\ calculate. .fromInt(input(0).toInt)( number._1, number._2 ) .calculate() // calculate takes the input numbers from Arithm\ eticOperation, and creates the return statement // _ <- printLine("Typed, parse operation: " // + // operation) // output <// input(0) match // case "2" => // ZIO { // s"Subtracting $number1 and $number2: // ${number1 - number2}" // } // case "3" => // ZIO { Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 189 Experiments // s"Multiplying $number1 and $number2: // ${number1 * number2}" // } // case badIndex => ZIO.fail("unknown program // index: " + badIndex) yield result def run = println("In tester") val stringRef = Ref.make( Seq("1", "2", "3") ) // This is used in the automation software for running examples. val operated = for // console <// FakeConsole.createConsoleWithInput(Seq("1", // "24", "8")) console <console .FakeConsole .withInput( "2", "96", "8" ) // Run this program with the following inputs i <input.provide(ZLayer.succeed(console)) output <operate(i).catchAll { case x: String => ZIO.succeed("Input failure: " + x) case x: Throwable => ZIO .succeed("toFloat failure: " + x) } _ <- printLine(output) yield () operated.exitCode end run Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 190 Experiments end CalculatorExample Parallelism experiments/src/main/scala/Parallelism/BasicFiber.scala package Parallelism import java.io.IOException import zio.Console import zio.{Fiber, IO, Runtime, UIO, ZIO, ZLayer} object BasicFiber: // // // // // Fibers model a running IO: Fiber[E,A]. They have an error type, and a success type. They don't need an input environment type. They are not technically effects, but they can be converted to effects. object computation: // This object performs a computation that takes \ a long time. It is a recursive Fibonacci Sequence generator. def fib(n: Long): UIO[Long] = ZIO .succeed { if (n <= 1) ZIO.succeed(n) else fib(n - 1).zipWith(fib(n - 2))(_ + _) } .flatten // Fork will take an effect, and split off a // Fiber version of it. // This ZIO will output a Fiber that is // computing the 100th digit of the Fibonacci // Sequence. val fib100: UIO[Fiber[Nothing, Long]] = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 191 Experiments for fiber <- computation.fib(100).fork yield fiber // Part of the power of Fibers is that many of // them can be described and run at once. // This function uses two numbers (n and m), // and outputs two Fibers that will find the // n'th and m'th Fibonacci numbers val n: Long = 50 val m: Long = 100 val fibNandM : UIO[Vector[Fiber[Nothing, Long]]] = for fiberN <- computation.fib(n).fork fiberM <- computation.fib(m).fork yield Vector(fiberN, fiberM) end BasicFiber experiments/src/main/scala/Parallelism/Compose.scala package Parallelism import import import import java.io.IOException zio._ zio.Console._ zio.Fiber._ class Compose: // Composing Fibers will combine 2 or more // fibers into a single fiber. This new fiber // will produce the results of both. If any of // the fibers fail, the entire zipped fiber // will also fail. // Note: The results of the zipped fibers will // be put into a tuple. val helloGoodbye: UIO[Tuple] = for Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 192 Experiments greeting <- ZIO.succeed("Hello!").fork farewell <- ZIO.succeed("GoodBye!").fork totalFiber = greeting.zip( farewell ) // Note the '=', not '<-' tuple <- totalFiber.join yield tuple // // // // // // A very useful fiber method or composing is the 'orElse' method. This method will combine two fibers. If the first succeeds, the composed fiber will succeed with first fiber's result. If the first fails, the second will be used. val isPineapple: IO[String, String] = ZIO.succeed("Pineapple!") val notPineapple: IO[String, String] = ZIO.fail("Banana...") val composeFruit: IO[String, String] = for fFiber <notPineapple .fork // notPineapple will fail sFiber <isPineapple .fork // isPineapple will succeed totalFiber = fFiber.orElse(sFiber) output <totalFiber .join // The output effect will end up using isPineapple. yield output end Compose experiments/src/main/scala/Parallelism/Finalizers.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 193 Experiments package Parallelism import java.io.IOException import zio.Console.printLine import zio.{ Console, Fiber, IO, Runtime, Scope, UIO, URIO, ZIO, ZLayer } import scala.io.Source.* object Finalizers extends zio.ZIOAppDefault: // // // // // // In this example, we create a ZIO that uses file IO. It opens a file to read it, but gets failed half way through. We use a finalizer to ensure that even if the ZIO fails unexpectedly, the file will still be closed. def finalizer( source: scala.io.Source ) = // Define the finalizer behavior here ZIO.succeed { println("Finalizing: Closing file reader") source.close // Close the input source } val readFileContents : ZIO[Scope, Throwable, Vector[String]] = ZIO .acquireRelease( ZIO.succeed( scala .io Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 194 Experiments .Source .fromFile( "src/main/scala/Parallelism/csvFile.csv" ) ) )(finalizer) .map { bufferedSource => // Use the bracket method with the finalizer \ defined above to define behavior on fail. val lines = for line <- bufferedSource.getLines yield line if ( true ) // Simulating an enexpected error/exception throw new IOException("Boom!") Vector() ++ lines } def run = // Use App's run function println("In main") val ioExample: ZIO[ Scope, Throwable, Unit ] = // Define the ZIO contexts for fileLines <- readFileContents _ <printLine( fileLines.mkString("\n") ) // Combine the strings of the output vector into a single s\ tring, separated by \n yield () ioExample .catchAllDefect(exception => printLine( "Ultimate error message: " + Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 195 Experiments exception.getMessage ) ) .exitCode // Call the Zio with exitCode. end run end Finalizers experiments/src/main/scala/Parallelism/Interrupt.scala package Parallelism import import import import java.io.IOException zio._ zio.Console._ zio.durationInt class Interrupt: val n = 100 // This ZIO does nothing but count to n. // It is not productive, but it uses resources. val countToN: ZIO[Clock, Nothing, Unit] = for _ <- ZIO.sleep(n.seconds) yield () // This effect will create a fiber vrsion of // countToN. // It will then interrupt the fiber, which // returns an exit object. // Note: Interrupting Fibers is completely // safe. // Interrupt safely releases all resources, and // runs the finalizers. val noCounting: ZIO[Clock, Nothing, Exit[ Nothing, Unit ]] = for fiber <- countToN.fork exit <- fiber.interrupt Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 196 Experiments yield exit end Interrupt experiments/src/main/scala/Parallelism/Join.scala package Parallelism import java.io.IOException import zio.{Fiber, IO, Runtime, UIO, ZIO, ZLayer} class Join: // Joining a fiber converts it into an effect. // This effect will succeed or fail depending // on the fiber. val joinedFib100 : UIO[Long] = // This function makes a fiber, then joins the fibe\ r, and returns it as an effect for fiber <computation .fib(100) .fork // Fiber is made to find 100th value of Fib output <fiber .join // Fiber is converted into an effect, then returned. yield output // This object performs a computation that // takes a long time. It is a recursive // Fibonacci Sequence generator. object computation: def fib(n: Long): UIO[Long] = ZIO .succeed { if (n <= 1) ZIO.succeed(n) else fib(n - 1).zipWith(fib(n - 2))(_ + _) } Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 197 Experiments .flatten end Join experiments/src/main/scala/Parallelism/JustSleep.scala package Parallelism import java.io.IOException import zio.{ Fiber, IO, Runtime, UIO, Unsafe, ZIO, ZIOAppDefault, ZLayer, durationInt } import scala.concurrent.Await object JustSleep extends ZIOAppDefault: override def run = ZIO.collectAllPar( (1 to 10000).map(_ => ZIO.sleep(1.seconds)) ) *> ZIO.debug( "Finished far sooner than 10,000 seconds" ) @main def ToFuture() = Await.result( Unsafe.unsafe { (u: Unsafe) => given Unsafe = u zio .Runtime .default Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 198 Experiments // .unsafe .runToFuture(ZIO.sleep(1.seconds)) .getOrThrowFiberFailure() }, scala.concurrent.duration.Duration.Inf ) game_theory experiments/src/main/scala/game_theory/DecisionService.scala package game_theory import zio.{Ref, ZIO, ZLayer} trait DecisionService: def getDecisionsFor( prisoner1: Prisoner, prisoner2: Prisoner ): ZIO[Any, String, RoundResult] object DecisionService: class LiveDecisionService( history: Ref[DecisionHistory] ) extends DecisionService: private def getDecisionFor( prisoner: Prisoner, actionsAgainst: List[Action] ): ZIO[Any, String, Decision] = for action <prisoner.decide(actionsAgainst) yield Decision(prisoner, action) def getDecisionsFor( prisoner1: Prisoner, prisoner2: Prisoner ): ZIO[Any, String, RoundResult] = for Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 199 Experiments prisoner1history <history .get .map(_.historyFor(prisoner1)) prisoner2history <history .get .map(_.historyFor(prisoner2)) decisions <getDecisionFor( prisoner1, prisoner2history ).zipPar( getDecisionFor( prisoner2, prisoner1history ) ) roundResult = RoundResult(decisions._1, decisions._2) _ <history.updateAndGet(oldHistory => DecisionHistory( roundResult :: oldHistory.results ) ) yield roundResult end LiveDecisionService object LiveDecisionService: def make(): ZIO[ Any, Nothing, LiveDecisionService ] = for history <Ref.make(DecisionHistory(List.empty)) yield LiveDecisionService(history) val liveDecisionService: ZLayer[ Any, Nothing, Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 200 Experiments LiveDecisionService ] = ZLayer.fromZIO(LiveDecisionService.make()) end DecisionService experiments/src/main/scala/game_theory/SingleBasic.scala package game_theory import game_theory.Action.{Betray, Silent} import game_theory.Outcome.{ BothFree, BothPrison, OnePrison } import zio.Console.printLine import zio.* case class Decision( prisoner: Prisoner, action: Action ) case class RoundResult( prisoner1Decision: Decision, prisoner2Decision: Decision ): override def toString: String = s"RoundResult(${prisoner1Decision.prisoner}:${prisoner1Decision .action} ${prisoner2Decision.prisoner}:${prisoner2Decision.acti\ on})" case class DecisionHistory( results: List[RoundResult] ): def historyFor( prisoner: Prisoner ): List[Action] = results.map(roundResult => if ( roundResult.prisoner1Decision.prisoner == prisoner ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 201 Experiments roundResult.prisoner1Decision.action else roundResult.prisoner2Decision.action ) trait Strategy: def decide( actionsAgainst: List[Action] ): ZIO[Any, Nothing, Action] case class Prisoner( name: String, strategy: Strategy ): def decide( actionsAgainst: List[Action] ): ZIO[Any, Nothing, Action] = strategy.decide(actionsAgainst) override def toString: String = s"$name" val silentAtFirstAndEventuallyBetray = new Strategy: override def decide( actionsAgainst: List[Action] ): ZIO[Any, Nothing, Action] = if (actionsAgainst.length < 3) ZIO.succeed(Silent) else ZIO.succeed(Betray) val alwaysTrust = new Strategy: override def decide( actionsAgainst: List[Action] ): ZIO[Any, Nothing, Action] = ZIO.succeed(Silent) val silentUntilBetrayed = new Strategy: override def decide( actionsAgainst: List[Action] Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 202 Experiments ): ZIO[Any, Nothing, Action] = if (actionsAgainst.contains(Betray)) ZIO.succeed(Betray) else ZIO.succeed(Silent) enum Action: case Silent case Betray enum Outcome: case BothFree, BothPrison case OnePrison(prisoner: Prisoner) object SingleBasic extends ZIOAppDefault: def play( prisoner1: Prisoner, prisoner2: Prisoner ): ZIO[DecisionService, String, Outcome] = for decisionService <ZIO.service[DecisionService] roundResult <decisionService .getDecisionsFor(prisoner1, prisoner2) .debug("Decisions") outcome = ( roundResult.prisoner1Decision.action, roundResult.prisoner2Decision.action ) match case (Silent, Silent) => BothFree case (Betray, Silent) => OnePrison(prisoner2) case (Silent, Betray) => OnePrison(prisoner1) case (Betray, Betray) => BothPrison Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 203 Experiments yield outcome val bruce = Prisoner( "Bruce", silentAtFirstAndEventuallyBetray ) val bill = Prisoner("Bill", silentUntilBetrayed) def run = play(bruce, bill) .debug("Outcome") .repeatN(4) .provideLayer( DecisionService.liveDecisionService ) end SingleBasic scenarios experiments/src/main/scala/scenarios/CivilEngineering.scala package scenarios import zio.ZIOAppArgs import zio.{ZIOAppDefault, ZIO} object CivilEngineering extends ZIOAppDefault: trait Company[T]: def produceBid( projectSpecifications: ProjectSpecifications[ T ] ): ProjectBid[T] object Companies: def operatingIn[T]( state: State ): ZIO[World, Nothing, AvailableCompanies[ Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 204 Experiments T ]] = ??? trait ProjectSpecifications[T] trait LegalRestriction case class War(reason: String) trait UnfulfilledPromise trait ProjectBid[T] val run = ??? val installPowerLine = ??? case class AvailableCompanies[T]( companies: Set[Company[T]] ): def lowestBid( projectSpecifications: ProjectSpecifications[ T ] ): ProjectBid[T] = ??? trait World object World: def legalRestrictionsFor( state: State ): ZIO[World, War, Set[LegalRestriction]] = ??? def politiciansOf( state: State ): ZIO[World, War, Set[LegalRestriction]] = ??? trait OutOfMoney trait PrivatePropertyRefusal def build[T](projectBid: ProjectBid[T]): ZIO[ Any, UnfulfilledPromise | OutOfMoney | PrivatePropertyRefusal, T ] = ??? Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 205 Experiments def stateBid[T]( state: State, projectSpecifications: ProjectSpecifications[ T ] ): ZIO[ World, War | UnfulfilledPromise | OutOfMoney | PrivatePropertyRefusal, T ] = for availableCompanies <Companies.operatingIn[T](state) legalRestrictions <World.legalRestrictionsFor(state) politicians <- World.politiciansOf(state) lowestBid = availableCompanies .lowestBid(projectSpecifications) completedProject <- build(lowestBid) yield completedProject end CivilEngineering enum State: case TX, CO, CA def buildABridge() = trait Company[T] trait Surveyor trait CivilEngineer trait ProjectSpecifications trait Specs[Service] trait LegalRestriction trait ProjectBid trait InsufficientResources def createProjectSpecifications(): ZIO[ Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 206 Experiments Any, LegalRestriction, ProjectSpecifications ] = ??? case class AvailableCompanies[T]( companies: Set[Company[T]] ) trait Concrete trait Steel trait UnderWaterDrilling trait ConstructionFirm: def produceBid( projectSpecifications: ProjectSpecifications ): ZIO[ AvailableCompanies[Concrete] & AvailableCompanies[Steel] & AvailableCompanies[UnderWaterDrilling], InsufficientResources, ProjectBid ] trait NoValidBids def chooseConstructionFirm( firms: Set[ConstructionFirm] ): ZIO[Any, NoValidBids, ConstructionFirm] = ??? end buildABridge experiments/src/main/scala/scenarios/PhonyZIO.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 207 Experiments package atomic case class Schedule() trait ZIO[R, E, A]: def map[B](f: A => B): ZIO[R, E, B] = ??? def flatMap[R2, E2, B]( f: A => ZIO[R2, E2, B] ): ZIO[R, E, B] = ??? def retry(schedule: Schedule): ZIO[R, E, A] = ??? def catchAll( handler: (E => A) ): ZIO[R, Nothing, A] = ??? case class UIO[A]() extends ZIO[Any, Nothing, A] case class URIO[R, A]() extends ZIO[R, Nothing, A] case class Task[A]() extends ZIO[Any, Throwable, A] case class RIO[R, A]() extends ZIO[R, Throwable, A] case class IO[E <: Throwable, A]() extends ZIO[Any, E, A] object ZIO: def apply[T]( body: => T ): ZIO[Any, Nothing, T] = ??? trait Has[A] Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 208 Experiments experiments/src/main/scala/scenarios/SecuritySystem.scala package scenarios import zio.{ Duration, Schedule, Unsafe, ZIO, ZLayer, durationInt } import zio.Console.printLine import scala.concurrent.TimeoutException import time.scheduledValues import izumi.reflect.Tag case class TempSense( z: ZIO[ Any, HardwareFailure, ZIO[Any, TimeoutException, Degrees] ] ) /** Situations: Security System: Should monitor * - Motion * - Heat/Infrared * - Sound Should alert by: * - Quiet, local beep * - Loud Local Siren * - Ping security company * - Notify police */ object SecuritySystem: // TODO Why can't I use this??? val s: zio.ZLayer[ Any, Nothing, scenarios.TempSense Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 209 Experiments ] = SensorData.live[Degrees, TempSense]( x => TempSense(x), (1.seconds, Degrees(71)), (2.seconds, Degrees(70)) ) val fullServiceBuilder: ZLayer[ Any, Nothing, scenarios.MotionDetector & scenarios.ThermalDetectorX & AcousticDetectorX & SirenX ] = MotionDetector.live ++ ThermalDetectorX( (1.seconds, Degrees(71)), (1.seconds, Degrees(70)), (3.seconds, Degrees(98)) ) // ++ s ++ AcousticDetectorX( (4.seconds, Decibels(11)), (1.seconds, Decibels(20)) ) ++ SirenX.live end fullServiceBuilder val accessMotionDetector: ZIO[ scenarios.MotionDetector, scenarios.HardwareFailure, scenarios.Pixels ] = ZIO.serviceWithZIO(_.amountOfMotion()) def securityLoop( amountOfHeatGenerator: ZIO[ Any, scala.concurrent.TimeoutException | scenarios.HardwareFailure, scenarios.Degrees ], amountOfMotion: Pixels, acousticDetector: ZIO[ Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 210 Experiments Any, scala.concurrent.TimeoutException | scenarios.HardwareFailure, scenarios.Decibels ] ): ZIO[ SirenX, scala.concurrent.TimeoutException | HardwareFailure, Unit ] = for amountOfHeat <- amountOfHeatGenerator noise <- acousticDetector _ <ZIO.debug( s"Heat: $amountOfHeat Motion: $amountOfMotion ) securityResponse = determineResponse( amountOfMotion, amountOfHeat, noise ) _ <securityResponse match case Relax => ZIO.debug("No need to panic") case LowBeep => SirenX.lowBeep case LoudSiren => SirenX.loudSiren yield () Noise: $noise" def shouldAlertServices[ T <: MotionDetector & ThermalDetectorX & SirenX & AcousticDetectorX ](): ZIO[ T, scenarios.HardwareFailure | TimeoutException, String Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 211 Experiments ] = for amountOfMotion <MotionDetector .acquireMotionMeasurementSource() amountOfHeatGenerator <ThermalDetectorX .acquireHeatMeasurementSource acousticDetector <AcousticDetectorX.acquireDetector _ <securityLoop( amountOfHeatGenerator, amountOfMotion, acousticDetector ).repeat( Schedule.recurs(5) && Schedule.spaced(1.seconds) ) yield "Fin" def shouldTrigger( amountOfMotion: Pixels, amountOfHeat: Degrees ): Boolean = amountOfMotion.value > 10 && amountOfHeat.value > 95 def determineResponse( amountOfMotion: Pixels, amountOfHeat: Degrees, noise: Decibels ): SecurityResponse = val numberOfAlerts = List( amountOfMotion.value > 50, amountOfHeat.value > 95, noise.value > 15 ).filter(_ == true).length if (numberOfAlerts == 0) Relax Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 212 Experiments else if (numberOfAlerts == 1) LowBeep else LoudSiren end determineResponse def determineBreaches( amountOfMotion: Pixels, amountOfHeat: Degrees, noise: Decibels ): Set[SecurityBreach] = List( Option.when(amountOfMotion.value > 50)( SignificantMotion ), Option.when( amountOfHeat.value > 95 && amountOfHeat.value < 200 )(BodyHeat), Option .when(amountOfHeat.value >= 200)(Fire), Option.when(noise.value > 15)(LoudNoise) ).flatten.toSet end SecuritySystem trait SecurityBreach object BodyHeat object Fire object LoudNoise object SignificantMotion extends extends extends extends SecurityBreach SecurityBreach SecurityBreach SecurityBreach trait SecurityResponse object Relax extends SecurityResponse object LowBeep extends SecurityResponse object LoudSiren extends SecurityResponse @main def useSecuritySystem = import zio.Runtime.default.unsafe println( "Final result: " + Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 213 Experiments Unsafe.unsafe { (u: Unsafe) => given Unsafe = u unsafe .run( SecuritySystem .shouldAlertServices() .provide( SecuritySystem.fullServiceBuilder ) .catchSome { case _: TimeoutException => printLine( "Invalid Scenario. Ran out of sensor data." ) } ) .getOrThrowFiberFailure() } ) end useSecuritySystem trait HardwareFailure case class Decibels(value: Int) case class Degrees(value: Int) case class Pixels(value: Int) trait MotionDetector: def amountOfMotion() : ZIO[Any, HardwareFailure, Pixels] object MotionDetector: object LiveMotionDetector extends MotionDetector: override def amountOfMotion() : ZIO[Any, HardwareFailure, Pixels] = ZIO.succeed(Pixels(30)) def acquireMotionMeasurementSource(): ZIO[ MotionDetector, HardwareFailure, Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 214 Experiments Pixels ] = ZIO .service[MotionDetector] .flatMap(_.amountOfMotion()) val live : ZLayer[Any, Nothing, MotionDetector] = ZLayer.succeed(LiveMotionDetector) end MotionDetector trait ThermalDetectorX: def heatMeasurementSource() : ZIO[Any, Nothing, ZIO[ Any, TimeoutException | scenarios.HardwareFailure, Degrees ]] object ThermalDetectorX: def apply( value: (Duration, Degrees), values: (Duration, Degrees)* ): ZLayer[Any, Nothing, ThermalDetectorX] = ZLayer.succeed( // that same service we wrote above new ThermalDetectorX: override def heatMeasurementSource() : ZIO[Any, Nothing, ZIO[ Any, TimeoutException | scenarios.HardwareFailure, Degrees ]] = scheduledValues(value, values*) ) // This is preeeetty gnarly. How can we // improve? def acquireHeatMeasurementSource[ T <: scenarios.ThermalDetectorX: Tag Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 215 Experiments ]: ZIO[T, Nothing, ZIO[ Any, scala.concurrent.TimeoutException | scenarios.HardwareFailure, scenarios.Degrees ]] = ZIO.serviceWithZIO[ThermalDetectorX]( _.heatMeasurementSource() ) end ThermalDetectorX trait AcousticDetectorX: def acquireDetector(): ZIO[Any, Nothing, ZIO[ Any, TimeoutException | scenarios.HardwareFailure, Decibels ]] object AcousticDetectorX: def apply( value: (Duration, Decibels), values: (Duration, Decibels)* ): ZLayer[Any, Nothing, AcousticDetectorX] = ZLayer.succeed( // that same service we wrote above new AcousticDetectorX: override def acquireDetector() : ZIO[Any, Nothing, ZIO[ Any, TimeoutException | scenarios.HardwareFailure, Decibels ]] = scheduledValues(value, values*) ) // This is preeeetty gnarly. How can we // improve? def acquireDetector[ T <: scenarios.AcousticDetectorX: Tag ]: ZIO[T, Nothing, ZIO[ Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 216 Experiments Any, scala.concurrent.TimeoutException | scenarios.HardwareFailure, scenarios.Decibels ]] = ZIO.serviceWithZIO[ scenarios.AcousticDetectorX ](_.acquireDetector()) end AcousticDetectorX object Siren: trait ServiceX: def lowBeep(): ZIO[ Any, scenarios.HardwareFailure, Unit ] val live : ZLayer[Any, Nothing, Siren.ServiceX] = ZLayer.succeed( // that same service we wrote above new ServiceX: def lowBeep(): ZIO[ Any, scenarios.HardwareFailure, Unit ] = ZIO.debug("beeeeeeeeeep") ) end Siren trait SirenX: def lowBeep() : ZIO[Any, scenarios.HardwareFailure, Unit] def loudSiren() : ZIO[Any, scenarios.HardwareFailure, Unit] object SirenX: object SirenXLive extends SirenX: Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 217 Experiments def lowBeep(): ZIO[ Any, scenarios.HardwareFailure, Unit ] = ZIO.debug("beeeeeeeeeep") def loudSiren(): ZIO[ Any, scenarios.HardwareFailure, Unit ] = ZIO.debug("WOOOO EEEE WOOOOO EEEE") val live: ZLayer[Any, Nothing, SirenX] = ZLayer.succeed(SirenXLive) val lowBeep: ZIO[ SirenX, scenarios.HardwareFailure, Unit ] = ZIO.serviceWith(_.lowBeep()) val loudSiren: ZIO[ SirenX, scenarios.HardwareFailure, Unit ] = ZIO.serviceWith(_.loudSiren()) end SirenX class SensorD[T]( z: ZIO[ Any, HardwareFailure, ZIO[Any, TimeoutException, T] ] ) // TODO Figure out how to use this object SensorData: def live[T, Y: zio.Tag]( c: ZIO[ Any, Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 218 Experiments HardwareFailure, ZIO[Any, TimeoutException, T] ] => Y, value: (Duration, T), values: (Duration, T)* ): ZLayer[Any, Nothing, Y] = ZLayer.succeed( // that same service we wrote above c(scheduledValues[T](value, values*)) ) def liveS[T: zio.Tag]( value: (Duration, T), values: (Duration, T)* ): ZLayer[Any, Nothing, SensorD[T]] = ZLayer.succeed( // that same service we wrote above SensorD(scheduledValues[T](value, values*)) ) end SensorData logging experiments/src/main/scala/logging/Logging.scala package logging import zio.logging.* import zio.* import zio.logging.LogFormat.{ label, line, quoted, text } import java.time.ZonedDateTime import java.time.format.DateTimeFormatter object Logging extends ZIOAppDefault: Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 219 Experiments lazy val minimal: LogFormat = label("message", quoted(line)).highlight lazy val locationLogger: LogFormat = location(new WackyGps) |-| label("message", quoted(line)).highlight lazy val coloredLogger = Runtime.removeDefaultLoggers >>> console( // LogFormat.colored locationLogger ) def run = ZIO.log("Hi").provide(coloredLogger) // val timestamp: LogFormat = timestamp(DateTimeFormatter.ISO_OFFSET_D\ ATE_TIME) def location(gps: Gps): LogFormat = text { gps.currentLocation().toString } end Logging enum Continent: case NorthAmerica, SouthAmerica, Europe, Asia, Antarctica, Australia, Africa trait Gps: def currentLocation(): Continent class WackyGps extends Gps: def currentLocation(): Continent = Continent .values Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 220 Experiments .apply( scala .util .Random .between(0, Continent.values.length) ) fibers experiments/src/main/scala/fibers/CancellingATightLoop.scala package fibers import org.apache.commons.lang3.RandomStringUtils import org.apache.commons.text.similarity.LevenshteinDistance import zio.* val input = RandomStringUtils.random(70_000) val target = RandomStringUtils.random(70_000) val leven = LevenshteinDistance.getDefaultInstance object PlainLeven extends App: leven(input, target) object CancellingATightLoop extends ZIOAppDefault: val scenario = ZIO .attemptBlocking(leven(input, target)) .mapBoth( error => ZIO.succeed("yay!"), success => ZIO.fail("Oh no!") ) def run = // For timeouts, you need fibers and // cancellation scenario Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 221 Experiments .timeout(50.seconds) .timed .debug("Time:") experiments/src/main/scala/fibers/HowMany.scala package fibers import zio.* import zio.Console.* object HowMany extends ZIOAppDefault: def run = ZIO.foreachPar(Range(0, 1_000_000))(_ => ZIO.sleep(1.second) ) prelude experiments/src/main/scala/prelude/NewTypes.scala package prelude import import import import import import zio.prelude.Newtype zio.ZIOAppDefault zio.ZIO zio.Console.printLine zio.prelude.Assertion._ zio.prelude.Assertion /* Notes: Only works for primitive types. You * can't get compile-time guarantees for custom * classes */ type NewSpecialClass = NewSpecialClass.Type object NewSpecialClass extends Newtype[OurPrimitiveClass]: override inline def assertion : Assertion[OurPrimitiveClass] = Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 222 Experiments greaterThan( OurPrimitiveClass("ignored_id", age = 0) ) object SecurePassword extends Newtype[String]: extension (n: SecurePassword) def salt = SecurePassword.unwrap(n) + "random_salt_bits" override inline def assertion : Assertion[String] = hasLength(greaterThanOrEqualTo(10)) && specialCharacters inline def specialCharacters : Assertion[String] = contains("$") || contains("!") type SecurePassword = SecurePassword.Type object NewTypeDemos extends ZIOAppDefault: def run = val badValue = "Special String #$" for primitiveClassResult <ZIO.fromEither( OurPrimitiveClass .safeConstructor("idValue", -10) ) specialClassResult <ZIO.succeed( NewSpecialClass.make( OurPrimitiveClass("idValue", -10) ) ) accountNumber <ZIO.succeed { SecurePassword("Special String #$") } accountNumbers <Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 223 Experiments ZIO.succeed { SecurePassword( "Special String $", "bad string!" ) } accountNumberRuntime <ZIO.succeed { SecurePassword.make(badValue) } _ <printLine( s"Account Number: $accountNumber" ) _ <printLine( s"Account Number salted: ${accountNumber.salt}" ) yield () end for end run end NewTypeDemos experiments/src/main/scala/prelude/OurPrimitiveClass.scala package prelude import import import import import import zio.prelude.Newtype zio.ZIOAppDefault zio.ZIO zio.Console.printLine zio.prelude.Assertion._ zio.prelude.Assertion case class OurPrimitiveClass( id: String, age: Int ): assert(age > 0) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 224 Experiments object OurPrimitiveClass: def safeConstructor( id: String, age: Int ): Either[String, OurPrimitiveClass] = if (age > 0) Right(OurPrimitiveClass(id, age)) else Left("Invalid age") implicit val ordering : Ordering[OurPrimitiveClass] = new Ordering[OurPrimitiveClass]: def compare( x: OurPrimitiveClass, y: OurPrimitiveClass ): Int = x.age.compare(y.age) std_type_conversions_to_zio experiments/src/main/scala/std_type_conversions_to_zio/EitherToZio.scala // EitherToZio.scala package std_type_conversions_to_zio import zio.{ZIO, ZIOAppDefault} import scala.util.{Left, Right} case class InvalidIntegerInput(value: String) object EitherToZio extends ZIOAppDefault: val goodInt: Either[InvalidIntegerInput, Int] = Right(42) val zEither : ZIO[Any, InvalidIntegerInput, Int] = ZIO.fromEither(goodInt) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 225 Experiments def run = zEither.debug("Converted Either") experiments/src/main/scala/std_type_conversions_to_zio/FutureToZio.scala package std_type_conversions_to_zio import zio.{ZIO, ZIOAppDefault} import scala.concurrent.Future object FutureToZio extends ZIOAppDefault: val zFuture = ZIO.fromFuture(implicit ec => Future.successful("Success!") ) val zFutureFailed = ZIO.fromFuture(implicit ec => Future.failed(new Exception("Failure :(")) ) val run = zFutureFailed.debug("Converted Future") experiments/src/main/scala/std_type_conversions_to_zio/OptionToZio.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 226 Experiments package std_type_conversions_to_zio import java.io import zio._ import java.io.IOException class OptionToZio extends ZIOAppDefault: val alias: Option[String] = Some("Buddy") // sOption is either 1 or None val aliasZ: IO[Option[Nothing], String] = ZIO.fromOption(alias) val run = aliasZ experiments/src/main/scala/std_type_conversions_to_zio/TryToZio.scala package std_type_conversions_to_zio import import import import zio._ java.io java.io.IOException scala.util.Try object TryToZio extends ZIOAppDefault: val dividend = 42 val divisor = 7 // Significant Note: Try is a standard // collection by-name function. This makes // it a good candidate for introducting that // concept. def sTry: Try[Int] = Try(dividend / divisor) val zTry: IO[Throwable, Int] = ZIO.fromTry(sTry) val run = zTry Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 227 Experiments interpreter-chaining_with_previous_result experiments/src/main/scala/interpreter/chaining_with_previous_result/ChainedWithPreviousResult.scala package interpreter.chaining_with_previous_result import environment_exploration.ToyEnvironment import zio.{ZIO, ZIOAppDefault} import scala.reflect.{ClassTag, classTag} import scala.util.Random trait Operation case class Value(value: String) extends Operation case class StringManipulation( action: String => String ) extends Operation: def actOn(input: String): String = action(input) case class Print() extends Operation case class RandomString() extends Operation val program = Seq( Value("Hello There"), Print(), StringManipulation(_.toUpperCase()), Print(), StringManipulation(_.take(5)), Print(), RandomString(), Print(), StringManipulation(_.toUpperCase()), Print() ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 228 Experiments def interpret(program: Seq[Operation]): String = program.foldLeft("") { (acc, op) => op match case Print() => println(acc) acc case RandomString() => scala .util .Random .alphanumeric .take(10) .mkString case Value(value) => value case StringManipulation(action) => action(acc) } @main def demoInterpreter() = interpret(program) trait Printer: def print(input: String): Unit def interpretWithEnvironment( program: Seq[Operation], environment: ToyEnvironment[Printer & Random] ): String = program.foldLeft("") { (acc, op) => op match case Print() => environment.get[Printer].print(acc) acc case RandomString() => environment .get[Random] .alphanumeric .take(10) .mkString case Value(value) => Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 229 Experiments value case StringManipulation(action) => action(acc) } experiments-src-test-scala-zio_test experiments/src/test/scala/zio_test/Shared.scala package zio_test import zio.{Ref, Scope, ZIO, ZLayer} object Shared: val layer: ZLayer[Any, Nothing, Ref[Int]] = ZLayer.scoped { ZIO.acquireRelease( Ref.make(0) <* ZIO.debug("Initializing!") )( _.get .debug( "Number of tests that used shared layer" ) ) } case class Scoreboard(value: Ref[Int]): def display(): ZIO[Any, Nothing, String] = for current <- value.get yield s"**$current**" val scoreBoard: ZLayer[ Scope with Ref[Int], Nothing, Scoreboard ] = for value <- ZLayer.service[Ref[Int]] res <ZLayer.scoped[Scope] { Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 230 Experiments ZIO.acquireRelease( ZIO.succeed(Scoreboard(value.get)) <* ZIO.debug( "Initializing scoreboard!" ) )(_ => ZIO.debug("Shutting down scoreboard") ) } yield res end Shared experiments/src/test/scala/zio_test/UseComplexLayer.scala package zio_test import zio.* import zio.test.* import zio_test.Shared.Scoreboard object UseComplexLayer extends ZIOSpec[Scoreboard]: def bootstrap : ZLayer[Any, Nothing, Scoreboard] = ZLayer.make[Scoreboard]( Shared.layer, Shared.scoreBoard, Scope.default ) def spec = test("use scoreboard") { for _ <ZIO .serviceWithZIO[Scoreboard]( _.display() ) .debug yield assertCompletes } end UseComplexLayer Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 231 Experiments experiments/src/test/scala/zio_test/UseSharedLayerA.scala package zio_test import zio.test.{ TestAspect, ZIOSpec, assertCompletes } import zio.{Ref, ZIO} object UseSharedLayerA extends ZIOSpec[Ref[Int]]: def bootstrap = Shared.layer def spec = test("Test A") { for _ <ZIO.serviceWithZIO[Ref[Int]]( _.update(_ + 1) ) yield assertCompletes } experiments/src/test/scala/zio_test/UseSharedLayerB.scala package zio_test import zio.test.{ TestAspect, ZIOSpec, assertCompletes } import zio.{Ref, Scope, ZIO, ZLayer} object UseSharedLayerB extends ZIOSpec[Ref[Int]]: def bootstrap = Shared.layer def spec = test("Test B") { Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 232 Experiments for _ <ZIO.serviceWithZIO[Ref[Int]](count => count.update(_ + 1) ) yield assertCompletes } helloworld experiments/src/main/scala/helloworld/HelloWorld.scala package helloworld import zio.* import scala.annotation.{experimental, nowarn} def blah = ??? /* @experimental * @nowarn * @zioMain def run = Console.printLine("hello, * world") */ experiments-src-test-scala-layers experiments/src/test/scala/layers/FestivalFencingUnavailableS Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 233 Experiments package layers import zio.* import zio.test.* import zio.test.TestAspect.* object FestivalFencingUnavailableSpec extends ZIOSpecDefault: val missingFencing: ZIO[Any, String, Fencing] = ZIO.fail("No fencing!") private val brokenFestival : ZLayer[Any, String, Festival] = ZLayer.make[Festival]( festival, ZLayer.fromZIO(missingFencing), stage, speakers, wires, amplifiers, soundSystem, toilets, foodtruck, security, venue, permit ) val spec = suite("Play some music")( test("Good festival")( ( for _ <- ZIO.service[Festival] yield assertCompletes ).provide(brokenFestival) .withClock(Clock.ClockLive) .catchAll(e => ZIO.debug("Expected error: " + e) *> ZIO.succeed(assertCompletes) ) ) ) end FestivalFencingUnavailableSpec Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 234 Experiments experiments/src/test/scala/layers/FestivalShortedOutSoundSys package layers import zio.* import zio.test.* import zio.test.TestAspect.* object FestivalShortedOutSoundSystemSpec extends ZIOSpecDefault: val brokenFestival : ZLayer[Any, String, Festival] = ZLayer.make[Festival]( festival, fencing, stage, speakers, wires, amplifiers, soundSystemShortedOut, toilets, foodtruck, security, venue, permit ) val spec = suite("Play some music")( test("Good festival")( ( for _ <- ZIO.service[Festival] yield assertCompletes ).provide(brokenFestival) .withClock(Clock.ClockLive) .catchAll(e => ZIO.debug("Expected error: " + e) *> ZIO.succeed(assertCompletes) ) ) ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 235 Experiments end FestivalShortedOutSoundSystemSpec experiments/src/test/scala/layers/FestivalSpec.scala package layers import zio.* import zio.test.* import zio.test.TestAspect.* object FestivalSpec extends ZIOSpec[Festival]: val bootstrap = ZLayer.make[Festival]( festival, fencing, stage, speakers, wires, amplifiers, soundSystem, toilets, foodtruck, security, venue, permit ) val spec = suite("Play some music")( test("Good festival")(assertCompletes) ) end FestivalSpec experiments-src-test-scala-bigdec experiments/src/test/scala/bigdec/MainSpec.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 236 Experiments package bigdec import zio.ZIO import zio.test.ZIOSpecDefault import zio.test.Assertion.{ diesWithA, equalTo, fails, failsWithA, isSubtype } import zio.test.{ ErrorMessage, TestConsole, TestResult, assert, assertCompletes, assertTrue } import zio.test.TestAspect.silent object MainSpec extends ZIOSpecDefault: def spec = suite("MainSpec")( test("must succeed with valid value") { for _ <- TestConsole.feedLines("1") result <inputBigDecimalValue("Num: ", 1, 10) yield assertTrue(result == BigDecimal(1)) }, test("must fail with non-parsable input") { for _ <- TestConsole.feedLines("a") error <inputBigDecimalValue("Num: ", 1, 10) .mapError(_.getMessage) .exit yield assert(error)( fails(equalTo("Invalid input.")) ) }, Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 237 Experiments test("must fail with out-of-range input") { for _ <- TestConsole.feedLines("0") error <inputBigDecimalValue("Num: ", 1, 10) .mapError(_.getMessage) .exit yield assert(error)( fails( equalTo( "Input out of the range from 1 to 10" ) ) ) } ) @@ silent end MainSpec virtual_meeting experiments/src/main/scala/virtual_meeting/package.scala package virtual_meeting import zio._ import zio.stream._ trait Rule object Rule: trait MaximumContinuousSpeaker trait DismissNonParticipants trait MaximumParticipants trait MaximumLength trait OneSpeakerAtATime trait MaximumStartDelay extends extends extends extends extends extends Rule Rule Rule Rule Rule Rule trait CompositeRule extends Rule Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 238 Experiments trait Participant trait ParticipantAction object ParticipantAction: trait Speak extends ParticipantAction trait Emoji extends ParticipantAction trait TextChat extends ParticipantAction trait ParticipantStatus object ParticipantStatus: trait VideoAndAudio extends trait VideoOnly extends trait AudioOnly extends trait NoVideoNoAudio extends ParticipantStatus ParticipantStatus ParticipantStatus ParticipantStatus trait CorrectiveAction object CorrectiveAction: trait Disband extends CorrectiveAction trait WarnParticipant extends CorrectiveAction trait MuteParticipant extends CorrectiveAction trait DismissParticipant extends CorrectiveAction trait SplitMeeting extends CorrectiveAction case class MeetingMoment( actions: Set[ParticipantAction] ) trait MeetingEnforcer: def process( meetingMoment: MeetingMoment ): Option[CorrectiveAction] case class Meeting( frames: zio.stream.ZStream[ Any, Nothing, MeetingMoment ] ) case class Meeting2( Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 239 Experiments frames: Ref[Set[zio.stream.ZStream[ Any, Nothing, MeetingMoment ]]] ) zio experiments/src/main/scala/zio/zioMain.scala package zio def bah = ??? /* * * * * * * * * * * * * * * * * * * * * * * * * import scala.annotation.MainAnnotation.{ Info, Parameter } import scala.annotation.{ MainAnnotation, experimental, nowarn } import scala.util.CommandLineParser.FromString @experimental class zioMain extends MainAnnotation[ FromString, ZIO[ZIOAppArgs, Any, Any] ]: def argGetter[T]( param: Parameter, arg: String, defaultArgument: Option[() => T] )(using parser: FromString[T]): () => T = ??? def command( info: Info, args: Seq[String] ): Option[Seq[String]] = Some(Seq.empty) def run( program: () => ZIO[ZIOAppArgs, Any, Any] ): Unit = ZIOAppDefault .fromZIO(program()) .main(Array.empty) def varargGetter[T]( param: Parameter, args: Seq[String] )(using parser: FromString[T]): () => Seq[T] = ??? end zioMain */ Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 240 Experiments experiments-src-test-scala-time experiments/src/test/scala/time/ScheduledValuesSpec.scala package time import zio.* import zio.test.* import java.time.Instant import scala.concurrent.TimeoutException object ScheduledValuesSpec extends ZIOSpecDefault: def spec = suite("ScheduledValues")( suite("scheduledValues")( test( "querying after no time has passed returns the first value, i\ f duration was > 0" )( for valueAccessor <scheduledValues( (1.seconds, "First Section") ) firstValue <- valueAccessor yield assertTrue( firstValue == "First Section" ) ), test( "querying after no time has passed fails when the duration ==\ 0" )( for valueAccessor <scheduledValues( (0.seconds, "First Section") ) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 241 Experiments _ <- valueAccessor.flip yield assertCompletes ), test( "next value is returned after enough time has elapsed" )( for valueAccessor <scheduledValues( (1.seconds, "First Section"), (2.seconds, "Second Section") ) _ <- TestClock.adjust(1.seconds) secondValue <- valueAccessor yield assertTrue( secondValue == "Second Section" ) ), test("time range end is not inclusive")( for valueAccessor <scheduledValues( (1.seconds, "First Section") ) _ <- TestClock.adjust(1.seconds) _ <- valueAccessor.flip yield assertCompletes ) ) ) end ScheduledValuesSpec zio_intro experiments/src/main/scala/zio_intro/AuthenticationFlow.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 242 Experiments package zio_intro import zio.{ZIO, ZIOAppDefault} object AuthenticationFlow extends ZIOAppDefault: val activeUsers : ZIO[Any, DiskError, List[UserName]] = ??? val user: ZIO[Any, Nothing, UserName] = ??? def authenticateUser( users: List[UserName], currentUser: UserName ): ZIO[ Any, UnauthenticatedUser, AuthenticatedUser ] = ??? val fullAuthenticationProcess: ZIO[ Any, DiskError | UnauthenticatedUser, AuthenticatedUser ] = for users <- activeUsers currentUser <- user authenticatedUser <authenticateUser(users, currentUser) yield authenticatedUser def run = fullAuthenticationProcess.orDieWith(error => new Exception("Unhandled error: " + error) ) end AuthenticationFlow trait UserName case class FileSystem() trait DiskError trait EnvironmentVariableNotFound case class UnauthenticatedUser(msg: String) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 243 Experiments case class AuthenticatedUser(userName: UserName) experiments/src/main/scala/zio_intro/FirstMeaningfulExample.scala package zio_intro import zio.{Clock, ZIO, ZIOAppDefault, System} import zio.Console.{readLine, printLine} object HelloWorld extends ZIOAppDefault: def run = printLine("Hello World") object FirstMeaningfulExample extends ZIOAppDefault: def run = for _ <- printLine("Give us your name:") name <- readLine _ <- printLine(s"$name") yield () experiments/src/main/scala/zio_intro/ProgressBar.scala package zio_intro import zio.{Ref, *} import zio.Console.printLine import java.util.concurrent.TimeUnit trait ProgressBar /* import io.AnsiColor.* * * object ClockAndConsole extends ZIOAppDefault: * val renderCurrentTime = * for currentTime <Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 244 Experiments * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Clock.currentTime(TimeUnit.SECONDS) _ <renderRemainingTime(currentTime) .repeat(Schedule.recurs(10)) yield () val saveCursorPosition = Console.print("\u001b7") val loadCursorPosition = Console.print("\u001b8") def renderRemainingTime(startTime: Long) = for currentTime <Clock.currentTime(TimeUnit.SECONDS) timeElapsed = (currentTime - startTime) .toInt timeRemaining = 10 - timeElapsed // NOTE: You can only reset the cursor // position once in a single SBT session _ <- saveCursorPosition _ <- Console.print( s"${BOLD}$timeRemaining seconds remaining ${RESET}" ) _ <progressBar(timeRemaining) _ <ZIO.sleep(1.seconds) _ <- loadCursorPosition yield () def progressBar(length: Int) = val color = if (length > 3) GREEN_B else RED_B Console.printLine( s"""${color}${" " * length}${RESET}""" ) def run = renderCurrentTime end ClockAndConsole object ClockAndConsoleDifficultEffectManagement extends ZIOAppDefault: val renderCurrentTime = for currentTime <Clock.currentTime(TimeUnit.SECONDS) _ <renderRemainingTime(currentTime) .repeat(Schedule.recurs(10)) _ <renderRemainingTime( Integer.max(currentTime.toInt - 5, 0) ).repeat(Schedule.recurs(10)) yield () Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 245 Experiments * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * val saveCursorPosition = Console.print("\u001b7") val loadCursorPosition = Console.print("\u001b8") def renderRemainingTime(startTime: Long) = for currentTime <Clock.currentTime(TimeUnit.SECONDS) timeElapsed = (currentTime - startTime) .toInt timeRemaining = 10 - timeElapsed // NOTE: You can only reset the cursor // position once in a single SBT session _ <- saveCursorPosition _ <- Console.print( s"${BOLD}$timeRemaining seconds remaining ${RESET}" ) _ <progressBar(timeRemaining) _ <ZIO.sleep(1.seconds) _ <- loadCursorPosition yield () def progressBar(length: Int) = val color = if (length > 3) GREEN_B else RED_B Console.printLine( s"""${color}${" " * length}${RESET}""" ) def run = renderCurrentTime end ClockAndConsoleDifficultEffectManagement object ClockAndConsoleImproved extends ZIOAppDefault: val renderCurrentTime = for currentTime <Clock.currentTime(TimeUnit.SECONDS) racer1 <LongRunningProcess( "Shtep", currentTime, 3 ) racer2 <- LongRunningProcess("Zeb", currentTime, 5) raceFinished: Ref[Boolean] <Ref.make[Boolean](false) winnersName <raceEntities( racer1.run, racer1.run, raceFinished ) zipParLeft monitoringLogic( racer1, racer2, raceFinished ) _ <printLine(s"\nWinner: $winnersName") yield () Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 246 Experiments * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * def monitoringLogic( racer1: LongRunningProcess, racer2: LongRunningProcess, raceFinished: Ref[Boolean] ) = renderLoop( for racer1status <racer1.status.get racer2status <racer2.status.get _ <progressBar(racer1.name, racer1status) _ <printLine("") _ <- progressBar(racer2.name, racer2status) yield () ).repeatWhileZIO(_ => raceFinished.get) def raceEntities( racer1: ZIO[Any, Nothing, String], racer2: ZIO[Any, Nothing, String], raceFinished: Ref[Boolean] ): ZIO[Any, Nothing, String] = racer1 .race(racer2) .flatMap { success => raceFinished.set(true) *> ZIO.succeed(success) } val saveCursorPosition = Console.print("\u001b7") val loadCursorPosition = Console.print("\u001b8") def renderLoop[T]( drawFrame: ZIO[T, Any, Unit] ) = for _ <- saveCursorPosition _ <- drawFrame _ <- ZIO.sleep(1.second) _ <- loadCursorPosition yield () def timer(startTime: Long, secondsToRun: Int) = for currentTime <Clock.currentTime(TimeUnit.SECONDS) timeElapsed = (currentTime - startTime) .toInt yield Integer .max(secondsToRun - timeElapsed, 0) object LongRunningProcess: def apply( name: String, startTime: Long, secondsToRun: Int ): ZIO[Any, Nothing, Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 247 Experiments * * * * * * * * * * * * * * * * * * * * * * * * LongRunningProcess] = for status <- Ref.make[Int](4) yield new LongRunningProcess( name, startTime, secondsToRun, status ) class LongRunningProcess( val name: String, startTime: Long, secondsToRun: Int, val status: Ref[Int] ): val loopAndCheck = for timeLeft <- timer(startTime, secondsToRun) _ <- status.set(timeLeft) yield timeLeft val run: ZIO[Any, Nothing, String] = loopAndCheck .repeatUntil(_ == 0) .map(_ => name) def progressBar(label: String, length: Int) = val barColor = if (length > 3) GREEN_B else RED_B Console.print( s"""$label$barColor${" " * length}$RESET""" ) def run = renderCurrentTime end ClockAndConsoleImproved */ experiments-src-test-scala-concurrency experiments/src/test/scala/concurrency/LunchVoteTest.scala Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 248 Experiments package concurrency import import import import import zio.test._ zio.test.TestAspect._ zio._ LunchVote._ LunchVote.Vote._ object LunchVoteTest extends ZIOSpecDefault: def spec = suite("voting situations")( test("3 quick yays") { for interruptedVoters <- Ref.make(0) voters = List( Voter("Alice", 0.seconds, Yay), Voter("Bob", 0.seconds, Yay), Voter("Charlie", 0.seconds, Yay), Voter("Dave", 1.seconds, Nay, onInterrupt = interruptedVo\ ters.update(_ + 1)), Voter("Eve", 1.seconds, Nay, onInterrupt = interruptedVot\ ers.update(_ + 1)) ) result <- LunchVote.run(voters) totalInterrupted <- interruptedVoters.get // _ <- ZIO.withClock(Clock.ClockLive)(ZIO.sleep(1.seconds)) yield assertTrue(result == Yay) && assertTrue(totalInterrupted\ == 2) } @@ flaky, // Flaky because Interruption count is not reliable test("3 quick nays") { val voters = List( Voter("Alice", 0.seconds, Nay), Voter("Bob", 0.seconds, Nay), Voter("Charlie", 0.seconds, Nay), Voter("Dave", 1.seconds, Yay), Voter("Eve", 1.seconds, Yay) ) for result <- LunchVote.run(voters) yield assertTrue(result == Nay) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 249 Experiments }, test("slow voters") { val voters = List( Voter("Alice", 10.seconds, Nay), Voter("Bob", 10.seconds, Nay), Voter("Charlie", 10.seconds, Nay), Voter("Dave", 10.seconds, Yay), Voter("Eve", 10.seconds, Yay) ) for resultF <- LunchVote.run(voters, 1.seconds).fork _ <- TestClock.adjust(2.seconds) timeout <- resultF.join.flip yield assertTrue(timeout == None) } ) interpreter-chaining_with_monads experiments/src/main/scala/interpreter/chaining_with_monads/ChainingMonads.scala package interpreter.chaining_with_monads import scala.annotation.tailrec sealed trait Operation: def map(mf: String => String): MapOp def flatMap(mf: String => Operation): FlatMapOp class Value(val s: String) extends Operation: override def map(mf: String => String): MapOp = MapOp(_ => mf(s)) override def flatMap( mf: String => Operation ): FlatMapOp = FlatMapOp(_ => mf(s)) class MapOp(val f: String => String) Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 250 Experiments extends Operation: override def map(mf: String => String): MapOp = MapOp(f.andThen(mf)) override def flatMap( mf: String => Operation ): FlatMapOp = FlatMapOp(f.andThen(mf)) class FlatMapOp(val f: String => Operation) extends Operation: override def map(mf: String => String): MapOp = MapOp(identity).flatMap(f).map(mf) override def flatMap( mf: String => Operation ): FlatMapOp = FlatMapOp { s => f(s).flatMap(mf) } @tailrec def interpret(program: Operation): String = program match case v: Value => println("run Value") v.s case mo: MapOp => println("run MapOp") mo.f("") case fmo: FlatMapOp => println("run FlatMapOp") val next = fmo.f("") interpret(next) @main def demoInterpreter() = val value = Value("asdf") println(interpret(value) == "asdf") val upperOp = MapOp(_.toUpperCase) println( Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 251 Experiments interpret(upperOp) == "" ) // applies the operation to the default empty string val valueUpperOp = value.map(_.toUpperCase) println(interpret(valueUpperOp) == "ASDF") val valueUpperTakeTwoOp = valueUpperOp.map(_.take(2)) println(interpret(valueUpperTakeTwoOp) == "AS") val flatMapOpToValue = FlatMapOp(_ => Value("asdf")) println(interpret(flatMapOpToValue) == "asdf") val flatMapOpToFlatMapOpToValue = FlatMapOp(_ => FlatMapOp(_ => Value("asdf"))) println( interpret(flatMapOpToFlatMapOpToValue) == "asdf" ) val flatMapOpToMapOp = FlatMapOp(_ => MapOp(_ => "asdf")) println(interpret(flatMapOpToMapOp) == "asdf") val valueFlatMapOpToValue = value.flatMap(Value(_)) println( interpret(valueFlatMapOpToValue) == "asdf" ) val valueFlatMapOpToValueMapOp = value.flatMap(asdf => Value(asdf).map(_.toUpperCase) ) println( interpret(valueFlatMapOpToValueMapOp) == "ASDF" ) val valueFlatMapOpToValueToFlatMap = value Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward 252 Experiments .flatMap(asdf => Value(asdf.toUpperCase)) .flatMap(upper => Value(upper.take(2))) println( interpret(valueFlatMapOpToValueToFlatMap) == "AS" ) val program = Value("asdf").flatMap { asdf => println(s"asdf = $asdf") Value(asdf.toUpperCase).map { upper => println(s"upper = $upper") upper.take(2) } } println(interpret(program)) val programFor = for asdf <- Value("asdf") upper <- Value(asdf.toUpperCase) yield upper.take(2) println(interpret(programFor)) end demoInterpreter Effect-Oriented Programming ©2021 Bill Frasure, Bruce Eckel & James Ward