{"id":23902683,"url":"https://github.com/mtumilowicz/scala213-functional-programming-collections-workshop","last_synced_at":"2025-07-01T14:11:01.610Z","repository":{"id":110879276,"uuid":"270117883","full_name":"mtumilowicz/scala213-functional-programming-collections-workshop","owner":"mtumilowicz","description":"Introduction to Scala and functional programming collections: list, stream and tree.","archived":false,"fork":false,"pushed_at":"2024-12-01T20:55:36.000Z","size":139,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-06-09T11:06:43.227Z","etag":null,"topics":["functional-programming-in-scala","immutable-collections","implicits","lazy-evaluation","non-strict","pattern-matching","persistent-collections","persistent-data-structure","scala","workshop","workshop-materials","workshops"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mtumilowicz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-06-06T22:09:12.000Z","updated_at":"2024-12-01T20:55:39.000Z","dependencies_parsed_at":"2025-06-09T11:06:43.965Z","dependency_job_id":"35f05cd6-81da-41ec-9c75-59853948d644","html_url":"https://github.com/mtumilowicz/scala213-functional-programming-collections-workshop","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/mtumilowicz/scala213-functional-programming-collections-workshop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mtumilowicz%2Fscala213-functional-programming-collections-workshop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mtumilowicz%2Fscala213-functional-programming-collections-workshop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mtumilowicz%2Fscala213-functional-programming-collections-workshop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mtumilowicz%2Fscala213-functional-programming-collections-workshop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mtumilowicz","download_url":"https://codeload.github.com/mtumilowicz/scala213-functional-programming-collections-workshop/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mtumilowicz%2Fscala213-functional-programming-collections-workshop/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262978722,"owners_count":23394016,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["functional-programming-in-scala","immutable-collections","implicits","lazy-evaluation","non-strict","pattern-matching","persistent-collections","persistent-data-structure","scala","workshop","workshop-materials","workshops"],"created_at":"2025-01-04T22:49:50.272Z","updated_at":"2025-07-01T14:11:01.586Z","avatar_url":"https://github.com/mtumilowicz.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://app.travis-ci.com/mtumilowicz/scala213-functional-programming-collections-workshop.svg?branch=master)](https://travis-ci.com/mtumilowicz/scala213-functional-programming-collections-workshop)\n[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)\n\n# scala213-functional-programming-collections-workshop\n* references\n    * https://stackoverflow.com/questions/4085118/why-foldright-and-reduceright-are-not-tail-recursive\n        * https://stackoverflow.com/a/4086098\n    * https://www.nurkiewicz.com/2012/04/secret-powers-of-foldleft-in-scala.html\n    * https://blog.codecentric.de/en/2016/02/lazy-vals-scala-look-hood/\n    * https://stackoverflow.com/questions/9809313/scalas-lazy-arguments-how-do-they-work\n        * https://stackoverflow.com/a/9809731\n    * https://booksites.artima.com/programming_in_scala_4ed\n    * https://medium.com/@wiemzin/variances-in-scala-9c7d17af9dc4\n    * https://docs.scala-lang.org/overviews/scala-book/classes.html\n    * https://dzone.com/articles/scala-generics-part-2-covariance-and-contravariance-in-generics\n    * https://www.manning.com/books/functional-programming-in-scala\n    * https://chatgpt.com/\n\n# preface\n* goals of this workshop:\n    * gentle introduction to Scala syntax and type system\n    * discuss some Scala features: \n        * variance, \n        * pattern matching, \n        * sealed classes, \n        * lazy evaluation and non-strictness\n        * implicit\n    * implementation of functional data structures: list, stream and tree\n    * practice recursion\n        * please refer beforehand https://github.com/mtumilowicz/java12-fundamentals-tail-recursion-workshop\n* answers with correctly implemented `workshop` tasks are in `answers` package\n\n# introduction to scala\n## class\n* class hierarchy\n    * at the top of the hierarchy is class `Any`\n        * every class inherits from `Any`\n            ```\n            val x: Int = 42\n            val y: Any = x\n            ```\n        * defines methods\n            ```\n            final def ==(that: Any): Boolean\n            final def !=(that: Any): Boolean\n            def equals(that: Any): Boolean\n            def ##: Int\n            def hashCode: Int\n            def toString: String\n            ```\n        * has two subclasses: `AnyVal` and `AnyRef`\n    * `AnyVal`\n        * parent class of value classes in Scala\n        * for a class to be a value class, it must\n            * have exactly one parameter\n            * have nothing inside it except `defs`\n            * no other class can extend a value class\n            * cannot redefine equals or hashCode\n        * nine value classes built into Scala\n            * `Byte`, `Short`, `Char`, `Int`, `Long`, `Float`, `Double`, `Boolean`, and `Unit`\n                * `Unit` corresponds roughly to Java’s void type\n                * `Unit` has a single instance value, which is written `()`\n                * Scala stores integers in the same way as Java—as 32-bit words\n                    * uses `java.lang.Integer` whenever an integer needs to be seen as a (Java) object\n                        * for example, when invoking the `toString` method\n        * in Java, a `new Integer(1)` does not equal a `new Long(1)`\n            *  this discrepancy is corrected in Scala\n        * there are implicit conversions between different value class types\n            * for example, `Int` is automatically widened to `scala.Long` when required\n        * implicit conversions are also used to add more functionality to value types\n            * for example, methods `min`, `max`, `until`, and `abs` are in `scala.runtime.RichInt`\n            and there is an implicit conversion from class `Int` to `RichInt`\n    * `AnyRef`\n        * base class of all reference classes\n        * just an alias for `java.lang.Object`\n    * bottom of the hierarchy: `Null` and `Nothing`\n        * handle some \"corner cases\" of Scala’s object-oriented type system in a uniform way\n    * `Null` is the type of the null reference\n        * subtype of every type that inherits from `AnyRef`\n        * not compatible with value types\n    * `Nothing`\n        * subtype of every type\n            ```\n            def x(): Int = y()\n\n            def y(): Nothing = ???\n            ```\n        * there exist no values of this type\n        * one use is that it signals abnormal termination\n        * another use - parametrization of empty collection subtype\n* public is default access level\n* defined class and gave it a var field\n    ```\n    class ChecksumAccumulator {\n        private var sum = 0\n    }\n    ```\n* `class MyClass(index: Int, name: String)`\n    * compiler will produce a class\n        * two private instance variables\n        * two args constructor\n* `class MyClass(val index: Int, val name: String)`\n    * readonly fields with getters\n        * if `var` instead of `val` - also setters\n    * `println(p.firstName + \" \" + p.lastName)`\n* main/primary constructor is defined when you define your class\n    ```\n    class Person(var firstName: String, var lastName: String) {\n    \n        println(\"the constructor begins\")\n    \n        // some methods\n        def printHome(): Unit = println(s\"HOME = $HOME\") // processed string literal with s string interpolator\n        def fullName(): String = {\n            firstName + \" \" + lastName // no explicit return statement = return last computed value\n        }\n    \n        // secondary constructor\n        def this(firstName: String) {\n          this(firstName, \"\", 0);\n        }\n      \n        printHome()\n        println(\"you've reached the end of the constructor\")\n    }\n    ```\n  \n## singleton\n* classes in Scala cannot have static members\n* instead - singleton objects\n    * for Java programmers - think of singleton objects as the home for static methods\n* singleton object is more than a holder of static methods\n    * it is a first-class object\n* singleton object with the same name as a class - it is called class’s companion object\n    * class is called the companion class of the singleton object\n* class and its companion object can access each other’s private members\n* singleton objects cannot take parameters, whereas classes can\n* singleton object is initialized the first time some code accesses it\n\n## variance\n* please refer: https://github.com/mtumilowicz/java11-covariance-contravariance-invariance\n* variance annotations: `+` and `-` symbols you can place next to type parameters\n    * covariant: `trait Queue[+T] { ... }`\n    * nonvariant: `trait Queue[T] { ... }`\n    * contravariant: `trait Queue[-T] { ... }`\n* to verify correctness Scala compiler classifies all positions in a class or trait body \nas positive, negative or neutral\n    * \"position\" is any location in the class or trait where a type parameter may be used\n    * for example, every method value parameter\n    * type parameters annotated with + may only be used in positive positions\n    * type parameters annotated with - may only be used in negative positions\n    * type parameter with no variance annotation may be used in any position\n        * the only kind of type parameter that can be used in neutral positions of the class body\n    * compiler checks that each type parameter is only used in positions that are classified \n    appropriately\n* examples\n    * covariant position\n        ```\n        class Pets[+A](val pets: ...) {\n          def add(newPet: A): ...\n        }\n      \n        error: covariant type A occurs in contravariant position in type A of value newPet\n        ```\n        why?\n        ```\n        val pets: Pets[Animal] = Pets[Cat](List(Cat)) // it is actually a Pets[Cat]\n        pets.add(Dog) // accepts Animal or any subtype of Animal\n        ```\n    * contravariant position\n        ```\n        class Pets[-A](val pet:A) // contravariant type A occurs in covariant position in type =\u003e A of value pet\n        ```\n        why?\n        ```\n        Pets[Cat] = Pets[Animal](new Animal)\n        pets.pet.meow() // pets.pet is not Cat — it is an Animal\n        ```\n    * function `S =\u003e T` is contravariant in the function argument and covariant in the result type\n        * `val x1: Int =\u003e CharSequence = (x: AnyVal) =\u003e x.toString`\n\n## varargs\n* variadic function syntax: `def apply[A](as: A*): List[A] = ...`\n    * `apply(Array(1,2,3))`\n    * `apply(1,2,3)`\n    * any application of an object to some arguments in parentheses will be transformed to an `apply` \n    method call\n        * `f(x)` -\u003e `f.apply(x)`\n        * digression: `x(0) = \"Hello\"` -\u003e `x.update(0, \"Hello\")`\n* convert to varargs: `as.tail: _*`\n\n## underscore notation for anonymous functions\n* examples\n    * `var f: List[String] =\u003e List[String] = _.tail`\n    * `var f: (List[String], Int) =\u003e List[String] = _ drop _`\n    * `var f: (Int, Int) =\u003e Int = _ + _`\n    * `List(-11, -10, -5, 0, 5, 10).filter(_ \u003e 0)`\n* compiler must have enough information to infer missing parameter types\n* you can think of the underscore as a \"blank\" in the expression that needs to be \"filled in\"\n* multiple underscores mean multiple parameters\n    \n## pattern matching\n* examples\n    ```\n    case class Person(name: String, age: Int)\n    \n    person match {\n        case Person(\"Michal\", 29) =\u003e println(\"Hi Michi!\")\n        case Person(name, 65) =\u003e println(\"Hi \" + name + \", retired?\")\n        case Person(name, age) if age \u003e 100 =\u003e println(\"Hi \" + name + \", congratulations!\")\n        case Person(name, _) =\u003e println(\"Hi \" + name + \", age is a state of mind!\")\n        case _ =\u003e println(\"rest\")\n    }\n    ```\n* fancy switch statement that may descend into the structure of the expression it examines \nand extract subexpressions of that structure\n    * there are three differences to keep in mind\n        * match is an expression in Scala (i.e., it always results in a value)\n        * Scala’s alternative expressions never \"fall through\" into the next case\n        * if none of the patterns match, an exception named `MatchError` is thrown\n\n## case classes\n* example\n    ```\n    abstract class Animal\n    case class Cat(parameters list) extends Animal\n    case class Dog() extends Animal\n    ```\n* classes with case modifier a modifier are called case classes\n* Scala compiler adds some syntactic conveniences to your class\n    * adds a factory method with the name of the class\n    * construct an object: `Cat(\"x\")` vs `new Cat(\"x\")`\n    * all arguments in the parameter list get a val prefix, so they are maintained \n    as fields\n    * implementations of methods toString, hashCode and equals\n    * copy method for making modified copies\n        * The method works by using named and default parameters\n        * You specify the changes you’d like to make by using named parameters. For any\n        parameter you don’t specify, the value from the old object is used\n* the biggest advantage of case classes is that they support pattern matching\n    * twin constructs: case classes and pattern matching\n* case classes are Scala’s way to allow pattern matching without a boilerplate\n\n## sealed classes\n* cannot have any new subclasses added except the ones in the same file\n* if you match against case classes that inherit from a sealed class, the compiler will \nflag missing combinations of patterns with a warning message\n* Scala compiler help in detecting missing combinations of patterns in a match expression\n    * compiler needs to be able to tell which are the possible cases\n    * in general, this is impossible because new case classes can be defined at any time \n    and in arbitrary compilation units\n\n## non-strictness\n* non-strict function - function may choose not to evaluate its arguments\n    * formal definition\n        * we say that the expression doesn’t terminate or that it evaluates to bottom if the \n        evaluation of an expression runs forever or throws an error instead of returning\n        a definite value\n        * function f is strict if the expression f(x) evaluates to bottom for all x that\n        evaluate to bottom\n* example\n    * boolean functions \u0026\u0026 and || are non-strict\n* if you invoke `standard_method(sys.error(\"failure\"))` you’ll get an exception - `sys.error(\"failure\")` \nwill be evaluated before entering the body of the method\n* example\n    ```\n    def f(x: =\u003e Int): Unit = {\n        println(x + 1) // target type, better than trick with supplier\n    }\n  \n    f(1) // prints 2\n    ```\n* arguments we’d like to pass unevaluated have an arrow `=\u003e` immediately before their type\n    * we don’t need to do anything special to evaluate an argument annotated with `=\u003e`\n    * just reference the identifier as usual\n    * we don't need to do anything special to call this function\n    * just use the normal function call syntax\n        * Scala takes care of wrapping the expression in a thunk for us\n* Scala won’t (by default) cache the result of evaluating an argument\n* we say that a non-strict function in Scala takes its arguments by name rather than by value\n\n## lazy evaluation\n* example\n    ```\n    def f(x: =\u003e Int): Unit = {\n      println(\"evaluating f\")\n      println(x + 1)\n    }\n\n    lazy val x = {\n      println(\"evaluating x\")\n      1\n    }\n\n    f(x) // evaluating f evaluating x 2\n    f(x) // evaluating f 2\n    ```\n* lazy val\n    * delay evaluation of the right-hand side until it’s first referenced\n    * cache the result\n* lazy val initialization scheme uses double-checked locking to initialize the lazy val only once\n\n## implicit class\n* major use of implicit conversions is to simulate adding new syntax\n* example\n    * `case class Rectangle(width: Int, height: Int)`\n        ```\n        implicit class RectangleMaker(width: Int) {\n            def x(height: Int) = Rectangle(width, height)\n            implicit def RectangleMaker(width: Int) = new RectangleMaker(width)\n        }\n        \n        val myRectangle = 3 x 4\n        ```\n        * since type Int has no method named x - the compiler will look for an implicit conversion \n        from Int to something that does and find RectangleMaker\n        * compiler inserts a call to this conversion\n    * `Map(1 -\u003e \"one\", 2 -\u003e \"two\", 3 -\u003e \"three\")`\n        * `-\u003e` is not syntax - is a method of the class `ArrowAssoc`\n            * class defined inside the standard Scala preamble `scala.Predef`\n            * preamble also defines an implicit conversion from `Any` to `ArrowAssoc`\n\n##  function parameters\n* default values\n    ```\n    def printTime(out: java.io.PrintStream = Console.out) = \n        out.println(\"time = \" + System.currentTimeMillis())\n  \n    printTime() // out will be set to its default value of Console.out\n    ```\n    * very handy with auxiliary tailrec functions\n        ```\n        def length2(): Int = {\n          @scala.annotation.tailrec\n          def loop(list: List[A], size: Int = 0): Int = {\n            list match {\n              case Nil =\u003e size\n              case _ :: tail =\u003e loop(tail, size + 1)\n            }\n          }\n        \n          loop(this)\n        }\n        ```\n* type inference\n    * is flow based\n        ```\n        def flow[A](list: List[A], f: A =\u003e A): List[A] = {\n            list.map(f)\n        }\n      \n        flow(List(1), _ + 1) // error: missing parameter type for expanded function\n        flow(List(1), x =\u003e x + 1) // error: missing parameter type\n        flow(List(1), (x: Int) =\u003e x + 1) // OK\n        ```\n        ```\n        def flow[A](list: List[A])(f: A =\u003e A): List[A] = { // curried\n            list.map(f)\n        }\n      \n        flow(List(1))(x =\u003e x + 1) // OK\n        flow(List(1))(_ + 1) // OK\n        ```\n    * when designing a polymorphic method that takes some non-function argu-\n      ments and a function argument, place the function argument last in a curried\n      parameter list on its own\n      \n# structures\n* immutable data structure\n    * how we modify them?\n        * for example: when we add an element to the front of an existing list `xs`\n        we return `List(new element, xs)`\n        * we don’t need to actually copy `xs` - we can just reuse it\n            * it is called data sharing\n* functional data structures are persistent - existing references are never changed by \noperations on the data structure\n## list\n```\nsealed trait List[+A] // data type\ncase object Nil extends List[Nothing] // represents the empty lis\ncase class Cons[+A](head: A, tail: List[A]) extends List[A] // represents nonempty lists\n```\n* `Cons` traditionally short for construct\n    * nonempty list consists of an initial element - head followed by a List (tail) - possibly  empty\n* `foldRight`\n    ```\n    def foldRight[A,B](z: B)(f: (A, B) =\u003e B): B =\n        this match {\n        case Nil =\u003e z\n        case Cons(x, xs) =\u003e f(x, foldRight(xs, z)(f))\n    }\n    ```\n    * it replaces `Nil` and `Cons` with `z` and `f`\n        * `Cons(1, Cons(2, Nil)) -\u003e f (1, f (2, z ))`\n    * example\n        ```\n        Cons(1, Cons(2, Cons(3, Nil))).foldRight(0)(_ + _)\n        1 + Cons(2, Cons(3, Nil)).foldRight(0)(_ + _)\n        1 + (2 + Cons(3, Nil).foldRight(0)(_ + _)\n        1 + (2 + (3 + Nil.foldRight(0)(_ + _)\n        1 + (2 + (3 + (0)))\n        6\n        ```\n    * why `foldRight` cannot be tailrec?\n        * `Seq(1, 2, 3).foldLeft(10)(_ - _)` is evaluated as `(((10 - 1) - 2) - 3)`\n        * `Seq(1, 2, 3).foldRight(10)(_ - _)` is evaluated as `(1 - (2 - (3 - 10)))`\n        * imagine pulling the numbers 1, 2, and 3 from a bag and making the calculation pencil-on-paper\n            * `foldRight` case\n                1. pull a number n from the bag\n                1. write \"n - ?\" on the paper\n                1. if there are numbers left in the bag, pull another n from the bag, else go to 6.\n                1. erase the question mark and replace it with \"(n - ?)\"\n                1. repeat from 3.\n                1. erase the question mark and replace it with 10\n                1. perform the calculation\n            * `foldLeft` case\n                1. write 10 on the paper\n                1. pull a number n from the bag\n                1. subtract n from the value you have, erase the value and write down the new value instead\n                1. repeat from 2.\n                * regardless of how many numbers there are in the bag, you only need to have one value written \n                on paper\n                * Tail Call Elimination (TCE) means that instead of building a large structure of recursive \n                calls on the stack, you can pop off and replace an accumulated value as you go along\n* standard library\n    *  `1 :: 2 :: Nil` = `1 :: 2` = `List(1,2)`\n    * pattern matching\n        * `case h :: t` - split into head and tail\n\n## stream\n```\nsealed trait Stream[+A]\ncase object Empty extends Stream[Nothing]\ncase class Cons[+A](h: () =\u003e A, t: () =\u003e Stream[A]) extends Stream[A] // head and a tail are both non-strict\n\nobject Stream {\n    def cons[A](hd: =\u003e A, tl: =\u003e Stream[A]): Stream[A] = { // cache the head and tail as lazy values to avoid repeated evaluation\n        lazy val head = hd\n        lazy val tail = tl\n        Cons(() =\u003e head, () =\u003e tail)\n    }\n\n    def empty[A]: Stream[A] = Empty\n    def apply[A](as: A*): Stream[A] = if (as.isEmpty) empty else cons(as.head, apply(as.tail: _*))\n}\n```\n* identical to List, except that the `Cons` takes suppliers (thunks) instead of strict values\n    * thunk is a subroutine used to inject an additional calculation into another subroutine\n* `foldRight`\n    ```\n    def foldRight[B](z: =\u003e B)(f: (A, =\u003e B) =\u003e B): B =\n        this match {\n            case Cons(h,t) =\u003e f(h(), t().foldRight(z)(f))\n            case _ =\u003e z\n    }\n    ```\n    * combining function is non-strict in its second parameter\n    * if f chooses not to evaluate its second parameter, this terminates the traversal early\n        ```\n        def exists(p: A =\u003e Boolean): Boolean = foldRight(false)((a, b) =\u003e p(a) || b\n        ```\n* `filter`\n    ```\n    def filter(p: A =\u003e Boolean): StreamFp[A] = {\n        foldRight(StreamFp.empty[A])((h, t) =\u003e\n            if (p(h)) StreamFp.cons(h, t) \n            else t)\n    }\n    ```\n    * tries to find the first matching value and if there is none, it will search forever\n        * `s.filter(_ =\u003e false)` will never terminates on infinite stream\n        * Stream from standard library - same problem\n        * LazyList - OK\n    * we can reuse filter to define find\n        * filter transforms the whole stream, but transformation is done lazily\n        ```\n        def find(p: A =\u003e Boolean): Option[A] = filter(p).headOption\n        ```\n* method implementations are incremental — they don’t fully generate their answers\n    * we can call these functions one after another without fully instantiating the intermediate results\n* corecursion\n    * a recursive function consumes data, a corecursive function produces data\n    ```\n    def constant[A](a: A): StreamFp[A] = {\n        StreamFp.cons(a, constant(a))\n    }\n    ```\n### standard library\n* `LazyList`\n    ```\n    list match {\n      case LazyList.empty =\u003e 1\n      case h #:: t =\u003e h\n    }\n    ```\n## trees\n```\nsealed trait Tree[+A]\ncase object Empty extends Tree[Nothing]\ncase class Branch[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A]\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmtumilowicz%2Fscala213-functional-programming-collections-workshop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmtumilowicz%2Fscala213-functional-programming-collections-workshop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmtumilowicz%2Fscala213-functional-programming-collections-workshop/lists"}