Semigroups

In computer science, a Semigroup is an algebraic structure that has a set and a binary operation that takes two elements in the set and returns a Semigroup that has an associative operation. An associative operation is a binary operation that has a valid rule of replacement or transformation for expressions.

To start, we need to have a set and a specific binary operation, or we can make this behavior generic and define a protocol as follows:

protocol Semigroup { 
func operation(_ element: Self) -> Self
}

Any type that conforms to this protocol requires us to implement the operation method. Here, self presents the type that is conforming to this protocol. For instance, we can extend Int to conform to the Semigroup protocol and provide a summation on itself:

extension Int: Semigroup { 
func operation(_ element: Int) -> Int {
return self + element
}
}

We can test this as follows:

let number: Int = 5 
number.operation(3)

This test does not ensure the associativity of the binary operations. Let's try this:

let numberA: Int = 3 
let numberB: Int = 5
let numberC: Int = 7

if numberA.operation(numberB.operation(numberC)) == (numberA.operation(numberB)).operation(numberC) {
print("Operation is associative")
}

The preceding code ensures that our binary operator is associative; therefore, our Semigroup is verified. It does not look very nice though; let's implement an operator for our operation to make it look better and more math friendly:

precedencegroup AssociativityLeft { 
associativity: left
}

infix operator <> : AssociativityLeft

func <> <S: Semigroup> (x: S, y: S) -> S {
return x.operation(y)
}

Let's re-write our test with the <> operator:

if (numberA <> (numberB <> numberC)) == ((numberA <> numberB) <> numberC) { 
print("Operation is associative")
}

So far, we extended only Int but we can extend any type. Let's extend arrays as an example:

extension Array: Semigroup { 
func operation(_ element: Array) -> Array {
return self + element
}
}

The operation method is very similar to what we have for Int. The only difference is in the type, which is an array in this case:

print([1, 2, 3, 4] <> [5, 6, 7]) // prints "[1, 2, 3, 4, 5, 6, 7]" 

Furthermore, we can extend String as follows:

extension String: Semigroup { 
func operation(_ element: String) -> String {
return "(self)(element)"
}
}

We have established a general principle of composition (two objects combining into one) using a protocol. This pattern can be used for different purposes. For instance, we can implement a shorter version of reduce for arrays over Semigroups:

func sconcat <S: Semigroup> (initial: S, elements: [S]) -> S { 
return elements.reduce(initial, <>)
}

The sconcat function name stands for Semigroup sconcat; we can test it as follows:

print(sconcat(initial: 0, elements:[1, 2, 3])) // 6 
print(sconcat(initial: "", elements: ["A", "B", "C"])) // ABC
print(sconcat(initial: [], elements: [[1, 2], [3, 4, 5]])) // [1, 2, 3, 4, 5]

Our last sconcat example works like flatMap and flattens elements.

Finally, our Semigroup becomes the following:

precedencegroup AssociativityLeft { 
associativity: left
}

infix operator <> : AssociativityLeft

func <> <S: Semigroup> (x: S, y: S) -> S {
return x.operation(y)
}

protocol Semigroup {
func operation(_ element: Self) -> Self
}

extension Int: Semigroup {
func operation(_ element: Int) -> Int {
return self + element
}
}

extension String : Semigroup {
func operation(_ element: String) -> String {
return self + element
}
}

extension Array : Semigroup {
func operation(_ element: Array) -> Array {
return self + element
}
}

func sconcat <S: Semigroup> (initial: S, elements: [S]) -> S {
return elements.reduce(initial, <>)
}

A Semigroup is a great example of a simple data structure, but it is not as popular as a Monoid, which we will examine in the next section.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset