[{"content":"Union-Find (or Disjoint Set) is a data structure used to manage disjoint sets of elements through two primary operations: find(x) and union(A, B). The find(x) operation determines which set the element xxx belongs to, while the union(A, B, C) operation merges the sets AAA and BBB into a single set CCC. In this article, we will explore Robert Endre Tarjan’s work on determining the upper bound of the time complexity for these Union-Find operations.\nWho this article is for:\nYou already know and have implemented the union-find data structure and want to understand its time complexity. You want to learn about the inverse Ackermann function, α(m,n)\\alpha(m, n)α(m,n), in the context of union-find time complexity. None of the above, but you are willing to use Google or ChatGPT to learn as you read through the article, and you’re not intimidated by math equations. Who this article is NOT for:\nYou’re looking for an introduction to the union-find data structure. In this case first read about it from cp-algorithms and then return to this article for the time complexity. You’ve already read the referenced paper in the reference section. You’re seeking the lower bound of the union-find time complexity. You want to gain first-hand experience by reading the paper directly. Note:\nThe idea discussed in this article is a slight variation of the algorithm presented in the paper “Efficiency of a Good But Not Linear Set Union Algorithm” by Robert Endre Tarjan. I highly recommend reading the original paper in the reference section.\nIn the paper, the size heuristic (also known as union by size) is used. However, in this article, we explore another variation known as \u0026ldquo;union by depth\u0026rdquo; of the trees.\nIntroduction Union Find (or Disjoint Set) is a data structure to manipulate disjoint set of elements using the following operations.\nfind(x): This function is used to determine which set the element xxx belongs to. It locates the node containing xxx and follows pointers up to the root of the corresponding tree to identify the name of the set. The collapsing rule may be applied to flatten the tree structure, optimizing future queries.\nunion(A, B, C): This function is used to combine the sets AAA and BBB into a new set named CCC. It locates the roots of the sets AAA and BBB, makes one root a child of the other, and names the new root as CCC. The choice of which root becomes the child can be made either randomly or by applying the weighted union rule, where the smaller tree is attached under the larger tree to keep the resulting structure balanced.\nCollapsing Rule (aka. path compression) After a find operation, make all the vertices traversed during the operation direct children of the root of the tree. (Refer to below figure)\nFig 1:Collapsing rule Weighted Union Rule (\u0026ldquo;union by depth\u0026rdquo; for the context of this article): If depth of the tree formed by set A is greater than that of set B, make B son of A. Otherwise make A son of B.\nFig 2: Weighted union rule Note: The depth of the tree rooted at r1 is less than that of the tree rooted at r2. Therefore, r1 is made a child of r2. In the original paper, the size-based variation was discussed.\nThe Claim Suppose we are given nnn elements, and we want to perform m≥nm \\geq nm≥n find operations and n−1n-1n−1 union operations. Let t(m,n)t(m, n)t(m,n) denote the maximum time required for a sequence of mmm intermixed find operations and nnn union operations.\nBy only using collapsing rule, we can achieve the following upper bound, for some positive constant kkk. t(m,n)≤k⋅m⋅max(1,log(n2/m)log(2m/n)) t(m,n) \\leq k \\cdot m \\cdot max \\Big(1, \\frac{log(n^2/m)}{log(2m/n)} \\Big) t(m,n)≤k⋅m⋅max(1,log(2m/n)log(n2/m)​) By using both collapsing rule and weighted union rule, we can achieve the following upper bound, for some positive constant kkk and a variation of inverse Ackermann function α(m,n)\\alpha(m,n)α(m,n). t(m,n)≤k⋅m⋅α(m,n) t(m,n) \\leq k \\cdot m \\cdot \\alpha(m,n) t(m,n)≤k⋅m⋅α(m,n) An Upper Bound (Proof) Let’s build a tree (called TTT) by performing all n−1n-1n−1 union operations first. Assign a rank to each vertex, equal to the height of the tree. This rank will remain unchanged in the future. (Refer to the figure below.)\nFig 3: Building a tree using all the union operations. Let the given sequence of operations are as follows:\n1 2 3 4 5 6 7 8 9 union(N2, N5) union(N3, N6) union(N6, N7) union(N6, N8) find(N8) \u0026lt;-- We are here union(N1, N2) union(N1, N3) union(N2, N4) ... // other find operations To compute the find operation at line number 5 using tree TTT, we follow the path from node N8N_8N8​ to its furthest ancestor, but only using the edges formed by union operations prior to this find operation (operations from line 1 to 4). We apply the collapsing rule to flatten the tree during this process. Let’s call this a partial-find. (Refer to the figure below.)\nFig 4: Edges adjust using collapsing rule Note: To get an upper bound on t(m,n)t(m, n)t(m,n) by only using collapsing rule, we just need to bound sum of path lengths of all such partial findings on the tree TTT.\nUsing only Collapsing Rule Let’s attempt to bound the number of edges traversed during all partial find operations on the tree TTT. To do this, we define a few classes containing sequences of positive integers for some positive constants bbb and zzz, as follows:\nClass num1 num2 num3 num4 \u0026hellip; Class 0 000 111 222 333 \u0026hellip; Class 1 000 bbb 2b2b2b 3b3b3b \u0026hellip; Class 2 000 b2b^2b2 2b22b^22b2 3b23b^23b2 \u0026hellip; \u0026hellip; ...... ...... ...... ...... \u0026hellip; Class i 000 bib^ibi 2bi2b^i2bi 3bi3b^i3bi \u0026hellip; \u0026hellip; ...... ...... ...... ...... \u0026hellip; Class z 000 bzb^zbz 2bz2b^z2bz 3bz3b^z3bz \u0026hellip; Let’s assign classes to the edges x→yx \\rightarrow yx→y using following rules.\nChoose the minimum class number i∈{1,2,..,z}i \\in \\{1, 2, .., z\\}i∈{1,2,..,z}, if rank(x)rank(x)rank(x) and rank(y)rank(y)rank(y) can be fit in between two consecutive values. If some edges can not be put to any class from rule 1, put them in class z+1z+1z+1. For all the edges, we encounter during the mmm partial-find, can be divided into following three categories. (refer figure below)\nFinal edge from each class. (blue colour) Non-final edge belongs to class 0 till class z. (black colour) Non-final edge belongs to class z+1. (red colour) Fig 5: Various class of edges, after $m$ partial-find. c(x) means, edge belongs to class x. Bounding the edges Final edges. (Let’s denote the count as c1c_1c1​)\nFor each partial-find, we can encounter at most z+1z + 1z+1 final edges, since we only have z+1z + 1z+1 classes. Therefore, for mmm partial-find operations, the total number of final edges is bounded by (z+1)⋅m(z + 1) \\cdot m(z+1)⋅m.\nNon-final edges belong to class 0 till class z. (Let’s denote the count as c2c_2c2​)\nEach such edge comes from a vertex (bounded by nnn). Let’s say such an edge x→yx \\rightarrow yx→y coming out of vertex xxx, belongs to two consecutive value, ppp and qqq, of class i (represented as two red circle, under column class i of Figure 6). Since this is a non final edge in the partial-find, there could be another edge u→vu \\rightarrow vu→v, belongs to class i, in between ppp and qqq. After applying the collapsing rule, x→y′x \\rightarrow y\u0026#x27;x→y′ (the green edge) in Figure 6 is formed and x→yx \\rightarrow yx→y is removed. With this operation, the class of x→y′x \\rightarrow y\u0026#x27;x→y′ either remains the same as x→yx \\rightarrow yx→y or increases, since the rank difference (between y′y\u0026#x27;y′ and xxx) can only increase due to the collapsing rule.\nLet’s see the case where the class stays the same. In Figure 6, x→yx \\rightarrow yx→y and u→vu \\rightarrow vu→v present between two consecutive values of class i. After applying collapsing rule finally xxx will be attached to y′y\u0026#x27;y′ via the green edge. Let’s say after applying the rule, the class of the edge x→y′x \\rightarrow y\u0026#x27;x→y′ which is coming out of xxx does not change. Since u→vu \\rightarrow vu→v and x→yx \\rightarrow yx→y are present in class iii, they could not have been assigned to class i−1i-1i−1 by the definition of how classes are assigned to edges. This implies that there must be at least one value from class i−1i-1i−1 between r(u)r(u)r(u) and r(v)r(v)r(v), as well as between r(x)r(x)r(x) and r(y)r(y)r(y), as illustrated in Figure 6 (red circle under class i−1i-1i−1). Hence we have at least two values from class i−1i-1i−1 in between the ranks of xxx and y′y\u0026#x27;y′.\nFig 6: Applying collapsing rule. Red circle being numbers from the table for the class. Green edge was not present in original m partial-find, but is formed after applying collapsing rule. Observe that between two consecutive values of class iii, there can be at most b−1b-1b−1 values in class i−1i-1i−1, based on the way we have defined the sequence number for each class. After applying collapsing rule one time to the edge coming from xxx, we have at least two values from class i−1i-1i−1 in between x→y′x \\rightarrow y\u0026#x27;x→y′. So after applying such operation bbb times, the edge have to move to the next class. We can move at most zzz times up in the class, so at most b⋅zb \\cdot zb⋅z times edge traversal for one vertex. For nnn vertices, it would be n⋅b⋅zn \\cdot b \\cdot zn⋅b⋅z.\nNon final edge belongs to class z+1z+1z+1. (Let’s denote the count as c3c_3c3​)\nLet’s calculate total number of values in class zzz, where the value is less than or equal to the max rank nnn. Total number of such values is n/bzn/b^zn/bz\nk.bz≤n⇒k≤n/bz k.b^z \\leq n \\Rightarrow k \\leq n/b^z k.bz≤n⇒k≤n/bzTherefore, for an edge from a vertex in class z+1z+1z+1, we can apply the collapsing rule at most n/bz+1n/b^z + 1n/bz+1 times. Since, there are nnn vertices, total number of such application would be (n/bz+1)⋅n(n/b^z + 1) \\cdot n(n/bz+1)⋅n which equals to n2/bz+nn^2/b^z + nn2/bz+n.\nTotal number of edges traversed (equation-1)\n=c1+c2+c3 = c_1 + c_2 + c_3 =c1​+c2​+c3​ =(z+1)m+bzn+n2bz+n = (z+1)m + bzn + \\frac{n^2}{b^z} + n =(z+1)m+bzn+bzn2​+nWe can pick suitable value for bbb and zzz to minimise the number of edge traversed. Let’s pick the following values.\nb=2mnz=max(1,log(n2/m)log(b)) \\begin{aligned} \u0026amp;b = \\frac{2m}{n} \\\\ \u0026amp;z = max \\Big(1, \\frac{log(n^2/m)}{log(b)}\\Big) \\end{aligned} ​b=n2m​z=max(1,log(b)log(n2/m)​)​By putting the values of bbb and zzz in equation-1, we get the following result, for a suitable constant kkk. (z+1)m+bzn+n2bz+n (z+1)m + bzn + \\frac{n^2}{b^z} + n (z+1)m+bzn+bzn2​+n =3mz+m+n+n2bz = 3mz + m + n + \\frac{n^2}{b^z} =3mz+m+n+bzn2​ ≤kmz+n2/max(b,blog(n2/m)/logb) \\leq kmz + n^2 / max(b, b^{{log(n^2/m)}/logb}) ≤kmz+n2/max(b,blog(n2/m)/logb) ≤kmz+n2/max(b,2log(n2/m)) \\leq kmz + n^2 / max(b, 2^{log(n^2/m)}) ≤kmz+n2/max(b,2log(n2/m)) ≤kmz+n2/max(2m/n,n2/m) \\leq kmz + n^2 / max(2m/n, n^2/m) ≤kmz+n2/max(2m/n,n2/m) ≤kmz \\leq kmz ≤kmz ≤k⋅m⋅max(1,log(n2/m)log(2m/n)) \\leq k \\cdot m \\cdot max\\Big(1, \\frac{log(n^2/m)}{log(2m/n)}\\Big) ≤k⋅m⋅max(1,log(2m/n)log(n2/m)​)Using weighted union rule with collapsing rule We will again build the tree similar to what we have discussed here, but now we will use weighted union rule, while building the tree. Observe that, If rank(x)=krank(x) = krank(x)=k, then number of vertices in the tree rooted at xxx is at least 2k2^k2k. This implies there are no more than n/2kn/2^kn/2k vertices of rank kkk in the tree.\nLet’s redefine the classes (for the below table, iii represents the class iii). Instead of using simple geometric progression as we did previously, we will use a variation of Ackermann function.\nThe Ackermann function A(i,j)A(i, j)A(i,j) is defined as follows.\n1 2 3 4 A(0, x) = 2x A(i, 0) = 0 for i≥1 A(i,2) = 2 for i ≥ 1 A(i, x) = A(i-1, A(i, x-1)) for i ≥ 1, x ≥ 2 i\\x 0 1 2 3 4 \u0026hellip; x 0 0 222 444 666 888 \u0026hellip; 2x2x2x 1 0 222 444 888 161616 \u0026hellip; 2x2^x2x 2 0 222 444 161616 2162^{16}216 \u0026hellip; 222...2^{2^{2^{...}}}222... x-times 3 0 222 444 2162^{16}216 222...2^{2^{2^{...}}}222... 2162^{16}216-times \u0026hellip; 4 0 222 444 222...2^{2^{2^{...}}}222... 2162^{16}216-times \u0026hellip; \u0026hellip; \u0026hellip; .. .. .. .. \u0026hellip; \u0026hellip; \u0026hellip; Let’s take the first zzz sequence from the above table and try to assign the class to the edges in the same way we did when only applying the collapsing rule.\nBounding the edges To calculate the total time complexity, we will use the same approach as the previous edge bounding technique.\nFinal edges (Let’s denote the count as c4c_4c4​)\nSince there are at most z+1z+1z+1 final classes, there are atmost z+1z+1z+1 final edges present. For mmm queries, c4c_4c4​ is bound by (z+1)⋅m(z+1) \\cdot m(z+1)⋅m.\nNon-final edges belong to class 0 till class z (Let’s denote the count as c5c_5c5​)\nWe will follow the same argument as before. But, since the sequence of the table has changed, the number of element in the previous class (class i−1i-1i−1) has also changed. Observe that, now between two values of class iii, let’s say A(i,j)A(i, j)A(i,j) and A(i,j+1)A(i, j+1)A(i,j+1), there are at least A(i,j)A(i, j)A(i,j) values from the class i−1i-1i−1.\nThe number of node xxx, whose rank r(x)r(x)r(x) lies between A(i,j)A(i, j)A(i,j) and A(i,j+1)A(i, j+1)A(i,j+1) (Remember that, rank are related to height of the tree)\n≤∑r=A(i,j)A(i,j+1)n2r≤∑r=A(i,j)∞n2r≤2n2A(i,j) \\leq \\sum_{r=A(i, j)}^{A(i, j+1)} \\frac{n}{2^r} \\leq \\sum_{r=A(i, j)}^{\\infty} \\frac{n}{2^r} \\leq \\frac{2n}{2^{A(i, j)}} ≤r=A(i,j)∑A(i,j+1)​2rn​≤r=A(i,j)∑∞​2rn​≤2A(i,j)2n​For each nodes from class iii, we can perform at most A(i,j)−1A(i, j) - 1A(i,j)−1 operations while keeping the edge in the same class, since there are at most A(i,j)A(i, j)A(i,j) values in class i−1i-1i−1. So for all the nodes in class iii, we can bound the operation by the following summation to move all the nodes from ithi^{th}ith class to i+1th{i+1}^{th}i+1th class. ∑j=0∞(2n2A(i,j)⋅A(i,j)) \\sum_{j=0}^{\\infty} \\Big( \\frac{2n}{2^{A(i, j)}} \\cdot A(i, j) \\Big) j=0∑∞​(2A(i,j)2n​⋅A(i,j))To move all the edges from class 111 to z+1z+1z+1, we need to perform this operation zzz times. Hence all such operations can be bound by the following summation for some constant kkk and k′k\u0026#x27;k′.\n∑i=0z(∑j=0∞2n⋅A(i,j)2A(i,j)) \\sum_{i=0}^{z} \\Big( \\sum_{j=0}^{\\infty} \\frac{2n \\cdot A(i, j)}{2^{A(i, j)}} \\Big) i=0∑z​(j=0∑∞​2A(i,j)2n⋅A(i,j)​)=2n∑i=0z∑j=0∞A(i,j)2A(i,j) = 2n \\sum_{i=0}^{z} \\sum_{j=0}^{\\infty} \\frac{ A(i, j)}{2^{A(i, j)}} =2ni=0∑z​j=0∑∞​2A(i,j)A(i,j)​≤2n∑i=0zk′ \\leq 2n \\sum_{i=0}^{z} k\u0026#x27; ≤2ni=0∑z​k′=2⋅k′⋅z⋅n = 2 \\cdot k\u0026#x27; \\cdot z \\cdot n =2⋅k′⋅z⋅n≤k⋅z⋅n \\leq k \\cdot z \\cdot n ≤k⋅z⋅n =O(z⋅n) = O(z \\cdot n) =O(z⋅n) Non final edge belongs to class z+1 (Let’s denote the count as c6c_6c6​)\nThe total number of operations we can perform on class z+1z+1z+1 can be bound by bounding the number of elements in class zzz.\nThe max rank in the tree is bound by log(n)log(n)log(n), since the height of the tree can be at most log(n)log(n)log(n) by following the weighted union rule.\nLet’s define inverse to the above Ackermann function α(m,n)\\alpha(m, n)α(m,n)\nα(m,n)=min{i≥1∣A(i,m/n)\u0026gt;log2(n)} \\alpha(m,n) = min\\{i \\geq 1 | A(i, m/n) \u0026gt; log_2(n)\\} α(m,n)=min{i≥1∣A(i,m/n)\u0026gt;log2​(n)}Max rank in the class zzz is log(n)log(n)log(n). Now if we choose z=α(m,n)z=\\alpha(m,n)z=α(m,n), then we can observe that we can have at most m/nm/nm/n element in the class zzz. This is obvious from the way we define the inverse ackermann function. So for nnn nodes, we can do at most n⋅mn=mn \\cdot \\frac{m}{n} = mn⋅nm​=m operations.\nTotal time, by considering z=α(m,n)z = \\alpha(m, n)z=α(m,n) is (for some constant kkk)\n=c4+c5+c6 = c_4 + c_5 + c_6 =c4​+c5​+c6​ ≤(z+1)⋅m+z⋅n+m \\leq (z+1) \\cdot m + z \\cdot n + m ≤(z+1)⋅m+z⋅n+m ≤k⋅m⋅z \\leq k \\cdot m \\cdot z ≤k⋅m⋅z ≤k⋅m⋅α(m,n) \\leq k \\cdot m \\cdot \\alpha(m, n) ≤k⋅m⋅α(m,n)Therefore, the total time is bound by O(m⋅α(m,n))O(m \\cdot \\alpha(m, n))O(m⋅α(m,n)). Hence amortized time bound per operation is O(α(m,n))O(\\alpha(m, n))O(α(m,n)). And since A(3,4)A(3, 4)A(3,4) is such a huge number, as we see from the above Ackermann function calculation table, for all practical purpose α(m,n)≤3\\alpha(m, n) \\leq 3α(m,n)≤3.\nReference / Credits [Research Paper] Efficiency of a Good But Not Linear Set Union Algorithm [Youtube] A\u0026amp;DS. Time Complexity of Union-Find (inverse Ackermann function) ","permalink":"/posts/understanding-union-find/","summary":"\u003cp\u003eUnion-Find (or Disjoint Set) is a data structure used to manage disjoint sets of elements through two primary operations: \u003cstrong\u003efind(x)\u003c/strong\u003e and \u003cstrong\u003eunion(A, B)\u003c/strong\u003e. The find(x) operation determines which set the element \u003cspan class=\"katex\"\u003e\u003cspan class=\"katex-mathml\"\u003e\u003cmath xmlns=\"http://www.w3.org/1998/Math/MathML\"\u003e\u003csemantics\u003e\u003cmrow\u003e\u003cmi\u003ex\u003c/mi\u003e\u003c/mrow\u003e\u003cannotation encoding=\"application/x-tex\"\u003ex\u003c/annotation\u003e\u003c/semantics\u003e\u003c/math\u003e\u003c/span\u003e\u003cspan class=\"katex-html\" aria-hidden=\"true\"\u003e\u003cspan class=\"base\"\u003e\u003cspan class=\"strut\" style=\"height:0.4306em;\"\u003e\u003c/span\u003e\u003cspan class=\"mord mathnormal\"\u003ex\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e belongs to, while the union(A, B, C) operation merges the sets \u003cspan class=\"katex\"\u003e\u003cspan class=\"katex-mathml\"\u003e\u003cmath xmlns=\"http://www.w3.org/1998/Math/MathML\"\u003e\u003csemantics\u003e\u003cmrow\u003e\u003cmi\u003eA\u003c/mi\u003e\u003c/mrow\u003e\u003cannotation encoding=\"application/x-tex\"\u003eA\u003c/annotation\u003e\u003c/semantics\u003e\u003c/math\u003e\u003c/span\u003e\u003cspan class=\"katex-html\" aria-hidden=\"true\"\u003e\u003cspan class=\"base\"\u003e\u003cspan class=\"strut\" style=\"height:0.6833em;\"\u003e\u003c/span\u003e\u003cspan class=\"mord mathnormal\"\u003eA\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e and \u003cspan class=\"katex\"\u003e\u003cspan class=\"katex-mathml\"\u003e\u003cmath xmlns=\"http://www.w3.org/1998/Math/MathML\"\u003e\u003csemantics\u003e\u003cmrow\u003e\u003cmi\u003eB\u003c/mi\u003e\u003c/mrow\u003e\u003cannotation encoding=\"application/x-tex\"\u003eB\u003c/annotation\u003e\u003c/semantics\u003e\u003c/math\u003e\u003c/span\u003e\u003cspan class=\"katex-html\" aria-hidden=\"true\"\u003e\u003cspan class=\"base\"\u003e\u003cspan class=\"strut\" style=\"height:0.6833em;\"\u003e\u003c/span\u003e\u003cspan class=\"mord mathnormal\" style=\"margin-right:0.05017em;\"\u003eB\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e into a single set \u003cspan class=\"katex\"\u003e\u003cspan class=\"katex-mathml\"\u003e\u003cmath xmlns=\"http://www.w3.org/1998/Math/MathML\"\u003e\u003csemantics\u003e\u003cmrow\u003e\u003cmi\u003eC\u003c/mi\u003e\u003c/mrow\u003e\u003cannotation encoding=\"application/x-tex\"\u003eC\u003c/annotation\u003e\u003c/semantics\u003e\u003c/math\u003e\u003c/span\u003e\u003cspan class=\"katex-html\" aria-hidden=\"true\"\u003e\u003cspan class=\"base\"\u003e\u003cspan class=\"strut\" style=\"height:0.6833em;\"\u003e\u003c/span\u003e\u003cspan class=\"mord mathnormal\" style=\"margin-right:0.07153em;\"\u003eC\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e. In this article, we will explore Robert Endre Tarjan’s work on determining the upper bound of the time complexity for these Union-Find operations.\u003c/p\u003e","title":"Understanding Union Find from Robert Endre Tarjan"},{"content":"The Problem:\nTo find square of a number and add one. Add logs to the see how the result is formed. Should be able to add more functions easily like multiplyThree. Functions defined should be composable in any order (we can multiplyThree before addOne or vice versa). We will solve this problem using a monad design pattern in a step by step thought process.\nDisclaimer: scala code written here are just for the sake of demonstrating the concept of monads.\nWho should read: beginner software engineer who want to know what monad is, without going into category theory.\nPrerequisite: little bit of scala (not required, as you can use chatgpt for basic syntax)\nHow to read: This article contains 4 thoughts (0 to 3). Read through each thought by thought. At the end of each thought there is a thought (questions in mind) that is left behind. Try to come up with a solution for the question presented at end of each thought before moving to next one.\nLet\u0026rsquo;s try to find the solution: Thought 0 We can start by implementing two functions, addOne and square, for the first task. But how do we implement logging?\nOne approach is to explicitly add logs in the main function, as follows, detailing how the final result is calculated. However, as we add more functions and compose them in different orders, manually managing and printing logs becomes increasingly difficult.\nCode 0: 1 2 3 4 5 6 7 8 9 10 11 12 13 object MonadsExample { def main(args: Array[String]): Unit = { println(addOne(square(2))) println(\u0026#34;Squared 2 to get 4\u0026#34;) println(\u0026#34;Added 1 to 4 to get 5\u0026#34;) } def square(x: Int): Int = { x * x } def addOne(x: Int): Int = { x + 1 } } Ouput 0: 1 2 3 5 Squared 2 to get 4 Added 1 to 4 to get 5 Is there a better way to handle this?\nThought 1 It seems we can declare a wrapper type (in this case, a case class NumberWithLogs) to hold both the result and the logs together. We can then modify the return type of the square function to use this wrapper type (NumberWithLogs). This way, our return value contains not only the result but also the associated log.\nBut wait—there’s a problem! How can we pass the result from the square function to the addOne function? To address this, we can change the input type of addOne to accept NumberWithLogs. Great—it works now!\nCode 1: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 object MonadsExample { def main(args: Array[String]): Unit = { print(addOne(square(2))) } case class NumberWithLogs ( result: Int, logs: Seq[String] ) def square(x: Int): NumberWithLogs = { NumberWithLogs( x * x, Seq(s\u0026#34;Squared ${x} to get ${x * x}\u0026#34;) ) } def addOne(x: NumberWithLogs): NumberWithLogs = { NumberWithLogs( x.result + 1, x.logs :+ s\u0026#34;Added 1 to ${x.result} to get ${x.result + 1}\u0026#34; ) } } Output 1: 1 NumberWithLogs(5,List(Squared 2 to get 4, Added 1 to 4 to get 5)) However, what if we want to add one to the square of the square of 2? Since the square function takes an unwrapped value (i.e., Int) as its argument and returns a NumberWithLogs, we can only use it the first time. Is there a way to handle this?\nThought 2 We can modify the input type of the square function to accept NumberWithLogs. This allows us to chain two square functions together.\nBut wait—how do we pass input to the square function for the first time? To address this, we can define a new function, wrapWithLogs, which takes an unwrapped value (i.e., Int) and converts it into a NumberWithLogs. Yes, this works as expected now!\nCode 2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 object MonadsExample { def main(args: Array[String]): Unit = { print(addOne(square(wrapWithLogs(2)))) } def wrapWithLogs (x: Int): NumberWithLogs = { NumberWithLogs(x, Seq.empty) } case class NumberWithLogs ( result: Int, logs: Seq[String] ) def square(x: NumberWithLogs): NumberWithLogs = { NumberWithLogs( x.result * x.result, x.logs :+ s\u0026#34;Squared ${x.result} to get ${x.result * x.result}\u0026#34; ) } def addOne(x: NumberWithLogs): NumberWithLogs = { NumberWithLogs( x.result + 1, x.logs :+ s\u0026#34;Added 1 to ${x.result} to get ${x.result + 1}\u0026#34; ) } } Output 2 1 NumberWithLogs(5,List(Squared 2 to get 4, Added 1 to 4 to get 5)) However, did you notice that every function we have now needs to do the extra work of appending logs? It’s less than ideal for a function like square to handle the responsibility of appending logs. Can we do better?\nThought 3 Instead of having the individual functions (e.g., addOne) take responsibility for appending logs, how about introducing another function, runWithLogs? This function would take a NumberWithLogs instance and a transformation function (e.g., addOne, square, etc.).\nThe runWithLogs function can apply the transformation function to the value inside the NumberWithLogs instance (by unwrapping it). Now, we also have access to both the logs generated by the transformation and the previous logs (supplied by the input NumberWithLogs). We can simply append these logs and return the updated result.\nCode 3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 object MonadsExample { def main(args: Array[String]): Unit = { print( runWithLogs( runWithLogs(wrapWithLogs(2), square), addOne ) ) } def runWithLogs(input: NumberWithLogs, transform: Int =\u0026gt; NumberWithLogs):NumberWithLogs = { val newNumberWithLogs = transform(input.result) NumberWithLogs( newNumberWithLogs.result, input.logs ++ newNumberWithLogs.logs ) } def wrapWithLogs (x: Int): NumberWithLogs = { NumberWithLogs(x, Seq.empty) } case class NumberWithLogs ( result: Int, logs: Seq[String] ) def square(x: Int): NumberWithLogs = { NumberWithLogs( x * x, Seq(s\u0026#34;Squared ${x} to get ${x * x}\u0026#34;) ) } def addOne(x: Int): NumberWithLogs = { NumberWithLogs( x + 1, Seq(s\u0026#34;Added 1 to ${x} to get ${x + 1}\u0026#34;) ) } } Output 3 1 NumberWithLogs(5,List(Squared 2 to get 4, Added 1 to 4 to get 5)) This way, all the log-appending logic is centralized in a single function—runWithLogs.\nHow can we extend this ? To add a new transformer like multiplyByThree, just add a new transformation function. 1 2 3 4 5 6 def multiplyByThree(x: Int): NumberWithLogs = { NumberWithLogs( x * 3, Seq(s\u0026#34;Multiplied 3 to ${x} to get ${x * 3}\u0026#34;) ) } To chain the functions in any order, use following code. 1 2 3 4 5 6 print( runWithLogs( runWithLogs(wrapWithLogs(2), addOne), square ) ) What is a monad ? Monad in context of functional programming is the pattern that we just implemented. Just to recap it had following properties.\nWrapper Type: NumberWithLogs Wrap Function: wrapWithLogs Run Function: runWithLogs Visualization monad visualization In the “normal world,” we start with a value like 2. To enter the “monad world,” we wrap this value using NumberWithLogs. The runWithLogs function takes this wrapper type (NumberWithLogs containing the value 2) and a transformation function. It unwraps the NumberWithLogs, applies the given function to the unwrapped value (in this case, 2), and then rewraps the result along with updated logs. We can chain transformations by using another runWithLogs, which takes the result of the previous runWithLogs call and a new transformation function, continuing the process as described in the previous step. Further Readings You Could Have Invented Monads! (And Maybe You Already Have.) Reference / Credits [Youtube] The Absolute Best Intro to Monads For Software Engineers ","permalink":"/posts/monads-for-software-engineers/","summary":"\u003cp\u003e\u003cstrong\u003eThe Problem:\u003c/strong\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eTo find square of a number and add one.\u003c/li\u003e\n\u003cli\u003eAdd logs to the see how the result is formed.\u003c/li\u003e\n\u003cli\u003eShould be able to add more functions easily like multiplyThree.\u003c/li\u003e\n\u003cli\u003eFunctions defined should be composable in any order (we can multiplyThree before addOne or vice versa).\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eWe will solve this problem using a monad design pattern in a step by step thought process.\u003c/p\u003e","title":"Monads for Software Engineers"},{"content":"Hi, I\u0026rsquo;m Himanshu.\nEnjoy reading through the blog. It\u0026rsquo;s a journey. I hope you\u0026rsquo;ll enjoy it.\nTimeline 2024 — current Something is cooking. Stay tune for this. 2022 — 2024 ThoughtGenesis Technologies Private Limited - Hyderabad, TelanganaSoftware Engineer Gain professional experiance by working with Apple Maps products. Met few of the great seniors here. This is the first time I experienced the professional life. 2018 — 2022 B.Tech, IIT Bhilai - ChhatishgarhComputer Science \u0026amp; Engineering Attended and received a bachloer\u0026rsquo;s degree in computer science and engineering. It\u0026rsquo;s fun to attend college and meet more genius and smarter friends. This time also we have faced the famous covid-19 which just ate up few years of the college life. 2017 — 2018 C`ksha Educational Services Pvt Ltd - Bhubaneswar, Odisha Enjoyed the life little bit. But quickly started preparing for JEE again. Spend a little time around Bhubaneswar here while preparing for JEE. 2015 — 2017 High Schooling at JNV Rangareddy - Hyderabad, TelanganaPCM \u0026amp; IP stream Clearaed Dakshana Entrance exam and moved to JNV Rangareddy. Spend most of my hardworking life here. I would never work hard like this after this. I have prepared for JEE(both mains and advanced) - thanks to dakshana foundation. I would get a seat in IIT Indore(CSE branch), but would reject it. 2010 — 2015 Schooling at JNV Balasore - OdishaCBSE Board Hey it\u0026rsquo;s a little kid. Just cleared the JNVST entrance exam, left home for getting education. People believe hostel will take care of you, but in reality you learn to take care of you. This is the foundation. ","permalink":"/about/","summary":"About this blog","title":"About"}]