In this session, you'll get some taste how you can implement abstract algebra with type classes. Type classes let one define concepts that are quite abstract and that can be instantiated with many types. For instance, we could come up with the concept of a semi group. A type is an instance of a semi group if it has a combined method, a binary operator that takes two Ts and returns a T. There are many possible instance types of semi groups. For instance, int with addition is a semi group, so it's int with multiplication. So we have a choice what operator we want to use here. It's string with concatenation, so that would be string plus. It's any sequence really, so seq of some A type A with concatenation again. You can find many, many other examples. It's really very general. Semi group is a concept that comes from abstract algebra, from mathematics. There, to be a semi group, it's required that the operator is associative. In fact, if you look at these examples, each of these operators is indeed associative. Once we have defined semi groups, we can now define methods that work for all semi groups, so very general methods that work for all of these combinations up here. For instance, we could have a reduced method that reduces lists of T elements where T is a semi group. It would take the operator that is defined by the semi group and use that operator called combine to reduce the elements of the list, yielding a single value of type T. Depending on what the type T and its semi group instance are, reduce could do many different things. If you give it a list of ints and an instance of the sum semi group here, then it would give you back the sum of the numbers in the list. Or if it's a product semi group, the product. Or if you give it a list of strings, then it would give you back the concatenation of all these strings. It's a very general method that exists in many forms. In fact, if we look at abstract algebra than the type classes that we find there, form natural hierarchies. For instance, the next element down from semi group is usually called monoid. That's defined as a semi group with a left and right unit element. Here is its natural definition. A monoid of T is a semi group of T, and in addition to the combine method, the binary operator, it has an addition and unit method that returns the unit element of type T. Unit means that if you combine it to the left or to the right with anything, you get back the other operand be anything. As an exercise, let's generalize reduce to work on list of T where list has a monoid instance, such that it also works for empty lists. If you look at the reduce operator before, it wouldn't work for empty list because reduceLeft doesn't work for empty list, it gives you an exception, illegal operation. Now we want to get a version of reduced that works on monoids so that it returns the unit element for an empty list. I have pasted what we have so far in the worksheet so here's the definition on reduce on semi groups. Let's make it work for monoids and empty lists. We replace SemiGroup by monoid. Now we don't want to use a reduceLeft because reduceLeft doesn't work for empty lists, we want to use a foldLeft. It could also be a foldRight. It doesn't matter because combine is associative. But let's use a foldLeft. So we do a foldLeft. For foldLeft, we need a unit element. That would be m.unit, and we'd have the same combined method as before. That's a definition of reduce that we came up with. But one thing you have noticed is that I changed the header of reduce a little bit. Instead of a context bound, I was using a plain type parameter and a using clause because I knew the name for the actual monitoring question to select the unit element from that. The question is, can we also do without? Can we do with the context bound? What would we have to do to work with that? If we keep the context bound and reduce, would look like this. Then to actually get the monoid of T instance that we need, we can use a summon here, so we say reduceLeft, and then summon monoid of T, so that would give us the monoid that's implied by this implicit parameter and take the unit from that. This works, but it's unfortunately a bit clunky. If monoid is used a lot, which is maybe too expected because it's a very general type class, then it might be worthwhile to streamline the notation a little bit. We can do this by adding a helper method to the companion object of monoid. We define a companion object, monoid, and we give it an apply method, and that apply method is essentially a summon. It says, well, I need an implicit parameter of type monoid of T for a type parameter T and I return that implicit argument. So I return the current monoid of T instance. That defines now a global function Monoid.apply of T that returns the monoid T instance that's currently visible. With that helper, we can now write reduce like this. Reduce, it takes a context found as before, and we do reduceLeft with monoid of T.unit. Monoid of t is of course, Monoid.apply of T. As always, we insert and apply. It's this method here. What it returns is the current monoid of T instance that is in scope, and we take the unit element of that, so that's precisely what we want. That's a way to keep context bounds because they're nice and concise, and streamline the access to things that are not extension methods in the type class a little bit. Now, it's possible to have several given instances for a type class type pair. For instance, int could be a monoid in at least two ways. It could be a monoid with plus as combine and zero as unit, or it could be a monoid with times as combine and one as unit. Those are the two instance definitions, that given some monoid is this. Combine is plus and unit is zero, and the product monoid is this combine as times and unit is one. Assuming we have these given instances, define as an exercise, the sum and product functions on list of int in terms of reduce. Well, here's what you would get. Sum of xs is just reduce xs with the sumMonoid. Productxs is reducexs with the product monoid. That's all there is to it. Now, question to you. If you have done that in the worksheet, could you just quickly try out what happens if you leave out those two using arguments. I have the worksheet here with all definitions, and you see that indeed, sum of xs and product xs work as expected. What happens if I leave out the using clauses in the arguments here? I'll comment them out here. I get two errors. They say both ambiguous implicit arguments, both the sum of monoid object and the product monoid in class at match the type that's expected, which is semi group. That's quite normal because indeed, I have two given instances, sum monoid and product monoid, they're both applicable. They can be both passed to the using clause of reduce, and neither is better than the other so you get the classic ambiguity error. One thing that's important is that algebraic type classes are not just defined by their type signatures, but also by the laws that hold for them. For instance, if you have a given instance of monoid of T, then that instance should satisfy the laws of monoid, which are that combine is associative and that unit is a left and right unit. It should satisfy these laws for arbitrary values, x, y, z of type T. How do you verify the laws? Well, the type checker is not yet up to it to do that so you'll need a proof. That could be a formal proof on paper or an informal proof, or it could be a proof with a mechanized proof assistant such as Coq, or Agda, or Isabelle, or one of these, or you could just test them. A good way to test these laws, or a good way to test that an instance is lawful is by using randomized testing because that will exercise these laws at many different random values and that you can do with a tool like ScalaCheck that we have seen a couple of sessions ago.