67
votes

I've read about monoid homomorphism from Monoid Morphisms, Products, and Coproducts and could not understand 100%.

The author says (emphasis original):

The length function maps from String to Int while preserving the monoid structure. Such a function, that maps from one monoid to another in such a preserving way, is called a monoid homomorphism. In general, for monoids M and N, a homomorphism f: M => N, and all values x:M, y:M, the following equations hold:

f(x |+| y) == (f(x) |+| f(y))

f(mzero[M]) == mzero[N]

Does he mean that, since the datatypes String and Int are monoids, and the function length maps String => Int preserving the monoid structure (Int is a monoid), it is called monoid homomorphism, right?

4

4 Answers

85
votes

Does he mean, the datatype String and Int are monoid.

No, neither String nor Int are monoids. A monoid is a 3-tuple (S, ⊕, e) where ⊕ is a binary operator ⊕ : S×S → S, such that for all elements a, b, c∈S it holds that (a⊕b)⊕c=a⊕(b⊕c), and e∈S is an "identity element" such that a⊕e=e⊕a=a. String and Int are types, so basically sets of values, but not 3-tuples.

The article says:

Let's take the String concatenation and Int addition as example monoids that have a relationship.

So the author clearly also mentions the binary operators ((++) in case of String, and (+) in case of Int). The identities (empty string in case of String and 0 in case of Int) are left implicit; leaving the identities as an exercise for the reader is common in informal English discourse.

Now given that we have two monoid structures (M, ⊕, em) and (N, ⊗, en), a function f : M → N (like length) is then called a monoid homomorphism [wiki] given it holds that f(m1⊕m2)=f(m1)⊗f(m2) for all elements m1, m2∈M and that mapping also preserves the identity element: f(em)=en.

For example length :: String -> Int is a monoid homomorphism, since we can consider the monoids (String, (++), "") and (Int, (+), 0). It holds that:

  1. length (s1 ++ s2) == length s1 + length s2 (for all Strings s1 and s2); and
  2. length "" == 0.
23
votes

Datatype cannot be a monoid on its own. For a monoid, you need a data type T and two more things:

  • an associative binary operation, let's call it |+|, that takes two elements of type T and produces an element of type T
  • an identity element of type T, let's call it i, such that for every element t of type T the following holds: t |+| i = i |+| t = t

Here are some examples of a monoid:

  • set of integers with operation = addition and identity = zero
  • set of integers with operation = multiplication and identity = one
  • set of lists with operation = appending and identity = empty list
  • set of strings with operation = concatenation and identity = empty string

Monoid homomorphism

String concatenation monoid can be transformed into integer addition monoid by applying .length to all its elements. Both those sets form a monoid. By the way, remember that we can't just say "set of integers forms a monoid"; we have to pick an associative operation and a corresponding identity element. If we take e.g. division as operation, we break the first rule (instead of producing an element of type integer, we might produce an element of type float/double).

Method length allows us to go from a monoid (string concatenation) to another monoid (integer addition). If such operation also preserves monoid structure, it is considered to be a monoid homomorphism.

Preserving the structure means:

length(t1 |+| t2) = length(t1) |+| length(t2)

and

length(i) = i'

where t1 and t2 represent elements of the "source" monoid, i is the identity of the "source" monoid, and i' is the identity of the "destination" monoid. You can try it out yourself and see that length indeed is a structure-preserving operation on a string concatenation monoid, while e.g. indexOf("a") isn't.

Monoid isomorphism

As demonstrated, length maps all strings to their corresponding integers and forms a monoid with addition as operation and zero as identity. But we can't go back - for every string, we can figure out its length, but given a length we can't reconstruct the "original" string. If we could, then the operation of "going forward" combined with the operation of "going back" would form a monoid isomorphism.

Isomorphism means being able to go back and forth without any loss of information. For example, as stated earlier, list forms a monoid under appending as operation and empty list as identity element. We could go from "list under appending" monoid to "vector under appending" monoid and back without any loss of information, which means that operations .toVector and .toList together form an isomorphism. Another example of an isomorphism, which Runar mentioned in his text, is StringList[Char].

4
votes

Colloquially a homomorphism is a function that preserves structure. In the example of the length function the preserved structure is the sum of the lengths of two strings being equal to the length of the concatenation of the same strings. Since both strings and integers can be regarded as monoids (when equipped with an identity and an associative binary operation obeying the monoid laws) length is called a monoid homomorphism.

See also the other answers for a more technical explanation.

0
votes
trait Monoid[T] {
def op(a: T, b: T): T
def zero: T
}

val strMonoid = new Monoid[String] {
  def op(a: String, b: String): String = a ++ b
  def zero: String = ""
}

val lcMonoid = new Monoid[List[Char]] {
  def op(a: List[Char], b: List[Char]): List[Char] = a ::: b
  def zero = List.empty[Char]
}

homomorphism via function f

f{M.op(x,y)} = N.op(f(x),g(y))

for example, using toList available on String

//in REPL
scala> strMonoid.op("abc","def").toList == lcMonoid.op("abc".toList,"def".toList)
res4: Boolean = true

isomorphism via functions f and g

given bi-directional homomorphism between monoids M and N,

f{M.op(x,y)} = N.op(f(x),f(y))
g{N.op(x,y)} = M.op(g(x),g(y))

And if both (f andThen g) and (g andThen f) are identify functions, then monoids M and N are isomorphic via f and g

g{f{M.op(x,y)}} = g{N.op(f(x),f(y))} = M.op(g(f(x)),g(f(y))) = M.op(x,y)

for example, using toList available on String and toString available on List[Char] (where toList andThen toString and toString andThen toList are identity functions)

scala> ( strMonoid.op("abc","def").toList ).toString == ( lcMonoid.op("abc".toList,"def".toList) ).toString
res7: Boolean = true