Typeclasses

Jun. 6th, 2014 09:04 am
jducoeur: (Default)
[personal profile] jducoeur
[For programmers only]

Yesterday was another of those programming epiphanies, in which I finally not only understand what a functional-programming concept means, but (more importantly) grok *why* it works the way it does. As I did with Monads a few months ago, let's take a stab at explaining the concept, while it's still fresh in my brain. Today's concept is "Typeclasses", another Haskellism that has made its way into Scala. (The functional programmers with more experience than I are encouraged to correct any misconceptions below.)

That said, my first try at writing this started getting horribly long. So let's see if I can keep this to a sane length:

The name seems a bit weird to those of us from an OO background, but it's literally correct: a Type is (or more accurately, can have) an "instance" of a Typeclass in much the same way that an Object is an instance of a Class. That is, a Typeclass describes a somewhat abstract bit of functionality, and you can define an instance that maps from a Type to that functionality.

Why is it useful? The usual reason: once you've got the idea of this Typeclass, you can then write higher-order functions that use it, and thereby reduce duplication and boilerplate.

A simple example in pseudo-code: say I have an "Addable" Typeclass:
typeclass Addable[T] {
  def +(a:T, b:T):T
}
That is, an Addable is a Type for which there exists a binary function "+", which adds two values of this Type together, and returns another value of this Type. Using that, I can define sum:
def sum(elems:List[Addable]) = {
  if (elems.length == 0)
    0
  else
    elems.head + sum(elems.tail)
}
At this point, the sharp-eyed OO programmers are saying, "So, it's exactly the same as an interface, right?". Not quite. The thing is, a Typeclass instance is, like I said, a mapping from the Type to the Typeclass -- and that implies that you can have *multiple* such mappings.

The classic illustration seems to be the Monoid Typeclass, which seems to be basically the abstraction of our Addable above. It is a Typeclass that defines an "append" operation, which takes two values, combines them, and produces a new value of the same Typeclass instance. (With the added constraint that the operation must be associative, and you must have an "identity" or "zero" value.) So Monoid looks roughly like this:
typeclass Monoid[T] {
  def append(a:T, b:T):T
  def identity:T
}
The equivalent of our sum() function above is "concat" in Haskell-speak:
def concat(elems:List[Monoid]) = {
  if (elems.length == 0)
    identity
  else
    elems.head append concat(elems.tail)
}
Obviously, we can define an SumMonoid -- the Typeclass instance that represents the concept of "summing" -- by saying that append is (a+b), and identity is 0; that becomes exactly Addable, above. But you can also define a ProductMonoid -- representing the concept of "multiplying" -- by saying that append is (a*b), and identity is 1. There are at least two distinct and *separate* Monoids for Int, because "Monoid" isn't a Type -- it's a Type *plus* some operations.

(The same is true for Boolean and Monoids: the concept of the "conjunction" of a List of Booleans is the Monoid of "and" with the identity of true, and "disjunction" is the Monoid of "or" with the identity of false.)

In other words, Monoid is (roughly) the abstract concept of "an operation that you can perform on a couple of Foos, and get another Foo". That's a Typeclass.

How much does this matter in real-world programming? I'm honestly not certain yet, but I'm slowly starting to appreciate that it helps you separate concerns, even without the multiple-definition thing. It's very un-OO, but thinking of a Typeclass instance as being something separate from the Type itself allows you to add functionality without endlessly complicating your inheritance trees -- it's not *part* of your class, it's a kind of thing that you can do *with* your class. (Or course, carried to its extreme, this logic gets very anti-OO; I suspect it helps explain the visceral functional-vs-OO wars I sometimes see.)

And of course, I wind up asking: is any of this going to get into Querki? I suspect so eventually, although not soon. I am likely to add the concept of Typeclasses internally at first, for some of the numeric operations (eg, I might actually implement sum() in terms of Monoids, as described above, to prove out the concept). Eventually it'll likely become user-visible, although only around the edges for high-end power-user use.

Questions? The above is largely me getting the ideas straight in my head, so discussion is welcome...

(no subject)

Date: 2014-06-06 01:34 pm (UTC)
From: [identity profile] fitzw.livejournal.com
I work with SQL on a regular basis, which brings up the following question: Are the elements nullable? How would that affect your "Haskell-speak" declarations for the operations? I assume it would just be an expansion on the if-then block structure, but you would first need a good concept of your rules for how each of the instances would handle that case. (Seems like an obvious statement, but misunderstanding of handling of nullable values in expressions in SQL has often caused us grief.)


(no subject)

Date: 2014-06-06 02:31 pm (UTC)
From: [identity profile] fitzw.livejournal.com
It is, perhaps, a question of semantics and the same word being used for multiple concepts.

Yes, SQL is usually explicit about whether a value is required or not (NULL is effectively no value, rather than a null pointer as with the JVM), but not in all cases, and if you don't specify, the default for whether or not a value is required can vary, even in the same database engine depending on whether or not ANSI SQL is specified. For example, in the extensions to SQL in MS SQL Server, declared variables are nullable, meaning values are always optional, while columns in table declarations may default to required (not nullable) or optional (nullable), depending on the ANSI SQL setting.

The usual interpretation of any of the expressions that involve a NULL (no value) element is also NULL. If you're working with a Boolean interpretation of that result, the interpretation is NOT TRUE (effectively FALSE, but NOT TRUE is often used to express that the inverse of the expression is also NOT TRUE). The result of using null values in expressions is probably the source of the most problems that I have to troubleshoot, especially as the database engines that I work with vary in how they handle NULL values in expressions.

I also have to deal with the different interpretations of zero-length strings (both as literals and variables) in different database engines. For example, in Oracle 12c, for instance, a zero-length string is interpreted as a NULL value (as it formerly was for Sybase), whereas in Informix Dynamic Server 11.70 it is interpreted as a string that happens to have zero length (as it is for MS SQL Server and PostgreSQL). Testing that string, appending it onto another, storing it in a required column et al has different results because of that.

So I'm kind of sensitized to the question, even when it may not apply. ;-)

(no subject)

Date: 2014-06-06 02:01 pm (UTC)
From: [identity profile] goldsquare.livejournal.com
Caveats: I'm way behind you in understanding Scala, and I'm only partway through Odersky's MOOC on Scala, and he hasn't covered typclasses yet: maybe he won't.

So, these are statements, but also perhaps questions.



I'm not sure that I see typeclasses quite as you do.

I see typeclasses as a way for a type to nominate itself for polymorphism of a type T while accepting certain default behaviors or code for that type T. In that sense, it is a step toward making types just as first-class as a classes and objects.

Classic OO polymorphism means that you must be a subclass of a common ancestor to be polymorphic. The addition of multiple inheritance (and later, its replacement by interfaces) added the ability of classes to be multiply polymorphic.

Scala traits add the ability to be multiply polymorphic in your class and (new!) inherit code behaviors. (And override them only on purpose).

Types fail to have that multiple polymorphism. But typeclasses move some of that multiple class polymorphism into the type structure. (And add default behaviors, 'cause Scala that's why).

Does that make sense?

(no subject)

Date: 2014-06-06 02:35 pm (UTC)
From: [identity profile] goldsquare.livejournal.com
The follow-up class, I'm told, is largely Akka. This class is about functional programming, using his language as the class language. He makes free with references to others (at times).

I'm finding the artificiality of a class schedule to be less than helpful: my real life doesn't always give me the same amount of free time each week. So, the class is hard to schedule.

The class is challenging on a mental level: not in the sense of "I can't do this", but literally: functional programming is striking at many of the reflexive assumptions that constitute my daily tools and career. It's fun, and it's tickling bits of my brain that have been quiescent for a long time.

I doubt I'll have a chance to, professionally, take advantage of Scala, but I'd like to.

So far, I've managed a perfect score on all the programming assignments, which is nice. :-)

(no subject)

Date: 2014-06-06 05:46 pm (UTC)
From: [identity profile] goldsquare.livejournal.com
Thank you for the offer: it will not be forgotten. :-)

The Akka class, according to my co-worker who has taken it, is taught by a number of people and is bit less well organized - he said. I don't know.

Functional techniques will be everywhere, soon enough, if by soon enough you mean "a decade" or so. F# is being adopted, Ruby has functional features (I am told). I doubt Haskell will be widely adopted, but one never knows. I don't care that JavaScript is technically qualified as a functional language, but it is I guess...

As a quality assurance engineer, sometimes I code, but as often as not I don't. My particular value-add is the ability to think just like a programmer, and to be conversant enough in programming that I can find a bug "in the code", or code review as well as test. People don't tend to expect that skill set in someone with "quality" in their title, but they are happy to exploit it when they get it. :-)

Plus, keeping my brain amused, challenged and expanding is a critical part of my "this is how I have fun in life" definition. :-) Same thing, again and again, every day, is my definition of Hell.

(no subject)

Date: 2014-06-06 07:07 pm (UTC)
From: [identity profile] goldsquare.livejournal.com
Around here, if you are "between projects" you are expected to use that time to self-educate. Most people around here do so all the time - but we are a consulting organization, so it's "after you get your billing done".

Part of what I like about the place is that most people take the challenge seriously.

I've lined up my next "amusement" - once the Scala class is complete, a co-worker who is very up on mobile suggested a particular Android programming book. I bought it, and it is "stacked and waiting".

(no subject)

Date: 2014-06-06 07:08 pm (UTC)
From: [identity profile] goldsquare.livejournal.com
Interestingly, one company I recently encountered offered a different "random time" thing. Every employee is REQUIRED to put in 3 hours a week of some kind of community service, paid by the employer.

I thought that was very exciting, actually.

(no subject)

Date: 2014-06-18 03:06 pm (UTC)
From: [identity profile] learnedax.livejournal.com
(Replying rather belatedly)

I think I kind of see the conceptual distinction you're making, but in terms of the functionality provided it still seems like you get about the same thing with interfaces using generics and inheritance. E.g.
public interface Monoid<T> {
  T append(T a, T b);
  T identity();
}
public interface Addable<T> extends Monoid<T> {}

Or for something that uses our typeclass/interface directly (though admittedly I haven't thought much about possible categories of typeclass yet, so I don't know if this is conceptually that useful):
public interface Composable<T extends Composable<T,R>,R> {
  T compose(T a, T b);
  R apply(T a, R b);
}
public interface Filter<R> extends Composable<Filter<R>, R> {}

And this gives us a huge amount of flexibility in terms of chaining such definitions - languages with interfaces generally allow them to multiple-inherit as well, so Filter could be Composable and Applicable if we wanted those to be separate typeclasses.

Now, admittedly, Filter is not considered to be an instance of the Composable typeclass, but rather a child typeclass... but since that allows us to have an arbitrary number of levels of ancestry, I'm not sure that's a bad thing.

Am I missing something? - the concept is brand new to me.

(no subject)

Date: 2014-06-20 03:49 am (UTC)
From: [identity profile] learnedax.livejournal.com
Ah, yes, having it be external to the type does change things. Of course, the level of abstraction here also means that the inference path to do the implicit conversion can get very hairy indeed.

While I have worked in languages with implicit conversion on occasion, I've usually found it one of the finickiest features to use and maintain. Of course, most of the same effect you're trying to achieve here should work just as well with explicit conversion, which is less migraine-prone.

Profile

jducoeur: (Default)
jducoeur

July 2025

S M T W T F S
  12345
6789101112
13141516171819
20212223242526
27 28293031  

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags