Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Achieving the right abstractions with Haskell's type system

I'm having trouble using Haskell's type system elegantly. I'm sure my problem is a common one, but I don't know how to describe it except in terms specific to my program.

The concepts I'm trying to represent are:

  • datapoints, each of which takes one of several forms, e.g. (id, number of cases, number of controls), (id, number of cases, population)

  • sets of datapoints and aggregate information: (set of id's, total cases, total controls), with functions for adding / removing points (so for each variety of point, there's a corresponding variety of set)

I could have a class of point types and define each variety of point as its own type. Alternatively, I could have one point type and a different data constructor for each variety. Similarly for the sets of points.

I have at least one concern with each approach:

  • With type classes: Avoiding function name collision will be annoying. For example, both types of points could use a function to extract "number of cases", but the type class can't require this function because some other point type might not have cases.

  • Without type classes: I'd rather not export the data constructors from, say, the Point module (providing other, safer functions to create a new value). Without the data constructors, I won't be able to determine of which variety a given Point value is.

What design might help minimize these (and other) problems?

like image 606
zoo Avatar asked Aug 26 '11 19:08

zoo


2 Answers

To expand a bit on sclv's answer, there is an extended family of closely-related concepts that amount to providing some means of deconstructing a value: Catamorphisms, which are generalized folds; Church-encoding, which represents data by its operations, and is often equivalent to partially applying a catamorphism to the value it deconstructs; CPS transforms, where a Church encoding resembles a reified pattern match that takes separate continuations for each case; representing data as a collection of operations that use it, usually known as object-oriented programming; and so on.

In your case, what you seem to want is an an abstract type, i.e. one that doesn't export its internal representation, but not a completely sealed one, i.e. that leaves the representation open to functions in the module that defines it. This is the same pattern followed by things like Data.Map.Map. You probably don't want to go the type class route, since it sounds like you need to work with a variety of data points, rather than on an arbitrary choice of a single type of data point.

Most likely, some combination of "smart constructors" to create values, and a variety of deconstruction functions (as described above) exported from the module is the best starting point. Going from there, I expect most of the remaining details should have an obvious approach to take next.

like image 75
C. A. McCann Avatar answered Oct 14 '22 00:10

C. A. McCann


With the latter solution (no type classes), you can export a catamorphism on the type rather than the constructors..

data MyData = PointData Double Double | ControlData Double Double Double | SomeOtherData String Double

foldMyData pf cf sf d = case d of
       (PointData x y) -> pf x y
       (ControlData x y z) -> cf x y z
       (SomeOtherData s x) -> sf s x

That way you have a way to pull your data apart into whatever you want (including just ignoring the values and passing functions that return what type of constructor you used) without providing a general way to construct your data.

like image 3
sclv Avatar answered Oct 14 '22 01:10

sclv