PageSourceSearch

https://zio.dev/assets/js/a15cfc2b.f9413f5b.js

js zio.dev collected 2026-10-03 19:27:37 UTC 17,902 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkzio_site||=[]).push([[8908],{50416(e,n,t){t.r(n),t.d(n,{assets:()=>r,contentTitle:()=>c,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>l});const i=JSON.parse('{"id":"zio-prelude/functional-abstractions/concrete-types/associative","title":"Associative","description":"Associative[A] describes a way of combining two values of type A that is associative.","source":"@site/docs/zio-prelude/functional-abstractions/concrete-types/associative.md","sourceDirName":"zio-prelude/functional-abstractions/concrete-types","slug":"/zio-prelude/functional-abstractions/concrete-types/associative","permalink":"/zio-prelude/functional-abstractions/concrete-types/associative","draft":false,"unlisted":false,"editUrl":"https://github.com/zio/zio-prelude/edit/series/2.x/docs/functional-abstractions/concrete-types/associative.md","tags":[],"version":"current","frontMatter":{"id":"associative","title":"Associative"},"sidebar":"ecosystem-sidebar","previous":{"title":"Introduction","permalink":"/zio-prelude/functional-abstractions/concrete-types/"},"next":{"title":"Commutative","permalink":"/zio-prelude/functional-abstractions/concrete-types/commutative"}}');var s=t(74848),a=t(28453);const o={id:"associative",title:"Associative"},c=void 0,r={},l=[];function d(e){const n={code:"code",p:"p",pre:"pre",...(0,a.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"Associative[A]"})," describes a way of combining two values of type ",(0,s.jsx)(n.code,{children:"A"})," that is associative."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"trait Associative[A] {\n  def combine(left: => A, right: => A): A\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["If we import ",(0,s.jsx)(n.code,{children:"zio.prelude._"})," we can use the ",(0,s.jsx)(n.code,{children:"<>"})," operator to combine any two values of type ",(0,s.jsx)(n.code,{children:"A"})," that have an ",(0,s.jsx)(n.code,{children:"Associative"})," instance defined for them."]}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"combine"})," operator must be associative, meaning that if we combine ",(0,s.jsx)(n.code,{children:"a"})," with ",(0,s.jsx)(n.code,{children:"b"})," and then combine the result with ",(0,s.jsx)(n.code,{children:"c"})," we must get the same value as if we combine ",(0,s.jsx)(n.code,{children:"b"})," with ",(0,s.jsx)(n.code,{children:"c"})," and then combine ",(0,s.jsx)(n.code,{children:"a"})," with the result."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"(a <> b) <> c === a <> (b <> c)\n"})}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"Associative"})," abstraction allows us to combine values of a data type to build a new value of that data type with richer structure."]}),"\n",(0,s.jsx)(n.p,{children:"A variety of data types can be combined in an associative way:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'import zio.{Chunk, NonEmptyChunk}\nimport zio.prelude._\n\nval string: String =\n  "Hello, " <> "world!"\n// string: String = "Hello, world!"\n\nval chunk: Chunk[Int] =\n  Chunk(1, 2, 3) <> Chunk(4, 5, 6)\n// chunk: Chunk[Int] = IndexedSeq(1, 2, 3, 4, 5, 6)\n'})}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"Associative"})," abstraction provides several advantages over using existing operators like the ",(0,s.jsx)(n.code,{children:"++"})," operator on ",(0,s.jsx)(n.code,{children:"Chunk"})," directly."]}),"\n",(0,s.jsxs)(n.p,{children:["First, ",(0,s.jsx)(n.code,{children:"Associative"})," allows us to combine more complex data types as long as the data types they are composed of can be combined in an associative way."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"case class Topic(value: String)\n\ncase class Votes(value: Int)\n\nobject Votes {\n  implicit val VotesAssociative: Associative[Votes] =\n    new Associative[Votes] {\n      def combine(left: => Votes, right: => Votes): Votes =\n        Votes(left.value + right.value)\n    }\n}\n\ncase class VoteMap(map: Map[Topic, Votes])\n\nobject VoteMap {\n  def combine(left: VoteMap, right: VoteMap): VoteMap =\n    VoteMap(left.map <> right.map)\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["If we didn't have ZIO Prelude we would have to implement the operator to combine two ",(0,s.jsx)(n.code,{children:"VoteMap"})," values ourselves. This would require some relatively low level logic that would look like this."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"def combine(left: VoteMap, right: VoteMap): VoteMap =\n  VoteMap(right.map.foldLeft(left.map) { case (map, (k, v)) =>\n    map + (k -> map.get(k).fold(v)(v1 => Votes(v.value + v1.value)))\n  })\n"})}),"\n",(0,s.jsxs)(n.p,{children:["This code isn't the worst. It uses operators like ",(0,s.jsx)(n.code,{children:"foldLeft"})," to do this combining of the two maps in a relatively high level way."]}),"\n",(0,s.jsx)(n.p,{children:"But we're still having to implement our own collection operators, taking our attention away from implementing our business logic. And it would be hard to implement this without at least having to take a minute and to check our logic, especially if we are less familiar with immutable collection operators."}),"\n",(0,s.jsx)(n.p,{children:"ZIO Prelude lets us avoid all of this because it knows how to combine two maps as long as there is a way to combine the map values. And it was quite simple to describe how we combine the map values, we just add them."}),"\n",(0,s.jsxs)(n.p,{children:["Second, the ",(0,s.jsx)(n.code,{children:"Associative"})," abstraction lets us generalize over different ways of combining values if we want to."]}),"\n",(0,s.jsxs)(n.p,{children:["For example, we could define an operator for reducing any ",(0,s.jsx)(n.code,{children:"NonEmptyChunk"})," to a summary value like this."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"import zio.NonEmptyChunk\n\ndef reduce[A: Associative](as: NonEmptyChunk[A]): A =\n  as.reduce(_ <> _)\n"})}),"\n",(0,s.jsxs)(n.p,{children:["We can then reduce any ",(0,s.jsx)(n.code,{children:"NonEmptyChunk"})," to a summary value as long as there is a way of combining the elements of the ",(0,s.jsx)(n.code,{children:"NonEmptyChunk"}),", whether those elements are strings or vote maps."]}),"\n",(0,s.jsx)(n.p,{children:"So far we have not described some very basic ways of combining that are associative, such as integer addition."}),"\n",(0,s.jsx)(n.p,{children:"The reason for this is that some data types support more than one way of combining them that is associative. For example, both integer addition and multiplication are associative."}),"\n",(0,s.jsx)(n.p,{children:"This creates a potential issue when using the type class pattern because the Scala compiler looks up implicit values based on their type, so if there are two different implicit values of a given type the Scala compiler does not know which one to use."}),"\n",(0,s.jsxs)(n.p,{children:["For example, here is what happens if we try to define ",(0,s.jsx)(n.code,{children:"Associative"})," instances for ",(0,s.jsx)(n.code,{children:"Int"})," for both addition and multiplication."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"implicit val IntSumAssociative: Associative[Int] =\n  new Associative[Int] {\n    def combine(left: => Int, right: => Int): Int =\n      left + right\n  }\n\nimplicit val IntProdAssociative: Associative[Int] =\n  new Associative[Int] {\n    def combine(left: => Int, right: => Int): Int =\n      left * right\n  }\n\n2 <> 3\n// error: ambiguous implicit values:\n//  both value IntSumAssociative in object MdocApp of type zi
1o.prelude.Associative[Int]\n//  and value IntProdAssociative in object MdocApp of type zio.prelude.Associative[Int]\n//  match expected type zio.prelude.Associative[Int]\n// 2 <> 3\n// ^^^^^^\n"})}),"\n",(0,s.jsxs)(n.p,{children:["This makes sense because there are indeed two different instances of ",(0,s.jsx)(n.code,{children:"Associative[Int]"})," and there is no basis for choosing between them."]}),"\n",(0,s.jsx)(n.p,{children:'We could just define one of these instances as "primary" like some other functional programming libraries do and relegate the other to second class status but that would be arbitrary and reflect a lack of compositionality.'}),"\n",(0,s.jsx)(n.p,{children:"Instead we solve the problem in a way that fits with Scala's implicit resolution mechanism. If the Scala compiler looks up implicit values based on their types, then if we want two values we need two types."}),"\n",(0,s.jsx)(n.p,{children:"We can easily do this with ZIO Prelude's new type functionality."}),"\n",(0,s.jsx)(n.p,{children:'New types allow us to define new types that "wrap" existing types in a way that has no overhead at runtime. We can also define these new types so that the Scala compiler actually knows that the new type is a subtype of the underlying type.'}),"\n",(0,s.jsxs)(n.p,{children:["Using this technique, we can define new types ",(0,s.jsx)(n.code,{children:"Sum"})," and ",(0,s.jsx)(n.code,{children:"Prod"})," that can be combined using addition and multiplication, respectively. ZIO Prelude provides these and similar new types such as ",(0,s.jsx)(n.code,{children:"And"})," and ",(0,s.jsx)(n.code,{children:"Or"})," for logical conjunction and disjunction in the ",(0,s.jsx)(n.code,{children:"zio.prelude.newtypes"})," package."]}),"\n",(0,s.jsxs)(n.p,{children:["We can wrap any existing type in a new type using the ",(0,s.jsx)(n.code,{children:"apply"})," or ",(0,s.jsx)(n.code,{children:"wrap"})," operators on the new type object."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"import zio.prelude._\nimport zio.prelude.newtypes._\n\nval sumInt: Sum[Int] =\n  Sum(1)\n// sumInt: Sum[Int] = 1\n\nval prodInt: Prod[Int] =\n  Prod.wrap(2)\n// prodInt: Prod[Int] = 2\n"})}),"\n",(0,s.jsxs)(n.p,{children:["We can unwrap any new type to get back the original type using the ",(0,s.jsx)(n.code,{children:"unwrap"})," operator on the new type object."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val int: Int =\n  Sum.unwrap(sumInt)\n// int: Int = 1\n"})}),"\n",(0,s.jsxs)(n.p,{children:["However, we will typically not need to do that for new types like ",(0,s.jsx)(n.code,{children:"Sum"})," and ",(0,s.jsx)(n.code,{children:"Prod"})," because they are subtypes of ",(0,s.jsx)(n.code,{children:"Int"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val int: Int =\n  prodInt\n// int: Int = 2\n"})}),"\n",(0,s.jsx)(n.p,{children:"Let's use these types to solve our problem from above."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val sum: Int =\n  Sum(2) <> Sum(3)\n// sum: Int = 5\n\nval product: Int =\n  Prod(2) <> Prod(3)\n// product: Int = 6\n"})}),"\n",(0,s.jsxs)(n.p,{children:["These variants don't just work for ",(0,s.jsx)(n.code,{children:"Int"}),", they work for any numeric data type in the Scala standard library."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val sum: Double =\n  Sum(2.0) <> Sum(3.0)\n// sum: Double = 5.0\n\nval product: Double =\n  Prod(2.0) <> Prod(3.0)\n// product: Double = 6.0\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Note that the associativity of addition and multiplication for ",(0,s.jsx)(n.code,{children:"Double"}),' values is subject to floating point rounding errors. ZIO Prelude assumes that if we are working with "lossy" data types like this we are aware of these issues and provides these instances for us, unlike some other functional programming libraries.']}),"\n",(0,s.jsx)(n.p,{children:"ZIO Prelude provides several other new types that you can use to define how you want to combine various data types."}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"And"})," and ",(0,s.jsx)(n.code,{children:"Or"})," new types mentioned above let us define ",(0,s.jsx)(n.code,{children:"Boolean"})," values that can be combined using logical conjunction and disjunction."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val and: Boolean =\n  And(true) <> And(false)\n// and: Boolean = false\n\nval or: Boolean =\n  Or(true) <> Or(false)\n// or: Boolean = true\n"})}),"\n",(0,s.jsx)(n.p,{children:"Another way of combining two values that is associative is taking the first or the last of two values."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'val first: String =\n  First("Hello") <> First("World")\n// first: String = "Hello"\n\nval last: String =\n  Last("Hello") <> Last("World")\n// last: String = "World"\n'})}),"\n",(0,s.jsxs)(n.p,{children:["The minimum and maximum of two values for which an ordering is defined also constitutes an associative ",(0,s.jsx)(n.code,{children:"combine"})," operator."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val min: Int =\n  Min(1) <> Min(2)\n// min: Int = 1\n\nval max: Int =\n  Max(1) <> Max(2)\n// max: Int = 2\n"})}),"\n",(0,s.jsx)(n.p,{children:"These new types are particularly useful when dealing with more complex data types to specify how we want to combine part of them."}),"\n",(0,s.jsxs)(n.p,{children:["For example, let's go to our example with the ",(0,s.jsx)(n.code,{children:"VoteMap"})," but say that now we are not going to define the additional type for ",(0,s.jsx)(n.code,{children:"Topic"})," and ",(0,s.jsx)(n.code,{children:"Votes"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"case class VoteMap(map: Map[String, Int])\n"})}),"\n",(0,s.jsxs)(n.p,{children:["We need to tell ZIO Prelude how we want to combine ",(0,s.jsx)(n.code,{children:"Int"})," values. In this case we want to use the sum."]}),"\n",(0,s.jsxs)(n.p,{children:["We do this by using the ",(0,s.jsx)(n.code,{children:"wrapAll"})," operator. Just like ",(0,s.jsx)(n.code,{children:"wrap"})," wraps a single value in a new type, ",(0,s.jsx)(n.code,{children:"wrapAll"})," wraps a whole collection of values in a new type."]}),"\n",(0,s.jsx)(n.p,{children:"It does this without traversing the collection because ZIO Prelude knows internally that the new type and the underlying type are the same. This way we avoid any runtime overhead for using these abstractions."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"object VoteMap {\n  implicit val VoteMapAssociative: Associative[VoteMap] =\n    new Associative[VoteMap] {\n      def combine(left: => VoteMap, right: => VoteMap): VoteMap =\n        VoteMap(Sum.wrapAll(left.map) <> Sum.wrapAll(right.map))\n    }\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["You can see the documentation on new types for additional information about new types in general. But the material here should give you what you need to combine values of even complex data types and define your own instances of the ",(0,s.jsx)(n.code,{children:"Associative"})," abstraction for your own data types."]}),"\n",(0,s.jsxs)(n.p,{children:["If you are interested in combining values of collections it is also worth checking out the ",(0,s.jsx)(n.code,{children:"ForEach"})," functional abstraction, which describes ways to iterate over collection types. The ",(0,s.jsx)(n.code,{children:"ForEach"})," abstraction comes with a variety of built in operators for combining collection types using an associative operator."]}),"\n",(0,s.jsxs)(n.p,{children:["For example, using ",(0,s.jsx)(n.code,{children:"ForEach"}),", ",(0,s.jsx)(n.code,{children:"Associative"}),", and new types we could count the total number of words in a collection of lines like this:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'import zio.NonEmptyChunk\n\ndef wordCount(lines: NonEmptyChunk[String]): Int =\n  lines.reduceMap(line => Sum(line.split(" ").length))\n'})}),"\n",(0,s.jsxs)(n.p,{children:["This maps each element in a collection to a new data type for which an ",(0,s.jsx)(n.code,{children:"Associative"})," instance is defined and then reduces all of those values to a single summary value with the ",(0,s.jsx)(n.code,{children:"combine"})," operator."]}),"\n",(0,s.jsx)(n.p,{children:"We could instead count the number of occurrences of each word like this:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'def wordCount(lines: NonEmptyChunk[String]): Map[String, Int] =\n  lines.reduceMap { line =>\n    Sum.wrapAll(line.split(" ").groupBy(identity).view.mapValues(_.length).toMap)\n  }\n'})}),"\n",(0,s.jsxs)(n.p,{children:["This version is exactly the same except we mapped the elements to a different value. ZIO Prelude automatically applied the appropriate ",(0,s.jsx)(n.code,{children:"combine"})," operator."]}),"\n",(0,s.jsx)(n.p,{children:"This is a great example of how these abstractions can make it easy to combine values of different data types, cutting the boilerplate out of our code and reducing opportunities for bugs."})]})}function h(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},28453(e,n,t){t.d(n,{R:()=>o,x:()=>c});var i=t(96540);const s={},a=i.createContext(s);function o(e){const n=i.useContext(a);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),i.createElement(a.Provider,{value:n},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.